您的位置:首页 > 理论基础 > 计算机网络

用pcap编写网络嗅探器(Programming with pcap译文)

2010-10-05 00:00 211 查看
首先明确本文的阅读对象。显然,你需要一些C语言的基本知识,除非只想了解pcap的基本原理。你不用是一个编程高手;对于想深入 了解该领 域的编程者,我保证会尽量详细描述相关概念。另外,网络方面的一些知识对阅读本文是有帮助的。本文给出了一个网络包嗅探器,所有的代码已在默认内核 的FreeBSD 4.3上测试通过。

开始: pcap应用程序的格局

首先要了解的是pcap嗅探器的总体布局。代码流程如下:

我们首先要做的是决定要嗅探的接口。在Linux里它可能是eth0,BSD里可能是xl1等等。我们可以用字符串定义它,也可 以询问pcap得到所要使用的接口的名称。

初始化pcap。这里我们要告诉pcap对什么设备进行嗅探。如果愿意,我们可以嗅探多个设备。如何区分它们呢?答案是文件句柄 (File Handle)。和打开文件读写一样,我们必须为我们的嗅探“会话(session)”命名,以便区分其它的任务会话。

如 果我们只想嗅探特定通信(例如:仅TCP/IP包,仅流向23端口的包等等),我们必须建立一个规则集,“编译”之,然后应用它,这三个步骤关系密切。规 则集用一个字符串保存并转换成pcap认识的格式(因此要编译它),编译工作实际上只是在程序中调用一个函数,不涉及外部程序。然后告诉pcap在我们指 定的会话上应用这个规则。

最后,我们让pcap进入它的主循环。这时,pcap等待有数据包流入。每次得到新数据包,它就会调用我们指定的函数,我们可以 在这个函数里做任何我们想做的事:它可以解析数据包并输出给用户,也可以把数据保存成一个文件,或者什么也不做。

在嗅探到我们需要的东西以后,关闭会话,任务完成。

实际上这是一个很简单的过程。总共五步,其中一步是可选的(就是那个使你感到困惑的第三步)。下面我们开始研究每个步骤及如何实现它 们。

设置嗅探设备

这一步极其简单。有两种方法来设置我们要嗅探的设备。

第一种是简单地让用户告诉我们,考虑下面的程序:

#include <stdio.h>

#include <pcap.h>

int main(int argc, char *argv[])

{

char *dev = argv[1];

printf("Device: %s ", dev);

return(0);

}

用户指定设备名作为程序的第一个参数。现在,字符串"dev"保存了我们要嗅探的,pcap可以认识的接口名(当然,假设用户给我们的 是真实的接口)。

另一种方法也同样简单,看这个程序:

#include <stdio.h>

#include <pcap.h>

int main(int argc, char *argv[])

<
3ff0
li class="alt"> {

char *dev, errbuf[PCAP_ERRBUF_SIZE];

dev = pcap_lookupdev(errbuf);

if (dev == NULL) {

fprintf(stderr, "Couldn't find default device: %s ", errbuf);

return(2);

}

printf("Device: %s ", dev);

return(0);

}

在这里,由pcap自己来设置设备。“等一下,Tim,”你会说:“errbuf字符串代表什么?”。很多pcap命令允许我们把这个 字符串作为参 数。它的用途是:如果命令执行失败,它会得到出错的细节信息。在这段代码里,如果pcap_lookupdev()失败,errbuf会保存有一个错误信 息。很好,不是吗?

打开设备

建立嗅探会话的任务真的很简单,用pcap_open_live()就可以了。这个函数的原型(取自pcap man)如下:

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

第一个参数是设备名,我们在上一节已经介绍过了。snaplen是一个整型值,定义由pcap抓取的包的最大字节数。 promisc,当设置为true时,使接口处于混杂模式(不管怎样,即使设置为false,在一些特定情形下接口可能还是处于混杂模式)。to_ms是 读超时(read time out),单位为毫秒(0表示没有超时;在一些平台上,这意味着你可能会一直等待直到收到足够数量的包,所以你应该使用一个非零值)。最后,ebuf用于 保存出错信息(就象我们前面的errbuf)。函数返回会话句柄。

为了演示,考虑这个代码段:

#include <pcap.h>

...

pcap_t *handle;

handle = pcap_open_live(somedev, BUFSIZ, 1, 1000, errbuf);

if (handle == NULL) {

fprintf(stderr, "Couldn't open device %s: %s ", somedev, errbuf);

return(2);

}

