用pcap编程

http://www.tcpdump.org/pcap.html

用pcap编程


入门:pcap应用程序的格式

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

  1. 我们首先确定我们想要嗅探哪个接口。在Linux中,这可能是eth0接口,我们可以用一个字符串来定义这个设备,或者我们可以让pcap为我们提供一个可以完成这个工作的接口名称。
  2. 初始化pcap。这是我们实际上告诉pcap我们正在嗅探什么设备的地方。如果我们想要,我们可以在多个设备上嗅探。我们如何区分它们?使用文件句柄。就像打开一个阅读或写作文件一样,我们必须命名我们的嗅探“会话”,以便我们可以将其与其他此类会话区分开来。
  3. 如果我们只想嗅探特定的流量(例如:只有TCP / IP数据包,只有端口23的数据包等),我们必须创建一个规则集,“编译”它并应用它。这是一个三阶段的过程,所有这些都是密切相关的。规则集保存在一个字符串中,并转换成pcap可以读取的格式(因此编译它)。编译实际上是通过调用我们程序中的函数完成的; 它不涉及使用外部应用程序。然后我们告诉pcap将其应用于我们希望过滤的任何会话。
  4. 最后,我们告诉pcap输入它的主执行循环。在这种状态下,pcap会等待,直到收到我们想要的很多数据包。每次它收到一个新的数据包时,它会调用我们已经定义的另一个函数。它所调用的功能可以做我们想做的任何事情; 它可以解析数据包并将其打印到用户,它可以将其保存在文件中,或者它什么也不做。
  5. 在我们的嗅探需求得到满足后,我们结束会议并完成。

这实际上是一个非常简单的过程。总共五步,其中一步是可选的(步骤3,以防万一)。让我们来看看每个步骤以及如何实施它们。

设置设备

这非常简单。有两种技术可用于设置我们希望嗅探的设备。

