C++ 捕获与解析IP数据包 (winpcap)

源代码链接:https://download.csdn.net/download/chd_lcj/10891420

(不知道为什么积分涨到4积分了。。。原先只是单纯的想分享下的,结果最低只能设置1积分)

 

//网络编程学习经验记录贴

winpcap简介

WinPcap 是由伯克利分组捕获库派生而来的分组捕获库,它是在Windows 操作平台上来实现对底层包的截取过滤。

WinPcap 为用户级的数据包提供了Windows 下的一个平台。

WinPcap 是 BPF 模型和 Libpcap 函数库在 Windows 平台下网络数据包捕获和网络状态分析的一种体系结构,这个体系结构是由一个核心的包过滤驱动程序,一个底层的动态连接库 Packet.dll 和一个高层的独立于系统的函数库 Libpcap 组成。底层的包捕获驱动程序实际为一个协议网络驱动程序,通过对 NDIS 中函数的调用为 Win95、Win98、WinNT、和 Win2000 提供一类似于 UNIX 系统下 Berkeley Packet Filter 的捕获和发送原始数据包的能力。Packet.dll 是对这个 BPF 驱动程序进行访问的 API 接口,同时它有一套符合 Libpcap 接口(UNIX 下的捕获函数库)的函数库。WinPcap的结构图如图1。

WinPcap 包括三个部分:

第一个模块NPF(Netgroup Packet Filter),是一个虚拟设备驱动程序文件。它的功能是过滤数据包,并把这些数据包原封不动地传给用户态模块,这个过程中包括了一些操作系统特有的代码。

第二个模块packet.dll为win32平台提供了一个公共的接口。不同版本的Windows系统都有自己的内核模块和用户层模块。Packet.dll用于解决这些不同。调用Packet.dll的程序可以运行在不同版本的Windows平台上,而无需重新编译。

第三个模块 Wpcap.dll是不依赖于操作系统的。它提供了更加高层、抽象的函数。

packet.dll和Wpcap.dll:packet.dll直接映射了内核的调用。 Wpcap.dll提供了更加友好、功能更加强大的函数调用。WinPcap的优势提供了一套标准的抓包接口,与libpcap兼容,可使得原来许多UNIX平台下的网络分析工具快速移植过来便于开发各种网络分析工具,充分考虑了各种性能和效率的优化,包括对于NPF内核层次上的过滤器支持,支持内核态的统计模式,提供了发送数据包的能力。

利用winpcap进行网络数据包的捕获和过滤的设计步骤
1)打开网卡,并设为混杂模式。

2)回调函数 Network Tap 在得到监听命令后,从网络设备驱动程序处收集数据包把监听到的数据包负责传送给过滤程序。

3)当 Packet filter 监听到有数据包到达时,NDIS 中间驱动程序首先调用分组驱动程序,该程序将数据传递给每一个参与进程的分组过滤程序。

4)然后由 Packet filter 过滤程序决定哪些数据包应该丢弃,哪些数据包应该接收,是否需要将接收到的数据拷贝到相应的应用程序。

5)通过分组过滤器后,将数据未过滤掉的数据包提交给核心缓冲区。然后等待系统缓冲区满后,再将数据包拷贝到用户缓冲区。监听程序可以直接从用户缓冲区中读取捕获的数据包。

6)关闭网卡。

网络数据包捕获的原理

