简介
Libpcap是一个运行在类UNIX系统下的网络数据包捕获函数库,可以捕获网卡上的数据,也可以发送数据包,相应的Windows版本为WinPcap。
Libpcap是对系统网卡硬件驱动接口封装后的成果,如图所示。
编程指南
配置设备
配置我们想要嗅探的网卡设备。
#include <stdio.h>
#include <pcap.h>
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);
}
打开设备进行嗅探(Sniffing)
我们使用pcap_open_live()这个函数
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms,
char *ebuf)
第一个参数device是我们在之前函数中获得的设备名称
第二个参数snaplen是一个整数,定义了pcap捕获的最大字节数
第三个参数promisc是一个表示布尔值的整数,当值位true时候,接口会进入promiscuous mode (混杂模式:是指一台机器能够接收所有经过它的数据流,而不论其目的地址是否是他。是相对于通常模式而言的,这被网络管理员使用来诊断网络问题,但是也被无认证的想偷听网络通信的人利用)
第四个参数to_ms是读取数据超时时间,单位是毫秒(值为0,表示不超时)
第五个参数ebuf是存储错误信息的字符串
该函数返回会话处理程序
捕获数据的示例代码:
#include <pcap.h>
...
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);
}
并不是所有的设备都在读取的包中提供相同类型的数据链路层头部,所以要使用pcap_datalink()甄别数据链路层的头部类型(数据链路层的头部类型详情参考:http://www.tcpdump.org/linktypes.html)。
如果程序不支持设备提供的链路层头类型:
if (pcap_datalink(handle) != DLT_EN10MB) {
fprintf(stderr, "Device %s doesn't provide Ethernet headers - not supported\n", dev);
return(2);
}
如果设备不提供以太网报头,则会失败。这将适用于以下的代码,因为它假定带有以太网头。
流量过滤
通常我们的嗅探器可能只对特定的流量感兴趣。例如,可能有时候我们想要的是在端口23(telnet)上检索密码。或者我们想要高速缓存通过端口21(FTP)发送的文件。也许我们只想要DNS流量(端口53 UDP)。无论如何,我们很少只是盲目地嗅探所有的网络流量。
pcap_compile()和pcap_setfilter()。
函数原型:
先要调用pcap_compile()函数
int pcap_compile(pcap_t * p,struct bpf_program * fp,char * str,int optimize,
bpf_u_int32 netmask)
第一个参数p是我们会话句柄
第二个函数(“Following that is a reference to the place we will store the compiled version of our filter”不知如何恰当翻译。。。)
最后一个参数netmask是过滤器适用的网络的网络掩码
然后调用pcap_setfilter()
int pcap_setfilter(pcap_t * p,struct bpf_program * fp)
第一个参数p是我们的会话处理句柄
第二个参数fp是对表达式的编译版本的引用(一般情况是与pcap_compile()的第二个参数相同的变量)
代码示例:
#include <pcap.h>
...
pcap_t *handle; /* 会话句柄 */
char dev[] = "rl0"; /* 待嗅探设备 */
char errbuf[PCAP_ERRBUF_SIZE]; /* 错误字符串 */
struct bpf_program fp; /* The compiled filter expression(编译后的过滤器表达式) */
char filter_exp[] = "port 23"; /* The filter expression (过滤器表达式)*/
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\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);
}
开始嗅探
捕获数据包有两种主要技术:
一种是我们可以一次捕获一个数据包,也可以进入一个循环,等待 n个数据包被嗅探完成。我们将从查看如何捕获单个数据包开始,然后查看使用循环的方法。为此,我们使用pcap_next()。
函数原型:
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
第一个参数p是会话句柄
第二个参数h是一个指向一个结构的指针,该结构保存有关数据包的一般信息,比如说嗅探的时间,该数据包的长度以及它的特定部分的长度(例如它被分段)
返回值是一个指向该结构描述的数据包的u_char指针
示例代码:
#include <pcap.h>
#include <stdio.h>
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);
}
该应用程序通过将其放入混杂模式来嗅探pcap_lookupdev()回的任何设备。它发现第一个通过23端口(telnet)的数据包,并告诉用户数据包的大小(以字节为单位)。这个程序包括一个新的函数调用,pcap_close(),我们稍后再讨论。
当然,还有其它的嗅探技术,会比前者复杂但是更有用。
几个嗅探器(如果有的话)实际上使用pcap_next()。他们通常使用pcap_loop()或pcap_dispatch()(接着自己调用pcap_loop())。
要理解这两个函数的用法,你必须了解回调函数的概念。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
pcap_loop()的原型如下:
int pcap_loop(pcap_t * p,int cnt,pcap_handler callback,u_char * user)
第一个参数p是我们的会话句柄
第二个参数cnt是一个整数,告诉pcap_loop()在返回之前应该嗅探多少个数据包(负值意味着它应该嗅到直到发生错误)
第三个参数callback是回调函数的名称(只是其标识符,没有括号)
最后一个参数user在某些应用程序中很有用,但很多时候只是将其设置为NULL
在我们提供一个使用pcap_loop()的例子之前,我们必须检查回调函数的格式。我们不能任意定义我们的回调原型; 否则,pcap_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; / *时间戳* /
bpf_u_int32 caplen; / *目前部分的长度* /
bpf_u_int32 len; / *数据包长度* /
};
pcap_loop()函数最后一个参数是他们最有趣的一个。它是另一个指向u_char的指针,它指向包含整个数据包的数据部分的第一个字节。
但是如何使用这个变量(在我们的原型中命名为“包”)?一个包包含许多属性,它不是一个字符串,而是一个结构的集合(例如,一个TCP / IP数据包将有一个以太网头部,一个IP头部,一个TCP头部,最后一个,分组的有效载荷)。这个u_char指针指向这些结构的序列化版本。为了使用它,我们必须做一些类型转换。
首先,我们必须先确定实际的结构,然后才能对它们进行类型转换。以下是用于通过以太网描述TCP / IP数据包的结构定义。
/* 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 */
};
/* IP header */
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 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 */
#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 */
};
那么这些结构定义出现在数据包的数据中的头。那么我们怎么能把它分开呢?
我们将假设我们正在通过以太网处理TCP / IP数据包。这种技术适用于任何数据包; 唯一的区别是你实际使用的结构类型。那么让我们先来定义我们需要解构分组数据的变量和编译时定义。
/ *以太网头总是正好14个字节* /
/* 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 */
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\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);
这个怎么用?考虑内存中数据包数据的分布。u_char指针只是一个包含内存中地址的变量。这是一个指针; 它指向内存中的一个位置。
为了简单起见,我们会说这个指针设置的地址是值X.那么如果我们的三个结构是连在一起,那么它们中的第一个(sniff_ethernet)位于地址X的内存中,那么我们可以很容易地找到结构之后的地址; 该地址是X加上以太网报头的长度,即14,或SIZE_ETHERNET。
类似地,如果我们拥有该头部的地址,那么它之后的结构的地址是该头部的地址加上该头部的长度。IP报头,不像以太网报头,并不具有固定的长度; 它的长度是由IP报头的头长度字段指定的,每4字节计数。由于它是每4字节计数,它必须乘以4,以字节数来表示大小。该头的最小长度为20字节。
TCP头也是可变长度; 其长度由TCP头部的“数据偏移”域指定为每4字节计数,其最小长度也为20个字节。
所以让我们做一个图表:
变量 | 位置(字节) |
---|---|
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} |
payload:有效载荷,指除去协议首部之外实际传输的数据