您好,欢迎来到尚车旅游网。
搜索
您的当前位置:首页利用Winpcap捕获发送数据包

利用Winpcap捕获发送数据包

来源:尚车旅游网
利用winpcap捕获数据包、发送数据包

在上一章里面,我们学会了如何获取适配器的相关配置信息,在这一章里面,我们将继续更有意义的内容,就是捕获和发送数据包。

3.1 winpcap捕获数据包流程与相关函数

计算机是通过网卡和网络中其他的主机进行通信的,网卡相当于数据包进出的大门,我们平时讲的数据包的捕获相当于大门的门卫在检查进出的行人一样。在网络基础我们学习过,数据包的发送是一个封装的过程,而数据包的接收则是解封装的过程,但是封装和解封装都是在OS内核来完成的,一般的应用程序没办法获取数据包原始的内容,而Winpcap却能提供这样的功能,在数据链路层捕获数据包,提供最原始的信息。其中Winpcap捕获数据的原理在第一章已经介绍过了,大家可以回顾下。

另外,数据捕获只能捕获通过本主机网卡的数据,没法捕获其他主机上网卡的数据。 下面先看看Winpcap捕获数据时的工作流程。

选择捕获数据的网卡打开该网卡开始捕获结束 数据捕获的流程

1. 发现网络设备的函数(find_dev_ex)以前已经介绍过了。 2.打开网卡的函数

