基于libcap、winpcap的网络编程

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

第五章基于libpcap的网络编程技术
5. 1常见的包捕获机制简介
包捕获就是利用以太网的介质共享的特性,通过将网络适配器设置为混杂模式的方法,
接收到所有网络上的以太网帧。

包捕获的机制大致可以分为两类:一类是由操作系统内核提供的捕获机制。

另一类是由应用软件或系统开发包捕获驱动程序提供的捕获机制。

常见的包捕获机制如表5-1所示。

其中最主要的是下列4种:
BPF ( Berkeley Packet Filter )
DLPI ( Data Link Provider In terface )
NIT ( Network In terface Tap ) SOCK-PACKET 类型套接口。

BPF由基于BSD的Unix系统内核所实现。

DLPI是Solaris (和其他System V UNIX ) 系统的内嵌子系统。

NIT是SUN OS4 系统的一部分,但在Solaris /SUN OS5 中被DLPI 所取代。

Linux核心则实现了SOCK-PACKET 的包捕获机制。

从性能上看,BPF比DLPI 和NIT 好得多,SOCK-PACKET 最弱。

表5-1常用的包捕获机制
由于现在很多局域网为NT网,其网络传输方式大多采用以太网标准,所以涉及的编程
也是在Windows 环境下实现的。

Windows 操作系统没有提供包捕获机制,只提供了数量很少并且功能有限的API调用。

在Windows 环境下由于其自身的封装性,很难对其底层进行编程。

本章将对BSD系列的libpcap进行深入地介绍。

5.2 Libpcap 与 BPF
(1
) libpcap 概述
libpcap(Packet Capture
library)
,即数据包捕获函数库。

该库提供的 C 函数接口可
用于捕获经过网络接口 (只要经过该接口,目标地址不一定为本机)的数据包。

它是由洛仑兹 伯克利
试验室的研究人员 Steven McCanne 和Van Jacobson 于1993 年在Usenix'93 会议上正式提出的一种用于 Unix 内核数据包过滤体制。

该函数库支持Linux 、Solaris 和
BSD 系统平台。

采用libpcap 可以捕获本地网络数据链路层上的数据。

libpcap 库是基于 BPF(Berkeley Packet Filter : BSD 包过滤器)系统的。

BPF 是 BSD 系统在的TCP/IP 软件在实现的时候所提供的一个接口,
通过这个接口,外部程序可以得到
到达本机的数据链路层网络数据,
的数据包。

同时也可以设置过滤器, 嵌入到网络软件中, 获得过滤后
(2)BPF 结构及工作原理
是一种由 steven McCanne 和 Van Jacobson 提出的
图5-1 BPF 工作原理
络适配器的数据包。

win pcap
的核心网络驱动程序也是采用这种结构的。

它主要由两部分
组成:Network tap 和 Packet Filter o Network tap 是一个回调函数(callback function ), 并不是直接由BPF 执行,当一个新的数据包到达时,由网络适配器的设备驱动程序激活, 它从网络拷贝每个数据包,并且将它们分发到对应的应用程序。

Packet filter 来决定一个
BPF 或者 BSD Packet Filter
内核包捕获的体系结构,它使得
UNIX 下的应用程序通过一个高度优化的方法读取流经网
数据包是否需要进行接收和拷贝到相应的应用程序,同时还提供一种很复杂的功能,可以确
定需要进行拷贝的数据的长度。

BPF的工作原理如图5-1所示。

由Network tap不断地从网络适配器获得数据包,通过packet filter的判断后,拷
贝数据到内核store buffer ,当store buffer 填满后,将和hold buffer 进行数据交换,
再由hold buffer 将数据拷贝到处于用户层的user buffer 中。

由于采用这种内核过滤、数据包双缓冲、数据包批拷贝的方式,减少了用户态进程调用内核的次数,极大地提高了数据包的处理能力。

在系统实现的过程中可以充分利用BPF过滤模型的这种特性,通过根据不
同的网络侦听的需求,如针对特定的协议、特定的机器等,设置内核过滤器,减少捕获的数据的大小,同时增加内核缓冲区的大小,使系统能够获得一个很好的性能。

