tcpdump原理之利用libpcap实现抓包

45 篇文章 2 订阅
3 篇文章 0 订阅

tcpdump原理之利用libpcap实现

(转载请标明出处,请勿用于商业用途)

http://blog.csdn.net/linux_embedded/article/details/8826429

Linux 下赫赫有名的抓吧工具tcpdump,想必使用过的人都十分的清楚。但是,其实现的原理却很少人提及过,今天就tcpdump的实现原理做简单的介绍。

tcpdump 首先利用libpcap工具,将linux网络栈中的数据包抓取上来,然后,tcpdump在按照用户的需求完成数据包的分析工作。下面就如何通过libpcap实现数据包的抓取做简单的介绍。

开始:libpcap的使用方式

首先,我们需要了解一下pcap 嗅探器使用的一般布局,下面分为几个部分简单介绍。

  1. 首先我们需要定义我们需要使用的网络接口。在linux下,我们一般会定义eth0或ethx。在BSD下,可能是xl1。我们可以把网络接口定义为字符串,或者可以通过pcap获得可用的网络接口的名字。
  2. 初始化pcap。现在,我们可以将我们将要监听的网络设备告诉pcap。如果有需要的话,我们可以使pcap同时监听多个网络接口。我们可以通过“文件句柄”来区分不同的网络接口,就像我们打开文件进行文件的读取、写入一样,我们必须定义区分我们的监听“回话”,否则我们没有办法区分不同的监听对象(网络设备)。
  3. 如果我们仅仅想监听特殊的网络数据(例如,我们想监听TCP业务,或者我们只想监听端口号为23的业务)。我们可以自己定义一个监听规则的集合,“编译”它,然后在应用它。上面三个步骤,连接的十分紧密,那一个步骤都不能丢掉。规则其实就是定义好的字符串,我们需要将其转化为pcap可以是别的格式(所以我们需要编译)。“编译器”仅仅通过内置的函数就可以实现上述的格式转换。然后我们可以告诉pcap执行规则完成数据包的过滤。
  4. 之后,我们会告诉pcap进入主要的循环执行状态。在该状态下,pcap 会一直运行截获到我们需要的网络数据包的数量为止。每次pcap抓取到我们需要的数据包后就会调用我们事先定义好的回调函数完成后续的数据包的处理工作。该回调函数为将数据包存储到一个文件中,亦或是打印到屏幕上,或者是你想要的任何事情,她都能够办到。
  5. 当我们完成数据检测后,我们需要关闭本次事务。
  6. 通过上面5个步骤(其中步骤3是可选的),我们就可以完成数据包的截获和后续的数据包的处理,是不是很简单啊?下面我们会详细介绍每一步是如何实现的。

设置设备

设备的设置十分的简单,存在两种方式可以完成网络设备的设置,即:通过传递参数完成设定;通过pcap提供的函数完成设备的检测、设定。

  1. 首先,我们可以通过程序启动时,传递参数的方式来实现网络设备的设置。代码示例如下:
    #include <stdio.h>
    #include <pcap.h>
    
    int main(int argc, char *argv[])
    {
    	 char *dev = argv[1];
    
    	 printf("Device: %s\n", dev);
    	 return(0);
    }
       2. 第二种方法就是,通过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_lookupdev()就可以实现设备的自动获取。其中参数,errbuf为了记录函数调用失败时的原因。可以通过man手册参看其使用方法。

打开设备,开始数据包的监测

可以通过函数pcap_open_live()打开需要监测的网络设备。函数原型以及使用方式如下:

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

参数:
  • device:前面定义的网络设备名称。
  • snaplen:一个整型数据,用来定义pcap抓取的数据的字节数。
  • promise:如果该参数为真,表示王珂会被设置为混杂模式(如果参数为假,在某些情况下,网卡还是会被设置为混杂模式)。
  • to_ms:读取数据包的超时时间,单位为ms(0表示没有超时,一般不为0)。
  • ebuf:记录错误信息。
