libpcap(packet capture library),即数据包捕获函数库,是Unix/Linux平台下的网络数据包捕获函数库。它是一个独立于系统的用户层的包捕获API接口,为底层网络监测提供了一个可移植的框架。
一、工作原理
libpcap主要由两部分组成:网络分接头(network tap)和数据过滤器(packet filter)。网络分接头从网络设备驱动程序中收集数据进行拷贝,过滤器决定是否接收该数据包。libpcap利用BSD packet filter(BPF)算法对网卡接收到的链路层数据包进行过滤。BPF算法的基本思想是在有BPF监听的网络中,网卡驱动将接收到的数据包复制一份交给BPF过滤器,过滤器根据用户定义的规则决定是否接收此数据包以及需要拷贝该数据包的哪些内容,然后将过滤后的数据交给与过滤器关联的上层应用程序。
libpcap的包捕获机制就是在数据链路层加一个旁路处理。当一个数据包到达网络接口时,libpcap首先利用已经创建的套接字从链路层驱动程序中获得该数据包的拷贝,再通过Tap函数将数据包发给BPF过滤器。BPF过滤器根据用户已经定义好的过滤规则对数据包进行逐一匹配,匹配成功则放入内核缓冲区,并传递给用户缓冲区,匹配失败则直接丢弃。如果没有设置过滤规则,所有数据包都将放入内核缓冲区,并传递给用户层缓冲区。
二、libpcap抓包框架
其实pcap的应用格式很简单,总的来说可以分为如下5部分:
- 首先我们需要定义我们要进行嗅探的接口(网卡)。在Linux平台可能是eth0等,在BSD平台可能是xl1等。我们可以自己用一个字符串定义接口,也可以要求pcap提供接口的名字
- 接下来是初始化pcap,这也是我们告知pcap嗅探哪个接口。我们可以嗅探多个接口,使用句柄进行区分。就像打开一个文件用于写或读一样,我们必须命名我们所嗅探的“session”(会话),这样使其和其他类似的会话区分开
- 如果我们想要嗅探特定的包(例如TCP/IP包,发往端口23的包等),我们需要创建一个过滤器。创建过滤器的过程总共有三步,首先将规则集合置于一个字符串内,然后是编译这个字符串得到pcap可读的形式,最终应用这个可读形式来过滤得到我们想要的包
- 经过上面三步后,我们可以告知pcap执行抓包过程。在这个阶段内,pcap一直工作直到它接受了到了所有我们想要的包为止。
- 在嗅探完成后,我们最终关闭会话结束整个过程
具体对应函数如下:
- pcap_lookupdev()用于寻找网络设备(网卡),返回可被pcap_open_liv()函数调用的网络设备名指针
- pcap_open_live()函数用于打开网络设备,并且返回用于捕获网络数据包的数据包捕获文件描述符。对于此网络设备的操作都要基于这个描述符。
- pcap_lookupnet()函数获得指定网络设备的网络号和子网掩码
- pcap_compile()函数用于将用户指定的过滤策略编译到过滤程序中
- pcap_setfilter()函数用于设置过滤器
- pcap_loop()函数和pcap_dispatch()函数用于捕获数据包,捕获后还可以进行处理,此外pcap_next()和pcap_next_ex()两个函数也可以用来捕获数据包
- pcap_close()函数用于关闭网络设备,释放资源
三、实现libpcap的每一个步骤
3.1 设置设备
有两种方法设置我们想要嗅探的设备。
第一种,我们可以简单的让用户告诉我们,考虑下面的程序:
#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[]){
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",dev);
return 0;
}
3.2 打开句柄并设定参数
打开设备使用pcap_open_live()函数,其函数原型如下:
pcap_t *pcap_open_live(const char *device,int snaplen,int promisc,int to_ms,char *errbuf);
使用这个函数的代码片如下:
#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);
}
上面的代码嗅探字符串dev指定的设备,告知pcap读取BUFSIZ(这个参数在pcap.h中进行了定义)个字节。设定设备为混杂模式。
获得指定网络设备的网络号和子网掩码使用pcap_lookupnet函数,其函数原型如下:
int pcap_lookupnet(const char *device, bpf_u_int32 *netp, bpf_u_int32 *maskp, char *errbuf);
当函数执行成功时返回0,执行失败时返回-1
3.3 过滤通信
实现这一过程由pcap_compile()与pcap_setfilter()这两个函数完成。总的过程分三步:
- 构造一个过滤表达式
- 编译这个表达式
- 应用这个过滤器
过滤表达式被保存在一个字符串中(字符数组),其在pcap_filter中定义了格式。
一些过滤表达式的例子如下:
- src host 192.168.1.177:只接收源ip地址是192.168.1.177的数据包
- dst port 80:只接收tcp/udp的目的端口是80的数据包
- not tcp:只接收不使用tcp协议的数据包
- tcp[13] == 0x02 and (dst port 22 or dst port 23):只接收SYN标志位置位且目标端口是22或23的数据包(tcp首部开始的第13个字节)
- icmp[icmptype] == icmp-echoreply or icmp[icmptype] == icmp-echo:只接收icmp的ping请求和ping响应的数据包
- ehter dst 00:e0:09:c1:0e:82:只接收以太网mac地址是00:e0:09:c1:0e:82的数据包
- ip[8] == 5:只接收ip的ttl=5的数据包(ip首部开始的第8个字节)
构造完过滤表达式后,我们需要编译它,使用的是pcap_compile函数,其函数原型如下:
int pcap_comile(pcap_t *p,struct bpf_program *fp,const char *str,int optimize, bpf_u_int32 netmask);
表达式被编译后就可以使用了,此时需要执行pcap_setfilter,函数原型如下:
int pcap_setfilter(pcap_t *p, struct bpf_program *fp);
下面的代码使嗅探器嗅探经由端口23的所有通信,使用混杂模式,设备是eth0:
#include<stdio.h>
#include<pcap.h>
int main(){
pcap_t *handle; //会话句柄
char dev[]="eth0"; //执行嗅探的设备
char errbuf[PCAP_ERRBUF_SIZE]; //存储错误信息的字符串
struct bpf_program filter; //已经编译好的过滤表达式
char filter_app[]="port 23"; //过滤表达式
bpf_u_int32 mask; //执行嗅探的设备的网络掩码
bpf_u_int32 net; //执行嗅探的设备的IP地址
pcap_lookupnet(dev,&net,&mask,errbuf);
handle=pcap_open_live(dev,DEV_SIZE,1,0,errbuf);
pcap_compile(handle,&filter,filter_app,0,net);
pcap_setfilter(handle,&filter);
}
3.4 实际进行嗅探
打开网络接口就已经开始监听了,我们有如下三种方式来获取数据包:
3.4.1 pcap_next
函数原型如下:
const u_char *pcap_next(pcap_t *p,struct pcap_pkthdr *h);
注意这个函数只要收到一个数据包后就会立即返回。
下列程序演示了使用过程:
#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_next()。通常程序使用pcap_loop()或者pcap_dispatch()(注意,dispatch本身就使用了pcap_loop())。
3.4.2 pcap_loop
pcap_loop的函数原型如下:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
这个函数抓取cnt个包后返回,其使用例子如下:
#include<stdio.h>
#include<pcap.h>
#include<time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <arpa/inet.h>
//int pcap_loop(pcap_t * p, int cnt, pcap_handler callback, u_char * user)
//void callback(u_char * userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
void callback(u_char *userarg,const struct pcap_pkthdr * pkthdr, const u_char * packet)
{
int * id = (int *)userarg;
printf("id: %d\n", ++(*id));
printf("length: %d\n", pkthdr->len);
printf("Number of bytes: %d\n", pkthdr->caplen);
printf("time: %s", ctime((const time_t *)&pkthdr->ts.tv_sec));
int i;
for(i=0; i<pkthdr->len; ++i)
{
printf(" %0x", packet[i]);
if( (i + 1) % 16 == 0 )
{
printf("\n");
}
//sleep(1);
}
printf("\n\n");
}
int main(void)
{
char *device=NULL;
char *errBuf[PCAP_ERRBUF_SIZE]={0};
pcap_t *head;
device = pcap_lookupdev(errBuf);
if(device)
{
printf("lookup is ok %s\n",device);
}
else
{
printf("lookup is error %s\n",errBuf);
return 0;
}
head = pcap_open_live(device,65535,1,0,errBuf);
if(head)
{
printf("open is ok\n");
}
else
{
printf("open is error %s\n",errBuf);
return 0;
}
int id = 0;
pcap_loop(head, -1, callback, (u_char *)&id);
pcap_close(head);
return 0;
}
3.4.3 pcap_dispatch
函数原型如下:
int pcap_dispatch(pcap_t * p, int cnt, pcap_handler callback, u_char * user)
这个函数和pcap_loop十分类似,只不过创建pcap_t句柄时如果设置了超时时间to_ms,那么在超过to_ms毫秒后就会返回(to_ms是pcap_open_live()的第4个参数)。
3.5 数据解析
在使用packet变量时我们需要注意,一个数据包包含许多属性,因此我们可以想象它不只是一个字符串,而实质上是一个结构体的集合(比如,一个TCP/IP包会有一个以太网的头部,一个IP头部,一个TCP头部,还有此包的有效载荷)。这个u_char就是这些结构体的串联版本。为了使用它,我们必须做一些有趣的匹配工作。
下面是一些数据包的结构体:
//以太网帧头部
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 //don't fragment flag
#define IP_MF 0x2000 //more fragments flag
#define IP_OFFMASK 0x1fff //mask for fragmenting bits
u_char ip_ttl; //数据包存活时间
u_char ip_p; //使用的协议
u_short ip_sum; //校验和
struct in_addr ip_src,ip_dst; //源地址和目的地址
}
#define IP_HL(ip) (((ip)->ip_vhl)&0x0f)
#define IP_V(ip) (((ip)->ip_vhl)>>4)
//TCP数据包的首部
typedef u_int tcp_seq;
struct sniff_tcp{
u_short th_sport; //源端口
u_short th_dport; //目的端口
tcp_seq th_seq; //包序号
tcp_seq th_ack; //确认序号
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; //TCP滑动窗口
u_short th_sum; //头部校验和
u_short th_urp; //紧急服务位
}
pcap嗅探数据包时使用的就是这些结构。接下来,它简单的创建一个u_char字符串并且将这些结构体填入。假定我们要对以太网上的TCP/IP包进行处理(同样的方式可以应用于任何数据包,唯一的区别是我们实际使用的结构体类型):
const struct sniff_ehternet *ethernet; //以太网帧首部
const struct sniff_ip *ip; //IP包头部
const struct sniff_tcp *tcp; //TCP包头部
const char *payload; //数据包的有效载荷
//计算每个变量的大小
int size_ethernet=sizeof(struct sniff_ethernet);
int size_ip=sizeof(struct sniff_ip);
int size_tcp=sizeof(struct sniff_tcp);
//接下来就可以使用指针指向这些部分
ethernet=(struct sniff_ethernet*)packet;
ip=(struct sniff_ip*)(packet+size_ethernet);
tcp=(struct sniff_tcp*)(packet+size_ethernet+size_ip);
payload=(u_char*)(packet+size_ethernet+size_ip+size_tcp);
代码例子:
#include<stdio.h>
#include<pcap.h>
#include<time.h>
#include<unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
void callback(u_char *user, const struct pcap_pkthdr *pkthdr, const u_char *packet)
{
int * id = (int *)user;
struct in_addr addr;
struct iphdr *ipptr;
struct tcphdr *tcpptr;//太次片,,ip,tcp数据结构
struct ether_header *eptr;//以太网字头
u_char *ptr;
char *data;
int i;
printf("id: %d\n", ++(*id));
printf("Packet length: %d\n", pkthdr->len);
printf("Number of bytes: %d\n", pkthdr->caplen);
printf("Recieved time: %s", ctime((const time_t *)&pkthdr->ts.tv_sec));
eptr = (struct ether_header*)packet;//得到以太网字头
if (ntohs(eptr->ether_type) == ETHERTYPE_IP)
{
printf ("Ethernet type hex:%x dec:%d is an IP packet/n",
ntohs(eptr->ether_type), ntohs(eptr->ether_type));
}
else
{
if (ntohs(eptr->ether_type) == ETHERTYPE_ARP)
{
printf ("Ethernet type hex:%x dec:%d is an ARP packet/n",
ntohs(eptr->ether_type), ntohs(eptr->ether_type));
}
else
{
printf ("Ethernet type %x not IP/n", ntohs(eptr->ether_type));
exit (1);
}
}
ptr = eptr->ether_dhost;
i = ETHER_ADDR_LEN;
printf ("i=%d/n", i);
printf ("Destination Address: ");
do
{
printf ("%s%x", (i == ETHER_ADDR_LEN)?"":":", *ptr++);
}while(--i>0);
printf ("/n");
//printf ("%x/n",ptr);
ptr = eptr->ether_shost;
i = ETHER_ADDR_LEN;
printf ("Source Address: ");
do
{
printf ("%s%x", (i == ETHER_ADDR_LEN)?"":":", *ptr++);
}while(--i>0);
printf ("/n");
printf ("Now decoding the IP packet.\n");
ipptr = (struct iphdr*)(packet+sizeof(struct ether_header));//得到ip包头
printf ("the IP packets total_length is :%d\n", ipptr->tot_len);
printf ("the IP protocol is %d\n", ipptr->protocol);
printf("\n\n");
addr.s_addr = ipptr->daddr;
printf ("Destination IP: %s\n", inet_ntoa(addr));
addr.s_addr = ipptr->saddr;
printf ("Source IP: %s\n", inet_ntoa(addr));
printf ("Now decoding the TCP packet.\n");
tcpptr = (struct iphdr*)(packet+sizeof(struct ether_header)
+sizeof(struct iphdr));//得到tcp包头
printf ("Destination port : %d\n", tcpptr->dest);
printf ("Source port : %d\n", tcpptr->source);
printf ("the seq of packet is %d\n", tcpptr->seq);
//以上关于ip、tcp的结构信息请查询/usr/include/linux/ip.h | tcp.h
data = (char*)(packet+sizeof(struct ether_header)+sizeof(struct iphdr)
+sizeof(struct tcphdr));//得到数据包里内容,不过一般为乱码。
printf("\n\n");
printf ("the content of packets is /n%s/n",data);
}
int main()
{
char *device;
char errBuf[PCAP_ERRBUF_SIZE];
pcap_t *head;
device = pcap_lookupdev(errBuf);
if(device)
{
printf("lookup is ok %s\n",device);
}
else
{
printf("lookup is error %s\n",errBuf);
return 0;
}
head = pcap_open_live(device,65535,1,0,errBuf);
if(head)
{
printf("open is ok\n");
}
else
{
printf("open is error %s\n",errBuf);
return 0;
}
// typedef void (*pcap_handler)(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes);
// int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user);
int i = 0;
pcap_dispatch(head, 0,callback, (u_char *)&i);
pcap_close(head);
return 0;
}
参考
http://blog.chinaunix.net/uid-21556133-id-120228.html
https://blog.csdn.net/u011573853/article/details/49963567
https://www.tcpdump.org/pcap.html