Pcap的那点事儿——使用WinpCap编程

WinpCap是在Windows平台下的一个网络数据操作集成模块(Pcap的Windows版本),如果你不知道WinpCap这玩意儿的话,那么Wireshark你总应该不会太陌生(Wireshark抓包真心不要太舒服,而其抓包功能则来自于WinpCap。)。在Windows系统下除了TCP/UDP数据包以外如果需要发送RAW SOCKET的话是一件比较麻烦的事情,尤其是在自己构造二层协议之类的情况,在没有驱动的情况下似乎比较困难。而在这种情况下WinpCap本身集成了比较多的网络数据包的功能(比如抓包、发送自定义数据包等等),因此WinpCap也就成了一个比较好的选择。 那么说了一段废话,这边文章的主要目的只是简单介绍一个流程,以及如何使用C/C++在Windows下利用Winpcap来进行网络数据编程。 首先介绍一下抓包-》发包的基本流程:

获取网卡-》选择并打开需要的网卡-》设置封包过滤条件-》loop数据包事件,在loop中处理抓获的数据包。

0.预先准备

为了进行编译,你需要从winpcap官网下载SDK,然后将解压获得的头文件和lib文件导入到你的项目中。

1.获取网卡

在WinpCap的API中提供了 一个名为 pcap_findalldevs 的函数来获取当前计算机的所有可用网卡(包括虚拟网卡以及系统环回),其原型为:

int pcap_findalldevs (pcap_if_t **alldevsp, char *errbuf)

此函数用于构建一个可用网卡列表,这些“网卡”将在后续被用于pcap_open_live函数(打开网卡)。其第一个参数 alldevsp 用于存放返回的网卡列表的指针,errbuf为错误信息的内容。 ** alldevsp 为一个指向指针的指针,因此实际上这里需要传递的是一个 pcap_if_t * 变量。有关指针的问题你需要好好理解下C语言。 那么,举个栗子:

pcap_if_t *alldevs; // 用于存放设备列表的指针变量。

char errbuf[PCAP_ERRBUF_SIZE]; // PCAP_ERRBUF_SIZE是PCAP SDK已定义的

if (pcap_findalldevs(&alldevs, errbuf) < 0){
return NULL;
}

//go what you want to do

如果函数返回值为-1表示失败,此时errbuf会有对应提示信息。0表示获取成功,此时alldevs指向的就是以获取到的设备列表

2.我有哪些网卡呢?

上一步中你已经获取到了你电脑里面的网卡了,然而,我TM怎么知道这些网卡是什么呢?这些“网卡”有哪些特征可以用? 你还记得上一步中获取到的pcap_if_t的那个指针嘛?我们来看下定义就知道了

struct pcap_if {
struct pcap_if next;
char *name; /\
name to hand to “pcap_open_live()” /
char *description; /\
textual description of interface, or NULL /
struct pcap_addr *addresses;
bpf_u_int32 flags; /
PCAP_IF_ interface flags */
};

typedef struct pcap_if pcap_if_t;

所以如上图所示,获取到的信息为一个链表结构,每一个节点包含网卡名称、描述、pcap_addr(网卡地址),以及接口标记。 遍历链表应该还是比较好说的:

pcap_if_t *d;
int i = 0;
for (d = alldevs; d; d = d->next){
printf(“%d . %s (%s)\n” , i++ , d->name , d->description);
}

3.打开我选择的网卡

在pcap api中,我们使用pcap_open_live函数来打开一个网卡。在第二步中你应该知道了如何去遍历获取到的网卡列表,那么这一步老子要打开网卡了,问题是我需要啥才能打开?请看函数原型:

pcap_t* pcap_open_live ( const char * device,
int snaplen,
int promisc,
int to_ms,
char * ebuf
)