以太网(Ethernet)具有共享介质的特征,信息是以明文的形式在网络上传输,当网络适配器设置为监听模式(混杂模式,Promiscuous)时,由于采用以太网广播信道争用的方式,使得监听系统与正常通信的网络能够并联连接,并可以捕获任何一个在同一冲突域上传输的数据包。IEEE802.3 标准的以太网采用的是持续 CSMA 的方式,正是由于以太网采用这种广播信道争用的方式,使得各个站点可以获得其他站点发送的数据。运用这一原理使信息捕获系统能够拦截的我们所要的信息,这是捕获数据包的物理基础。
以太网是一种总线型的网络,从逻辑上来看是由一条总线和多个连接在总线上的站点所组成各个站点采用上面提到的 CSMA/CD 协议进行信道的争用和共享。每个站点(这里特指计算机通过的接口卡)网卡来实现这种功能。网卡主要的工作是完成对于总线当前状态的探测,确定是否进行数据的传送,判断每个物理数据帧目的地是否为本站地址,如果不匹配,则说明不是发送到本站的而将它丢弃。如果是的话,接收该数据帧,进行物理数据帧的 CRC 校验,然后将数据帧提交给LLC 子层。

网卡具有如下的几种工作模式:
1)   广播模式(Broad Cast Model):它的物理地址(MAC)地址是 0Xffffff 的帧为广播帧,工作在广播模式的网卡接收广播帧。
2)多播传送(MultiCast Model):多播传送地址作为目的物理地址的帧可以被组内的其它主机同时接收,而组外主机却接收不到。但是,如果将网卡设置为多播传送模式,它可以接收所有的多播传送帧,而不论它是不是组内成员。
3)直接模式(Direct Model):工作在直接模式下的网卡只接收目地址是自己 Mac地址的帧。
4)混杂模式(Promiscuous Model):工作在混杂模式下的网卡接收所有的流过网卡的帧,信包捕获程序就是在这种模式下运行的。
网卡的缺省工作模式包含广播模式和直接模式,即它只接收广播帧和发给自己的帧。如果采用混杂模式,一个站点的网卡将接受同一网络内所有站点所发送的数据包这样就可以到达对于网络信息监视捕获的目的。

IP数据包头字段说明

