pcap入门教程

第一件事就是需要了解 pcap 应用的格式

1.发现哪个网卡需要我们去sniff 嗅探
linux下也许是eth0
BSD下可能是 xl1
我们可以将设备定义为string然后打开该设备,或者我们可以让pcap自己查找然后提供我们网卡的名称

2.初始化pcap ,实际上就是我们告诉pcap 去嗅探哪个网卡设备。如果我们想的话,可以同时嗅探多个网卡设备。
我们使用文件描述符去区分这些网卡,就像我们打开一个文件一样对它进行读取和写入。我们必须要对嗅探session(会话)进行命名,以此来区分。

3.如果我们只想去嗅探指定的 traffic,我们就需要设置一个 rule 规则,编译这个规则并使用,编译命令为compile->apply。
这个过程总共分为三步,并且都联系的非常紧密。首先将rule规则保存在一个string中,然后将这个规则编译成pcap 可以读取的格式format。最后我们可以将该规则应用到我们需要过滤的会话session中。

4.最后我们告诉pcap 进入主循环。在这个状态下,pcap 会一直等待,直到接收到指定数量的 packet 数据包。
每次获得一个新的packet ,都会调用一个我们已经定义好的回调函数。在这个函数中,我们可以做任何我们想做的事。
它可以详细分析并打印给使用者查看,可以将其保存到一个文件中。

5.当我们的嗅探结束后,我们就可以关闭会话session。

Setting the device

命令行参数设置设备

	#include 
	#include 

	int main(int argc, char *argv[])
	{
		 char *dev = argv[1];
		 printf("Device: %s\n", dev);
		 return(0);
	}

由用户告诉嗅探的设备

#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);
	}

pcap自己查找网卡设备

Opening the device for sniffing

创建一个嗅探会话 sniff session可以使用函数 pcap_open_live()
函数原型如下
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
device 设备名称
snaplen 抓取的最大比特
promisc 如果设置为true ,会将网卡设置为混合模式,将会捕获经过网卡的所有数据包
to_ms 毫秒值,抓取数据包的超时时间,设置为0 表示一直等待
ebuf 用于存储任何的错误信息

renturn 返回值是一个 会话句柄 pcap_t

参考以下代码示例:

#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);
	 }

混合嗅探模式和非混合嗅探模式的区别:
标准的非混合嗅探模式,只能检测到与该网卡直接关联的数据包。只有源自、目标或者路由经过这个host的数据会被嗅探,但是混合嗅探模式会捕获所有经过该wire的数据。也就是说混合嗅探模式可以捕获更多的数据包,但是值得注意的是如果开启了混合嗅探模式是可以被检测出来的。其次,混合嗅探模式只能运行在non-switched环境。

数据包的链路层报头是不同的,网卡设备或者一些非以太网设备可能会提供以太网报头,但是loopback devices(回环设备)、ppp interfaces、wifi interfaces不会提供。

我们需要检测不同的链路层link-layer报头类型,并在处理数据包的时候根据不同的类型处理。 pcap_datalink(handle) 可以用于检查link-layer header 类型。

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 the DLT_ values in that list.

函数pcap_datalink()返回链路层的类型

	if (pcap_datalink(handle) != DLT_EN10MB) {
		fprintf(stderr, "Device %s doesn't provide Ethernet headers - not supported\n", dev);
		return(2);
	}

Filtering traffic

过滤数据包
通常我们只对特定的数据包感兴趣,我们就可以使用pcap_compile()和pcap_setfilter()。
使用起来也很简单,只要我们调用pcap_open_live()并获得一个正在工作中的sniffing session就够了。但是在我们应用该过滤filter之前,我们必须要先compile编译它。过滤器表达式被存储在传统的字符串string或者字符数组char[]中,表达式的编写我们可以在linux中man tcpdump查看。
编译过滤表达式,可以使用pcap_compile()函数,原型定义如下:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask);
第一个参数是我们的会话句柄session handle,第二个参数存储我们的编译结果(用于后续的使用),接下来的str就是我们用于编译的过滤器表达式了。Optimize选项表示我们是否需要优化表达式(0表示不需要优化,1表示需要优化该表达式),最后我们必须要指定一个网络掩码network mask用于指定当前过滤表达式作用的网络network。函数如果执行失败,返回-1。
表达式expression被编译成功后,我们就可以应用了。使用的函数是pcap_setfilter(),原型如下:
Int pcap_setfilter(pcap_t *p, struct bpf_program *fp);
如下是一个方便理解的例子:

#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);
	 }

上面的example使用了嗅探器去获得所有经过端口port 23的数据,并且使用了混合嗅探模式,在名为rl0的网卡设备上。
函数pcap_loopupnet(),传入一个网卡的名称name,可以得到该网卡的ipv4地址和正确的网络掩码network mask。

The actual sniffing

在上面我们学会了如何定义一个设备,准备开始嗅探,使用过滤表达式去获得我们需要的数据包。现在我们可以开始真正的获取一些数据包了。
获取数据包有两个重要的技术,我们可以一个一个的获取数据包,也可以在循环中一次性获得n个数据包。获取一个数据包使用函数pcap_next(),原型如下:
U_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h);
第二个参数是一个指针,指向一个存储基本数据包信息的结构体,其中包括了被嗅探捕获的时间,数据包的长度,以及它是否只是一个片段。
以下是一个使用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);
	 }

该应用捕获经过端口port 23上的第一个数据包,告诉用户这个数据包的大小size,然后使用pcap_close()函数关闭了一个会话session。

第二种方法更加的使用的普遍一些,使用pcap_loop()/pcap_dispatch(),为了理解这个函数,我们必须了解回调callback的含义。这两个函数都会在收到满足条件的数据包后执行回调函数。
函数定义如下:
Int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user);
cnt参数表示在函数执行结束前,需要捕获多少数据包,如果是负数表示会一直监听捕获直到出现错误。Callback是用户设置的回调函数,最后的user是用户传入回调函数的参数。pcap_dispatch()也是相同的用法,区别在于dispatch只会处理第一批(batch)从系统接收到的数据,loop会持续的处理。
回调函数的定义类型如下:
void got_packet(u_char *args, const struct pcap_pkthdr *header,const u_char *packet);
第一个参数对应了pcap_loop的最后一个参数,第二个参数表示的就是捕获得到的数据包。
数据包结构定义如下:

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

最后一个参数pcaket是最有意思的,也是最容易迷惑开发者的。它是另一个u_char指针,指向了数据包整体数据的第一个byte,该数据包中包含了许多有趣的信息,例如一个TCP/IP数据包里面包括以太网头部、ip头部、tcp头部以及有效载荷。
以下是我们的数据结构定义:

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

接下来我们就可以使用指针pointer去处理数据包了,以下假设我们处理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 */
	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);

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!
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:
1.Redistribution must retain the above copyright notice and this list of conditions.
2.The name of Tim Carstens may not be used to endorse or promote products derived from this document without specific prior written permission.

你可能感兴趣的:(Linux,C++)