WinPcap权威指南(三):ARP协议

。_饼干妹妹 提交于 2020-05-02 16:56:58

      ARP协议在局域网内使用的非常广泛,它的数据包类型分为请求包和答复包。Windows系统内部有一个缓冲区,保存了最近的ARP信息,可以在cmd下使用命令arp -a来显示目前的缓存,或者使用命令arp -d来清除该缓存(Win7下需要以管理员权限运行cmd)。

        在局域网内,两台机器之间通信,实际上靠的是网卡的物理地址。比如说,本机的IP是192.168.1.80,现在想往另外一台IP为192.168.1.138的机器发送一个ICMP包,或者发起一个TCP连接,操作系统第一步会先获取目标机器的网卡物理地址,获取的步骤是先在ARP缓存里面查找,如果没有找到,则发送ARP请求包(一般是广播方式,询问这个IP的网卡物理地址是多少,目标机器收到请求包,发现请求的目标IP是自己,则反馈一个ARP回复包)。如果目标IP的网卡物理地址获取失败,则无法通信—因为不管TCP、UDP还是ICMP,这些基于以太网的数据包,都有一个以太网帧头。如果没有目标的网卡物理地址,底层就无法填充目的地结构,也不知道发送给哪个网卡了。

        下面我们结合程序来做个实验。先说说如何调用WinPcap发送数据包。WinPcap的发包函数是PacketSendPacket,这里我们封装一个函数来调用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function SendPackets(pBuffer: PAnsiChar ; dwSize: Cardinal ; bFree: Boolean ): Boolean ;
var
   pSendPacket: LPPACKET;
begin
   Result := False ;
 
   pSendPacket := PacketAllocatePacket;
   if pSendPacket = nil then Exit;
 
   //初始化一个_PACKET结构。
   PacketInitPacket(pSendPacket, pBuffer, dwSize);
 
   //发送一个或多个数据报的副本。
   if (PacketSendPacket(g_pAdapter, pSendPacket, 1 ) = 0 ) then
   begin
     PacketFreePacket(pSendPacket);
     Exit;
   end ;
 
   if bFree then FreeMem(pBuffer);
 
   PacketFreePacket(pSendPacket);
 
   Result := True ;
end ;

        我们先把程序里面TAnalysePacketsThread的ProcessUDPPacket(LPUDPPacket(pBuffer));注释掉,只留下ProcessARPPacket函数,用意其实是我们只处理ARP包,暂时不理会其它类型的。然后打开cmd,执行命令arp -d清除缓冲区。然后输入命令”ping 192.168.1.138″,这时候,可以看到我们的程序捕获到两个ARP包:一个是本机发送的ARP请求包,这是一个目标物理地址为FF:FF:FF:FF:FF的广播包,类型是请求包,内容是询问192.168.1.138的物理地址;另外一个就是对方的回复包了。我们再arp -a显示,发现这个IP对应的物理地址已经在缓冲区里面的(不过ICMP包被对方的防火墙拦截了,所以ping没返回)。

winpcap3
winpcap4

然后我们再连接这个IP,这时候,机器不再发送请求包了(因为缓冲区内还有这个记录)。 另外,我们在目标机器用arp -a命令,可以看到该机器也有发送端的物理地址记录了:
winpcap5

        回到我们程序的主界面,细心的朋友可能发现网关物理地址是空的。实际上,我们的电脑如果和外网通信,比如说连接微软的站点,操作系统发送数据包的时候,是如何获取微软主机的网卡物理地址呢?答案是获取不到,也无须获取。操作系统组包的时候,目标物理地址会填写网关的物理地址,然后将数据包发送出去。网关路由器收到数据包后,发现目的IP地址不是自己,就将源物理地址改成自己的,目标物理地址改成下一级的路由的物理地址,然后发送出去。下一级路由则同样重复这个过程,直到数据到底最终目的地。

        我们先来解决这个界面问题—获取网关的物理地址。方法1是从本机的ARP缓存获取(一般地说,只要连接过外网,缓存肯定有对应的记录),类似cmd下的arp -a命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function GetMacAddrInCache(DestIP: DWORD; pMAC: PULONG): Boolean ;
var
   dwSize: Cardinal ;
   dwRet: Cardinal ;
   pIpNetTable: PMIB_IPNETTABLE;
   i: Cardinal ;