这个代码打开"somedev"字符串指定的设备,告诉它每次读BUFSIZ字节(定义于pcap.h中),置设备于混杂模式。嗅探直 到有错误发生,错误信息保存到errbuf中,用于后面的错误信息输出。

关于混杂模式vs.非混杂模式:这是两个非常不同的风格。非混杂模式嗅探只监听与本地有直接关系的包。只有发往、源自或本地路由的包会 被嗅 探器捕获。另一方面,混杂模式监听所有线上的通信。在无交换环境中(non-switched environment),所以网络通信都会被监听。它可以让我们得到更多的包,但是,这是可以被检测的:可 以通过测试强可靠性来发现网络中是否有主机正在以混合模式监听,另外混杂工作模式仅仅在非交换式的网络中有效,而且在一个高负载的网络环境中,混杂模式将 消耗大量的系统资源。

通信过滤

通常我们只对特定网络通信感兴趣。比如我们只打算嗅探23端口(telnet)用于搜索密码信息,或者劫持发往21端口的文件 (FTP), 也可能是DNS通信(port 53 UDP)。无论哪种情形,我们很少会盲目地嗅探所有的网络通信。考虑使用pcap_compile()和 pcap_setfilter()函数。

这个步骤也是相当的简单。调用pcap_open_live()之后我们已经有了一个可用的嗅探会话,可以应用我们的过滤器。为什么不 用if/else语句?两个原因:首先,pcap的过滤器有更好的效率,因为它直接作用于BPF(BSD Packet Filter)同时又减少了直接操作BPF所需的大量步骤。第二,这样简单得多:)

应用我们的过滤器之前,我们必须“编译”它。过滤器表达式为一个规则字符串(char数组)。在tcpdump的man文档里有其语法 的说明 书。阅读语法说明的工作得你自己去做。尽管如此,我们将会尽量使用简单的过滤器表达式,因此你也许足够聪明从而可以从我的例子中领悟出来。

通过pcap_compile()函数来“编译”。它的原型为:

int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize,
bpf_u_int32 netmask)

第一个参数是我们的会话句柄(前文的例子里是pcap_t *handle)。接下来的参数用于指向存放编译后过滤器的空间。然后是过滤表达式。下一个optimize整数决定表达式是否是“优化的”(0为 false、1为true)。最后,我们要指定网络掩码。函数失败时返回-1;其它值表示成功。

表达式被编译之后,就可以应用它了。使用pcap_setfilter()函数,下面是pcap_setfilter()原型:

int pcap_setfilter(pcap_t *p, struct bpf_program *fp)

很直白,第一个参数是会话句柄,第二个是编译后的表达式。

也许这个代码示例可以帮助你更好地理解:

#include <pcap.h>

...

pcap_t *handle; /* 会话句 柄 */

char dev[] = "rl0"; /* 被嗅探的 设备 */

char errbuf[PCAP_ERRBUF_SIZE]; /* 错误信息 */

struct bpf_program fp; /* 编译后的过滤表达式 */

char filter_exp[] = "port 23"; /* 过 滤表达式 */

bpf_u_int32 mask; /* 嗅探设备的网络掩码 */

bpf_u_int32 net; /* 嗅探设备 的IP */

if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {

fprintf(stderr, "Can't get netmask for device %s ", 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 ",

somedev, errbuf);

return(2);

}

if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {

fprintf(stderr, "Couldn't parse filter %s: %s ",

filter_exp, pcap_geterr(handle));

return(2);

}

if (pcap_setfilter(handle, &fp) == -1) {

fprintf(stderr, "Couldn't install filter %s: %s ",

filter_exp, pcap_geterr(handle));

return(2);

}

这个程序在rl0设备上以混杂模式嗅探发往或源自23端口的所有通信。

你可能注意到了,这个例子中有一个之前没讨论过的函数:pcap_lookupnet()。给一个设备名,得到它的IP和网络掩码。为 了应用过滤器,我们就要知道网络掩码,这个函数就可以派上用场了。

经试验发现这个过滤器并不能在所有的操作系统上正常工作。在我的测试环境中,我发现OpenBSD 2.9支持这种过滤器,而FreeBSD 4.3却不行。

正式开始嗅探

到这里我们已经学习了如何定义设备、准备嗅探以及应用过滤器来过滤我们不想嗅探的部分。现在是时候嗅探数据包了。

嗅探数据包有两种主要方法。我们可以一次捕获一个单独的包,也可以进入一个循环,等待N个包流入。我们首先关注如何捕获单个包,之后再 研究循环的方法。就单个包而言,我们用pcap_next()。