BPF的结构如图5-2所示。

libpcap在BPF的基础上提供了实用的接口函数供其它程序调用,既可以捕获网络上的数据,也可以打开tcpdump和tcpslice格式的数据,进行分析。

同时也可以根据需要,设置过滤器,获得感兴趣的数据包。

可以编辑简单的BPF过滤器对数据进行过滤,获得指定的
IP包、TCP包等。

如下所示:可以用tcpdump的过滤表达式进行过滤:
tcpdump tcp 21
表示获得FTP端口(21)的所有的TCP数据,即通过21端口进出的数据,其它的数据全
图5-2 BPF 的结构
Libpcap软件包可从/ 下载,然后依此执行下列三条命令即可
安装,但如果希望libpcap能在linux上正常工作,则必须使内核支持"packet"协议,也即在
编译内核时打开配置选项CONFIG_PACKET(选项缺省为打开)。

./con figure
./make
./make install
libpcap 库函数与数据结构
) libpcap 库函数
libpcap 所提供的主要函数如下:
pcap_t *pcap_open_live(); 用于获取一个包捕获描述符 char *pcap_lookupdev(); 返回一个适于 pcap_open_live()
和 pcap_lookupnet()
函数使用的指向网络设备的指针
int pcap_lookupnet(); 用于判断与网络设备相关的网络号和掩码 或 int pcap_loop(); 收集和处理数据包
将一个包输出到由 pcap_dump_open ()打开的保存文件中
int pcap_compile(); 用于将过滤规则字符串编译成一个内核过滤程序 int pcap_setfilter(); 设定一个过滤程序
int pcap_datalink(); 返回数据链路层类型,如 10M 以太网, SLIP , PPP , FDDI ,
ATM , IEEE802.3 等
void pcap_close(); 关闭关联文件并回收资源 int pcap_stats(pcap_t *, struct pcap_stat *);
int pcap_read(pcap_t *, int cnt, pcap_handler , u_char *);
除了上面列出的主要函数外, libpcap 还提供一些其他的函数,将在后面介绍。

dump 文件格式: 文件头: struct pcap_file_header {
bpf_u_int32 magic;
// 0xa1b2c3d4 u_short version_major;
u_short version_minor; bpf_int32 thiszone; bpf_u_int32 sigfigs; bpf_u_int32 snaplen; bpf_u_int32 linktype;
}; 每一个包的包头和数据 struct pcap_pkthdr { struct timeval ts; };
5.3
(1 1) 2)
3) 4) 5) 6) 7) 8)
9) 10) 11)
int pcap_dispatch() void pcap_dump();
bpf_u_int32 caplen; bpf_u_int32 len;
(2 ) libpcap 数据结构
相关的数据结构:
typedef void (*pcap_ha ndler)(u_char *, const struct pcap_pkthdr *,c onst u_char *); typedef struct pcap pcap_t;
struct bpf_program {u_int bf_le n; struct bpf_ insn *bf_ insn s;};
下面列出libpcap 应用过程中的几个关键函数。

获取设备名
char *pcap_lookupdev(char *errbuf)
该函数用于返回可被 pcap_open_live() 或pcap_lookup net() 函数调用的网络设备
名(一个字符串指针)。

如果函数出错,则返回NULL ,同时errbuf 中存放相关的错误消息。

获取网络号和掩码
int pcap_lookup net(char *device, bpf_ u_in t32* netp,bpf_ u_in t32 *maskp, char *errbuf)
(3 )利用libpcap 函数库开发应用程序的基本步骤
图5-3说明了 libpcap 的应用步骤。

图5-3 libpcap 的调用流程
获得指定网络设备的网络号和掩码。

netp 参数和 maskp 参数都是 bpf_u_int32 指针。

如果函数出错,则返回 -1 ,同时 errbuf 中存放相关的错误消息。

打开设备 pcap_t * pcap_open_live ( char * device, int snaplen, int promisc, int to_ms, char *
errbuf ); 获得用于捕获网络数据包的数据包捕获句柄,后续很多 libpcap 函数将使用该句柄,类 似
于文件操作函数频繁使用文件句柄。

