做过网管或协议分析的人一般都熟悉sniffer这个工具,它可以捕捉流经本地网卡的所有数据包。抓取网络数据包进行分析有很多用处,如分析网络是否有网络病毒等异常数据,通信协议的分析(数据链路层协议、IP、UDP、TCP、甚至各种应用层协议),敏感数据的捕捉等。下面我们就来看看在windows下如何实现数据包的捕获。
WINSOCK本身就提供了抓取流经网卡的所有数据包的函数,虽然只能在IP协议层上捕捉,但只要您的工作没有涉及到数据链路层的话,这也就足够用了。抓取数据包的编程方法基本和编写其它网络应用程序一样,只需多一个步骤,即将SOCKET设置为接收所有数据的模式,这是用WSAIoctl来实现的。
编程实现主要有以下几个步骤:
1. 初始化WINSOCK库;
2. 创建SOCKET句柄;
3. 绑定SOCKET句柄到一个本地地址;
4. 设置该SOCKET为接收所有数据的模式;
5. 接收数据包;
6. 关闭SOCKET句柄,清理WINSOCK库;
除第4个步骤外,其它的步骤都和编写其它网络应用程序一样。那我们就主要来看一下第4个步骤WSAIoctl。
WSAIoctl是定义在mstcpip.h里面的。系统SDK里面本身就有。函数原型如下:
int WSAIoctl(
SOCKET s,
DWORD dwIoControlCode,
LPVOID lpvInBuffer,
DWORD cbInBuffer,
LPVOID lpvOutBuffer,
DWORD cbOutBuffer,
LPDWORD lpcbBytesReturned,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
参数很多,但我们只需用到5个。s是步骤2创建的句柄;dwIoControlCode设为SIO_RCVALL;lpvInBuffer和cbInBuffer是输入参数,SIO_RCVALL的输入参数是u_long,给它一个1的值就可以了;lpcbBytesReturned是返回字节数,给一个DWORD变量的地址;其它的都没用,传0就可以了;
u_long sioarg
=
1
;
DWORD wt
=
0
;
WSAIoctl( h, SIO_RCVALL ,
&
sioarg,
sizeof
(sioarg),NULL,
0
,
&
wt,NULL,NULL ) ;
下面我们就来看一下完整的代码:
#include
<
winsock2.h
>
#include
<
windows.h
>
#include
<
Mstcpip.h
>

#pragma
comment(lib,"Ws2_32.lib")

#include
<
iostream
>
using
namespace
std;

//
IP首部
typedef
struct
tIPPackHead
{
enum PROTOCOL_TYPE{
PROTOCOL_TCP = 6,
PROTOCOL_UDP = 17,
PROTOCOL_ICMP = 1,
PROTOCOL_IGMP = 2
};
inline unsigned HeadLen() const
{
//首部长度单位为4bytes。因此乘4
return (ver_hlen & 0x0F) << 2;
}
inline unsigned PackLen() const
{
return wPacketLen;
}
BYTE ver_hlen; //IP协议版本和IP首部长度。高4位为版本,低4位为首部的长度(单位为4bytes)
BYTE byTOS; //服务类型
WORD wPacketLen; //IP包总长度。包括首部,单位为byte。[Big endian]
WORD wSequence; //序号,一般每个IP包的序号递增。[Big endian]
WORD wMarkFragPoi;
BYTE byTTL; //生存时间
BYTE byProtocolType; //协议类型,见PROTOCOL_TYPE定义
WORD wHeadCheckSum; //IP首部校验和[Big endian]
DWORD dwIPSrc; //源地址
DWORD dwIPDes; //目的地址
}
IP_PK_HEAD;


int
DecodeIP(
char
*
buf,
int
len);

int
DecodeIP(
char
*
buf,
int
len)
{
int n = len;

if( n >= sizeof(IP_PK_HEAD) )
{
IP_PK_HEAD iphead;
memcpy( &iphead, buf, sizeof(iphead) );

//以下三个为Big Endian字节顺序,转换成主机字节顺序
iphead.wPacketLen = ntohs( iphead.wPacketLen );
iphead.wSequence = ntohs( iphead.wSequence );
iphead.wHeadCheckSum = ntohs( iphead.wHeadCheckSum );

in_addr src,dst;
src.S_un.S_addr = iphead.dwIPSrc;
dst.S_un.S_addr = iphead.dwIPDes;

char strsrc[20],strdst[20];
strcpy(strsrc, inet_ntoa(src) );
strcpy( strdst , inet_ntoa(dst));

printf( "IP数据包: ver=%d,hlen=%d,protocol=%d,pklen=%d,seq=%d,src=%s,dst=%s _fcksavedurl="%s,dst=%s" ",
iphead.ver_hlen >> 4,
(iphead.ver_hlen & 0x0F) << 2,
iphead.byProtocolType,
iphead.wPacketLen,
iphead.wSequence,
strsrc,
strdst );
}

return 0;
}

void
AutoWSACleanup()
{
::WSACleanup();
}

int
_tmain(
int
argc, _TCHAR
*
argv[])
{
//初始化winsock库,使用2.2版本
u_short wVersionRequested = 0x0202;
WSADATA wsaData;
if( SOCKET_ERROR == WSAStartup( wVersionRequested, &wsaData ) )
{
cout << WSAGetLastError();
return 0;
}
atexit( AutoWSACleanup );

//创建SOCKET
SOCKET h = socket( AF_INET, SOCK_RAW, IPPROTO_IP);
if( h == INVALID_SOCKET )
{
cout << WSAGetLastError();
return 0;
}

//获取本机地址
char FAR name[128];
if( -1 == gethostname(name, sizeof(name)) )
{
closesocket( h );
cout << WSAGetLastError();
return 0;
}

struct hostent FAR * pHostent;
pHostent = gethostbyname(name);

//绑定本地地址到SOCKET句柄
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr = *(in_addr*)pHostent->h_addr; //IP
addr.sin_port = 0; //端口,IP层端口可随意填
if( SOCKET_ERROR == bind( h,(sockaddr *)&addr,sizeof(addr) ) )
{
closesocket( h );
cout << WSAGetLastError();
return 0;
}

//设置该SOCKET为接收所有流经绑定的IP的网卡的所有数据,包括接收和发送的数据包
//该函数在mstcpip.h里面,详见MSDN帮助
u_long sioarg = 1;
DWORD wt=0;
if( SOCKET_ERROR == WSAIoctl( h, SIO_RCVALL , &sioarg,sizeof(sioarg),NULL,0,&wt,NULL,NULL ) )
{
closesocket( h );
cout << WSAGetLastError();
return 0;
}

//我们只需要接收数据,因此设置为阻塞IO,使用最简单的IO模型
u_long bioarg = 0;
if( SOCKET_ERROR == ioctlsocket( h, FIONBIO , &bioarg ) )
{
closesocket( h );
cout << WSAGetLastError();
return 0;
}

//开始接收数据
//因为前面已经设置为阻塞IO,recv在接收到数据前不会返回。
//当返回<=0时表示接收失败,退出循环
//可以在另一个线程执行此循环,主线程closesocket可以使recv失败而结束循环
char buf[102400];
int len = 0;
do
{
len = recv( h, buf, sizeof(buf),0);
if( len > 0 )
{
DecodeIP( buf, len );
}
}while( len > 0 );

closesocket( h );

return 0;
}

绑定地址时,必须获取到本机的一个真实IP地址,不能用0.0.0.0或127.0.0.0.1。端口就没有限制了。接收数据的方法和基于TCP或UDP的接收是一样的。也可以用非阻塞IO,并采用select、WSAAsyncSelect、WSAEventSelect等IO模型来处理。但我们要做的只是简单地接收数据而已,不需要任何交互,实在没有必要给自己找麻烦。这里我用的是阻塞IO,即在接收到数据前recv不会返回,一旦返回了,要不就是接收到数据,要不就是接收失败。当接收失败时循环就退出了。不过因为是阻塞IO的关系,主线程就无法进行其它操作,最好把接收数据的循环放到第二线程内。主线程通过closesocket就可以使接收失败从而结束接收数据的循环了。
通过这种方法接收到的数据是个IP层的数据包,每个数据包的开始是至少20个字节的IP首部,IP首部定义见tIPPackHead结构体,关于IP首部的更详细的资料可以参考《TCP-IP详解卷1:协议》第3章,在此就不多作描述。DecodeIP是对IP首部的解析,IP首部后面的数据则需要根据首部里面的协议类型进行各自不同处理。如果是TCP协议则按照TCP协议解析,UDP则按UDP协议解析。下面附上TCP和UDP首部的定义,更详细的信息请参考《TCP-IP详解卷1:协议》第11和17章。