按照官方文档的说法:“pcap_open_live用于获取抓包描述符,以抓取在这个网络上所发送/接受的数据包。device参数为设备名称(即*pcap_if_t -> name),snaplen指定了抓获数据包的最大是多少个字节,如果你所设置的snaplen小于所抓到的包的长度(比如你设置了1000,但是抓到了一个1200字节的数据包),则只会返回该数据包的前snaplen个字节的数据。promisc指定是否以“混乱模式”。to_ms指定读取超时的时间(看文档的意思应该是,当抓到一个数据包以后,在指定时间内不马上返回数据包,而是等待相应时间后把后续抓到的数据包也一起返回,如果设置为0会导致无期限的等待,同时会返回错误。)” 这个函数在操作成功后返回实际的pcap_t实例,代表这块网卡。 (话说我现在写这个才发现我之前模拟协议怎么那么慢。。。妈的to_ms设置的1000,难怪慢的要死) 所以参数介绍完了以后,来,怎么调用。 之前枚举网卡做了是吧?可以获取到name撒,把你要的网卡的name取出来,然后丢到device参数 snaplen默认65535就好啦,除非你的数据包长度有限 promisc这东西不知道干嘛的,反正我给的1,各位欢迎拍砖 to_ms这东西。。。。。仁者见仁智者见智,如果对延迟要求比较高的话,建议设置50以内(单位是ms),如果希望能一次处理多个数据的话,可以稍微设置的大一点。 ebuf我就不解释了嘛。。。。 那么最后的调用就成了:

pcap_t * netcard = NULL;
if ((netcard = pcap_open_live(d->name, 65536, 1, 10, errbuf)) == NULL){
log(“WARNING : Handle Failed”);
return 1;
}

//continue

4.设置封包过滤条件

网卡已经打开了,接下来做啥? 你不是要抓包吗,抓包要有指定的对象嘛,比如说我要抓pppoe的,或者我要抓udp的,对不对,那么我就应该设置条件。那么这一步就是设置条件咯。 以前用wireshark的时候直接输入条件就行,比如”ppp || pppoes”,然而对于计算机来说他根本不知道你说的什么鸟语,就和你写了C语言但是不编译电脑不知道怎么做一样,所以pcap里面提供一个compile函数来将你指定的条件转换为“binary code”(其实应该不是这个吧。。。他的实际类型是bpf_program) 使用pcap_compile来编译你的过滤条件,然后再使用pcap_setfilter使其生效。对于pcap_compile函数,你需要提供上一步中返回的netcard,一个bpf_program实例的指针,你设置的过滤条件(比如”ppp || pppoes”),再加上。。。之前那个网卡的地址。 一个栗子如下:

bpf_program fcode;
//下面的 d 为第二步中你所选中的网卡的结构体指针
if (pcap_compile(netcard, &fcode, “pppoed || pppoes”, 1, d->addresses ? ((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr : 0xffffff) >= 0){
//go continue
}else{
//go error
}

5.应用过滤,设置回调函数,执行循环

到了这一步,我们已经做了

  1. 枚举网卡
  2. 选中网卡
  3. 打开网卡
  4. 编译过滤条件

那么接下来我们要做的,首先把编译好的过滤条件应用上去,并且设置好捕捉到数据包时的回调函数以进行处理,然后启动loop开始抓包。

if (pcap_setfilter(netcard, &fcode) >= 0){
//开始loop,使用的参数为 netcard , -1 (这里负数表示永久loop) , packet_handler为回掉函数的名称,最后一个参数我这里设置为NULL
//-1 is returned on an error; 0 is returned if cnt is exhausted; -2 is returned if the loop terminated due to a call to pcap_breakloop() before any packets were processed.
pcap_loop(netcard, -1, packet_handler, NULL);
log(“INFO : loop quit!”);
}
else{
log(“WARNING : Set Filter Error”);
}

而对应的回掉函数 packet_handler,其原型应当为:

void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
//header包含了时间戳以及数据包长度
//pkt_data则为原始的数据包内容