原文链接:http://www.tcpdump.org/pcap.html
大家好,这是在http://www.tcpdump.org/#contribute主页中的一篇关于如何使用pcap编写抓包分析工具的文章,小逸我除了翻译外还加入了一些自己觉得有助于理解的信息以及使用经历。第一次翻译这么长的外文文章,如果有错误或者不准确的地方还请大家指出,共同学习。
Programming with pcap
Tim Carstens
timcarst at yahoo dot com
Further editing and development by Guy Harris
guy at alum dot mit dot edu
译者:简逸
Ok, let's begin by defining who this document is written for. Obviously, some basic knowledge of C is required, unless you only wish to know the basic theory. You do not need to be a code ninja; for the areas likely to be understood only by more experienced programmers, I'll be sure to describe concepts in greater detail. Additionally, some basic understanding of networking might help, given that this is a packet sniffer and all. All of the code examples presented here have been tested on FreeBSD 4.3 with a default kernel.
好啦,先让我们定义一下这篇文档的受众。很明显,除非你只希望了解一些基础的理论,不然一些C语言的基础知识是必要的。你不需要成为一个代码忍者(点击查看code ninja的解释);对于可能只有更有经验的程序员才能理解的部分,我将确保用更多的细节来描述这些观点。此外,考虑到这毕竟是一个数据包抓包分析工具,一些对于网络的基本理解也会有所帮助。这里展示的所有代码都已经在装有默认内核的FreeBSD 4.3下验证通过(本人在mac os x 10.10.5下载编译libpcap后使用xcode执行这些代码也都验证通过)。
Getting Started: The format of a pcap application
The first thing to understand is the general layout of a pcap sniffer. The flow of code is as follows:
开始:一个pcap应用的格式
首先,你需要理解一个pcap抓包分析工具的通用格式。编码流程如下所述:
1. We begin by determining which interface we want to sniff on. In Linux this may be something like eth0, in BSD it may be xl1, etc. We can either define this device in a string, or we can ask pcap to provide us with the name of an interface that will do the job.
1. 首先确定我们要监听哪个接口。在linux操作系统系统下,这个接口可能是eth0,在BSD下,可能是xl1等等。我们既可以用一个字符串来定义这个设备,也可以让pcap提供给我们将要做这个工作的接口名。
2. Initialize pcap. This is where we actually tell pcap what device we are sniffing on. We can, if we want to, sniff on multiple devices. How do we differentiate between them? Using file handles. Just like opening a file for reading or writing, we must name our sniffing "session" so we can tell it apart from other such sessions.
2. 初始化pcap。事实上,在这步我们告诉pcap我们正在监听哪个设备。如果想的话,我们能监听多个设备。那么我们如何区分这些设备呢?答案是使用文件句柄。就像打开一个文件进行读写操作一样,我们必须命名我们的嗅探“会话“,这样我们就能够从分辨每一个设备。
3. In the event that we only want to sniff specific traffic (e.g.: only TCP/IP packets, only packets going to port 23, etc) we must create a rule set, "compile" it, and apply it. This is a three phase process, all of which is closely related. The rule set is kept in a string, and is converted into a format that pcap can read (hence compiling it.) The compilation is actually just done by calling a function within our program; it does not involve the use of an external application. Then we tell pcap to apply it to whichever session we wish for it to filter.
3. 如果我们只想监听定的流量(例如:只监听TCP/IP数据包,只监听传向23号端口的数据包等),我们必须构建一个规则集合,”编译“它并且应用它。这三个阶段操作彼此紧密相连。规则集合被保存在一个字符串中,并且被转换成一种pcap能理解的格式(之后pcap编译这个规则)。在程序中,通过调用一个函数,我们就完成了规则的编译;这个过程不牵涉到一个外部应用的使用。在这两步之后,对于每个我们希望过滤的对话,我们告诉pcap应用这个规则。
4. Finally, we tell pcap to enter it's primary execution loop. In this state, pcap waits until it has received however many packets we want it to. Every time it gets a new packet in, it calls another function that we have already defined. The function that it calls can do anything we want; it can dissect the packet and print it to the user, it can save it in a file, or it can do nothing at all.
4. 最后,我们告诉pcap进入它的主要执行循环。在这个状态下,pcap将会等待,直到它接收到的数据包数量达到我们的要求。每当它收到了一个新的数据包,它会调用我们已经定义好的另一个函数。这个被调用的函数能做任何我们想要它做的事情。它可以剖析数据包并将其打印给用户看,也可以把数据包保存为个文件,或者压根什么都不做。
5. After our sniffing needs are satisfied, we close our session and are complete.
在我们的嗅探需求得到满足后,我们关闭我们的会话并且一切都完成了。
This is actually a very simple process. Five steps total, one of which is optional (step 3, in case you were wondering.) Let's take a look at each of the steps and how to implement them.
这实际上是一个非常简单的过程,总共只有五步,而且第三步是可以选作的。现在让我们来看看具体的每一步以及如何执行它们。
Setting the device
This is terribly simple. There are two techniques for setting the device that we wish to sniff on.
The first is that we can simply have the user tell us. Consider the following program:
这一步非常简单。有两种技术可以用来设定我们希望监听的设备。
第一种技术是我们直接让用户告诉我们。程序如下:
#include
#include
int main(int argc, char *argv[])
{
char *dev = argv[1];
printf("Device: %s\n", dev);
return(0);
}
The user specifies the device by passing the name of it as the first argument to the program. Now the string "dev" holds the name of the interface that we will sniff on in a format that pcap can understand (assuming, of course, the user gave us a real interface).
通过将设备名作为程序的第一个参数,用户指定了要监听的设备。现在字符串”dev“以一种pcap能够理解的格式保存了我们将要嗅探的接口名(当然,我们假设用户传递的参数是一个真实的接口名)。
The other technique is equally simple. Look at this program:
另一个技术是同样地简单。来看这个程序:
#include
#include
int main(int argc, char *argv[])
{
char *dev, errbuf[PCAP_ERRBUF_SIZE];
dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
printf("Device: %s\n", dev);
return(0);
}
In this case, pcap just sets the device on its own. "But wait, Tim," you say. "What is the deal with the errbuf string?" Most of the pcap commands allow us to pass them a string as an argument. The purpose of this string? In the event that the command fails, it will populate the string with a description of the error. In this case, if pcap_lookupdev() fails, it will store an error message in errbuf. Nifty, isn't it? And that's how we set our device.
在这个例子中,pcap自己设定了设备。”但是等等,Tim“,你说。”errbuf字符串是处理什么的呢?“ 大部分的pcap命令允许我们传递给它们一个字符串作为参数。那么传递这个字符串的目的是什么呢?如果命令失败,错误描述会被填入这个字符串中。在这个例子中,如果pcap_lookupdev()函数执行失败了,错误消息会被存入errbuf中。完成的非常精巧,不是吗?以上就是设置设备的全部内容。
Opening the device for sniffing
The task of creating a sniffing session is really quite simple. For this, we use pcap_open_live(). The prototype of this function (from the pcap man page) is as follows:
开启要嗅探的设备
创建一个嗅探会话真的相当简单。我们使用pcap_open_live()函数来完成这个操作。
函数原型如下(来自pcap 手册页):
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms,
char *ebuf)
The first argument is the device that we specified in the previous section. snaplen is an integer which defines the maximum number of bytes to be captured by pcap. promisc, when set to true, brings the interface into promiscuous mode (however, even if it is set to false, it is possible under specific cases for the interface to be in promiscuous mode, anyway). to_ms is the read time out in milliseconds (a value of 0 means no time out; on at least some platforms, this means that you may wait until a sufficient number of packets arrive before seeing any packets, so you should use a non-zero timeout). Lastly, ebuf is a string we can store any error messages within (as we did above with errbuf). The function returns our session handler.
To demonstrate, consider this code snippet:
第一个参数device是我们在之前的章节中指定的设备。第二个参数snaplen是一个整型参数,该参数定义了pcap能捕获的最大字节数。第三个参数是promisc,当这个参数被设定为True值时,该接口进入混杂模式(然而,即使这个参数被设置为False值,在某些特定情形下,该接口还是会进入混杂模式)。第四个参数是to_ms,这个参数代表读出时间,单位是毫秒(该参数设为0意味着读出时间为0;至少在一些平台上,这意味着在收到任何数据包之前,你也许会等待,直到收到足够数目的数据包,所以你应该使用一个非零的读出时间)。最后一个参数ebuf是字符串类型的,我们能在这个字符串中存储任何错误消息(正如我们之前对errbuf的使用一样)。这个函数返回会话句柄。
下面展示这个函数如何使用,思考这个代码片段:
#include
...
pcap_t *handle;
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
return(2);
}
This code fragment opens the device stored in the strong (个人认为应该是string)"dev", tells it to read however many bytes are specified in BUFSIZ (which is defined in pcap.h). We are telling it to put the device into promiscuous mode, to sniff until an error occurs, and if there is an error, store it in the string errbuf; it uses that string to print an error message.
这个代码段开启了保存在字符串”dev“中的设备,告诉pcap读取BUFSIZ定义的字节数信息(BUFSIZ被定义在pcap.h中)。我们设置设备处于混杂模式,一直监听直到有错误发生。如果有错误发生,将错误存储在字符串errbuf中;代码将使用这个字符串去打印错误消息。
A note about promiscuous vs. non-promiscuous sniffing: The two techniques are very different in style. In standard, non-promiscuous sniffing, a host is sniffing only traffic that is directly related to it. Only traffic to, from, or routed through the host will be picked up by the sniffer. Promiscuous mode, on the other hand, sniffs all traffic on the wire. In a non-switched environment, this could be all network traffic. The obvious advantage to this is that it provides more packets for sniffing, which may or may not be helpful depending on the reason you are sniffing the network. However, there are regressions. Promiscuous mode sniffing is detectable; a host can test with strong reliability to determine if another host is doing promiscuous sniffing. Second, it only works in a non-switched environment (such as a hub, or a switch that is being ARP flooded). Third, on high traffic networks, the host can become quite taxed for system resources.
关于混杂嗅探模式和非混杂模式嗅探的注解:这两种技术在形式上非常不同。在标准的非混杂模式中,只有当流量直接与主机相关时,主机才会被监听。只有发给主机,从主机发出以及通过主机路由的流量会被抓包程序捕获。而在混杂模式中,抓包分析程序将监听线路中的所有流量。在一个无交换的网络环境中,这可能意味着捕获所有网络流量。这是该模式一个明显的优点,用户可以监听更多的数据包,至于是否有帮助,这还要取决于你监听网络的原因。然而,这种模式有一些退步。首先,混杂模式嗅探是可以检测到的;一个主机能通过高可靠性测试确定是否另一个主机正在做混杂模式嗅探。其次,该模式仅在非交换环境下有效(例如一个集线器,或者一个使用ARP泛洪的交换器)。最后,在大流量网络中,这台主机会变成系统资源的负担。
Not all devices provide the same type of link-layer headers in the packets you read. Ethernet devices, and some non-Ethernet devices, might provide Ethernet headers, but other device types, such as loopback devices in BSD and OS X, PPP interfaces, and Wi-Fi interfaces when capturing in monitor mode, don't.
在你读到的数据包中,并不是所有的设备都提供相同类型的链路层报头。以太网设备和一些非以太网设备也许提供以太网报头,但是其他类型设备,例如在BSD和OS X中的回送设备,点对点协议接口,以及一些WiFi接口,当捕获处在监控状态时,不提供以太网报头。
You need to determine the type of link-layer headers the device provides, and use that type when processing the packet contents. The pcap_datalink() routine returns a value indicating the type of link-layer headers; see the list of link-layer header type values. The values it returns are theDLT_values in that list.
你需要确定设备提供的链路层报头类型,并且当处理数据包内容时,使用对应的类型。pcap_datalink函数会返回一个值,这个值指示了数据链路层提供的报头类型(详情点击上面链接)。这个函数返回的值在上面链接列表中DLT_values一栏。
If your program doesn't support the link-layer header type provided by the device, it has to give up; this would be done with code such as
如果你的程序不支持设备提供的数据链路层报头,程序不得不停止,这件事情将用下面的代码处理:
if (pcap_datalink(handle) != DLT_EN10MB) {
fprintf(stderr, "Device %s doesn't provide Ethernet headers - not supported\n", dev);
return(2);
}
which fails if the device doesn't supply Ethernet headers. This would be appropriate for the code below, as it assumes Ethernet headers.
如果设备提供的不是以太网报头,程序会失效。由于判断条件假设设备提供的是以太网报头,所以下面的代码是合理的。
Filtering traffic
Often times our sniffer may only be interested in specific traffic. For instance, there may be times when all we want is to sniff on port 23 (telnet) in search of passwords. Or perhaps we want to highjack a file being sent over port 21 (FTP). Maybe we only want DNS traffic (port 53 UDP). Whatever the case, rarely do we just want to blindly sniff all network traffic. Enter pcap_compile() and pcap_setfilter().
流量过滤
通常我们的抓包分析工具只对特定的流量感兴趣。例如,有很多时候,我们想监听23号端口(远程登录),寻找用户的密码。或者劫持通过21号端口(文件传输协议)发送的文件。有时也许我们只想要DNS流量(53号端口UDP协议)。无论是哪种情形,我们很少想盲目地监听所有网络流量。下面进入pcap_compile函数和pcap_setfilter函数的讲解。
The process is quite simple. After we have already called pcap_open_live() and have a working sniffing session, we can apply our filter. Why not just use our own if/else if statements? Two reasons. First, pcap's filter is far more efficient, because it does it directly with the BPF filter; we eliminate numerous steps by having the BPF driver do it directly. Second, this is a lot easier : )
这个过程相当的简单。在我们已经调用pcap_open_live函数并且有一个工作的嗅探会话之后,我们就能应用我们的过滤器。为什么不使用if/else if 语句呢?这里有两个原因。原因一,pcap提供的过滤器是更高效的,因为它直接使用带通滤波器完成该工作;而通过直接使用带通滤波器驱动,我们也能省略许多步骤。原因二,这么做更加简单。
Before applying our filter, we must "compile" it. The filter expression is kept in a regular string (char array). The syntax is documented quite well in the man page for tcpdump; I leave you to read it on your own. However, we will use simple test expressions, so perhaps you are sharp enough to figure it out from my examples.
在应用我们的过滤器之前,我们必须”编译“它。过滤器表达式被保存在一个正则表达式中(字符数组)。在tcpdump的手册页中,详细的介绍了该表达式的语法;这个文档留给你们自己阅读。然而,我们将使用简单的测试表达式,也许你足够机敏,通过我的一些例子,就能理解它。
To compile the program we call pcap_compile(). The prototype defines it as:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize,
bpf_u_int32 netmask)
The first argument is our session handle (pcap_t *handle in our previous example). Following that is a reference to the place we will store the compiled version of our filter. Then comes the expression itself, in regular string format. Next is an integer that decides if the expression should be "optimized" or not (0 is false, 1 is true. Standard stuff.) Finally, we must specify the network mask of the network the filter applies to. The function returns -1 on failure; all other values imply success.
为了编译程序,我们调用pcap_compile函数。函数原型定义如下:
第一个参数p是我们的会话句柄(就是我们在先前的例子中提到的pcap_t*类型的handle)。第二个参数fp是一个引用,我们将在那里保存过滤器的编译版本。第三个参数str是一个字符指针,它就是我们要编译的表达式,并且是按照正则表达式的格式保存。第四个参数optimize是一个整形变量,它决定了这个表达式是否应该被优化(跟其他标准一样,此处0代表不优化,1代表优化)最后一个参数是netmask,通过这个参数我们具体说明要应用过滤器的网络的子网掩码。当函数执行失败的时候,返回-1;返回其他值代表执行成功。
After the expression has been compiled, it is time to apply it. Enter pcap_setfilter(). Following our format of explaining pcap, we shall look at the pcap_setfilter() prototype:
在表达式被编译之后,是时候去应用它啦。下面介绍pcap_setfilter函数。按照我们解释pcap函数用法的格式,我们先看一看pcap_setfilter函数的原型:
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
This is very straightforward. The first argument is our session handler, the second is a reference to the compiled version of the expression (presumably the same variable as the second argument to pcap_compile()).
这是非常易懂的。第一个参数p是会话句柄,第二个参数fp是对编译版本表达式的一个引用(很有可能和pcap_compile函数的第二个参数是相同的变量)
Perhaps another code sample would help to better understand:
也许再一个代码例子将帮助我们更好的理解上面所讲的:
#include
...
pcap_t *handle; /* Session handle */
char dev[] = "rl0"; /* Device to sniff on */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
struct bpf_program fp; /* The compiled filter expression */
char filter_exp[] = "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 */
if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
fprintf(stderr, "Can't get netmask for device %s\n", dev);
net = 0;
mask = 0;
}
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
return(2);
}
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
This program preps the sniffer to sniff all traffic coming from or going to port 23, in promiscuous mode, on the device rl0.
这个程序让抓包程序准备嗅探所有来自或者发往23号端口的流量,嗅探器处于混杂模式,监视设备rl0。
You may notice that the previous example contains a function that we have not yet discussed. pcap_lookupnet() is a function that, given the name of a device, returns one of its IPv4 network numbers and corresponding network mask (the network number is the IPv4 address ANDed with the network mask, so it contains only the network part of the address). This was essential because we needed to know the network mask in order to apply the filter. This function is described in the Miscellaneous section at the end of the document.
你也许注意到先前的例子中包含了一个我们还未探讨过的函数pcap_lookupnet。通过给定的设备名,该函数返回它的ipv4地址网络号和对应的子网掩码(网络号是IPv4地址和子网掩码做与运算得到的,所以它只包含IPv4地址的网络部分)。这步至关重要的。因为为了应用过滤器,我们需要知道子网掩码。这个函数在文档结尾杂项部分中会有描述。
It has been my experience that this filter does not work across all operating systems. In my test environment, I found that OpenBSD 2.9 with a default kernel does support this type of filter, but FreeBSD 4.3 with a default kernel does not. Your mileage may vary.
我的个人经历表明这个过滤器不能跨所有平台工作。在我的测试环境中,我发现装有默认内核的OpenBSD2.9支持这种类型的过滤器,但是装有默认内核的FreeBSD4.3不能正常工作。这因系统而异。
The actual sniffing
At this point we have learned how to define a device, prepare it for sniffing, and apply filters about what we should and should not sniff for. Now it is time to actually capture some packets.
实际的嗅探器
此时,我们已经学习了如何定义一个设备,让该设备准备被监听,并且应用过滤器,来定义我们要监听的和不要监听的。现在是时候去实际地捕获一些数据包啦。
There are two main techniques for capturing packets. We can either capture a single packet at a time, or we can enter a loop that waits for n number of packets to be sniffed before being done. We will begin by looking at how to capture a single packet, then look at methods of using loops. For this we use pcap_next().
捕获数据包有两种主要的技术。我们可以一次捕获一个数据包,也可以进入一个循环,直到捕获到n个数据包才结束。我们从如何捕获一个数据包开始学习,然后学习使用循环的方法。对于捕获单个数据包,我们使用pcap_next函数。
The prototype for pcap_next() is fairly simple:
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
The first argument is our session handler. The second argument is a pointer to a structure that holds general information about the packet, specifically the time in which it was sniffed, the length of this packet, and the length of his specific portion (incase it is fragmented, for example.) pcap_next() returns a u_char pointer to the packet that is described by this structure. We'll discuss the technique for actually reading the packet itself later.
pcap_next的函数原型相当简单:第一个参数p是我们的会话句柄。第二个参数h是一个指向结构体的指针,这个结构体保存了关于数据包的一般信息,更确切地说是保存了数据包被捕获的时间,长度,以及特定部分的长度(例如,数据包是分片的)。pcap_next函数返回一个指向u_char类型的指针,该指针指向实际的数据包,数据包的内容在结构体h中描述。我们将稍后论述实际阅读数据包的技术。
Here is a simple demonstration of using pcap_next() to sniff a packet.
这是一个简单的示范,讲解了如何使用pcap_next监视一个数据包。
#include
#include
int main(int argc, char *argv[])
{
pcap_t *handle; /* Session handle */
char *dev; /* The device to sniff on */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string */
struct bpf_program fp; /* The compiled filter */
char filter_exp[] = "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);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
/* Find the properties for the device */
if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
fprintf(stderr, "Couldn't get netmask for device %s: %s\n", dev, errbuf);
net = 0;
mask = 0;
}
/* Open the session in promiscuous mode */
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
return(2);
}
/* Compile and apply the filter */
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
/* 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);
}
This application sniffs on whatever device is returned by pcap_lookupdev() by putting it into promiscuous mode. It finds the first packet to come across port 23 (telnet) and tells the user the size of the packet (in bytes). Again, this program includes a new call, pcap_close(), which we will discuss later (although it really is quite self explanatory).
设置为混杂模式,这个应用会监听pcap_lookupdev函数返回的任何设备。这个应用会发现通过23号端口的第一个数据包(telent远程登陆)并且告诉用户数据库的大小,单位是字节。和上一个程序类似,这个程序也包含了一个新的函数调用pcap_close,我们将稍后讨论它(尽管这个函数名本身就解释了这个函数的用途)。
The other technique we can use is more complicated, and probably more useful. Few sniffers (if any) actually use pcap_next(). More often than not, they use pcap_loop() or pcap_dispatch() (which then themselves use pcap_loop()). To understand the use of these two functions, you must understand the idea of a callback function.
另一个我们能使用的技术是更加复杂的,不过很有可能更加有用。现实中,很少有抓包分析程序(如果有的话)使用pcap_next函数。通常情况下,抓包分析程序使用pcap_loop或者pcap_dispatch函数(该函数本身使用了pcap_loop函数)。为了理解这两个函数的使用,你必须理解回调函数的思想。
Callback functions are not anything new, and are very common in many API's. The concept behind a callback function is fairly simple. Suppose I have a program that is waiting for an event of some sort. For the purpose of this example, let's pretend that my program wants a user to press a key on the keyboard. Every time they press a key, I want to call a function which then will determine that to do. The function I am utilizing is a callback function. Every time the user presses a key, my program will call the callback function. Callbacks are used in pcap, but instead of being called when a user presses a key, they are called when pcap sniffs a packet. The two functions that one can use to define their callback is pcap_loop() and pcap_dispatch(). pcap_loop() and pcap_dispatch() are very similar in their usage of callbacks. Both of them call a callback function every time a packet is sniffed that meets our filter requirements (if any filter exists, of course. If not, then all packets that are sniffed are sent to the callback.)
回调函数并不是什么新的概念,它们被非常普遍的使用在许多API中。回调函数中蕴含的概念非常简单。假设有这样一个程序,它正在等待一个某种类型的事件。考虑到这个例子的目的,我们假装程序想让用户在键盘上按下一个键。用户每次按下一个键,我想调用一个函数,这个函数决定这个按键的相应。这个函数就是一个回调函数。每次用户按键,我的程序就会调用回调函数。pcap中也使用了回调函数,不过不是在用户按下一个按键时调用,而是当pcap捕获到一个数据包时被调用。pcap中有两个这种类型的函数,分别是pcap_loop和pcap_dispatch,其中pcap_loop能定义他们的回调函数。这两个函数在使用回调函数的方法上非常相似。每次抓包程序捕获到一个满足我们过滤要求(当然,前提是我们有一些过滤器,如果没有的话,所有被捕获到的数据包会被发送给回调函数)的数据包,这两个函数都会调用一个回调函数。
The prototype for pcap_loop() is below:
pcap_loop函数的原型如下:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
The first argument is our session handle. Following that is an integer that tells pcap_loop() how many packets it should sniff for before returning (a negative value means it should sniff until an error occurs). The third argument is the name of the callback function (just its identifier, no parentheses). The last argument is useful in some applications, but many times is simply set as NULL. Suppose we have arguments of our own that we wish to send to our callback function, in addition to the arguments that pcap_loop() sends. This is where we do it. Obviously, you must typecast to a u_char pointer to ensure the results make it there correctly; as we will see later, pcap makes use of some very interesting means of passing information in the form of a u_char pointer. After we show an example of how pcap does it, it should be obvious how to do it here. If not, consult your local C reference text, as an explanation of pointers is beyond the scope of this document. pcap_dispatch() is almost identical in usage. The only difference between pcap_dispatch() and pcap_loop() is that pcap_dispatch() will only process the first batch of packets that it receives from the system, while pcap_loop() will continue processing packets or batches of packets until the count of packets runs out. For a more in depth discussion of their differences, see the pcap man page.
第一个参数p是我们的会话句柄。第二个参数是一个整形参数cnt,这个参数告诉pcap_loop函数,在返回之前应该监听多少数据包(负数意味着直到有错误发生函数才返回)。第三个参数callback是回调函数的名字(就是它的标识符,不包括括号)。最后一个参数user在一些应用中是有用的,但是很多时候该参数被简单地设为NULL。假设除了pcap_loop传递给回调函数的参数以外,我们还希望传递一些参数给回调函数,我们将会用到这个参数。很显然,你必须对这些要传递的参数进行类型抓换,使其类型为指向u_char的指针,这样才能确保结果正确。正如我们之后将看到的,pcap使用了一些非常有趣的方法传递信息,这些信息是以u_char指针的形式组成的。在我们展示pcap如何使用这个参数的例子之后,你会清楚pcap是如何做这件事的。如果你还没有理解的话,你可以查看一下你的c语言参考书。关于指针的解释超出了这篇文档的写作范围,这里就不加解释啦。pcap_dispatch函数的用法基本和pcap_loop相同。唯一的区别是pcap_dispatch将只处理它从系统收到的第一批数据包,然而pcap_loop将持续处理数据包或几批数据包,直到监听到要求的数量。有关这两个函数区别的更深层次讨论,请看pcap手册。
Before we can provide an example of using pcap_loop(), we must examine the format of our callback function. We cannot arbitrarily define our callback's prototype; otherwise, pcap_loop() would not know how to use the function. So we use this format as the prototype for our callback function:
在我们举例如何使用pcap_loop函数之前,我们必须检查回调函数的格式。我们不能随意的定义回调函数原型;不然,pcap_loop函数将不知道如何使用这个回调函数。所以我们使用如下的格式作为我回调函数的原型:
void got_packet(u_char *args, const struct pcap_pkthdr *header,
const u_char *packet);
Let's examine this in more detail. First, you'll notice that the function has a void return type. This is logical, because pcap_loop() wouldn't know how to handle a return value anyway. The first argument corresponds to the last argument of pcap_loop(). Whatever value is passed as the last argument to pcap_loop() is passed to the first argument of our callback function every time the function is called. The second argument is the pcap header, which contains information about when the packet was sniffed, how large it is, etc.
让我们仔细检查回调函数的更多细节。首先,你将注意到这个函数返回void。这是合乎逻辑的,因为pcap_loop不知道如何处理回调函数的返回值。第一个参数args对应着pcap_loop的最后一个参数user。每次回调函数被调用的时候,无论传递给pcap_loop的最后一个参数是什么,都会被作为回调函数的第一个参数。第二个参数是pcap的首部,包含了数据包是什么时候被监听到的,数据包多大等信息。
The pcap_pkthdr structure is defined in pcap.h as:
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) */
};
These values should be fairly self explanatory. The last argument is the most interesting of them all, and the most confusing to the average novice pcap programmer. It is another pointer to a u_char, and it points to the first byte of a chunk of data containing the entire packet, as sniffed by pcap_loop().
有关pcap_pkthdr结构体的定义在pcap.h头文件中,该结构体中值的名字本身就解释了它们的含义。这里就不赘述啦。回到回调函数原型的探讨中,最后一个参数packet是三个参数中就有趣的,并且对于水平一般的pcap编程新手来说,这个参数也是最令人困惑的。这是另一个指向u_char类型的指针。这个指针指向一大块数据的首字节,这些数据包含了pcap_loop函数监听到的所有数据包。
But how do you make use of this variable (named "packet" in our prototype)? A packet contains many attributes, so as you can imagine, it is not really a string, but actually a collection of structures (for instance, a TCP/IP packet would have an Ethernet header, an IP header, a TCP header, and lastly, the packet's payload). This u_char pointer points to the serialized version of these structures. To make any use of it, we must do some interesting typecasting.
但是如何使用packet变量呢?一个数据包包含了许多属性,正如你能想象到的,这个变量不真的是一个字符串,而是一个结构体的集合(例如,一个TCP/IP的数据包会包括一个以太网报头,一个IP报头,一个TCP报头,最后是数据包装载的信息)。这个指向u_char类型的指针指向这些结构体的序列化版本。为了使用这个变量,我们必须做一些有趣的类型转化。
First, we must have the actual structures define before we can typecast to them. The following are the structure definitions that I use to describe a TCP/IP packet over Ethernet.
首先,在对这个数据包中包含的属性进行类型转换之前,我们必须有关于这些属性的实际的结构体定义。下面是描述在以太网上的TCP/IP数据包的结构体定义。
注:如果你想定义这些结构体,必须了解这些帧或者报的组成形式,详情可以参考以太网,IP,TCP,UDP数据包分析。
/* Ethernet addresses are 6 bytes */
#define ETHER_ADDR_LEN 6
/* Ethernet header */
struct sniff_ethernet {
u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */
u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */
u_short ether_type; /* IP? ARP? RARP? etc */
/*ehter_type的值代表着数据报类型是IP报文,ARP报文还是RARP报文或者其它*/
};
/* IP header */
struct sniff_ip {
u_char ip_vhl; /* version << 4 | header length >> 2 */
/*因为版本字段和首位长度字段各占4位,所以将它们放在一个字节里面存储,所以左移4位得到的是版本字段,因为数据长度是4个字节的整数倍,所以再右移两位得到数据长度(乘16再除4)*/
u_char ip_tos; /* type of service */
u_short ip_len; /* total length */
u_short ip_id; /* identification */
u_short ip_off; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
u_char ip_ttl; /* time to live */
u_char ip_p; /* protocol */
u_short ip_sum; /* checksum */
struct in_addr ip_src,ip_dst; /* source and dest address */
};
//HL 是header length的意思,V是version的意思,为何这么计算参见上面那篇文章IP报头
#define IP_HL(ip) (((ip)->ip_vhl) & 0x0f)
#define IP_V(ip) (((ip)->ip_vhl) >> 4)
/* TCP header */
typedef u_int tcp_seq;
struct sniff_tcp {
u_short th_sport; /* source port */
u_short th_dport; /* destination port */
tcp_seq th_seq; /* sequence number */
tcp_seq th_ack; /* acknowledgement number */
u_char th_offx2; /* data offset, rsvd */
//取得4位首部长度
#define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4)
u_char th_flags;
//定义的这些宏只有1位为1,为了与th_flags做与运算取得标志位的值
#define TH_FIN 0x01
#define TH_SYN 0x02
#define TH_RST 0x04
#define TH_PUSH 0x08
#define TH_ACK 0x10
#define TH_URG 0x20
#define TH_ECE 0x40
#define TH_CWR 0x80
//外国人非常喜欢用宏来写一些像是函数的东西
#define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
u_short th_win; /* window */
u_short th_sum; /* checksum */
u_short th_urp; /* urgent pointer */
};
So how does all of this relate to pcap and our mysterious u_char pointer? Well, those structures define the headers that appear in the data for the packet. So how can we break it apart? Be prepared to witness one of the most practical uses of pointers (for all of those new C programmers who insist that pointers are useless, I smite you).
所有的这些是如何和pcap以及难以理解的指向u_char类型的指针联系在一起呢?哦,那些结构体定义了数据包报头,这些报头出现在数据包数据中。所以我们如何能把他们分离开呢?现在准备好见证指针使用中最实用的一个案例吧(对于所有那些坚持指针无用的c语言编程新手,我会重击你们)
Again, we're going to assume that we are dealing with a TCP/IP packet over Ethernet. This same technique applies to any packet; the only difference is the structure types that you actually use. So let's begin by defining the variables and compile-time definitions we will need to deconstruct the packet data.
再一次,我们假设正在处理以太网上传输的TCP/IP数据包。相同的技术可以应用到任何类型的数据包上;唯一的区别是你实际使用的结构体类型。所以让我们从定义变量和编译时定义开始。之后我们使用这些去析构数据包数据。
/* ethernet headers are always exactly 14 bytes */
#define SIZE_ETHERNET 14
const struct sniff_ethernet *ethernet; /* The ethernet header */
const struct sniff_ip *ip; /* The IP header */
const struct sniff_tcp *tcp; /* The TCP header */
//此处建议将char改为u_char,不然下面类型转换时会有警告
const char *payload; /* Packet payload */
u_int size_ip;
u_int size_tcp;
And now we do our magical typecasting:
ethernet = (struct sniff_ethernet*)(packet);
ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
size_ip = IP_HL(ip)*4;
if (size_ip < 20) {
printf(" * Invalid IP header length: %u bytes\n", size_ip);
return;
}
tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
size_tcp = TH_OFF(tcp)*4;
if (size_tcp < 20) {
printf(" * Invalid TCP header length: %u bytes\n", size_tcp);
return;
}
payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
How does this work? Consider the layout of the packet data in memory. The u_char pointer is really just a variable containing an address in memory. That's what a pointer is; it points to a location in memory.
这段代码是如何工作的?思考一下数据包中数据在存储中的布局。指向u_char类型的指针仅仅是一个变量,它指向的区域包含了内存中的一个地址。那就是一个指针的本质,它指向存储中的一个地址。
For the sake of simplicity, we'll say that the address this pointer is set to is the value X. Well, if our three structures are just sitting in line, the first of them (sniff_ethernet) being located in memory at the address X, then we can easily find the address of the structure after it; that address is X plus the length of the Ethernet header, which is 14, or SIZE_ETHERNET.
出于简化的目的,我们假设这个指针指向的地址是X。如果Ethernet,ip和tcp这三个结构体排成一列,它们中的第一个(sniff_ethernet)位于内存地址X,然后我们能简单地找到它之后的那个结构体地址,那个地址是X加上以太网报头的长度14或者SIZE_ETHERNET(定义的一个宏名,代表14)。
Similarly if we have the address of that header, the address of the structure after it is the address of that header plus the length of that header. The IP header, unlike the Ethernet header, does not have a fixed length; its length is given, as a count of 4-byte words, by the header length field of the IP header. As it's a count of 4-byte words, it must be multiplied by 4 to give the size in bytes. The minimum length of that header is 20 bytes.
相似地,如果我们有一个报头的地址,在这个报头之后的下一个报头地址是该报头地址加上该报头长度。与以太网报头不同,IP报头没有一个固定的长度。它的长度是特定的,为IP报头长度域所包含的4字节个数。由于它是一个4字节个数的计数,所以乘以4之后得到才是IP报头的字节数。IP报头的最小长度是20字节。
The TCP header also has a variable length; its length is given, as a number of 4-byte words, by the "data offset" field of the TCP header, and its minimum length is also 20 bytes.
So let's make a chart:
TCP报头长度也是不固定的;它的长度也是特定的,为截止到TCP报头数据偏移字段的4字节个数。TCP报头的最小长度也是20字节。
让我们画一个图表吧。
The sniff_ethernet structure, being the first in line, is simply at location X. sniff_ip, who follows directly after sniff_ethernet, is at the location X, plus however much space the Ethernet header consumes (14 bytes, or SIZE_ETHERNET). sniff_tcp is after both sniff_ip and sniff_ethernet, so it is location at X plus the sizes of the Ethernet and IP headers (14 bytes, and 4 times the IP header length, respectively). Lastly, the payload (which doesn't have a single structure corresponding to it, as its contents depends on the protocol being used atop TCP) is located after all of them.
sniff_ethernet结构体位于列首,很容易得到它的位置为X。接跟在sniff_ethernet之后的是sniff_ip结构体,它的位置为X加上以太网报头占用的空间(以太网报头占用14个字节或者SIZE_ETHERNET)。在上述两个结构体之后的是sniff_tcp结构体,所以它的位置是X加上以太网报头占用的空间和IP报头占用的空间(分别是14个字节和IP报头长度域值的4倍)。最后,有效负荷(由于它的内容依赖于TCP层之上的协议,所以没有一个单独的结构体与之对应)地址位于X加上所有上述3个结构体的长度。
So at this point, we know how to set our callback function, call it, and find out the attributes about the packet that has been sniffed. It's now the time you have been waiting for: writing a useful packet sniffer. Because of the length of the source code, I'm not going to include it in the body of this document. Simply download sniffex.c and try it out.
此时,我们知道如何设置我们的回调函数,调用它并且找出被监听到的数据包的属性。现在就是你期待已久的时刻:写一个有用的抓包分析工具。由于源代码的长度,我将不会将它放在正文中。你可以简单的下载sniffex.c文件并且尝试它。
Wrapping Up
At this point you should be able to write a sniffer using pcap. You have learned the basic concepts behind opening a pcap session, learning general attributes about it, sniffing packets, applying filters, and using callbacks. Now it's time to get out there and sniff those wires!
此时你应该能使用pcap写一个抓包分析工具啦。你已经学习了一些基本的概念,包括开启一个pcap会话,了解它的通用属性,监听数据包,应用过滤器以及使用回调函数。现在是时候出去大展身手并且监听那些线路啦。
This document is Copyright 2002 Tim Carstens. All rights reserved. Redistribution and use, with or without modification, are permitted provided that the following conditions are met:
这篇文档版权归属Tim Carstens。所有权利被保留。下面提出的条件被满足时,重新发布和使用,修改或者不修改是被允许的。
Redistribution must retain the above copyright notice and this list of conditions.
重新发布必须保留上述的版权记录和这个条件列表。
The name of Tim Carstens may not be used to endorse or promote products derived from this document without specific prior written permission.
在未经过明确的书面许可之前,来自这篇文档的名字Tim Carstens不被用来背书或者推广产品
/* Insert 'wh00t' for the BSD license here */