打开设备的函数是 pcap_open()。下面是参数 snaplen, flags 和 to_ms 的解释说明 pcap_t* pcap_open ( const char * source, // 指定的网卡的名称 int snaplen, // 帧的长度

int flags, // 网卡捕获的模式 int read_timeout, // 超时

struct pcap_rmtauth * auth, // 是否要求认证 char * errbuf // 错误信息存储 )

snaplen 制定要捕获数据包中的哪些部分。 在一些操作系统中 (比如 xBSD 和 Win32), 驱动可以被配置成只捕获数据包的初始化部分: 这样可以减少应用程序间复制数据的量,从而提高捕获效率。本例中,我们将值定为65535,它比我们能遇到的最大的MTU还要大。因此,我们确信我们总能收到完整的数据包。

flags: 最最重要的flag是用来指示适配器是否要被设置成混杂模式。 一般情况下,适配器只接收发给它自己的数据包, 而那些在其他机器之间通讯的数据包,将会被丢弃。 相反,如果适配器是混杂模式,那么不管这个数据包是不是发给我的,我都会去捕获。也就是说,

我会去捕获所有的数据包。 这意味着在一个共享媒介(比如总线型以太网),WinPcap能捕获其他主机的所有的数据包。 大多数用于数据捕获的应用程序都会将适配器设置成混杂模式,所以,我们也会在下面的范例中,使用混杂模式。

to_ms 指定读取数据的超时时间,以毫秒计(1s=1000ms)。在适配器上进行读取操作(比如用 pcap_dispatch() 或 pcap_next_ex()) 都会在 to_ms 毫秒时间内响应,即使在网络上没有可用的数据包。 在统计模式下,to_ms 还可以用来定义统计的时间间隔。 将 to_ms 设置为0意味着没有超时,那么如果没有数据包到达的话,读操作将永远不会返回。 如果设置成-1,则情况恰好相反,无论有没有数据包到达,读操作都会立即返回。 3.通过回调方式捕获数据的函数

程序功能:打开指定的网卡捕获数据帧,输出数据帧的捕获时间和数据帧的大小等信息。 程序运行结果如下图所示:

代码如下: //@filename: PacketCap1.cpp //程序功能:在指定的网卡上捕获数据帧,并输出数据帧的长度和捕获时间 #include \"pcap.h\" #include \"remote-ext.h\" #pragma comment(lib, \"wpcap\") // 包处理函数 void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data); // 主函数 int main() { pcap_if_t *alldevs; // 设备列表 pcap_if_t *d; // 网卡节点指针 int inum; int i=0; pcap_t *adhandle; // 要打开的网卡句柄 char errbuf[PCAP_ERRBUF_SIZE]; // 错误消息 // 获取本机设备列表 if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1) { fprintf(stderr,\"Error in pcap_findalldevs: %s\\n\ exit(1); } // 打印列表 for(d=alldevs; d; d=d->next) { printf(\"%d. %s\ if (d->description) printf(\" (%s)\\n\ else printf(\" (No description available)\\n\"); } if(i==0) { printf(\"\\nNo interfaces found! Make sure WinPcap is installed.\\n\"); return -1; } // 选择监听的网卡 printf(\"Enter the interface number (1-%d):\ scanf(\"%d\ if(inum < 1 || inum > i) { printf(\"\\nInterface number out of range.\\n\"); // 释放设备列表 pcap_freealldevs(alldevs); return -1; } // 跳转到选中的适配器 for(d=alldevs, i=0; i< inum-1; d=d->next, i++); // 打开设备 if ( (adhandle = pcap_open(d->name, // 设备名 65536, // 65535保证能捕获到不同数据链路层上的每个数据包的全部内容 0, // 0表示普通模式,1表示混杂模式 1000, // 读取超时时间 NULL, // 远程机器验证 errbuf // 错误信息缓存 )) == NULL) { fprintf(stderr,\"\\nUnable to open the adapter. %s is not supported by WinPcap\\n\d->name); // 释放设备列表 pcap_freealldevs(alldevs); return -1; } printf(\"\\nlistening on %s...\\n\ // 释放设备列表 pcap_freealldevs(alldevs); // 开始捕获 pcap_loop(adhandle, 0, packet_handler, NULL); return 0; } // 每次捕获到数据包时,libpcap都会自动调用这个回调函数 void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) { struct tm *ltime; char timestr[16]; time_t local_tv_sec; // 将时间戳转换成可识别的格式 local_tv_sec = header->ts.tv_sec; ltime=localtime(&local_tv_sec); strftime(timestr, sizeof timestr, \"%H:%M:%S\ printf(\"%s,%.6d len:%d\\n\} 函数pcap_loop用户捕获数据,有一个回调 参数, packet_handler指向一个可以接收数据包的函数。这个函数会在收到每个新的数据包并收到一个通用状态时被libpcap所调用 ( 与函数 pcap_loop() 和 pcap_dispatch() 中的 user 参数相似),数据包的首部一般有一些诸如时间戳,数据包长度的信息,还有包含了协议首部的实际数据。 注意:冗余校验码CRC不再支持,因为帧到达适配器,并经过校验确认以后,适配器就会将CRC删除,与此同时,大部分适配器会直接丢弃CRC错误

头部原始帧原始帧pcap_pkthdr-ts : struct timeval<未指定>-caplen : bpf_u_int32<未指定>-len : bpf_u_int32<未指定> 图1 Winpcap捕获数据帧时的过程

其中pcap_pkthdr为Winpcap添加上的头部,在pcap.h头文件中定义,其定义如下: Struct pcap_pkthdr {

struct timeval ts; // 时间 bpf_u_int32 caplen; // 长度 bpf_u_int32 len; // 帧长度 };

其中timeval的结构如下: struct timeval {

long tv_sec; /* seconds */

long tv_usec; /* and microseconds */

};

Bpf_u_int32 结构如下: typedef u_int bpf_u_int32;

为了以后简化相关的程序编写过程,我们对上述程序的相关功能做了一个函数封装,简化了主函数的代码。

主要包含以下3个函数:

(1)pcap_if* GetAllAdapters(); /<**

函数功能:获取本主机网络列表的头指针 参数:

返回值: 函数成功,返回网卡节点的头指针,失败返回NULL */

(2) pcap_if* SelectAdapter(pcap_if_t *alldev); /<**

函数功能: 显示网卡的列表,并获取用户的要选择侦听的网卡 参数: pcap_if *p 指向网卡列表的头指针 返回值: 返回用户选择的网卡的pcap_if指针 */

(3) pcap_t* OpenAdapter(pcap_if* p, int mode = 0); /<**

函数功能:打开选定的网卡,可以进行侦听或者在该网卡发送数据 参数: pcap_if* p 网卡列表的头指针 int index 要操作的网卡索引号

int mode 设置网卡的模式, 0表示普通模式,1表示混杂模式,默认为普通模式 返回值: 成功返回该网卡的句柄, 失败返回NULL */

那么主程序的代码如下:

#include \"common/PcapTools.h\" void PacketHandle(u_char* param, pcap_pkthdr *header, const u_char* pk_data); // 主函数 int main() { pcap_if* alldev; pcap_if* d; // 网卡列表头节点指针 // 选择的网卡序号 // 网卡句柄 pcap_t* handle; alldev = GetAllAdapters(); // 获取网卡列表 d = SelectAdapter(alldev); // 提示用户选择侦听的网卡 printf(\"在网卡%s\\n侦听....\\n\ handle= OpenAdapter(d); // 打开该网卡 // 通过回调函数捕获数据 pcap_loop(handle, 0, pcap_handler(PacketHandle), NULL); return 0; } // 回调函数 void PacketHandle(u_char* param, pcap_pkthdr *header, const u_char* pk_data) { static int i = 0; // 输出数据包长度 printf(\"数据包%d \长度:%d字节\时间:%s\\n\ } ++i, header->len, GetTime(header->ts).c_str());

程序执行的结果:

4.非回调函数捕获数据的函数

本讲的范例程序所实现的功能和效果和上一讲的非常相似 (打开适配器并捕获数据包), 但本讲将用 pcap_next_ex() 函数代替上一讲的 pcap_loop()函数。

pcap_loop()函数是基于回调的原理来进行数据捕获,这是一种精妙的方法,并且在某些场合中,它是一种很好的选择。 然而,处理回调有时候并不实用 -- 它会增加程序的复杂度,特别是在拥有多线程的C++程序中。

可以通过直接调用pcap_next_ex() 函数来获得一个数据包 -- 只有当编程人员使用了 pcap_next_ex() 函数才能收到数据包。

这个函数的参数和捕获回调函数的参数是一样的 -- 它包含一个网络适配器的描述符和两个可以初始化和返回给用户的指针 (一个指向 pcap_pkthdr 结构体,另一个指向数据报数据的缓冲)。

int pcap_next_ex ( pcap_t * p, // 打开的网卡句柄

struct pcap_pkthdr ** pkt_header, // Winpcap添加在帧上的头部

const u_char ** pkt_data // 数据部分 )

1 if the packet has been read without problems

0 if the timeout set with pcap_open_live() has elapsed. In this case pkt_header and pkt_data don't point to a valid packet -1 if an error occurred

-2 if EOF was reached reading from an offline capture #include \"common/PcapTools.h\" int main() { pcap_if* alldev; pcap_if* d; pcap_t* handle; struct pcap_pkthdr *header; const u_char *pkt_data; char errBuff[256]; static int i = 0; int res; alldev = GetAllAdapters(); // 获取网卡列表 d = SelectAdapter(alldev); // 选择指定的网卡 handle = OpenAdapter(d); // 打开网卡 // 开始捕获数据包 while ((res = pcap_next_ex(handle, &header, &pkt_data)) >= 0) { } return 0; } if(res == 0) continue; printf(\"数据包%4d \长度:%d字节\时间:%s\\n\ ++i, header->len, GetTime(header->ts).c_str()); 程序运行结果如下图所示:

3.3 捕获数据包的过滤器设置

我们在上两节学习了如何使用Winpcap捕获数据,但是在进行数据包分析中,我们往往不需要了解全部的数据包,只需要了解自己感兴趣的数据包,这时候Winpcap数据包过滤引擎登场了。数据包过滤,作为一个嗅探器中最强大的特性之一,在Winpcap中也不例外。它能够提供有效的方法去获取网络中的某些数据包,用来进行过滤数据包的函数是pcap_compile()和pcap_setfilter()。

Pcap_compile()将一个高层的过滤表达式编译成一个能够被过滤引擎所解释的底层的字节码。也就是执行过滤表达式的编译。其中过滤表达式编写的语法参考附录1.

Pcap_setfilter()将一个过滤器与内核捕获会话相关联。当pcap_setfilter被调用时,这个过滤器将被应用到来自网络的所有数据包,并且只捕获过滤器认为合法的包,将这些数据包复制给应用程序。

下面的代码片断中,要进行过滤的条件为\"ip and tcp\",这说明我们只希望保留IPv4和TCP的数据包,并把他们发送给应用程

if (d->addresses != NULL) /* 获取接口第一个地址的掩码 */ netmask=((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr; else /* 如果这个接口没有地址,那么我们假设这个接口在C类网络中 */ netmask=0xffffff; compile the filter 编译过滤器 if (pcap_compile(adhandle, &fcode, \"ip and tcp\ { fprintf(stderr,\"\\nUnable to compile the packet filter. Check the syntax.\\n\"); /* 释放设备列表 */ pcap_freealldevs(alldevs); return -1; } set the filter // 设置过滤器 if (pcap_setfilter(adhandle, &fcode) < 0) { fprintf(stderr,\"\\nError setting the filter.\\n\"); /* 释放设备列表 */ pcap_freealldevs(alldevs); return -1; } 为了提高代码的重用性,我们把上面的代码封装成了一个函数,

/** 函数功能:在指定网卡上设置过滤器. 参数:pcap_if *p 侦听的网卡节点指针 char* filter 过滤器的字符表达式 返回值: 设置成功返回true, 失败返回false */ bool SetFiliter(const pcap_if* p, cosnt char* filter) 3.4 利用winpcap捕获并分析以太网帧结构

有了前面几节的基础知识,我们接下来可以做些有意义的事情了,要做的是进行一个简单数据的协议分析,要进行协议分析,首先的是要了解网络传输时数据的封装过程。目前的网络普遍是使用TCP/IP网络协议来进行通信,发送时数据从应用层往下层传递,是一个封装的过程,每经过一层,都会加上相应的头部。封装的过程终结于数据链路层,到数据链路层的是协议数据单元PDU称为帧,帧再往下传输是就是转换为二进制比特流传递,最后变成电信号或者光信号在传输媒体中传递。所以说,数据帧是网络传输时最底层,最原始的数据形式了。我们接下来要做的事情就是捕获以太网的帧并解析其中的帧结构相关信息。

包的传输方向Ethernet报头(14byte)IP报头(14byte)TCP报头(14byte)应用数据(0~1460byte)DST MAC:Ethernet报头 (14byte)SRC MAC:TYPE:V:ID:HL:TOS:P:`TL:0FMFO:CKSUM: IP报头(20byte)TTL:SRC IP:DST IP:SRC PORT:SEQ:DST PORT: TCP报头(20byte)ACK:WIN:CKSUM:URG:DATA

应用数据(0~1460byte) 数据传递的过程图

Ethernet帧的格式和结构体的定义

下图所示为一个Ethernet帧的结构,最前面存储的地址是发送目的地址的MAC地址,其后存储的是发送端的MAC地址。在类型域中,存储着用于识别Ethernet上一层协议的号码。

帧的传输方向目的地的MAC址址6byte发送端的MAC地址6byte类型2byte数据46~1500byteFCS4byte 关于Ethernet报头的结构体,所定义的结构体如图所示。

typedef struct ether_header { u_char dstMac[ETHER_ADDR_LEN]; u_char srcMac[ETHER_ADDR_LEN]; u_short ethType;

// 目的MAC地址长度 // 源MAC地址长度

// 协议类型

} ETHERNET_HEADER; // 以太网头部结构体

由于ETHER_ADDR_LEN定义为6,MAC地址使用了char数据类型的一个数组,所以该数组的元素个数为6。在对MAC地址进行比较或赋值的进修,作为一个数组进行处理,必须使用memcpy函数进行处理。

在上述类型域中所能够赋予的值,在C语言的隐含文件中进行了一些说明,其中选出一个较好的(如图),在C语言的文件中,对类型域中所赋予的值进行了一些说明。在上一层为IP协议时,Ethernet类型域的值为0x0800;但是,在程序中,如果直接书写0x0800,则程序变得比较难懂。最后的帧检验序列(FCS)用于检查在数据发达过程中,从报头开始到数据结束,是否由于噪声等原因造成了数据损坏。在发送时的FCS生成处理及在接收到时FCS检查计算,都是由硬件自动完成的。因此,没有必要在C语言中直接对FCS值进行设定。

#define ETHERTYPE_IP 0x0800 // IP protocol (IP协议) #define ETHERTYPE_ARP 0x0806 // Address Resolution Protocol (地址解析协议) #define ETHERYTPE_RAPR 0x8035 // Reverse Address Resolution Protocol (逆地址解析协议) 3.5 winpcap发送数据包的相关函数

使用winpcap可以向网络发送自己构造的数据包,发送数据包的过程如下: (1) 使用pcap_open()函数打开网卡

(2) 使用pacap_sendpacket()函数发送自己构造的数据包。

如果发送的数据的长度小于60字节,则网卡会自动填充到60字节。

3.5 发送自己构造的以太网帧

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- sceh.cn 版权所有

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务