begin
   Result := False ;
 
   //从本机ARP表中来查询得到
   dwSize := 0 ;
   dwRet := GetIpNetTable( nil , dwSize, False );
   if (dwRet <> ERROR_INSUFFICIENT_BUFFER) then Exit;
 
   GetMem(pIpNetTable, dwSize);
   if pIpNetTable = nil then Exit;
   dwRet := GetIpNetTable(pIpNetTable, dwSize, False );
   if (dwRet <> ERROR_SUCCESS) then
   begin
     FreeMem(pIpNetTable);
     Exit;
   end ;
 
   for i := 0 to pIpNetTable^.dwNumEntries - 1 do
   begin
     //pIpNetTable^.table[i].dwIndex;      //适配器索引
     //pIpNetTable^.table[i].dwPhysAddrLen;//物理接口长度
     //pIpNetTable^.table[i].bPhysAddr;    //物理地址
     //pIpNetTable^.table[i].dwAddr;       //IP地址
     //pIpNetTable^.table[i].dwType;       //ARP条目类型
     //MIB_IPNET_TYPE_STATIC;
     if (DestIP = pIpNetTable^.table[i].dwAddr) then //列表中找到该IP信息
     begin
       CopyMemory(pMAC, @pIpNetTable^.table[i].bPhysAddr, 6 );
       FreeMem(pIpNetTable);
       Result := True ;
       Exit;
     end ;
   end ;
 
   if pIpNetTable <> nil then
   begin
     FreeMem(pIpNetTable);
     //pIpNetTable := nil;
   end ;
end ;

        方法2就是发送请求包,调用了iphlpapi.dll里面的SendARP函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function GetMACAddrByARP(DestIP: DWORD; pMAC: PULONG): Boolean ;
var
   dwSize: Cardinal ;
   dwRet: Cardinal ;
begin
   Result := False ;
   dwSize := 6 ;
   dwRet := SendARP(DestIP, 0 , pMAC, dwSize);
   if (dwRet = NO_ERROR) and (dwSize <> 0 ) then
   begin
     Result := True ;
     Exit;
   end ;
end ;

        界面的处理过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
procedure TFrmMain . ComboBox_GatewayIPsChange(Sender: TObject);
var
   IP: Cardinal ;
   Mac: TMacAddr;
begin
   Edit_GatewayMac . Clear;
   if ComboBox_GatewayIPs . Text = '' then Exit;
   IP := inet_addr( PAnsiChar (ComboBox_GatewayIPs . Text));
   if not GetMacAddrInCache(IP, @Mac) then
     if not GetMACAddrByARP(IP, @Mac) then Exit;
 
   Edit_GatewayMac . Text := Mac2Str(Mac);
end ;

        我们前面已经介绍了WinPcap的发包函数,那么我们还可以使用第三种方法,自己构造arp请求包,函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function SendARPPacket( const DestMac, SourceMac: TMacAddr; const strDestIP, strSourceIP: AnsiString ; bReQuest: Boolean ): Boolean ;
var
   ARPPacket: TARPPacket;
begin
   //1、填充TEthernetHeader
   CopyMemory(@ARPPacket . EthernetHeader . DestMac, @DestMac, sizeof(TMacAddr));
   CopyMemory(@ARPPacket . EthernetHeader . SourceMac, @SourceMac, sizeof(TMacAddr));
   ARPPacket . EthernetHeader . EthernetType := htons(ETHERTYPE_ARP);
 
   //2、填充TARPHeader
   ARPPacket . ARPHeader . HardwareType := htons(ARP_HARDWARE);
   ARPPacket . ARPHeader . ProtocolType := htons(ETHERTYPE_IP);
   ARPPacket . ARPHeader . HrdAddrlen := 6 ;
   ARPPacket . ARPHeader . ProAddrLen := 4 ;
   if bReQuest then ARPPacket . ARPHeader . Operation := htons(ARP_REQUEST)
   else ARPPacket . ARPHeader . Operation := htons(ARP_REPLY);
 
   CopyMemory(@ARPPacket . ARPHeader . SenderMAC, @SourceMac, sizeof(TMacAddr));
   ARPPacket . ARPHeader . SenderIP := inet_addr( PAnsiChar (strSourceIP));
   CopyMemory(@ARPPacket . ARPHeader . TargetMAC, @DestMac, sizeof(TMacAddr));
   ARPPacket . ARPHeader . TargetIP := inet_addr( PAnsiChar (strDestIP));
 
   Result := SendPackets(@ARPPacket, sizeof(ARPPacket), False );
end ;

        例如,我们需要获取192.168.1.138的物理地址,则这样调用:

1
2
3
4
5
6
7
8
9
10
11
procedure TFrmMain . Button1Click(Sender: TObject);
var
   DestMac, SourceMac: TMacAddr;
   strDestIP, strSourceIP: AnsiString ;
begin
   FillChar(DestMac, sizeof(DestMac), $FF ); //广播包
   SourceMac := Str2Mac(Trim(Edit_OwnMac . Text));
   strSourceIP := Trim(ComboBox_OwnIPs . Text);
   strDestIP := '192.168.1.138' ;
   SendARPPacket(DestMac, SourceMac, strDestIP, strSourceIP, True );
end ;

        然后从程序主界面,或本机的arp -a命令,或目标机器的arp -a命令都可以验证这个过程。现在我们来看看电脑里面这个ARP缓存表是什么时候更新的。实际上,除了上面说的主动请求获取外,只要机器收到ARP包,不管是请求包还是反馈包,它都会添加或更新自己的缓存。修改一下上面的代码,我们广播一条ARP请求包,但是将源IP地址改成一个不存在的IP“192.168.1.111”:

1
2
3
4
5
6
7
8
9
10
11
procedure TFrmMain . Button1Click(Sender: TObject);
var
   DestMac, SourceMac: TMacAddr;
   strDestIP, strSourceIP: AnsiString ;
begin
   FillChar(DestMac, sizeof(DestMac), $FF ); //广播包
   SourceMac := Str2Mac(Trim(Edit_OwnMac . Text));
   strSourceIP := '192.168.1.111' ; //Trim(ComboBox_OwnIPs.Text);
   strDestIP := '192.168.1.138' ;
   SendARPPacket(DestMac, SourceMac, strDestIP, strSourceIP, True );
end ;

        在目标机器输入arp -a,发现的确出现了一条对应的记录。也就是说,如果现在在目标机器连接“192.168.1.111”,那么实际上会连接我们的机器。

winpcap6

        如果我们把源IP改成跟目标IP一样呢?

1
2
3
4
5
6
7
8
9
10
11
procedure TFrmMain . Button1Click(Sender: TObject);
var
   DestMac, SourceMac: TMacAddr;
   strDestIP, strSourceIP: AnsiString ;
begin
   FillChar(DestMac, sizeof(DestMac), $FF ); //广播包
   SourceMac := Str2Mac(Trim(Edit_OwnMac . Text));
   strSourceIP := '192.168.1.138' ; //Trim(ComboBox_OwnIPs.Text);
   strDestIP := '192.168.1.138' ;
   SendARPPacket(DestMac, SourceMac, strDestIP, strSourceIP, True );
end ;

        目标机器会出现如下提示框:

winpcap7
        
如果开一个线程重复这个过程,那台机器就无法上网了。

        最后我们讲述一下ARP欺骗和路由器。因为局域网内通信是依靠物理地址,而只要机器收到请求或反馈包,都会更新自己的缓存,所以可以利用这个特性,进行ARP欺骗。具体做法就是:欺骗目标电脑,告诉它网关的IP对应的物理地址是自己;欺骗网关,告诉它目标IP的物理地址是自己。也就是开一个线程不断的发送ARP请求或反馈包给这两个苦主,然后目标电脑发往外网的数据,都会发送到你的电脑,你的电脑收到数据包后,检查如果目标IP不是自己,说明应该转发,你就把该数据包的源物理地址改成自己的,目标物理地址改成网关的,然后发送出去(注意:源IP地址不要改变);外网返回数据后,将数据发给你的电脑,你的电脑收到数据包后,检查如果目标IP(这时候,明白为什么前面说源IP地址不要改变了吧?)不是自己,说明应该转发,你就把该数据包的源物理地址改成自己的,目标物理地址改成目标电脑的,然后发送出去。

        实际上,这个就是路由器的实现,只不过它是把这个代码写进了芯片里面。另外它不用ARP欺骗,而是你自己将网络连接的网关IP设置成它的。你也可以按照上面的流程,去掉ARP欺骗的过程,仅保留数据转发,然后将局域网另外一台机器的网关设置为你的机器,那么它就需要通过你来上网了。因为现在ARP欺骗不像2003年,到处都是代码,这里就不给出具体的实现,留作大家的课后作业吧。

        下一节我们将讲述UDP协议部分,进入初步的数据修改阶段。我们先实现一个应用层的DNS客户端和服务端,再来在驱动层玩玩。

附件下载:
本节代码

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!