首先是我们可以简单地让用户告诉我们。考虑以下程序:

	
        #include <stdio.h>
	#include <pcap.h>

	int main(int argc, char *argv[])
	{
		 char *dev = argv[1];

		 printf("Device: %s\n", 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\n", dev);
		return(0);
	}

在这种情况下,pcap只是自行设置设备。“但是,等等,蒂姆,”你说。“与errbuf字符串有什么关系?” 大多数pcap命令允许我们将它们作为参数传递给它们。这个字符串的目的是什么?如果命令失败,它将填充字符串和错误描述。在这种情况下,如果pcap_lookupdev()失败,它将在errbuf中存储错误消息。漂亮,不是吗?这就是我们如何设置我们的设备。

打开设备进行嗅探

创建嗅探会话的任务非常简单。为此,我们使用pcap_open_live()。该函数的原型(来自pcap手册页)如下所示:

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

参数device是上面的设备dev。参数snaplen是一个整数,它定义了pcap要捕获的最大字节数当参数promisc设置为true时,会将界面置于混杂模式(但是,即使设置为false,在特定情况下也可以使界面处于混杂模式)。参数to_ms是以毫秒为单位的读取超时(值为0表示没有超时;至少在某些平台上,这意味着您可能会等到足够数量的数据包到达之后再看到任何数据包,因此您应该 设置一个超时时间)。最后,参数ebuf是一个字符串,我们可以在其中存储任何错误消息(就像我们上面用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”中的设备,告诉它读取,但是在BUFSIZ(在pcap.h中定义)中指定了很多字节。我们告诉它将设备置于混杂模式,嗅探直到出现错误,如果出现错误,请将其存储在字符串errbuf中; 它使用该字符串打印错误消息。

关于混杂与非混杂嗅探的说明:这两种技术在风格上非常不同。在标准的非混杂嗅探中,主机只嗅探与其直接相关的流量。只有通过主机的流量才能被嗅探器获取。另一方面,混杂模式会嗅探网络上的所有流量。在非交换环境中,这可能是所有网络流量。这样做的明显优点是它提供了更多的嗅探数据包,根据嗅探网络的原因,这可能有用也可能不会有帮助。但是,有回归。混杂模式嗅探是可检测的; 主机可以测试具有强大的可靠性,以确定是否另一个主机正在做混杂的嗅探。其次,它仅适用于非交换环境(如集线器,或者被ARP泛滥的交换机)。第三,在高流量网络上,主机可能会对系统资源征税。

并不是所有的设备都在您读取的数据包中提供相同类型的链路层报头。以太网设备和一些非以太网设备可能会提供以太网头,但在监视模式下捕获时,其他设备类型(例如BSD和OS X中的环回设备,PPP接口和Wi-Fi接口)则不会。

您需要确定设备提供的链路层头的类型,并在处理数据包内容时使用该类型。pcap_datalink()例程返回一个值,指示链路层头的类型; 请参阅链接层标题类型值的列表它返回的 值是该列表中的DLT_值。

如果你的程序不支持设备提供的链路层头类型,它必须放弃; 这可以通过代码来完成

	
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_open_live()并且有一个正在工作的嗅探会话之后,我们可以应用我们的过滤器。为什么不使用我们自己的if / else if语句?两个原因。首先,pcap的过滤器效率更高,因为它直接使用BPF过滤器; 我们通过让BPF驱动程序直接执行它来消除许多步骤。第二,这个更容易。效率又高又容易做。

在应用我们的过滤器之前,我们必须“编译”它。过滤器表达式保存在一个常规字符串(char数组)中。语法在tcpdump的手册页中记录得非常好; 我留下你自己阅读。但是,我们将使用简单的测试表达式,因此您可能足够清晰以从我的示例中找出它。

编译我们称之为pcap_compile()的程序。原型将其定义为:

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

参数p是上面的会话句柄handle)fp对将存储我们过滤器的编译版本的地方的引用。参数str是表达式本身。参数optimize决定表达式是否应该被“优化”(0是假的,1是真的)。最后,指定过滤器适用的网络的网络掩码。该函数在失败时返回-1; 所有其他值都意味着成功。

表达式编译完成后,就可以应用它了。输入pcap_setfilter()。按照我们解释pcap的格式,我们将看看pcap_setfilter()原型:

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

参数P,是我们的会话处理程序handle,参数fp是对表达式的编译版本的引用(推测与pcap_compile()的第二个参数相同)。

也许另一个代码示例有助于更好地理解:

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

该程序使嗅探器准备好嗅探来自或去往端口23的所有流量,在混合模式下,在设备rl0上。

您可能会注意到前面的示例包含一个我们尚未讨论过的函数。pcap_lookupnet()是一个函数,它给定一个设备的名称,返回它的一个IPv4网络号和相应的网络掩码(网络号是IPv4地址与网络掩码的AND,因此它只包含地址的网络部分)。这很重要,因为我们需要知道网络掩码才能应用过滤器该功能在文档末尾的杂项部分进行了描述。

我的经验是,该过滤器不适用于所有操作系统。在我的测试环境中,我发现带有默认内核的OpenBSD 2.9确实支持这种类型的过滤器,但具有默认内核的FreeBSD 4.3不支持。

实际的嗅探

我们已经学会了如何定义设备,为嗅探做好准备,并应用关于我们应该和不应该嗅探的过滤器。现在是实际捕获一些数据包的时候了。

有两种捕获数据包的主要技术。我们可以一次捕获单个数据包,或者我们可以输入一个循环,在完成之前等待 n个数据包被嗅探。我们首先看看如何捕获单个数据包,然后看看使用循环的方法。为此,我们使用pcap_next()。

pcap_next()的原型非常简单:

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

参数P是会话处理器handle。第二个参数是指向一个结构的指针,该结构包含有关数据包的一般信息,特别是它被嗅探的时间,数据包的长度以及特定部分的长度(例如,如果数据包被碎片化)。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\n", errbuf);
			return(2);
		}
		/*查找设备属性 */
		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;
		}
		/*以混杂模式打开会话*/
		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);
		}
		/* 抓包、捕获 */
		packet = pcap_next(handle, &header);
		/* 打印它的长度 */
		printf("Jacked a packet with length of [%d]\n", header.len);
		/*关闭会话 */
		pcap_close(handle);
		return(0);
	 }

此应用程序通过将其放入混杂模式来嗅探pcap_lookupdev()返回的任何设备。它发现第一个数据包要通过端口23(telnet)并告诉用户数据包的大小(以字节为单位)。再次,这个程序包含一个新的调用pcap_close(),我们将在后面讨论它。

我们可以使用的另一种技术更复杂,可能更有用。很少有嗅探器实际使用pcap_next()。通常,它们使用pcap_loop()或pcap_dispatch()要理解这两个函数的用法,您必须了解回调函数的概念。

回调函数背后的概念非常简单。假设我有一个程序正在等待某种事件。为了这个例子的目的,让我们假装我的程序想让用户按下键盘上的一个键。每当他们按下一个键时,我想调用一个函数,然后决定要做什么。我正在使用的函数是一个回调函数。每次用户按下一个键时,我的程序都会调用回调函数。回调在pcap中使用,但不是在用户按下某个键时被调用,而是在pcap嗅探数据包时调用它们。可以用来定义回调函数的两个函数是pcap_loop()和pcap_dispatch()。pcap_loop()和pcap_dispatch()在使用回调方面非常相似。 所有被嗅探的数据包都被发送到回调。

pcap_loop()的原型如下:

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

参数P是会话句柄handle。参数cnt告诉pcap_loop()在返回之前它应该嗅探多少包(负值意味着它应该嗅探直到发生错误)。参数callback是回调函数的名称。最后一个参数在某些应用程序中很有用,但很多时候只是简单地设置为NULL

