Programming with pcap
Tim Carstens
timcarst at yahoo dot com
The latest version of this document can be found at http://broker.dhs.org/pcap.html
原文:http://www.tcpdump.org/pcap.htm
本文读者对象:需要基本的C语言基础知识,否则除非你只是想了解pcap编程的基本理论知识也可以阅读此文。当然你也不一定必须是网络编程的高手,因为本文所涉及的领域仅需要为有丰富网络编程经验的人所理解(言下之意是如果你对这方面不感兴趣或无意于向这方面发展就无所谓了)。本文中的所有代码示例均在缺省内核版本BSD4.3下经过测试(我在RedHat 6.2 with kernel-2.2.14-5下亦测试通过)。
Get Started: The format of a pcap application
首先让我们了解一个pcap应用程序的常用设计。代码的流程如下所示:
1、 首先决定将要用来sniff的网络接口。在Linux下可能是eth0在BSD下可能是xl1等。我们要么在一个字符串(char *)中定义这个设备,或用用户在命令行直接指定用来sniff的设备接口名。
2、 初始化pcap。这是明确指定用来sniff的的网络接口的地方。当然我们可以在多个接口设备上sniff。通过句柄(handle)我们可以区分这些不同的sniff设备接口。就像我们打开一个用来读或写的文件一样,我们必须命名我们的sniffer session 以便于区别这些不同的session。
3、 通常情况下我们只希望sniff特定的网络通信(比如:tcp数据包,所有发往23端口的tcp数据包)。通常我们制定这样一个定义特定网络通信的规则集,将其编译以后加载(apply to)pcap引擎上。这是编写pcap应用程序最主要的步骤,而且必须紧密关联。规则被保存在一个字符串中,通过编译被转换成pcap引擎能够识别的格式。事实上所谓的编译不过是在我们自己的程序中调用特定的函数就可以完成,并不涉及到任何外部的应用程序。然后我们可以告诉pcap引擎应用编译的规则作为我们sniff的规则(filter)。
4、 最后我们通知pcap引擎进入主要的处理流程:pcap接受并处理匹配指定规则的制定数目的数据包。每当捕获一个新的数据包,pcap调用自定义的回调函数进行相应的处理。在回调函数中可以做任何我们想做的事情:解剖捕获的数据包并打印到用户控制台,或者保存到文件中,当然也可以什么也不做(如果什么也不做,我们为什么要写这些代码呢?如果。。。那么。。。呢,呵呵已经很多人开始呕吐并晕倒了)
5、 结束sniff并关闭pcap会话句柄。
事实上,是用pcap编程是一个非常简单的过程,一共5个步骤,而且令你备感困惑的第3步还是可选的。详细实现如下。
Setting the Device
这是一个极其简单的操作(原文:This is terribly simple)。有两种方法可以设置用来sniff的网络接口。
1、 用户在命令行指定定监听的网络接口:
#include <stdio.h>
#include <pcap.h>
int main(int argc, char *argv[])
{
char *dev = argv[1];
printf("Device: %s\n", dev);
return(0);
}
用户通过命令行参数传入监听接口。
译注:在实际的项目开发中务必对命令行参数进行判断:
if (argc < 2) {
printf(“Usage: %s <option>\n”, argv[0]);
exit(1);
}
2、 通过pcap引擎设定监听的网络接口:
#include <stdio.h>
#include <pcap.h>
int main()
{
char *dev, errbuf[PCAP_ERRBUF_SIZE];
dev = pcap_lookupdev(errbuf);
printf("Device: %s\n", dev);
return(0);
}
在这种情况下,pcap引擎自己设置用来监听的接口。但是errbuf字符串用来做什么呢?大多数的pcap函数允许我们传递这样一个字符串作为其参数。这个字符串参数用来在pcap函数调用失败以后用来设置出错信息。在上面的例子中,如果pcap_lookup函数调用失败,出错信息将被保存在errbuf中。
译注:增加的错误检查的代码如下:
if (NULL == (dev = pcap_lookupdev(errbuf))) {
fprintf(stderr, “pcap_lookupdev() error: %s\n”, errbuf);
exit(-1);
}
printf(“Device: %s\n”, dev);
Opening the device for sniffing
创建sniff会话的任务非常简单。我们使用pcap_open_live()创建sniff会话。函数原型:
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
device:上节中我们制定的监听设备接口;
snaplen:制定pcap捕获的最大数目的网络数据包;
promisc:>0指定device接口工作在混杂模式(promiscous Mode);
to_ms:制定经过特定时间(ms)后读超时;0表示遇到错误退出,-1指定永不超时;
ebuf:制定用来存储出错信息的字符串
pcap_t:返回值为用于监听的pcap会话。
示例代码:
#include <pcap.h>
...
pcap_t *handle;
handle = pcap_open_live(somedev, BUFSIZ, 1, 0, errbuf);
上面的代码打开somedev指定的设备并读取(捕获)BUFSIZ字节,同时我们设置接口工作在混杂模式,一直监听到有任何错误发生则退出,并将出错信息保存在errbuf指定的字符串中。
关于混杂模式vs.非混杂模式:通常情况在非混杂模式下仅监听直接发往主机的数据包:发往、源自或通过主机路由的数据包都将被pcap捕获;混杂模式下,所有发送到物理链路上的数据包都将被捕获。在一个共享式的网络环境中,这将导致整个网络的数据流被监听。混合监听模式是可以被检测的:可以通过测试强可靠性来发现网络中是否有主机正在以混合模式监听,另外混杂工作模式仅仅在非交换式的网络中有效,而且在一个高负载的网络环境中,混杂模式将消耗大量的系统资源。
Filter traffic
通常我们只对特定网络通信感兴趣。比如我们只打算监听Telnet服务(port 23)以捕获用户名和口令信息。获知对FTP(port 21)或DNS(UDP port 53)数据流感兴趣。可以通过pcap_compile()和pcap_setfilter来设置数据流过滤规则(filter)
函数原型:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
p:表示pcap会话句柄;
fp:存放编译以后的规则;
str:规则表达式格式的过滤规则(filter),同tcpdump中的filter;
optimize:制定优化选项:0 false, 1 true;
netmask:监听接口的网络掩码;
返回值:-1表示操作失败,其他值表成功。
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
p:表示pcap的会话句柄;
fp:表示经过编译后的过滤规则;
返回值:-1表示操作失败,其他值表成功。
示例代码:
#include <pcap.h>
...
pcap_t *handle; /* Session handle */
char dev[] = "rl0"; /* Device to sniff on */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
struct bpf_program filter; /* The compiled filter expression */
char filter_app[] = "port 23"; /* The filter expression */
bpf_u_int32 mask; /* The netmask of our sniffing device */
bpf_u_int32 net; /* The IP of our sniffing device */
pcap_lookupnet(dev, &net, &mask, errbuf);
handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
pcap_compile(handle, &filter, filter_app, 0, net);
pcap_setfilter(handle, &filter);
上面的代码设备rl0上以混杂模式监听所有发往或源自端口23的数据包。Pcap_lookupnet()函数返回给定接口的IP地址和子网掩码。
The actual sniffing
现在我们开始准备捕获数据包:有两种方法可以用来捕获数据包。要么一次捕获一个满足条件的数据包,要么进入一个循环过程捕获指定数量数据包然后退出。首先来了解使用pcap_next()一次捕获单一数据包。
函数原型:
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
p:pcap会话句柄;
h:指向pcap_pkthdr接口的指针,在此结构中保存了所捕获的数据包的通用信息。包括:时间信息、数据包的长度和包头部分的长度(结构定义在后面定义)。
返回值:返回指向实际捕获的数据包的u_char *型指针。
代码示例:
#include <pcap.h>
#include <stdio.h>
int main()
{
pcap_t *handle; /* Session handle */
char *dev; /* The device to sniff on */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
struct bpf_program filter; /* The compiled filter */
char filter_app[] = "port 23"; /* The filter expression */
bpf_u_int32 mask; /* Our netmask */
bpf_u_int32 net; /* Our IP */
struct pcap_pkthdr header; /* The header that pcap gives us */
const u_char *packet; /* The actual packet */
/* Define the device */
dev = pcap_lookupdev(errbuf);
/* Find the properties for the device */
pcap_lookupnet(dev, &net, &mask, errbuf);
/* Open the session in promiscuous mode */
handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
/* Compile and apply the filter */
pcap_compile(handle, &filter, filter_app, 0, net);
pcap_setfilter(handle, &filter);
/* Grab a packet */
packet = pcap_next(handle, &header);
/* Print its length */
printf("Jacked a packet with length of [%d]\n", header.len);
/* And close the session */
pcap_close(handle);
return(0);
}
上面的代码将所有从pcap_lookupdev()返回的接口置于混杂模式监听状态。Pcap捕获端口23的一个数据包并打印该包的长度。然后调用pcap_close()关闭pcap会话。
当然我们可以使用更复杂和更强大的功能pcap_loop和pcap_dispatch。通常很少有sniffer使用pcap_next,他们更通常的使用pcap_loop或pcap_dispatch。为便于理解这两个函数,需要现了解回调函数的概念。
回调函数并不是一个新概念,在很多的API中都使用了回调函数的概念。可以通过pcap_loop或pcap_dispatch定义用户自己的回调函数。事实上pcap_loop和pcap_dispatch的功能非常相似,当pcap捕获的满足规则的数据包时,着两个函数将调用我们自己定义的回调函数执行我们自己的处理。
函数原型:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
p:pcap会话句柄;
cnt:定义sniff捕获的数据包的数目;
callback:自定义的回调函数句柄;
user:传递给回调函数的参数,如没有参数可以设为NULL;
函数pcap_dispatch和pcap_loop的用法几乎相同,两者之间的唯一的差别是处理超时的方式不同(在pcap_open_live()中设置的超时参数将在这里起作用:pcap_loop将忽略超时参数而pcap_dispatch在制定时间到时将产生读超时的错误)。查阅pcap的帮助获得更多信息。
回调函数的原型:
void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
args:对应于pcap_loop中的最后一个参数;
header:指向pcap数据包包头的指针;
packet:指向pcap捕获到的数据包的指针,packet指针指向的字符串包含了整个数据包;
返回值:回调函数不能返回任何值。
定义回调函数时,需要严格遵守原型定义,否则pcap_loop将不能正确调用回调函数。
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) */
};
怎样使用(处理)packet指针变量呢?一个packet指针所指的结构包含了很多属性,它并不是一个真正的字符串,而是多个结构组成的集合(比如:一个TCP/IP数据包包括以太网头、IP包头、TCP头和数据包中有效的数据负载)。首先需要定义这些结构:
/* Ethernet header */
...