pcap_next()的原型很简单:

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

首个参数是会话句柄。第二个参数是一个指针,它指向的结构用于存放数据包的一般信息,如捕获的时间,包长度,组成包的各部分长度。 pcap_next()返回的*u_char指向捕获的包,稍后我们将会讨论读取数据包本身的方法。

这个例子演示怎样使用pcap_next()来嗅探数据包:

#include <pcap.h>

#include <stdio.h>

int main(int argc, char *argv[])

{

pcap_t *handle; /* 会话句柄 */

char *dev; /* 嗅探的设备 */

char errbuf[PCAP_ERRBUF_SIZE]; /* 错误信息 */

struct bpf_program fp; /* 编译的过滤器 */

char filter_exp[] = "port 23"; /* 过 滤表达式 */

bpf_u_int32 mask; /* 网 络掩码 */

bpf_u_int32 net; /* IP */

struct pcap_pkthdr header; /* pcap头 */

const u_char *packet; /* 数据包 */

/* 定义设备 */

dev = pcap_lookupdev(errbuf);

if (dev == NULL) {

fprintf(stderr, "Couldn't find default device: %s ", errbuf);

return(2);

}

/* 取得设备属性 */

if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {

fprintf(stderr, "Couldn't get netmask for device %s: %s ",

dev, errbuf);

net = 0;

mask = 0;

}

/* 以混杂模式打开会话 */

handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);

if (handle == NULL) {

fprintf(stderr, "Couldn't open device %s: %s ",

somedev, errbuf);

return(2);

}

/* 编译并应用过滤器 */

if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {

fprintf(stderr, "Couldn't parse filter %s: %s ",

filter_exp, pcap_geterr(handle));

return(2);

}

if (pcap_setfilter(handle, &fp) == -1) {

fprintf(stderr, "Couldn't install filter %s: %s ",

filter_exp, pcap_geterr(handle));

return(2);

}

/* 抓取一个数据包 */

packet = pcap_next(handle, &header);

/* 输出数据包长度 */

printf("Jacked a packet with length of [%d] ",

header.len);

/* 关闭会话 */

pcap_close(handle);

return(0);

}

这个程序用pcap_lookupdev()取得设备并将其设置为混杂模式,然后开始嗅探。它取得23端口(telnet)上的首个包 后输出这个包 的大小(字节)。此外,这个程序有一个新的函数:pcap_close(),我们等会儿再讨论它(尽管函数名已经说明了一切)。

另一种方法要复杂一些,不过更有用。通常很少有嗅探器直接调用pcap_next()函数(如果有的话),它们更常用的是 pcap_loop()或pcap_dispatch()。要学会这两个函数,你必须先理解回调函数。

回调函数并不是新鲜事物,它们在不少API中普遍存在。回调的概念很简单。假设我的程序要等待某个事件,为简单起见,就说是等待用户输 入 吧。用户每按一次键,我想要通过函数来决定接下来做什么。这个函数就可以是回调函数,每次按下键盘,我的程序就会调用这个回调函数。回到pcap中,回调 函数被调用的时机由用户按下一个键改为pcap嗅探到一个数据包。pcap_loop() 和 pcap_dispatch() 的用法很相似。每次嗅探到一个符合过滤要求(如果存在过滤器的话)的包后就会调用回调函数。

pcap_loop()的原型是:

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

首个参数是会话句柄。接下来的cnt参数告诉pcap_loop()返回之前应该嗅探到多少个包(负数表示一直嗅探直到出错为止)。第 三个就是之前讨论的回调函数啦。最后一个参数的作用是传递附加的自定义数据给回调函数,在 一些应用时有用,很多时候直接设为NULL就行。后面我们将会以例子的形式看到pcap用u_char指针传递一些很有意思的信息。 pcap_dispatch()的用法几乎一样,唯一的区别是pcap_dispatch()只处理第一批从系统中收到的包,而pcap_loop()会 继续处理接下来的包直到达到指定数量为止。关于它们的细节差异,请参考pcap的man文档。

拿出cap_loop()的例子之前,我们得了解一下回调函数的原型:

void got_packet(u_char *args, const struct pcap_pkthdr *header,
const u_char *packet);

首先,它是一个无返回值的函数。这是可以理解的,因为pcap_loop()不知道怎样处理回调函数的返回值。

第一个参数就是我们传给pcap_loop()的最后一个数据。每次回调函数被调用时都可以取得这个数据。

第二个参数是pcap头结构,它含有包何时到达,多大等信息。pcap_pkthdr结构定义于pcap.h之中:

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) */
};