假设除了pcap_loop()发送的参数之外,我们还有我们自己想要发送给回调函数的参数。这是我们做的地方。显然,你必须对一个u_char指针进行类型转换,以确保结果正确地存在。我们将在后面看到,pcap利用一些非常有趣的方法以u_char指针的形式传递信息。在我们展示一个pcap如何做的例子之后,这应该是显而易见的。如果不是,请参考本地C参考文本,因为对指针的解释超出了本文的范围。

pcap_dispatch()在使用上几乎相同。pcap_dispatch()和pcap_loop()之间的唯一区别在于pcap_dispatch()只处理从系统接收的第一批数据包,而pcap_loop()将继续处理数据包或批处理包,直到数据包计数完成。有关其差异的更深入讨论,请参阅pcap手册页。

在我们提供使用pcap_loop()的例子之前,我们必须检查回调函数的格式。我们不能随意定义我们的回调原型; 否则,pcap_loop()将不知道如何使用该函数。所以我们使用这种格式作为回调函数的原型:

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

我们来更详细地研究一下。首先,你会注意到该函数有一个void返回类型。这是合乎逻辑的,因为pcap_loop()不知道如何处理返回值。第一个参数对应于pcap_loop()的最后一个参数。无论什么值作为pcap_loop()的最后一个参数传递给我们的回调函数的第一个参数,每次函数被调用。参数header是上面 pcap的头文件,其中包含有关数据包被嗅探的时间,数据大小等信息。pcap_pkthdr结构在pcap.h中定义为:

	
struct pcap_pkthdr {
		struct timeval ts; /*时间标识、时间戳*/
		bpf_u_int32 caplen; /* 目前部分的长度 */
		bpf_u_int32 len; /* 包长 */
	};

这些值应该相当自我解释。最后一个参数packet是他们中最有趣的一个,对于普通的新手pcap程序员来说最令人困惑。它是指向u_char的另一个指针,它指向包含整个数据包的数据块的第一个字节,正如pcap_loop()所探测的那样。

但是,你如何使用这个变量(在我们的原型中命名为“数据包”)一个数据包包含很多属性,所以你可以想象,它并不是一个真正的字符串,而是实际上是一个结构集合(例如,一个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? etc */
	};

	/*IP头 */
	struct sniff_ip {
		u_char ip_vhl;		/* ipv4头长度 */
		u_char ip_tos;		/* type of service 服务类型*/
		u_short ip_len;		/* total length */
		u_short ip_id;		/* id */
		u_short ip_off;		/* 移字段*/
	#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 头 */
	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 */
};

那么所有这些与pcap和我们神秘的u_char指针有什么关系呢?那么,这些结构定义了数据包中出现的标题。那么我们怎么能把它分开呢?准备目睹指针的最实际用途之一(对于所有那些坚持指针无用的新C程序员,我打你)。

再一次,我们假设我们正在通过以太网处理TCP / IP数据包。这种技术适用于任何数据包; 唯一的区别是你实际使用的结构类型。因此,我们首先定义我们需要解构分组数据的变量和编译时定义。

/* 以太网头总是占14个字节 */
#define SIZE_ETHERNET 14

	const struct sniff_ethernet *ethernet; /* 以太网头 */
	const struct sniff_ip *ip; /* IP头 */
	const struct sniff_tcp *tcp; /* TCP头*/
	const char *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头部也具有可变长度; 其长度由4个字节的字数表示,由TCP头部的“数据偏移”字段给出,其最小长度也为20个字节。

所以我们来制作一个图表:

变量 位置(以字节为单位)
sniff_ethernet X
sniff_ip X + SIZE_ETHERNET
sniff_tcp X + SIZE_ETHERNET + {IP标头长度}
有效载荷 X + SIZE_ETHERNET + {IP报头长度} + {TCP报头长度}

Sniff_ethernet结构是第一行,位于X处。sniff_ip紧跟在sniff_ethernet后面,位于X位置,以及以太网头占用的空间(14个字节或SIZE_ETHERNET)。sniff_tcp位于sniff_ip和sniff_ethernet之后,因此它位于X处加上以太网和IP报头的大小(14个字节,分别是IP报头长度的4倍)。最后,有效载荷(没有与之相对应的单一结构,因为其内容取决于在TCP上使用的协议)位于所有这些之后。

所以在这一点上,我们知道如何设置我们的回调函数,调用它,并找出有关被嗅探的数据包的属性。现在是你一直在等待的时间:写一个有用的数据包嗅探器。

由于源代码的长度,我不打算将它包含在本文档的正文中。只需下载 sniffex.c并尝试一下。

包起来

在这一点上,你应该可以使用pcap编写一个嗅探器。您已经了解了打开pcap会话,了解它的一般属性,嗅探数据包,应用过滤器和使用回调背后的基本概念。现在是时候走出去,嗅出那些电线!




猜你喜欢

转载自blog.csdn.net/qq_28657577/article/details/80679753