IP层网络数据抓包实现方法

    做过网管或协议分析的人一般都熟悉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章。


// UDP首部
typedef  struct  tUDPPackHead
{
     inline unsigned HeadLen() 
const
    
{
        
return 8;
    }

    inline unsigned PackLen() 
const
    
{
        
return wLength;
    }

    WORD wSPort;     
/*Source Port*/
     WORD wDPort;     
/*Destinate Port*/
     WORD wLength;    
/*UDP Length*/
     WORD wCheckSum;  
/*UDP CheckSum*/
}
 UDP_PK_HEAD;

// TCP首部
typedef  struct  tTCPPackHead  

     inline unsigned HeadLen() 
const
    
{
        
return ((lenres>>4& 0x0F<< 2;
    }


    WORD wSrcPort; 
//16位源端口 
    WORD wDstPort; //16位目的端口 
    DWORD dwSeq; //32位序列号 
    DWORD dwAck; //32位确认号 
    unsigned char lenres; //4位首部长度/6位保留字 (高4bit为head len)
    unsigned char flag; //6位标志位 (低6bit为标志)
    WORD wWin; //16位窗口大小 
    WORD wSum; //16位校验和 
    WORD wUrp; //16位紧急数据偏移量 
}
TCP_PK_HEAD; 


你可能感兴趣的:(网络编程)