结构成员名称完全可以自解释了。

最后的那个参数const u_char *packet是我们最关心的,也是最容易引起pcap初学者混乱的。它是一个u_char指针,指向被pcap_loop()嗅探到的整个数据包的第一 个字节。

怎样使用这个packet参数呢?数据包有很多属性,只要思考一下就会知道,它不是一个真正的字符串,而是一系列的结构(例如, TCP/IP包应该有以太头、IP头、TCP头,最后,还有包的载荷)。这个u_char指针指向的正是这些数据结构的序列化版本,所以在使用之前,要做 类 型转换工作。

首先,我们要定义这些结构,下面定义的是以太网TCP/IP数据包结构。

/* 以太网的地址占6字节 */
#define ETHER_ADDR_LEN 6

/* 以太网头 */
struct sniff_ethernet {
u_char ether_dhost[ETHER_ADDR_LEN]; /* 目的地址 */
u_char ether_shost[ETHER_ADDR_LEN]; /* 源地址 */
u_short ether_type; /* IP? ARP? RARP? 等 */
};

/* IP 头 */
struct sniff_ip {
u_char ip_vhl; /* version << 4 | header length >> 2 */
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 */
};
#define IP_HL(ip) (((ip)->ip_vhl) & 0x0f)
#define IP_V(ip) (((ip)->ip_vhl) >> 4)

/* TCP 头 */
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 */
#define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4)
u_char 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 */
};

注意:我发现在我的Slackware Linux 8(2.2.19内核)里不能编译这个结构。问题出现在include/features.h里,除非在包含它之前定义_BSD_SOURCE,否则将以 POSIX接口实现。所以建议在包含所有头文件之前先加入一行:

#define _BSD_SOURCE 1

这样能确保使用BSD风格的API。当然,如果你不想用预定义,你可以简单地改一下结构,就象我在这里做的那样。

那么,如何把我们神秘的u_char指针应用到pcap工作中来呢?嗯~~这些结构定义了包中的头部数据,那怎样提取这些部分呢?准备 见证指针的典型应用之一吧。

我们还是假设处理以太网的TCP/IP包。同样的方法可用于任何数据包,唯一的区别是你实际所使用的结构类型。让我们从用于解析数据包 的变量声明及预处理定义开始:

/* 以太网头总是14字节 */

#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 */

const char *payload; /* Packet payload */

u_int size_ip;

u_int size_tcp;

现在开始神奇的类型转换:

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 ", 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 ", size_tcp);

return;

}

payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);

它怎样工作?考虑一下数据包在内存中的布局。u_char指针只是一个包含内存地址的变量,这就是指针的实质,指出内存所在位置。

为了简单起见,就说这个指针指向的地址为X吧。如果我们的这三个结构是线性存储的,那么第一个(sniff_ethernet)结构就 位于地址为X的内存上,接下来我们可以很简单地找到后面的结构:X地址加上14(或SIZE_ETHERNET)字节的以太网头长度。

简而言之,如果我们有头地址,那么后面的结构地址就是当前头地址加上头长度。IP头和以太网头不一样,这的长度是不固定的。它的长度由 它的成员指定,以字(4byte)为单位,所以得到字节长度还得剩上4。最小长度是20字节。

TCP头也是变长的,同样以4字节为一个单位,最小长度也是20字节。

我们来做个表格:

变量 位置 (bytes)
sniff_ethernet X
sniff_ip X + SIZE_ETHERNET
sniff_tcp X + SIZE_ETHERNET + {IP header length}
payload X + SIZE_ETHERNET + {IP header length} + {TCP header length}
第一行的sniff_ethernet结构,正好在X处。sniff_ip,紧跟在sniff_ethernet之后,为X加上以太网 头所占空间(14字节,或SIZE_ETHERNET)。sniff_tcp在sniff_ip后面,因此它的位置是X加上以太网头和IP头的大小。最 后,payload (它不是一个单一的结构,这与上层的协议有关)位于最后。

到这里,我们了解了如何调用回调函数,调用它以及找到嗅探到的包的属性。你所期待的时刻到了:写一个有用的嗅探器。由于代码长度的关 系,我不打算把它放到本文中。你可以到这里下载 sniffex.c并测试它。

Wrapping Up

现在你应该能够用pcap写一个嗅探器了。你已经学习了打开pcap会话、关于它的属性、嗅探数据包、应用过滤器和回调函数的基本概 念。是时候开始嗅探数据了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  pcap 嗅探器