device 参数为指定打开的网络设备名,比如 "eth0 。

snaplen 参数定义捕获数据的最大字节数。

promisc 指定是否将网络接口置于混杂模式。

to_ms 参数指定超时时间(毫秒),测试下来的结论,
0 可能代表永不超时。

errbuf 参数则仅在 pcap_open_live() 函数出错返回 NULL 时用于传递错误消息。

/usr/include/pcap.h
/* Placeholder for filter code if bpf not in kernel. */
errbuf[PCAP_ERRBUF_SIZE];
};
编译、优化、调试过滤规则
int pcap_compile ( pcap_t * p, struct bpf_program * fp, char * str
,
int optimize, bpf_u_int32 netmask );
该函数用于解析过滤规则串,填写 bpf_program 结构。

str 指向过滤规则串,格式参看 tcpdump 的 man 手册,比如:
tcpdump -x -vv -n -t ip proto \\tcp and dst 192.168.8.90 and tcp[13] \& 2 = 2
pcap-int.h struct pcap
{
int 里定义了 struct pcap {} fd;
int
snapshot;
int
linktype;
int
tzoff; /* timezone offset */
int
offset;
/* offset for proper alignment */
struct pcap_sf sf;
struct pcap_md
md;
int
bufsize;
/* Read buffer */
u_char * buffer;
u_char *
bp;
int
cc;
u_char *
pkt
;
/* Place holder for pcap_next()
typedef struct pcap pcap_t;
struct bpf_program fcode;
*/
char
这条过滤规则将捕捉所有携带 SYN 标志的到192.168.8.90 的TCP 报文。

过滤规则串可以是 空串 (" ") ,表示抓取所有过路的报文。

optimize 为 1 表示对过滤规则进行优化处理。

netmask 指定子网掩码,一般从 pcap_lookupnet() 调用中获取。

返回值小于零表示 调用失败。

这个函数可能比较难于理解,涉及的概念源自 BPF ,Linux 系统没有这种概念,但是
libpcap 采用pcap_compile() 和pcap_setfilter() 结合的办法屏蔽了各种链路层支持的不 同,无论是 SOCK_PACKET ,还是 DLPI 。

int pcap_setfilter ( pcap_t * p, struct bpf_program * fp );
该函数用于设置pcap_compile() 解析完毕的过滤规则。

fp 参数是bpf_program 结构 指针,通常取自 pcap_compile() 函数调用。

出错时返回 -1 ;成功时返回 0 。

抓取下一个数 据包 抓取数据包
int pcap_dispatch ( pcap_t * p, int cnt, pcap_handler callback, u_char * user );
捕获并处理数据包,即该函数用于捕捉报文、分发报文到预先指定好的处理函数 (回调
函数 )。

其中, cnt 参数指定函数返回前所处理数据包的最大值, pcap_dispatch() 接收够 cnt 个报文便返回。

Cnt 为-1 表示在一个缓冲区中处理所有的数据包。

如果 cnt 为0 ,仅当发
生错误、读取到EOF 或者读超时到了 (pcap_open_live 中指定)才停止捕捉报文并返回。

callback 参数指定一个带有三个参数的回调函数用于处理 pcap_dispatch() 所捕获的报 文。

回调函数的三个参数为:
typedef void ( *pcap_handler ) ( u_char *, const struct pcap_pkthdr *, const u_char * );
一个从 pcap_dispatch() 函数传递过来的 u_char 指针,一个 pcap_pkthdr 结构的指 针,和一个数据包大小的 u_char 指针。

如果成功则 pcap_dispatch() 返回捕捉到的报文个 数,如果在读取静态文件(以前包捕捉过程中存储下来的
)时碰到EOF 则返回0。

返回-1表示
发生错误,此时可以用 pcap_perror() 、 pcap_geterr() 显示错误信息。

/usr/include/pcap.h
/* * Each packet in the dump file is prepended with this generic header * This gets around the problem of different headers for different * packet interfaces. */ struct pcap_pkthdr
{
/* time stamp caplen; /* length
of portion present
struct timeval
ts; bpf_u_int32 */
*/
};
/usr/include/net/bpf.h
/* * Structure prepended to each packet. */ struct bpf_hdr
plus alignment padding) */
};
/* * Because the structure above is not a multiple of 4 bytes, some compilers * will insist on inserting padding; hence, sizeof(struct bpf_hdr) won't work.
* Only the kernel needs to know about it; applications use bh_hdrlen. */ #ifdef KERNEL #define SIZEOF_BPF_HDR 18 #endif
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
功能基本与 pcap_dispatch() 函数相同,只不过此函数在 cnt 个数据包被处理或出现错 误时才返回,但读取超时不会返回。

