WinPcap权威指南(四):UDP与DNS欺骗

6条评论 2014-08-07 admin

        上一节介绍了ARP协议之后,群里面有朋友说ARP欺骗不是很简单么?实际上,实现ARP欺骗是很简单的,难点在于数据转发的速度。2004年我们做隔山打牛的时候,曾经去过一家网吧测试,刚开始是开了ARP欺骗几分钟就大量主机掉线,客人骂声一片(幸好是朋友的网吧,否则估计会被老板丢出去)。当然,那时候网上还没有ARP欺骗的介绍,更加没有什么ARP防火墙,ARP欺骗的防范是几年后的事情了。后来我们改进了算法,经过实际测试,250台电脑,原来的ICMP速度是多少(ping),开启ARP欺骗后就是多少,没有任何延迟,机器更加不会掉线。因为我们的代码仅出于演示目的,所以捕获线程和分析线程共用了一个List,然后加锁,如果你直接拿去网吧用,百分之百是被网吧老板抓住狂揍一顿的。

        群里还有朋友说cain一开,整个局域网会掉线,这个实际上是cain处理的不好:它启动ARP欺骗后,并没有对现有连接的seq和ack等做转换,另外还有一种解决方法是被动欺骗。不过实际上,数据修改并不一定要上ARP欺骗这种中间人方式,不使用ARP欺骗而达到数据修改,至少有三种方法,后面我们会介绍一两种,至于第三种,因为命中率百分之百,为了造成不良后果,以后有机会再说。

        上一节我们说了,网络通信的第一步是通过ARP协议获取目标电脑的物理地址—局域网内通信是获取目标电脑的,互联网是获取网关的,那第二步呢?答案当然是IP地址。所有基于IP的网络协议(例如icmp、tcp、udp)都必须拥有这个,但是因为ip难以记忆,而且有可能改变,所有实际应用中一般使用域名协议。例如,我们打开cmd,然后ping 一下我们的网站www.138soft.com,结果如下图:

xwinpcap8

        从上图可以看到,ICMP协议会先获取www.138soft.com对应的ip地址,然后再跟它收发数据,用过浏览器的朋友们,实际上当你们输入域名的时候,浏览器也会有这个过程。写过网络程序的朋友就清楚的多,因为这个就是gethostbyname函数。这个过程,实质上就是DNS协议。

        DNS协议的规范定义这里不作介绍了,我们只从程序的角度来说一下。DNS协议一般使用UDP,而且端口一般是53。DNS数据包分为查询包和应答包,其中查询包包含了这个包的ID(用于区别其它的查询包)、包的类型(这里为查询)、查询的类型(比如说:IP地址是A记录、MX发信地址是MX记录),DNS服务器的反馈包结构,前面是这个请求包(但包类型修改为反馈包),后面紧跟着具体的应答包。下面我们通过自己写的一个程序来重现这个过程(实际上,这个就是系统的gethostbyname函数的实现)。首先,我们使用iphlp库来获取本机的DNS服务器IP:

function GetDNSServerIP: AnsiString;
var
  dwBuffSize: Cardinal;
  pFinxedInfo: PFIXED_INFO;
  pIPAddr: PIP_ADDR_STRING;
begin
  Result := '';

  if (GetNetworkParams(nil, dwBuffSize) <> ERROR_BUFFER_OVERFLOW) then Exit;
  GetMem(pFinxedInfo, dwBuffSize);
  if pFinxedInfo = nil then Exit;
  if (GetNetworkParams(pFinxedInfo, dwBuffSize) <> ERROR_SUCCESS) then
  begin
    FreeMem(pFinxedInfo);
    Exit;
  end;
  pIPAddr := @pFinxedInfo^.DnsServerList;
  while (pIPAddr <> nil) do
  begin
    if Result = '' then Result := Format('%s', [pIPAddr^.IpAddress.S]);
    pIPAddr := pIPAddr^.Next;
  end;
  FreeMem(pFinxedInfo);
end;

        有个DNS地址,就可以构造请求包发送和接收了:

procedure TForm1.Button1Click(Sender: TObject);
//这里省略部分内部函数,具体看代码,函数来源于Indy
var
  AUDPSocket: TSocket;
  SockAddrIn: TSockAddrIn;
  strHostName: AnsiString;
  nDNSPort: Integer;
  QueryID: Word;
var
  nLen, nRet: Integer;