C++ 捕获与解析IP数据包 (winpcap)_第1张图片

  1. 版本号(Version):长度4比特。标识目前采用的IP协议的版本号。一般的值为0100(IPv4),0110(IPv6) 
  2. IP包头长度(HeaderLength):长度4比特。这个字段的作用是为了描述IP包头的长度,因为在IP包头中有变长的可选部分。该部分占4个bit位,单位为32bit(4个字节),即本区域值= IP头部长度(单位为bit)/(8*4),因此,一个IP包头的长度最长为“1111”,即15*4=60个字节。IP包头最小长度为20字节。 
  3. 服务类型(Type of Service):长度8比特。8位 按位被如下定义 PPP D T R C 0  但是TOS字段已经作为区分服务(Diffsrv)架构一部分被重新定义了,开始的6位构成区分服务代码点(DiffServ Code Piont,DSCP),利用这6位可以定义64个不同的服务类别。 
  4. ECN显式拥塞通知 
  5. IP包总长(Total Length):长度16比特。 以字节为单位计算的IP包的长度 (包括头部和数据),所以IP包最大长度65535字节。 
  6. 标识符(Identifier)(数据报ID):长度16比特。该字段和Flags和Fragment Offest字段联合使用,对大的上层数据包进行分段(fragment)操作。路由器将一个包拆分后,所有拆分开的小包被标记相同的值,以便目的端设备能够区分哪个包属于被拆分开的包的一部分。 
  7. 标记(Flags):长度3比特。该字段第一位不使用。第二位是DF(Don't Fragment)位,DF位设为1时表明路由器不能对该上层数据包分段。如果一个上层数据包无法在不分段的情况下进行转发,则路由器会丢弃该上层数据包并返回一个错误信息。第三位是MF(More Fragments)位,当路由器对一个上层数据包分段,则路由器会在除了最后一个分段的IP包的包头中将MF位设为1。 
  8. 片偏移(Fragment Offset):长度13比特。表示该IP包在该组分片包中位置,接收端靠此来组装还原IP包。 
  9. 生存时间(TTL):长度8比特。当IP包进行传送时,先会对该字段赋予某个特定的值。当IP包经过每一个沿途的路由器的时候,每个沿途的路由器会将IP包的TTL值减少1。如果TTL减少为0,则该IP包会被丢弃。这个字段可以防止由于路由环路而导致IP包在网络中不停被转发。 
  10. 协议(Protocol):长度8比特。标识了上层所使用的协议。 
  11. 以下是比较常用的协议号: 
    ICMP   IGMP   
    6      TCP  
    17     UDP  
    88     IGRP   
    89     OSPF 

     

  12. 头部校验(Header Checksum):长度16位。用来做IP头部的正确性检测,但不包含数据部分。 因为每个路由器要改变TTL的值,所以路由器会为每个通过的数据包重新计算这个值。 
  13. 起源和目标地址(Source and Destination Addresses):这两个地段都是32比特。标识了这个IP包的起源和目标地址。要注意除非使用NAT,否则整个传输的过程中,这两个地址不会改变。 
  14. 可选项(Options):这是一个可变长的字段。该字段属于可选项,主要用于测试,由起源设备根据需要改写。可选项目包含以下内容: 
  15. 松散源路由(Loose source routing):给出一连串路由器接口的IP地址。IP包必须沿着这些IP地址传送,但是允许在相继的两个IP地址之间跳过多个路由器。 
  16. 严格源路由(Strict source routing):给出一连串路由器接口的IP地址。IP包必须沿着这些IP地址传送,如果下一跳不在IP地址表中则表示发生错误。 
  17. 路由记录(Record route):当IP包离开每个路由器的时候记录路由器的出站接口的IP地址。 
  18. 时间戳(Timestamps):当IP包离开每个路由器的时候记录时间。 
  19. 填充(Padding):因为IP包头长度(Header Length)部分的单位为32bit,所以IP包头的长度必须为32bit的整数倍。因此,在可选项后面,IP协议会填充若干个0,以达到32bit的整数倍。

逻辑地址和物理地址:

C++ 捕获与解析IP数据包 (winpcap)_第2张图片

(1)逻辑地址:(工作在网络层,网络级)也称为IP地址,具有特征 ① 全局唯一性;② 使用软件来实现网络中地址管理;③ 占32位,4字节;

(2) 物理地址:也称为硬件地址、链路地址或MAC地址,(工作在网络接口层)具有特征:① 本地范围唯一性;② 使用硬件实现(路由器、计算机有设置MAC地址的位置);③ 占48位,12字节,16进制表示!例如:74-E5-0B-35-60-16 :0111  0100-1110  0101-0000  1011-0011  0101-0110  0000-0001  0110。

主要案例功能实现:

运行程序,按提示输入要选择的网卡序列,再次输入需要不活的IP数据包的个数,然后程序自动运行捕获。
捕获后开始解析,从数据链路层开始解析,
 
1)	如果网络层协议是IP协议,则开始解析网络层IP数据包。
2)	如果运输层协议是TCP协议则解析运输层TCP数据包。
3)	如果网络层协议是APP协议,则不在进一步解析网络层数据包。
4)	如果运输层协议是UDP协议,则不在进一步解析运输层数据包。

代码如下:

#include 
#include
#include "pcap.h"
#include "stdio.h"
#include
#include 
#include   //文件的输入输出;
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"wpcap.lib")
using namespace std;

/*下边是以太网的协议格式 */
struct ethernet_header
 { 
	u_int8_t ether_dhost[6];  /*目的以太地址*/
	u_int8_t ether_shost[6];  /*源以太网地址*/
	u_int16_t ether_type;      /*以太网类型*/
 };

/*ip地址格式*/
typedef u_int32_t in_addr_t;

