使用 libpcap 分析网络报文 (1)

最近要对 tcpdump 抓到的报文进行分析,开始的时候用 wireshark 的命令行工具 tshark 把分析的结果保存成文本文件然后再用正则表达式匹配需要的字段,这样好处是不用自己分析协议,只要抓取需要的字段就行了,缺点是相当地慢,330M 的 tcpdump 文件经过 tshark 处理后得到 5G+ 的文本文件,还要从这 5G+ 文本中做字符串匹配……

于是往下走一层,发现 wireshark 和 tcpdump 都使用了同样的库--libpcap,来实现抓包操作。对于抓包需要用到的 libpcap 函数并不多,主要是自己分析协议部分比较麻烦。网上找了些 libpcap 的资料,在这里总结一下。

简介(参考资料 [1])

libpcap 是一个 C 语言库,libpcap 的英文意思是 Packet Capture library,即数据包捕获函数库,其功能是通过网卡抓取网络以太网中的数据包。这个库为不同的平台提供了一致的 c 函数编程接口,在安装了 libpcap 的平台上,以 libpcap 为接口写的程序、应用,能够自由地跨平台使用。它支持多种操作系统。libpcap 结构简单,使用方便;它提供了 20 多个 api 封装函数,我们利用这些 api 函数即可完成本网络探测器所需的网络数据包监听功能。

第一个 pcap 程序

#include <stdio.h>
#include <pcap.h>

int main()
{
   pcap_t* pd;
   char ebuf[PCAP_ERRBUF_SIZE], *dev;
   const u_char* pkt;
   struct pcap_pkthdr ph;

   dev = pcap_lookupdev(ebuf);
   if (!dev) {
      fprintf(stderr, "%s\n", ebuf);
      return -1;
   }

   printf("get net device -> %s\n", dev);

   pd = pcap_open_live(dev, 65535, 0, 0, ebuf);
   if (!pd) {
      fprintf(stderr, "%s\n", ebuf);
      return -1;
   }

   pkt = pcap_next(pd, &ph);
   printf("A packet is captured.\n");

   pcap_close(pd);

   return 0;
}

使用 libpcap 的函数要包含头文件 pcap.h,编译时加上链接选项“-lpcap”。

函数

char *pcap_lookupdev(char *errbuf);

用来获取当前机器上可用的网络接口名称,如果找到则返回名称,找不到则返回 NULL,失败原因保存在 errbuf 中。errbuf 的长度至少为 PCAP_ERRBUF_SIZE。这个函数好像只查找以太网卡,并没有把我机器上的无线网卡也列出来,不知道如果有多块以太网卡的时候会怎样。

找到设备之后就可以进行监听了。函数

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

对 pcap_lookupdev() 找到的设备进行监听。参数 device 是可用的设备名称;snaplen 指定捕获报文的最大长度;promisc 设置网卡是否处于 混杂模式,0 为非混杂模式,1 为混杂模式;to_ms 设置捕获的时间,如果该时间段内没有数据包就超时返回,如果有就返回该时间段内捕获到的所有数据包,如果该参数为 0 说明如果没有数据到来时处于阻塞模式,直到收到符合要求的数据包为止;errbuf 用于保存出错信息。

如果要打开 tcpdump 或 wireshark 保存的数据文件可以使用函数

pcap_t *pcap_open_offline(const char *fname, char *errbuf);

其中 fname 是要打开的文件,errbuf 用于保存出错时的信息。

接下来开始捕获报文。函数

const u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h);

接收两个参数:第一个是使用 pcap_open_live() 或 pcap_open_offline() 得到的 pcap_t 指针,第二个参数是 pcap 为捕获到的每个包加上的信息。一般来说,由 pcap 捕获的数据包格式像下面这样:

+---------------------+-------------------+-----+
| struct ether_header | ip/arp/... header | ... |
+---------------------+-------------------+-----+