而如果为 pcap_open_live() 函数指定了一个非零值的 超时设置,然后调用 pcap_dispatch() 函数,则当超时发生时 pcap_dispatch() 函数会返 回。

cnt 参数为负值时
pcap_loop() 函数将始终循环运行,除非出现错误。

例如: pcap_loop(pd, 10, eth_printer , NULL); 主循环 ,开始抓包 , 共抓 10 个(由第二 个参数指定 ), 抓到包后就进入函数 eth_printer
u_char *pcap_next(pcap_t *p,
structpcap_pkthdr *h)
返回指向下一个数据包的 u_char 指针。

关闭
void pcap_close(pcap_t *p)
关闭p 参数相应的文件,并释放资源。

出错处理
象其它库一样 ,libpcap 也有自己的错误处理机制。

基本上每个函数都有返回值 ,出错时 返回值小于零。

void pcap_perror(pcap_t *, char *); char *pcap_strerror(int); char *pcap_geterr(pcap_t *);
在 pcap_t 中有一个成员存了错误字串
struct pcap {
{
struct timeval bh_tstamp; bpf_u_int32 bh_caplen; bpf_u_int32
bh_datalen;
u_short bh_hdrlen;
/* time stamp */
/* length of captured portion */ /* original length of packet */ /* length of bpf header (this struct
char errbuf[PCAP_ERRBUF_SIZE];
};
其他的辅助函数pcap_read() 打开设备pcap_next() 循环包捕获
脱机方式监听部分:
Libpcap 支持脱机监听。

即先将网络上的数据截获下来,存到磁盘上,再以后方便时从磁盘
上获取数据来做分析。

主要函数为
pcap_open_offline()
pcap_offline_read ()
FILE *pcap_file(pcap_t *p) 返回被打开文件的文件名。

int pcap_fileno(pcap_t *p) 返回被打开文件的文件描述字号码。

int pcap_snapshot(pcap_t *); 返回最长抓多少字节, 就是在pcap_open_live 中第二个参数设置int pcap_stats(pcap_t *, struct pcap_stat *);
计数,共抓了多少过滤掉了多少
struct pcap_stat {
u_int ps_recv; /* number of packets received */
u_int ps_drop; /* number of packets dropped */
u_int ps_ifdrop; /* drops by interface XXX not
yet supported */
};
int pcap_datalink(pcap_t *); 返回网络类型,如DLT_EN10MB 就是10M 以太网int pcap_major_version(pcap_t *); 返回libpcap 主版本号int pcap_minor_version(pcap_t *); 返回libpcap 次版本号
5.4 Libpcap 的应用实例
上面详细说明了libpcap 的调用过程和步骤,下面通过两个具体的例子来说明libpcap 库的应用。

例5-1 一个简单的Libpcap 测试程序(1 )test.CXX
#ifdef __cplusplus
extern "C" {
#endif
#include <pcap.h> #ifdef __cplusplus }
#endif void printer(u_char * user, const struct pcap_pkthdr * h, const u_char * p) {
printf("I get one packet! ");
}
#define DEFAULT_SNAPLEN 68
int main()
{
char ebuf[PCAP_ERRBUF_SIZE];
char *device = pcap_lookupdev(ebuf);
bpf_u_int32 localnet, netmask; pcap_lookupnet(device, &localnet, &netmask, ebuf); printf("%u.%u.%u.%u", localnet&0xff, localnet>>8&0xff, localnet>>16&0xff, localnet>>24&0xff);
printf(":%d.%d.%d.%d ", netmask&0xff, netmask>>8&0xff, netmask>>16&0xff, netmask>>24&0xff);
struct pcap_t *pd = pcap_open_live(device, DEFAULT_SNAPLEN, 0, 1000, ebuf);
if(pcap_datalink(pd) == DLT_EN10MB) printf("10Mb 以太网");
struct bpf_program fcode; pcap_compile(pd, &fcode, NULL, 1, 0);
pcap_setfilter(pd, &fcode);
pcap_loop(pd, 10, printer, NULL);
struct pcap_stat stat; pcap_stats(pd, &stat);
printf("recv %d, drop %d. ", stat.ps_recv, stat.ps_drop);
pcap_close(pd);
}
例5-2 .基于libpcap 的Sniffer 程序
以Sniffer程序的源代码为例。

所列代码节选自以开放源代码形式发行的IPGrab程序, 由Mike Borella 编写。

本例作了一定的修改和简化处理,代码遵循GNU/GPL协议。

这里
假设是在以太网(Ethernet)环境中处理问题。

1#i nclude <stdio.h>
2#in clude <uni std.h>
3#i nclude <stdlib.h>
4#in clude <pcap.h>
5#in clude "ope n_pcap.h"
6#in clude "ethernet.h"
7#i nclude "error .h"
8#in clude <n eti net/in.h>
9#defi ne SNAPLEN 1514
10#defi ne PROMISC 1
11#defi ne READ_TIMEOUT 500
12pcap_t *pd; /* pcap device descriptor */
13pcap_ha ndler dev_prcsr;
/* ptr to func that processes packet for a device */
14int mai n()
15{
16u_char *userdata;
17 userdata = NULL;
18 cnt = -1;
19 /* Open the pcap device for sniffing*/
20 open_pcap();
21 dev_prcsr = (pcap_func_t) dump_ethernet;
22 /* Read all packets on the device. Continue until cnt packets read
*/
23 f (pcap_loop(pd, cnt, dev_prcsr , userdata)
0)GWF_error_fatal("pcap_loop: %s", pcap_geterr(pd));
24/*Close the pcap device */
25pcap_close(pd);
26exit(0);
27}
28void open_pcap(void)
29{
30extern struct bpf_program fcode;
31char errorbuf[PCAP_ERRBUF_SIZE];
32char *fd;
33/*Look up the device and get a handle to it*/
34fd = pcap_lookupdev(errorbuf);
35if (fd == NULL) GWF_error_fatal("open_pcap: pcap_lookupdev() failed for %s: %s", fd, errorbuf);
36/* Get a file descriptor to the device*/
37pd = pcap_open_live(fd, SNAPLEN, PROMISC, READ_TIMEOUT , errorbuf);
38if (pd == NULL) GWF_error_fatal("open_pcap: pcap_open_live() failed for %s: %s", fd, errorbuf);
39/*Set the pcap filter with our fcode FSM. That should do it..*/
40if (pcap_setfilter(pd, &fcode) <
0)GWF_error_fatal("pcap_setfilter: %s", pcap_geterr(pd));
41}
42void dump_ethernet(u_char *user , const struct pcap_pkthdr *h, u_char *p)
43 {
44 int length;
45 int caplen;
46 int ether_type;
47 EtherHdr *ep;
48 /*Get total packet length and length of the captured section */
49 length = h->len;
50 caplen = h->caplen;
51 /*Dump header announcement*/
52 printf("Ethernet Header(%u.%06u)\n",(u_int32_t) h->_sec
(u_int32_t) h->_usec);
53/*Check for truncated header */
54if (caplen < sizeof(EtherHdr)) { printf("Ethernet header too short! (%d
bytes)\n", length);return;}
55/*Dump header fields*/
56ep = (EtherHdr *) p;
57ether_type = ntohs(ep->ether_type);
58printf("Hardware source: %s\n", etheraddr_string(ep->ether_src));
59printf("Hardware destination: %s\n",
etheraddr_string(ep->ether_dst));
60printf("Protocol type: %xH (%s)\n", ether_type, etherProto_string (ep->ether_type));
61printf("Length: %d\n", length+4); /* add FCS */
62}
程序语句说明:
12 行定义了指向描述数据包截获设备描述符结构(pcap_t) 即结构pcap ,,结构pcap 或者(pcap_t) 是实现数据包截获机制的一个关键数据结构,各个字段定义如下:
lfd :文件描述符,这里指数据包截获描述符,即底层过滤器特殊文件的描述符
Snapshot :快照,这里是所指定查看数据包的长度。

Linktype :底层数据链路层的结构类型。

Tzoff :时区与格林威治时区的偏移量。

Offset :对应于不同对齐方式的偏移量。

sf :表示数据包转储文件的文件结构。

md :表示数据包截获机制状态和相关设备状态的结构。

bufsize :数据包缓冲区大小。

buffer :数据包缓冲区的首地址。

bp :缓冲区指针。

cc :用于计数目的的整数。

pkt :用于特定函数目的的地址指针。

fcodebpf_program :数据结构类型的变量,为一指向过滤器程序代码的指针。

errbuf[] :用于存放错误信息字符串的数组。

14 :主函数main() 该程序的主过程代码分析如下所示:
20 :调用open_pcap() ( 28 行)来打开数据包截获设备,并将对应的描述符存储在第12 行所定义的全局变量中;
21:设置数据包处理函数指针地址值dump_ethernet() ( 42 行);
23 :调用pcap_loop 库函数读取打开的截获设备上的所有数据包,直到满足设置的最大截获数目。

如果失败返回,则显示出错信息。

该库函数实质上只是循环调用pcap_read 库函数,直至满足条件。

由于最大数目设置为(-1) 如18 行所示,该库函数在无错误情况下,将进入永久循环处理过程中;
25:在处理完毕后,调用pcap_close 库函数,关闭相应设备;
26 :正常退出。

open_pcap(28)
该函数的主要功能是打开数据包截获设备,并设置设备描述符值。

34 :调用 libpcap 中工具函数 pcap_lookupdev ,查询当前系统可用的数据包截获设 备,并返
回设备句柄;如果失败,则出错退出;
37 :调用 libpcap 库函数 pcap_open_live ,打开选定的截获设备,并返回该设备描 述符。


果返回空值,则出错返回。

该函数的调用参数分别指定选定的设备句柄、 数据包截 获长度、混杂模式、数据读取超时设置值以及出错消息缓存区等;
40 :调用 libpcap 库函数 pcap_setfilter ,设置数据包截获机制中的过滤器程序。

过 滤器程
序用 bpf_program 数据结构来描述。

该数据结构如下所示:
struct bpf_program {
u_int bf_len;
struct bpf_insn *bf_insns; };
其中, 主要包括两个字段。

第一个字段指示该段指令程序的长度, 而另外一个字段则为 指向一个过滤器指令链表头的指针值。

dump_ethernet(42)
该函数的功能是解析以太网协议的数据包,显示其中的报文头信息。

47 :定义了一个报文头结构类型 EtherHdr 的指针变量。

该报文头结构 EtherHdr 的 定义如下所
示:
typedef struct _EtherHdr {
unsigned char ether_dst[6]; unsigned char ether_src[6];
unsigned short ether_type;
} EtherHdr;
其中各字段分别指示了该数据包中的目标和源网卡地址值以及以太网类型信息。

49 :从 pcap_pkthdr 结构类型的变量中获得当前数据包的原始长度值和截获长度值。

pcap_pkthdr 结构是 libpcap 库中所定义的用于描述所截获数据包相关头信息的数据结 构,其定义如
下:
struct pcap_pkthdr {
/* time stamp */
/* length of portion present */ /* length this packet (off wire) */
};
52 :显示该数据包头信息中的相关时间戳信息。

54 :如果数据包截获长度小于报文头结构长度值,则显示出错信息,并退出。

56 :对所截获的数据包数据套用以太网报文头结构,并设置指向该头结构的指针。

57--61 :显示数据包所包含的各种头信息。

注意,其中使用到的若干工具函数,限于 篇幅不予
说明,如 etheraddr_string ,其主要作用是将二进制的字段值转换成可读的字符 串形式。

struct timeval ts; bpf_u_int32 caplen; bpf_u_int32 len;。

相关文档
最新文档