struct ip_header
 {
#ifdef WORKS_BIGENDIAN
  u_int8_t ip_version:4,    /*version:4*/
       ip_header_length:4; /*IP协议首部长度Header Length*/
#else
 u_int8_t ip_header_length:4,
       ip_version:4;
#endif
 u_int8_t ip_tos;         /*服务类型Differentiated Services  Field*/
 u_int16_t ip_length;  /*总长度Total Length*/
 u_int16_t ip_id;         /*标识identification*/
 u_int16_t ip_off;        /*片偏移*/
 u_int8_t ip_ttl;            /*生存时间Time To Live*/
 u_int8_t ip_protocol;        /*协议类型(TCP或者UDP协议)*/
 u_int16_t ip_checksum;  /*首部检验和*/
        struct in_addr  ip_source_address; /*源IP*/
        struct in_addr  ip_destination_address; /*目的IP*/
 };

/*关于tcp头部的定义*/
struct tcp_header
 { 
  u_int16_t tcp_source_port;		  //源端口号
  
  u_int16_t tcp_destination_port;	//目的端口号
  
  u_int32_t tcp_acknowledgement;	//序号
  
  u_int32_t tcp_ack;	//确认号字段
 #ifdef WORDS_BIGENDIAN
  u_int8_t tcp_offset:4 ,
     tcp_reserved:4;
 #else
  u_int8_t tcp_reserved:4,
     tcp_offset:4;
 #endif
  u_int8_t tcp_flags;
  u_int16_t tcp_windows;	//窗口字段
  u_int16_t tcp_checksum;	//检验和
  u_int16_t tcp_urgent_pointer;	//紧急指针字段
};



/*下边实现tcp数据包分析的函数定义tcp_protocol_packet_callback*/
void tcp_protocol_packet_callback(u_char *argument,const struct pcap_pkthdr*
packet_header,const u_char* packet_content)
{
     struct tcp_header *tcp_protocol ;     /*tcp协议变量*/
     u_char flags;                          /*标记*/
     int header_length;                  /*头长度*/
     u_short source_port;           /*源端口*/
     u_short destination_port;   /*目的端口*/
     u_short windows;                /*窗口大小*/
     u_short urgent_pointer;     /*紧急指针*/
     u_int sequence;                 /*序列号*/
     u_int acknowledgement;   /*确认号*/
     u_int16_t   checksum;       /*检验和*/
     tcp_protocol=(struct tcp_header *) (packet_content+14+20);  /*获得tcp首部内容*/
     source_port =ntohs(tcp_protocol->tcp_source_port);                  /*获得源端口号*/
     destination_port =ntohs(tcp_protocol->tcp_destination_port); /*获得目的端口号*/
     header_length =tcp_protocol->tcp_offset *4;                            /*获得首部长度*/
     sequence =ntohl(tcp_protocol->tcp_acknowledgement);        /*获得序列号*/
     acknowledgement =ntohl(tcp_protocol->tcp_ack);
     windows = ntohs(tcp_protocol->tcp_windows);
     urgent_pointer = ntohs(tcp_protocol->tcp_urgent_pointer);
     flags = tcp_protocol->tcp_flags;
     checksum =ntohs (tcp_protocol->tcp_checksum);
     printf("\n==========    运输层(TCP协议)    ==========\n");
     printf("源端口:\t %d\n",source_port);
     printf("目的端口:\t %d\n",destination_port);

     int min= ( destination_port tcp_reserved);
 printf("控制位:");
 if (flags & 0x08)  printf("\t【推送 PSH】");
 if (flags & 0x10)  printf("\t【确认 ACK】 ");
 if (flags & 0x02)  printf("\t【同步 SYN】");
 if (flags & 0x20)  printf("\t【紧急 URG】");
 if (flags & 0x01)  printf("\t【终止 FIN】");
 if (flags & 0x04)  printf("\t【复位 RST】");

 printf("\n");
 printf("窗口大小 :\t%d \n",windows);
 printf("检验和 :\t%d\n",checksum);
 printf("紧急指针字段 :\t%d\n",urgent_pointer);
}
/*下边实现IP数据包分析的函数定义ethernet_protocol_packet_callback*/
void ip_protocol_packet_callback(u_char *argument,const struct pcap_pkthdr*
packet_header,const u_char* packet_content)
 {
  struct ip_header *ip_protocol;   /*ip协议变量*/
  u_int  header_length;    /*长度*/
  u_int  offset;                   /*片偏移*/
  u_char  tos;                     /*服务类型*/
  u_int16_t checksum;    /*首部检验和*/
  ip_protocol=(struct ip_header*) (packet_content+14); /*获得ip数据包的内容去掉以太头部*/
  checksum=ntohs(ip_protocol->ip_checksum);      /*获得校验和*/
  header_length=ip_protocol->ip_header_length*4; /*获得长度*/
  tos=ip_protocol->ip_tos;    /*获得tos*/
  offset=ntohs(ip_protocol->ip_off);   /*获得偏移量*/
     printf("\n##########    网络层(IP协议)    ########### \n");
     printf("IP版本:\t\tIPv%d\n",ip_protocol->ip_version);
     printf("IP协议首部长度:\t%d\n",header_length);
     printf("服务类型:\t%d\n",tos);
     printf("总长度:\t\t%d\n",ntohs(ip_protocol->ip_length));/*获得总长度*/
     printf("标识:\t\t%d\n",ntohs(ip_protocol->ip_id));  /*获得标识*/
     printf("片偏移:\t\t%d\n",(offset&0x1fff)*8);    /**/
     printf("生存时间:\t%d\n",ip_protocol->ip_ttl);     /*获得ttl*/ 
     printf("首部检验和:\t%d\n",checksum);
     printf("源IP:\t%s\n",inet_ntoa(ip_protocol->ip_source_address));          /*获得源ip地址*/
     printf("目的IP:\t%s\n",inet_ntoa(ip_protocol->ip_destination_address));/*获得目的ip地址*/
     printf("协议号:\t%d\n",ip_protocol->ip_protocol);         /*获得协议类型*/
     cout<<"\n传输层协议是:\t";
 switch(ip_protocol->ip_protocol)
 {     
   case 6 :
       printf("TCP\n");
       tcp_protocol_packet_callback(argument,packet_header,packet_content);
       break; /*协议类型是6代表TCP*/
   case 17:
       printf("UDP\n");
       break;/*17代表UDP*/
   case 1:
       printf("ICMP\n");
       break;/*代表ICMP*/
   case 2:
       printf("IGMP\n");
       break;/*代表IGMP*/
  default :break;
 }
 }