函数返回会话的句柄。下面是该函数的使用示例:
  1.  #include <pcap.h>
    	 ...
    pcap_t *handle;
    handle = pcap_open_live(somedev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL
    {
             fprintf(stderr, "Couldn't open device %s: %s\n", somedev, errbuf);
             return(2);
    }
    
其中,BUFSIZE 被定义在 pcap.h中,程序会一直运行直到错误产生,错误信息会记录在errbuf中。

Note:
简单介绍一下,混杂模式和非混杂模式。
  • 非混杂模式:
           非混杂模式下,主机只会检测与其相关的数据。这些数据包括:源、目的为本主机,或者经过本机的路由数据包。
  • 混杂模式:
           混杂模式下,主机会检测线路上所有的数据包。在没有交换机的网络中,检测所有的数据包。这种模式的好处是,可以检测的数据会增加。利弊主要取决于你的目的。但是,混杂模式是可以探测的。一个主机可以通过可靠地手段检测到另一个主机的网卡是否被设置为混杂模式。再一个,混杂模式只会工作在没有交换机的网络,例如通过hub连接的,或者交换机被ARP淹没。混杂模式会导致,在网络流量巨大的时候,增加系统的负担。

过滤数据包

一般情况下,我们只会检测我们感兴趣的数据包。例如,我们会监听23(telnet)端口来搜索密码,监听21端口截获一个文件,或者仅仅为了监听DNS数据(端口53)。我们一般不会盲目监听网络上的数据包。

通过,pcap_compile()和pcap_setfilter()就可以实现对特定数据包的监听工作。

当我们创建了一个监听事务后,下一步我们就可以设置过滤的规则了。之所以使用pcap内置的过滤机制,而不是传统的if/else if 模式实现数据包的过滤主要考虑到两个原因:

  1. pcap的数据包的过滤机制十分的高效,其直接通关BPF实现,我们省去了很多步骤。
  2. 使用pcap十分的简单。
在应用过滤规则之前,我们需要”编译“它,过滤规则一般被存储在一个字符数组中,存储格式我们可以通过查看man tcpdump的规则。pcap_compile()和pcap_setfilter()函数的原型和使用方式如下:
  1. pcap_compile():
    int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
    
    参数: 
  • pt:会话句柄。
  • fp:表示编译过的过滤规则存储的位置。
  • str:字符串格式的过滤规则。
  • optimize:表示过滤规则是否需要的优化(1:need,0:no)
  • netmask:表示过滤应用的网络的子网掩码.
         如果该函数运行成功,返回一个非-1的整数。
      2.  pcap_setfilter():
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
           参数:
  • p:会话句柄。
  • fp:表示编译过的过滤规则存储的位置。
下面是两个函数使用的示例:
#include <pcap.h>
	 ...
	 pcap_t *handle;		/* Session handle */
	 char dev[] = "eth0";		/* 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", somedev, 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);
	 }
上面表示,eth0工作在混杂模式,检测端口号为23的数据包。
上面存在一个没有介绍过的函数pcap_lookupnet(),该函数的作用是通过dev获得其对应的IPV4 网络号和其对应的网络的子网掩码。后面的杂项中介绍该函数。

开始监测数据包

经过了上面一系列的准备工作,现在开始正的抓包。通常可以通过两张方法实现数据的抓取:一次只抓取一个数据包;每次抓取n个数据包。下面我们分别介绍一下,这两种方法。

single

利用函数pcap_next(),我们可以实现每次只抓取一个数据包,该函数的原型和使用方法如下:

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

参数:

  • p:会话句柄。
  • 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", somedev, 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)开始监测数据包,当获得一个数据包后,就将该数据包以字节的形式告诉用户。
 

multiple


另一种技术相对比较复杂,其实pcap_next()其实一般很少使用,更多的我们会使用pcap_loop()和pcap_dispatch()函数,上述两个函数的实现用到了回调函数机制。我们需要实现定义好的自己的回调函数,然后注册到pcap_next()或者pcap_dispatch()中,两个函数的使用方式和工作方式基本相同,每当检测到一个符合过滤规则的数据包,回调函数就会调用(当然,如果没有定义过滤规则,每个数据包都会调用其对应的回调函数)。pcap_look()函数的原型和函数的参数如下:

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


 参数:

  • p:检测会话句柄;
  • cnt:表示结束监听之前需要截获的数据包的数量(负数表示一直监听知道出错)。
  • callback:我们定义的回调函数的名字。
  • user:表示传递给回调函数的参数

pcap_dispatch()的使用方法几乎和pcap_look()一样,多不同是pcap_dispatch()只会读取"一些"数据包,之后就会退出循环。可以通过man手册获得更详细的关于pcap_look()和pcap_dispatch()的区别。接下来我么有必要介绍一些回调函数的格式,回调函数不能随便定义,否则pcap_dispatch()和pcap_look()会不知道该如何调用回调函数的。回调函数的原型和参数如下:

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


参数:

  • args:该参数就是我们在pcap_loop()和pcap_dispatch()最后的用户传递的参数,每当回调函数被调用时该参数都将被传递一次。
  • header:保存数据包截获的具体时间和数据包大小等信息。
  • packet:指向数据包的第一个字节(数据包已经被序列化)

pcap_pkthdr()结构体的具体格式如下:

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


在开始读取数据包中内同时我们首先需要对数据包就行反序列化,所以我们有必要了解一下TCP/IP下各个字段的含义,下面就对TCP/IP协议中各个数据包头进行简单的解释。假设TCP/IP的链路层协议为Ethernet:

/* 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 */
	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协议运行在Ethernet上。

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


我们解释一下,假设我们定义数据包的起始地址为X,则Ethernet头、IP、TCP/UDP头的起始地址分别为:

Variable

Location (in bytes)

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}

  1. 至此,我们学会如何定义一个设备,如何创建一个监听的会话,如何设置过滤条件,如何截获并分析数据包,现在你可以使用自己的sniffer,监听你想要获得数据包了,点击打开链接这里有一个完成例子。你可以在linux编译,调试一下!


 

  • 6
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现tcpdump实时抓包,可以使用libpcap库。使用该库可以捕获网络数据包,并对其进行处理。可以在程序中设置回调函数,每当捕获到一个数据包时,回调函数就会被调用。 为了边缓存边写入数据,你可以将捕获到的每个数据包存储在内存缓冲区中,并定期将缓冲区中的数据写入文件中。具体实现上,你可以使用一个循环队列来实现缓冲区,当队列满时,将最早的数据包覆盖掉,从而实现循环缓存。 下面是一些伪代码来帮助你更好地理解: ```python import pcap import time # 打开网卡 pc = pcap.pcap() # 打开文件 f = open('output.pcap', 'wb') # 设置缓冲区大小 buffer_size = 1000 # 初始化一个循环队列 buffer = [None] * buffer_size head = 0 tail = 0 # 回调函数,在捕获到每个数据包时都会调用 def packet_handler(timestamp, packet): global head, tail, buffer # 将数据包存储在缓冲区中 buffer[tail] = packet tail = (tail + 1) % buffer_size # 缓冲区已满,写入文件并清空缓冲区 if tail == head: for i in range(buffer_size): if buffer[head] is not None: f.write(buffer[head]) buffer[head] = None head = (head + 1) % buffer_size # 刷新文件缓冲区 f.flush() # 设置回调函数 pc.setfilter('tcp') pc.loop(packet_handler) # 关闭文件 f.close() ``` 以上伪代码仅供参考,具体实现还需要根据实际情况进行调整。同时,需要注意的是,实时抓包可能会对系统性能产生影响,因此需要进行适当的优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值