pcap_next() 会把 pcap 加上的信息填充到参数 h 指向的结构体中,并且返回指向实际数据包的指针(也就是 struct ether_header 的起始位置)。函数的返回时间取决于在 pcap_open_live 中的 to_ms 参数值,对于直接打开文件来说则不需要阻塞。

当 pcap_next() 返回后打印信息“A packet is captured.”(因为在 pcap_open_live() 中设置超时间隔为 0,也就是说 pcap_next() 会一直阻塞直到有数据包到达)。

当分析完成后使用函数

void pcap_close(pcap_t *p);

释放资源。

另一个 pcap 程序

#include <stdio.h>
#include <pcap.h>

static void printer(u_char* arg, const struct pcap_pkthdr* ph,
      const u_char* packet)
{
   printf("A packet is captured.\n");
}

int main()
{
   pcap_t* pd;
   char ebuf[PCAP_ERRBUF_SIZE], *dev;

   dev = pcap_lookupdev(ebuf);
   if (!dev) {
      fprintf(stderr, "%s\n", ebuf);
      return -1;
   }

   printf("get net device -> %s\n", dev);

   pd = pcap_open_live(dev, 65535, 0, 0, ebuf);
   if (!pd) {
      fprintf(stderr, "%s\n", ebuf);
      return -1;
   }

   pcap_dispatch(pd, 0, printer, NULL);
   pcap_close(pd);

   return 0;
}

前面打开设备的部分与第一个程序相同,不同的是捕获的部分使用了另一个函数

int pcap_dispatch(pcap_t *p, int cnt, pcap_handler callback, u_char *user);

pcap_dispatch() 直到读取完 cnt 个数据包,或者读取超时,或者读到文件或缓冲区的结尾时返回。如果 cnt 为 -1 或 0 表示读取全部数据包,直到读取超时或到结尾时返回。参数 user 是用户自定义的参数,用于传递给回调函数。每次捕获一个数据包时会调用回调函数 callback,其中 pcap_handler 的定义如下:

typedef void (*pcap_handler)(u_char *user, const struct pcap_pkthdr *h,
      const u_char *bytes);

第一个参数 user 就是传递给 pcap_dispatch() 的最后一个参数;第二个参数是指向 pcap 数据包起始位置的 struct pcap_pkthdr 的指针(见前面的图),bytes(或者叫 packet 比较符合实际)指向实际的网络报文起始位置(在前面的图中就是 struct ether_header 的位置),其中整个报文的长度(从 bytes 指向的位置开始到整个报文的结束)由 struct pcap_pkthdr 中的 caplen 指定。struct pcap_pkthdr 的定义为:

struct pcap_pkthdr {
   struct timeval ts;      /* time stamp */
   bpf_u_int32 caplen;     /* length of portion present */
   bpf_u_int32 len;        /* length this packet (off wire) */
};

第一个参数 ts 为捕获报文的时间,第三个参数 len 没搞明白,但是从实际输出来看和 caplen 的值一样。

从实际输出看到,每次捕获到一个报文都会调用回调函数 printer() 打印消息。

另外还有一个和 pcap_dispatch() 功能相似的函数

int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user);

这个函数在读取超时的时候不会返回,只在捕获了 cnt 个数据包或读取出错时才返回。

另外可以使用函数 pcap_dump_open(),pcap_dump() 和 pcap_dump_close() 把报文保存在文件中。tcpdump 和 wireshark 保存的报文格式都遵循 pcap 格式,所以它们可以打开对方的数据文件,不过直接使用 pcap 函数保存的文件会在文件开头加上一些 pcap 的信息,这个文件不能直接用 tcpdump 或 wireshark 打开,要把开头的 pcap 内容删掉才行。

好了,目前用到的函数主要就是这些,更多函数可以参考 [1] 和 pcap.h 的内容。

参考资料

[1] libpcap函数库
[2] The Sniffer's Guide to Raw Traffic (a libpcap tutorial)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注