void ethernet_protocol_packet_callback(u_char *argument,const struct pcap_pkthdr *packet_header,const u_char* packet_content)
 {
 u_short ethernet_type;                                     /*以太网协议类型*/
 struct ethernet_header *ethernet_protocol;  /*以太网协议变量*/
 u_char *mac_string;
 static int packet_number=1;
 printf("\n*** ****** ******* *********   ********* ******* ****** ***\n");
 printf("\t!!!!!#    第【 %d 】个IP数据包被捕获    #!!!!!\n",packet_number);
 printf("\n==========    链路层(以太网协议)    ==========\n");
 ethernet_protocol =(struct ethernet_header *) packet_content;  /*获得一太网协议数据内容*/
 printf("以太网类型为 :\t");
 ethernet_type=ntohs(ethernet_protocol->ether_type); /*获得以太网类型*/
 printf("%04x\n",ethernet_type);

  switch(ethernet_type)            /*判断以太网类型的值*/
      {
        case 0x0800 :  
            printf("网络层是:\tIPv4协议\n");break;
        case 0x0806 :  
            printf("网络层是:\tARP协议\n");break;
        case 0x8035 :  
            printf("网络层是:\tRARP 协议\n");break;
        default: break;
      }
/*获得Mac源地址*/
printf("Mac源地址:\t");
mac_string=ethernet_protocol->ether_shost;
printf("%02x:%02x:%02x:%02x:%02x:%02x:\n",*mac_string,*(mac_string+1),*(mac_string+2),*(mac_string+3),*(mac_string+4),*(mac_string+5));

/*获得Mac目的地址*/
printf("Mac目的地址:\t");
mac_string=ethernet_protocol->ether_dhost;
printf("%02x:%02x:%02x:%02x:%02x:%02x:\n",*mac_string,*(mac_string+1),*(mac_string+2),*(mac_string+3),*(mac_string+4),*(mac_string+5));

 switch (ethernet_type)
{
    case 0x0800:
        /*如果上层是IPv4ip协议,就调用分析ip协议的函数对ip包进行贩治*/ 
        ip_protocol_packet_callback(argument,packet_header,packet_content);
        break;
    default :break;
 }
  packet_number++;
}
main()
{
     cout<<"==========    解析IP数据包    ==========\n";
     pcap_if_t  *alldevs;
     pcap_if_t *d;
     int inum=0;
     int i=0;
     pcap_t *adhandle;
     char errbuf[PCAP_ERRBUF_SIZE];

  /* 获得网卡的列表 */
  if (pcap_findalldevs(&alldevs, errbuf) == -1)
  {
    fprintf(stderr,"Error in pcap_findalldevs: %s\n", errbuf);
    exit(1);
  }
 
  /* 打印网卡信息 */
  for(d=alldevs; d; d=d->next)
  {
    printf("%d. %s", ++i, d->name);
    if (d->description)
        printf(" (%s)\n", d->description);
    else
        printf(" (No description available)\n");
  }
 
  if(i==0)
  {
    printf("\n没有发现接口!确保安装了LibPcap.\n");
    return -1;
  }

  printf("\n【输入要选择打开的网卡号 (1-%d)】:\t",i);
  scanf("%d", &inum);               //输入要选择打开的网卡号
 
  if(inum < 1 || inum > i) //判断号的合法性
  {
    printf("\n网卡号超出范围.\n");
    /*释放设备列表 */
    pcap_freealldevs(alldevs);
    return -1;
  }
 
  /* 找到要选择的网卡结构 */
  for(d=alldevs, i=0; i< inum-1 ;d=d->next, i++);
 
  /* 打开选择的网卡 */
  if ( (adhandle= pcap_open_live(d->name, /* 设备名称*/
                  65536,   /* 最大值.*/
                  /*65536允许整个包在所有mac电脑上被捕获.*/
                  1,       /* 混杂模式*/

/*

混杂模式是指一台主机能够接受所有经过它的数据流,不论这个数据流的目的地址是不是它,它都会接受这个数据包。也就是说,混杂模式下,网卡会把所有的发往它的包全部都接收。在这种情况下,可以接收同一集线器局域网的所有数据。

*/
                  1000,     /* 读超时为1秒*/
                  errbuf   /* error buffer*/
                  ) ) == NULL)
  {
    fprintf(stderr,"\n无法打开适配器.\t %s 不被LibPcap支持\n");
    /* Free the device list */
    pcap_freealldevs(alldevs);
    return -1;
  }

  printf("\n监听 %s...\n", d->description);
  /* 现在,我们不再需要设备列表, 释放它 */
  pcap_freealldevs(alldevs);
  int cnt = -1;
  cout<<"\n【将要捕获数据包的个数】:\t\t";
  cin>>cnt;
  /* 开始以回调的方式捕获包
  函数名称:int pcap_loop(pcap_t * p,int cnt, pcap_handler callback, uchar * user);
  函数功能:捕获数据包,不会响应pcap_open_live()函数设置的超时时间
  */
  pcap_loop(adhandle,cnt, ethernet_protocol_packet_callback, NULL);
  cout<<"\n\t!!!!!#    解析IP数据包结束    #!!!!!\n";
  return 0;
}

C++ 捕获与解析IP数据包 (winpcap)_第3张图片

C++ 捕获与解析IP数据包 (winpcap)_第4张图片

 

你可能感兴趣的:(C++,网络)