作者:
CSDN VC/MFC 网络编程版主 PiggyXP
一.捕获数据包的实现原理:--------------------------------------------------------------------
在通常情况下,网络通信的套接字程序只能响应与自己硬件地址相匹配的或是以广播形式发出的数据帧,对于其他形式的数据帧比如已到达网络接口但却不是发给此地址的数据帧,网络接口在验证投递地址并非自身地址之后将不引起响应,也就是说应用程序无法收取与自己无关的的数据包。
所以我们要想实现截获流经网络设备的所有数据包,就要采取一点特别的手段了:
将网卡设置为混杂模式。
这样一来,该主机的网卡就可以捕获到所有流经其网卡的数据包和帧。
但是要注意一点,这种截获仅仅是数据包的一份拷贝,而不能对其进行截断,要想截断网络流量就要采用一些更底层的办法了,不在本文的讨论范围之内。
二. 捕获数据包的编程实现:
1.raw socket的实现方法--------------------------------------------------------------------
不同于我们常用的数据流套接字和数据报套接字,在创建了原始套接字后,需要用WSAIoctl()函数来设置一下,它的定义是这样的
int WSAIoctl(
SOCKET s,
DWORD dwIoControlCode,
LPVOID lpvInBuffer,
DWORD cbInBuffer,
LPVOID lpvOutBuffer,
DWORD cbOutBuffer,
LPDWORD lpcbBytesReturned,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
虽然咋一看参数比较多,但是其实我们最关心的只是其中的第二项而已,我们需要做的就是把第二项设置为SIO_RCVALL,讲了这么多其实要做的就是这么一行代码,很简单吧?^_^
当然我们还可以指定是否亲自处理IP头,但是这并不是必须的。
完整的代码类似与如下这样,加粗的代码是与平常不同的需要注意的地方:
( 为了让代码一目了然,我把错误处理去掉了,下同)
#include “WinSock2.h”
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
SOCKET SnifferSocket
WSADATA wsaData;
iFlag=WSAStartup(MAKEWORD(2,2),&wsaData); //开启winsock.dll
SnifferSocket=WSASocket(AF_INET, //创建raw socket
SOCK_RAW,IPPROTO_IP,NULL,0,WSA_FLAG_OVERLAPPED);
char FAR name[128]; //获取本机IP地址
gethostname(name, sizeof(name));
struct hostent FAR * pHostent;
pHostent = gethostbyname(name);
SOCKADDR_IN sa; //填充SOCKADDR_IN结构的内容
sa.sin_family = AF_INET;
sa.sin_port = htons(6000); // 端口号可以随便改,当然与当然系统不能冲突
memcpy(&(sa.sin_addr),pHostent->h_addr,pHostent->h_length);
bind(SnifferSocket,(LPSOCKADDR)&sa,sizeof(sa)); //绑定
// 置ioctl来接收所有网络数据,关键步骤
DWORD dwBufferLen[10] ;
DWORD dwBufferInLen = 1 ;
DWORD dwBytesReturned = 0 ;
WSAIoctl(SnifferSocket, IO_RCVALL,&dwBufferInLen, izeof(dwBufferInLen),
&dwBufferLen, sizeof(dwBufferLen),&dwBytesReturned , NULL , NULL );
至此,实际就可以开始对网络数据包进行嗅探了,而对于数据包的接收还是和普通的socket一样,通过recv()函数来完成,因为这里涉及到不同的socket模型,接收方法差别很大,所以在此就不提供接收的代码了。
2.winpcap的实现方法:-----------------------------------------------------------------------
winpcap驱动包,是我们玩转数据包不可或缺的好东东,winpcap的主要功能在于独立于主机协议(如TCP-IP)而发送和接收原始数据报,主要为我们提供了四大功能:
功能:
1> 捕获原始数据报,包括在共享网络上各主机发送/接收的以及相互之间交换的数据报;
2> 在数据报发往应用程序之前,按照自定义的规则将某些特殊的数据报过滤掉;
3> 在网络上发送原始的数据报;
4> 收集网络通信过程中的统计信息
如果环境允许的话(比如你做的不是木马程序),我还是推荐大家用winpcap来截获数据包,因为它的功能更强大,工作效率更高,唯一的缺点就是在运行用winpcap开发的程序以前,都要在主机上先安装winpcap的driver。
而且一会我们就会发现它比raw socket功能强大的多,而且工作得更为底层,最明显的理由就是raw socket捕获的数据包是没有以太头的,此乃后话。
至于怎么来安装使用,请参考本系列的系列一《手把手教你玩转ARP包中的》,里面有详细的加载winpcap驱动的方法^_^
废话不多说了,让我们转入正题, 具体用winpcap来截获数据包需要做如下的一些工作:
A . 枚举本机网卡的信息(主要是获得网卡的名称)
其中要用到pcap_findalldevs函数,它是这样定义的
/*************************************************
int pcap_findalldevs ( pcap_if_t ** alldevsp,
char * errbuf
)
功能:
枚举系统所有网络设备的信息
参数: alldevsp: 是一个pcap_if_t结构体的指针,如果函数pcap_findalldevs函数执行成功,将获得一个可用网卡的列表,而里面存储的就是第一个元素的指针。
Errbuf: 存储错误信息的字符串
返回值: int : 如果返回0 则执行成功,错误返回 -1。
*************************************************/
我们利用这个函数来获得网卡名字的完整代码如下:
pcap_if_t* alldevs;
pcap_if_t* d;
char errbuf[PCAP_ERRBUF_SIZE];
pcap_findalldevs(&alldevs,errbuf); // 获得网络设备指针
for(d=alldevs;d;d=d->next) // 枚举网卡然后添加到ComboBox中
{
d->name; // d->name就是我们需要的网卡名字字符串,按照你// 自己的需要保存到你的相应变量中去
}
pcap_freealldevs(alldevs); // 释放alldev资源
B. 打开相应网卡并设置为混杂模式:
在此之前肯定要有一段让用户选择网卡、并获得用户选择的网卡的名字的代码,既然上面已经可以获得所有网卡的名字了,这段代码就暂且略过了。 我们主要是要用到 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头等信息头的最原始的数据信息,这些信息保留了它在网络传输时的原貌。通过对这些在低层传输的原始信息的分析可以得到有关网络的一些信息。由于这些数据经过了网络层和传输层的打包,因此需要根据其附加的帧头对数据包进行分析。
呵呵,所以我们要走的路还很长,这只是刚刚入门而已^_^
CSDN VC/MFC 网络编程版主 PiggyXP
一.捕获数据包的实现原理:--------------------------------------------------------------------
在通常情况下,网络通信的套接字程序只能响应与自己硬件地址相匹配的或是以广播形式发出的数据帧,对于其他形式的数据帧比如已到达网络接口但却不是发给此地址的数据帧,网络接口在验证投递地址并非自身地址之后将不引起响应,也就是说应用程序无法收取与自己无关的的数据包。
所以我们要想实现截获流经网络设备的所有数据包,就要采取一点特别的手段了:
将网卡设置为混杂模式。
这样一来,该主机的网卡就可以捕获到所有流经其网卡的数据包和帧。
但是要注意一点,这种截获仅仅是数据包的一份拷贝,而不能对其进行截断,要想截断网络流量就要采用一些更底层的办法了,不在本文的讨论范围之内。
二. 捕获数据包的编程实现:
1.raw socket的实现方法--------------------------------------------------------------------
不同于我们常用的数据流套接字和数据报套接字,在创建了原始套接字后,需要用WSAIoctl()函数来设置一下,它的定义是这样的
int WSAIoctl(
SOCKET s,
DWORD dwIoControlCode,
LPVOID lpvInBuffer,
DWORD cbInBuffer,
LPVOID lpvOutBuffer,
DWORD cbOutBuffer,
LPDWORD lpcbBytesReturned,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
虽然咋一看参数比较多,但是其实我们最关心的只是其中的第二项而已,我们需要做的就是把第二项设置为SIO_RCVALL,讲了这么多其实要做的就是这么一行代码,很简单吧?^_^
当然我们还可以指定是否亲自处理IP头,但是这并不是必须的。
完整的代码类似与如下这样,加粗的代码是与平常不同的需要注意的地方:
( 为了让代码一目了然,我把错误处理去掉了,下同)
#include “WinSock2.h”
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
SOCKET SnifferSocket
WSADATA wsaData;
iFlag=WSAStartup(MAKEWORD(2,2),&wsaData); //开启winsock.dll
SnifferSocket=WSASocket(AF_INET, //创建raw socket
SOCK_RAW,IPPROTO_IP,NULL,0,WSA_FLAG_OVERLAPPED);
char FAR name[128]; //获取本机IP地址
gethostname(name, sizeof(name));
struct hostent FAR * pHostent;
pHostent = gethostbyname(name);
SOCKADDR_IN sa; //填充SOCKADDR_IN结构的内容
sa.sin_family = AF_INET;
sa.sin_port = htons(6000); // 端口号可以随便改,当然与当然系统不能冲突
memcpy(&(sa.sin_addr),pHostent->h_addr,pHostent->h_length);
bind(SnifferSocket,(LPSOCKADDR)&sa,sizeof(sa)); //绑定
// 置ioctl来接收所有网络数据,关键步骤
DWORD dwBufferLen[10] ;
DWORD dwBufferInLen = 1 ;
DWORD dwBytesReturned = 0 ;
WSAIoctl(SnifferSocket, IO_RCVALL,&dwBufferInLen, izeof(dwBufferInLen),
&dwBufferLen, sizeof(dwBufferLen),&dwBytesReturned , NULL , NULL );
至此,实际就可以开始对网络数据包进行嗅探了,而对于数据包的接收还是和普通的socket一样,通过recv()函数来完成,因为这里涉及到不同的socket模型,接收方法差别很大,所以在此就不提供接收的代码了。
2.winpcap的实现方法:-----------------------------------------------------------------------
winpcap驱动包,是我们玩转数据包不可或缺的好东东,winpcap的主要功能在于独立于主机协议(如TCP-IP)而发送和接收原始数据报,主要为我们提供了四大功能:
功能:
1> 捕获原始数据报,包括在共享网络上各主机发送/接收的以及相互之间交换的数据报;
2> 在数据报发往应用程序之前,按照自定义的规则将某些特殊的数据报过滤掉;
3> 在网络上发送原始的数据报;
4> 收集网络通信过程中的统计信息
如果环境允许的话(比如你做的不是木马程序),我还是推荐大家用winpcap来截获数据包,因为它的功能更强大,工作效率更高,唯一的缺点就是在运行用winpcap开发的程序以前,都要在主机上先安装winpcap的driver。
而且一会我们就会发现它比raw socket功能强大的多,而且工作得更为底层,最明显的理由就是raw socket捕获的数据包是没有以太头的,此乃后话。
至于怎么来安装使用,请参考本系列的系列一《手把手教你玩转ARP包中的》,里面有详细的加载winpcap驱动的方法^_^
废话不多说了,让我们转入正题, 具体用winpcap来截获数据包需要做如下的一些工作:
A . 枚举本机网卡的信息(主要是获得网卡的名称)
其中要用到pcap_findalldevs函数,它是这样定义的
/*************************************************
int pcap_findalldevs ( pcap_if_t ** alldevsp,
char * errbuf
)
功能:
枚举系统所有网络设备的信息
参数: alldevsp: 是一个pcap_if_t结构体的指针,如果函数pcap_findalldevs函数执行成功,将获得一个可用网卡的列表,而里面存储的就是第一个元素的指针。
Errbuf: 存储错误信息的字符串
返回值: int : 如果返回0 则执行成功,错误返回 -1。
*************************************************/
我们利用这个函数来获得网卡名字的完整代码如下:
pcap_if_t* alldevs;
pcap_if_t* d;
char errbuf[PCAP_ERRBUF_SIZE];
pcap_findalldevs(&alldevs,errbuf); // 获得网络设备指针
for(d=alldevs;d;d=d->next) // 枚举网卡然后添加到ComboBox中
{
d->name; // d->name就是我们需要的网卡名字字符串,按照你// 自己的需要保存到你的相应变量中去
}
pcap_freealldevs(alldevs); // 释放alldev资源
B. 打开相应网卡并设置为混杂模式:
在此之前肯定要有一段让用户选择网卡、并获得用户选择的网卡的名字的代码,既然上面已经可以获得所有网卡的名字了,这段代码就暂且略过了。 我们主要是要用到 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头等信息头的最原始的数据信息,这些信息保留了它在网络传输时的原貌。通过对这些在低层传输的原始信息的分析可以得到有关网络的一些信息。由于这些数据经过了网络层和传输层的打包,因此需要根据其附加的帧头对数据包进行分析。
呵呵,所以我们要走的路还很长,这只是刚刚入门而已^_^