begin
  (Sender as TButton).Enabled := False;
  try
    if Trim(Edit_Host.Text) = '' then
    begin
      ShowMessage('请输入域名!');
      Edit_Host.SetFocus;
      Exit;
    end;

    if Trim(Edit_DNSServer.Text) = '' then
    begin
      ShowMessage('请输入DNS服务器IP!');
      Edit_DNSServer.SetFocus;
      Exit;
    end;

    if Trim(Edit_DNSPort.Text) = '' then
    begin
      ShowMessage('请输入DNS服务器端口!');
      Edit_DNSPort.SetFocus;
      Exit;
    end;

    strHostName := AnsiString(Trim(Edit_Host.Text));

    nDNSPort := StrToIntDef(Trim(Edit_DNSPort.Text), 53);

    AUDPSocket := socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if AUDPSocket = INVALID_SOCKET then Exit;
    SockAddrIn.sin_family := AF_INET;
    SockAddrIn.sin_port := htons(nDNSPort);
    SockAddrIn.sin_addr.s_addr := inet_addr(PAnsiChar(AnsiString(Trim(Edit_DNSServer.Text))));


    Randomize;
    QueryID := Random(65535);
    Buffer.Count := 0;
    Buffer.ID := Swap(QueryID); //查询的ID
    Move(Header_Option[1], Buffer.Option, 10);
    StringToLabel(strHostName, SubLabel);
    Move(SubLabel[1], Buffer.Data[0], Length(SubLabel));
    Buffer.Count := Length(SubLabel);
    QType := #0#1; //A记录
    Move(QType[1], Buffer.Data[Buffer.Count], 2);
    Buffer.Count := Buffer.Count + 2;
    Move(QClass_Internet, Buffer.Data[Buffer.Count], 2);
    Buffer.Count := Buffer.Count + 14; // 2 + 12

    if sendto(AUDPSocket, PAnsiChar(@Buffer.ID)^, Buffer.Count, 0, SockAddrIn, sizeof(SockAddrIn)) = SOCKET_ERROR then
    begin
      CloseSocket(AUDPSocket);
      ShowMessage('数据发送失败!');
      Exit;
    end;

    nLen := sizeof(SockAddrIn);
    nRet := recvfrom(AUDPSocket, PAnsiChar(@Buffer.ID)^, sizeof(Buffer), 0, SockAddrIn, nLen);
    if nRet = SOCKET_ERROR then
    begin
      CloseSocket(AUDPSocket);
      ShowMessage('数据接收失败!');
      Exit;
    end;
    CloseSocket(AUDPSocket);
    //==========================================================================
    if Buffer.ID = Swap(QueryID) then
    begin
      i := 0;
      while Buffer.Data[i] <> #0 do Inc(i);
      i := i + 5;
      while (Buffer.Data[i] <> #32) and (i < nRet) do
      begin
        LabelToString(@Buffer.Data, i, RRDomain);
        Inc(i);
        RRDataType := Ord(Buffer.Data[i]);
        i := i + 9;
        case RRDataType of
          QType_CNAME: Memo1.Lines.Add(GetCNAME(Buffer, i));
          QType_NS: Memo1.Lines.Add(GetNS(Buffer, i));
          QType_A: Memo1.Lines.Add(GetA(Buffer, i));
          QType_SOA: Memo1.Lines.Add(GetSOA(Buffer, i));
          QType_PTR: Memo1.Lines.Add(GetPTR(Buffer, i));
          QType_MX: Memo1.Lines.Add(GetMX(Buffer, i));
        else
          begin
            uShort.B[1] := Buffer.nData[i - 2];
            uShort.B[0] := Buffer.nData[i - 1];
            i := i + uShort.Value;
          end;
        end;//end for "case"
      end;//end for "while"
    end;//end for "if"
  finally
    (Sender as TButton).Enabled := True;
  end;
end;

        程序运行界面如下:

xwinpcap9

        对应的,我们再来实现一个DNS服务器,代码如下:

const
    HOSTIP='127.0.0.1';//需要返回的IP地址

procedure TForm1.ReadData(var Message: TMessage);
var
  client_addr: TSockAddrIn;
  szBuffer: array[0..4095] of AnsiChar;
  len: integer;
  flen: integer;
  Event: word;
var
  strRecvData: AnsiString;

  ID, Code, QDCount: Word;
  BitCode: TDNSBitCode;
  i: integer;
  APos: Integer;
  DomainName: AnsiString;
  QueryClass: Word;
  QueryType: Word;

  strSend: AnsiString;
begin
  flen := sizeof(client_addr);
  Event := WSAGetSelectEvent(Message.LParam);
  if Event = FD_READ then
  begin
    len := recvfrom(m_UDPSocket, szBuffer, sizeof(szBuffer), 0, client_addr, flen);
    if len <= 0 then Exit;
    if len > 512 then Exit;

    SetLength(strRecvData, len);
    Move(szBuffer[0], strRecvData[1], len);

    if Length(strRecvData) < sizeof(TDNSHeader) then Exit;
    ID := TwoCharToWord(strRecvData[1], strRecvData[2]);
    Code := TwoCharToWord(strRecvData[3], strRecvData[4]);
    BitCode := GetDNSBitCode(Code);
    if BitCode.QR <> 0 then Exit; //0表示查询,1表示反馈
    QDCount := TwoCharToWord(strRecvData[5], strRecvData[6]);
    if QDCount <= 0 then Exit;

    APos := 13; // DNS头恒为12字节,我们从下一字节开始分析
    for i := 1 to QDCount do
    begin
      DomainName := DNSStrToDomain(strRecvData, APos);
      if DomainName = '' then Exit;
      QueryType := TwoCharToWord(strRecvData[APos], strRecvData[APos + 1]);
      Inc(APos, 2);
      QueryClass := TwoCharToWord(strRecvData[APos], strRecvData[APos + 1]);
      Inc(APos, 2);
      Memo1.Lines.Add(DomainName);
      (*
      m_strInfo := 'DNS查询包' + #$D#$A;
      m_strInfo := m_strInfo + SourceIP + ':' + IntToStr(SourcePort) + '--->' + DestIP + ':' + IntToStr(DestPort) + #$D#$A;
      m_strInfo := m_strInfo + 'DNSID:' + IntToStr(ID) + #$D#$A;
      m_strInfo := m_strInfo + 'Domain:' + DomainName + #$D#$A;
      m_strInfo := m_strInfo + 'QueryType:' + GetDNSTypeStr(QueryType) + #$D#$A;
      m_strInfo := m_strInfo + 'QueryClass:' + GetDNSClassStr(QueryClass) + #$D#$A;
      m_strInfo := m_strInfo + '===================================================';
      Synchronize(ShowInfo);
      *)
    end;
    strSend := BuilderDNSResponse(szBuffer, len, HOSTIP);//构造DNS应答包
    sendto(m_UDPSocket, strSend[1], Length(strSend), 0, client_addr, flen);
  end;
end;

        程序写的很简单,就是根据查询包,分析出客户端需要查询的域名,在Memo1显示出来,然后构造一个应答包返回。这里出于演示目的,我们简单的全部返回127.0.0.1,对于真正的DNS服务器,这里会使用gethostbyname之类往上一级查询,或直接在数据库和缓存里面查找,根据不同的域名返回对应的IP。我们先运行程序,然后把另外一台机器的DNS服务器指向这个IP:

xwinpcap10

        然后随便Ping 一下www.qq.com:

xwinpcap11

        可以看到,返回的IP地址是127.0.0.1。同时我们的DNS服务器的界面如下:

xwinpcap12

        最后,我们把DNS服务端的代码移植到我们的Demo代码里面,就实现了DNS欺骗。需要注意的地方是,基于IP的数据包都有一个校验和的,所以如果你修改了数据包,一定要重新计算一次校验和,具体过程见CheckSum函数。程序运行效果如下:

xwinpcap13

        DNS欺骗利用的原理是:DNS客户端发送请求包后,只要返回包里面的ID跟发送的一致,它就认为合法。如果有多个返回包,则它使用最前面的,这个是跟ARP刚好相反的(ARP会使用最新的覆盖原来的)。几年前,360云查杀的时候,我做过一个试验,就是在应用层开启RAWSOCKET来捕获数据,发现是请求360云服务器的话,则直接创建一个UDP SOCKET然后往请求地址发送一个回复,360就无法连接真正的云服务器了。实际上,不只是DNS协议如此,对于TCP协议,也是一样的。只要你捕获到它的数据包,就可以实现网页插代码、下载内容替换等等。这里再回头前面的话题:不用ARP欺骗就修改数据。方法之一就是在路由的镜像口接一台电脑,比如说,想欺骗TCP,只要你回复的数据包的SEQ和ACK按照顺序,客户端就会老老实实的被欺骗。因为你的程序工作在路由旁边,比真正的服务器回复的速度是更快的,甚至你可以再伪造回复的同时,顺便发送一个RST包给真正的服务器,让它断开连接。如果你无法接触路由,又不想使用ARP怎么办?那就用方法二或方法三了。

附件下载:
本节代码

分类:网络相关

6条评论 发表评论

  • 大白说道:

    请问鲸鱼老师,浏览器输入域名后,跳转到另一个域名或者类似情况,是属于DNS劫持吗?可有案例?

    • admin说道:

      这个应该不算DNS劫持。如果排除本机有程序改变了这个,那么有可能是TCP欺骗,例如302。

  • cnbc说道:

    好东西呀!!

  • 你2000年时候的网友!!说道:

    老陈!我2000年用你黑洞2000时认识你的,当时还加了你的QQ,还看过你的照片呢!你最早时候的电话是湖北的吧?现在过了十几年了,你还好吗?希望你把你现在的联系方式给我,我很想念你!我的QQ是 10100008

  • sb说道:

    额。应该说DNS欺骗、DNS欺骗能实现这样的效果。然后像你说的跳转可以是很多的情况的嘛。脚本层面上都有好多种方法。到协议上的话。DNS劫持。TCP劫持和欺骗。都是可以实现的。

  • sb说道:

    陈经韬一直是一个神一样的人,从黑洞开始知道,然后学习delphi的时候,各种看这种深入浅出的文章,一直受益匪浅,只是现在的干货越来少了,哈哈。
    从文章中也知道了原因,就是觉得技术开源未必是一个好事情。
    大家本来就是靠技术吃饭的,凭什么要把自己研究和学习到的东西无限的公开?这个确实是真的这个道理,我也不会随意的把我的东西公开和开源,就算毫无价值也都不愿意跟别人说。
    陈经韬能做成这样,已经是很无私的了。
    说实在的,看您的文章,确实省了很多的功夫!

发表评论

(必填)

(必填), (Hidden)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

TrackBack URL  |  这篇文章上的评论的RSS feed


近期文章

近期评论

文章归档

分类目录