使用Python来分离或者直接抓取pcap抓包文件中的HTTP流

python是世界上最好的语言!它使用不可见的制表键作为其语法的一部分!
Vim和Emacs的区别在于,它可以帮助乌干达的儿童...
不讨论哲学,不看第一印象,也没有KPI相逼,但是
Python真的做到了”你不用操心语言本身,只需要关注你自己的业务逻辑需求“!
我的需求比较简单,那就是:
使用tcpdump/tshark抓取且仅抓取一类TCP流,该TCP流是HTTP流,访问特定的URL,如果用我们熟悉的tcpdump命令来表示,它可能是以下的样子:
tcpdump -i eth0 tcp port 80 and url 'www.baidu.com' -n ...

这个需求在经理看来,是比较简单的,无非就是加一个参数嘛,然而经理永远都不会关注实现的细节(其实不是他们不关注,而是他们对此根本就不懂,都是领域外的)。我来问,请经理来答。首先,数据包在被抓取的地方,是无连接信息的,一个网卡不可能记录一个数据包属于哪个数据流,网卡抓取的数据包就是孤立的数据包,即便BPF可以过滤出特定的五元组信息,请问这个五元组怎么跟HTTP协议关联?
        好吧!如果不知道我在说什么,那么我可以更进一步。我们知道,一个访问特定URL的HTTP流的识别只有在客户端发出GET request的时候才能完成,而一个流的五元组的识别是在TCP连接发起的时候进行的,即SYN在GET之前。这可怎么办?
        事情做起来总是要比想的时候更难,这个问题是我工作中的一个真实的需求,我也确实需要这个功能。找方案是需要时间的,有这个时间的话,我如果能用一种编程语言把以上的需求描述出来,那就成功了。作为从业这么多年的底层程序员,我表示除了C和BASH之外,别的编程语言都不会,连C++都不会!学Python学了好几年都没有结果,但是对于这个需求,我想试试。
        Python以其功能丰富且强大的库著称,这也是其吸引诸多程序员的重要原因,然而,我更看重的是它简单的语法和语义,因为我没有时间去配置和学习那些纷乱的库。我看重的是Python组织数据的能力,虽然它并不直观的表达C语言中struct这样的东西,但是其pack/unpack以及List完全就可以满足我的需要。在我看来pack/unpack以及List就是一个结构和一个容器,结构+容器简直是万能的。所以,在本文中,我没有使用Python的pcap库,没有使用dpkt,而是字节解析pcap格式的文件。
        Python的List容器里面可以放进去几乎所有的类型,你只需要知道放进去的是什么,那么日后取出来的时候,它就是什么。
        现在,该展示脚本了。要承认的是,我不会编程,但也不是一点也不会,所以,我可以写出下面的代码,而且也能用,然而我的代码写得非常垃圾,我只是表达一下Python比较简单,如果有人有跟我一样的需求,看到这个代码,也可以拿去用,仅此而已。

0.pcap文件分流与归并排序

起初,我认为将一个偌大的包含N多个TCP流的pcap文件分解成一个个的包含单独TCP流的pcap文件,这是一件简单的事情。
        把整个pcap文件看作是一个数据集合,每一个数据包当作一个数据项,这个任务就是执行一次按照五元组排序的过程,此时我也再一次印证了最基础的排序算法是多么重要。需要强调的是,这个排序过程必须是稳定排序,也就是说排序过后,数据包的相对顺序不能发生改变,这是为了保证同一个流数据包的时间序。
这么简单的想法以至于我真想马上就做!
        然而当我想到各种在处理期间必须要面对的问题是,我就退缩了,比如要处理文件解析,字符串匹配,内存管理,内存比对...把这些加起来都是一个巨大的工程了(我在此奉劝那些眼高手低的博学之士或者那些没有做过一线coder的经理,不要再说”这个实现起来有什么困难吗?真的就那么难吗“,千万别再说这话,有本事你自己试一下就知道了,光说不练假把式)...
        于是,我想到了Python,号称可以不必处理内存分配之类,毕竟解释语言嘛,你写出语句表达你的处理逻辑即可,至于编程,交给解释器吧,这就是解释语言代码写起来就跟写600字作文一样,异常轻松,而诸如C语言,则更像是面对计算机的”兽语“,能信手拈来的,都是猛士。
        看情况吧,如果还有点时间,我会在最后给出Python版本的基于归并排序(其实嘛,只要是稳定排序均可)的数据流分离的实现,如果没有时间,就算了,这里仅仅作为一些个Tips。

1.编码之前

虽然我知道Python来实现排序算法要比C更加简洁和直接,但是在着手去编码之前,还是要经过一些思考,因为可能连排序算法都不用实现。
        看看有什么系统可以替我们做的。
        记得前年,也就是2014年的时候,当时要搞一个检测平台的UI。我们知道UI是一个复杂无比的东西,需要层次数据结构来管理其结构,一般会选用树,我不怎么懂Python,事实上我是一个长期搞底层的,除了用C或者BASH做实验之外,别的编程语言对我而言都太陌生,然而我也知道C语言搞UI简直就是噩梦,需要你自己完成所有的数据组织,那怎么办?
        用BASH做UI!
        这好像是在说笑话,然而确实,我真的用BASH完成了一个树形结构管理的UI,类似make menuconfig那样的(编程者们可能会笑我,这些难道不是很简单吗?Tcl,Perl,Lua不是都可以秒间完成吗?是可以秒间完成,然而我不会这些,我不怎么会编程...)。我的这个BASH UI代码量十分短,并且简单。我是怎么做的呢?
我使用了文件系统。
        如果用C语言来做一个树形结构,我们首先要定义结构体,然后分配内存对象并用数据填充,最后对这些数据进行增删改查。仔细考虑一下,建立一个内存文件系统,然后按照自己的需要去创建目录,文件,并且对这些个目录,文件的内容进行读写,是不是等价于C语言的做法呢?不同的是,操作系统内核的文件管理已经帮你完成了树形结构的管理。事实上,linux系统已经在使用这种方式了,比如sysfs,procfs这些都是采用文件系统的接口来管理复杂的树形数据结构的,特别是sysfs最能体现这一点,至于procfs则比较松散,其中比较典型的例子是procfs下的进程目录以及sysctl目录。
        好吧,可以开始了。
        直接采用文件系统的方式,不需要在内存中进行排序,所有的东西都隐藏在文件系统下面了。我可以做到只需要扫一遍pcap文件,就可以完成整个pcap文件的TCP流分离。举一个最简单的例子,如果你要维护一个连接跟踪表,必须要完成的是,当收到一个数据包的时候,要针对该数据包的五元组对既有的连接跟踪表进行查询,如果查找到则更新该表项,如果没有找到则创建一个新的表项并更新。这些逻辑要很多的代码方可完成,如果使用内存文件系统(我们利用文件的组织结构以及数据存储功能,不用其永久存储,因此避免耗时的IO,所以用内存文件系统),一个open调用就可以完成查找不成便创建这个双重操作,事实上,带有create标记的open调用在底层帮你完成了查找不成便创建这类操作。

2.版本一:用Python分离TCP流

首先来个简单的脚本,这个也比较容易看,它无法识别HTTP,它只是将一个偌大的pcap文件里面的所有TCP流里分离出来,这作为第一步,在这个基础上,我再去处理HTTP。第一个版本的程序列如下:
#!/usr/bin/python

# 用法:./pcap-parser_3.py test.pcap www.baidu.com
import sys
import socket
import struct

filename = sys.argv[1]
file = open(filename, "rb") 

pcaphdrlen = 24
pkthdrlen=16
linklen=14
iphdrlen=20
tcphdrlen=20
stdtcp = 20

files4out = {}

# Read 24-bytes pcap header
datahdr = file.read(pcaphdrlen)
(tag, maj, min, tzone, ts, ppsize, lt) = struct.unpack("=L2p2pLLLL", datahdr)

# 判断链路层是Cooked还是别的
if lt == 0x71:
	linklen = 16
else:
	linklen = 14

# Read 16-bytes packet header
data = http://blog.csdn.net/dog250/article/details/file.read(pkthdrlen)

while data:
	ipsrc_tag = 0
	ipdst_tag = 0
	sport_tag = 0
	dport_tag = 0

	(sec, microsec, iplensave, origlen) = struct.unpack("=LLLL", data)

	# read link
	link = file.read(linklen)
	
	# read IP header
	ipdata = file.read(iphdrlen)
	(vl, tos, tot_len, id, frag_off, ttl, protocol, check, saddr, daddr) = struct.unpack(">ssHHHssHLL", ipdata)
	iphdrlen = ord(vl) & 0x0F 
	iphdrlen *= 4

	# read TCP standard header
	tcpdata = file.read(stdtcp)	
	(sport, dport, seq, ack_seq, pad1, win, check, urgp) = struct.unpack(">HHLLHHHH", tcpdata)
	tcphdrlen = pad1 & 0xF000
	tcphdrlen = tcphdrlen >> 12
	tcphdrlen = tcphdrlen*4

	# skip data
	skip = file.read(iplensave-linklen-iphdrlen-stdtcp)

	print socket.inet_ntoa(struct.pack('i',socket.htonl(saddr)))
	src_tag = socket.inet_ntoa(struct.pack('i',socket.htonl(saddr)))
	dst_tag = socket.inet_ntoa(struct.pack('i',socket.htonl(daddr)))
	sp_tag = str(sport)
	dp_tag = str(dport)

	# 此即将四元组按照固定顺序排位,两个方向变成一个方向,保证四元组的唯一性
	if saddr > daddr:
		temp = dst_tag
		dst_tag = src_tag
		src_tag = temp
	if sport > dport:
		temp = sp_tag
		sp_tag = dp_tag
		dp_tag = temp
	
	name = src_tag + '_' + dst_tag + '_' + sp_tag + '_' + dp_tag
	
	if (name) in files4out:
		file_out = files4out[name]
		file_out.write(data)
		file_out.write(link)
		file_out.write(ipdata)
		file_out.write(tcpdata)
		file_out.write(skip)
		files4out[name] = file_out
	else:
		file_out = open(name+'.pcap', "wb")
		file_out.write(datahdr)
		file_out.write(data)
		file_out.write(link)
		file_out.write(ipdata)
		file_out.write(tcpdata)
		file_out.write(skip)
		files4out[name] = file_out

	# read next packet
	data = http://blog.csdn.net/dog250/article/details/file.read(pkthdrlen)

file.close
for file_out in files4out.values():
	file_out.close()

Python简单到无需任何解释。事实上,如果它的行为连人都看不懂的话,其解释器难道就能更好的看懂吗?
        这个里面的逻辑跟内核中的nf_conntrack是一样的。使用了文件系统,我们省去了一大堆的代码(最终我们的目标就是将分离的流写入文件,这一点上更加适合这个场景)。在接着完成HTTP的识别之前,首先要明确一个问题,这个算法的时间复杂度是O(n)吗?这要看你有没有把底层文件系统的操作算在内。

3.版本二:用Python分离HTTP流

然后,我们来看如何识别并分离特定的HTTP流,代码如下:
#!/usr/bin/python

# 用法:./pcap-parser_3.py test.pcap www.baidu.com
import sys
import socket
import struct

filename = sys.argv[1]
url = sys.argv[2]

file = open(filename, "rb") 

pcaphdrlen = 24
pkthdrlen=16
linklen=14
iphdrlen=20
tcphdrlen=20
stdtcp = 20
layerdict = {'FILE':0, 'MAXPKT':1, 'HEAD':2, 'LINK':3, 'IP':4, 'TCP':5, 'DATA':6, 'RECORD':7}

files4out = {}

# Read 24-bytes pcap header
datahdr = file.read(pcaphdrlen)
(tag, maj, min, tzone, ts, ppsize, lt) = struct.unpack("=L2p2pLLLL", datahdr)

if lt == 0x71:
	linklen = 16
else:
	linklen = 14

# Read 16-bytes packet header
data = http://blog.csdn.net/dog250/article/details/file.read(pkthdrlen)

while data:
	ipsrc_tag = 0
	ipdst_tag = 0
	sport_tag = 0
	dport_tag = 0

	(sec, microsec, iplensave, origlen) = struct.unpack("=LLLL", data)

	# read link
	link = file.read(linklen)
	
	# read IP header
	ipdata = file.read(iphdrlen)
	(vl, tos, tot_len, id, frag_off, ttl, protocol, check, saddr, daddr) = struct.unpack(">ssHHHssHLL", ipdata)
	iphdrlen = ord(vl) & 0x0F 
	iphdrlen *= 4

	# read TCP standard header
	tcpdata = file.read(stdtcp)	
	(sport, dport, seq, ack_seq, pad1, win, check, urgp) = struct.unpack(">HHLLHHHH", tcpdata)
	tcphdrlen = pad1 & 0xF000
	tcphdrlen = tcphdrlen >> 12
	tcphdrlen = tcphdrlen*4

	# skip data
	skip = file.read(iplensave-linklen-iphdrlen-stdtcp)
	content = url
	FLAG = 0
	
	if skip.find(content) <> -1:
		FLAG = 1

	src_tag = socket.inet_ntoa(struct.pack('i',socket.htonl(saddr)))
	dst_tag = socket.inet_ntoa(struct.pack('i',socket.htonl(daddr)))
	sp_tag = str(sport)
	dp_tag = str(dport)

	# 此即将四元组按照固定顺序排位,两个方向变成一个方向,保证四元组的唯一性
	if saddr > daddr:
		temp = dst_tag
		dst_tag = src_tag
		src_tag = temp
	if sport > dport:
		temp = sp_tag
		sp_tag = dp_tag
		dp_tag = temp
	
	name = src_tag + '_' + dst_tag + '_' + sp_tag + '_' + dp_tag + '.pcap'
	# 这里用到了字典和链表,这两类加一起简直了
	if (name) in files4out:
		item = files4out[name]
		fi = 0
		cnt = item[layerdict['MAXPKT']]
		# 我们预期HTTP的GET请求在前6个数据包中会到来
		if cnt < 6 and item[layerdict['RECORD']] <> 1:
			item[layerdict['MAXPKT']] += 1
			item[layerdict['HEAD']].append(data)
			item[layerdict['LINK']].append(link)
			item[layerdict['IP']].append(ipdata)
			item[layerdict['TCP']].append(tcpdata)
			item[layerdict['DATA']].append(skip)
			if FLAG == 1:
				# 如果在该数据包中发现了我们想要的GET请求,则命中,后续会将缓存的数据包写入如期的文件
				item[layerdict['RECORD']] = 1
				file_out = open(name, "wb")
				# pcap的文件头在文件创建的时候写入
				file_out.write(datahdr)
				item[layerdict['FILE']] = file_out
		elif item[layerdict['RECORD']] == 1:
			file_out = item[layerdict['FILE']]	
			# 首先将缓存的数据包写入文件
			for index in range(cnt+1):
				file_out.write(item[layerdict['HEAD']][index])
				file_out.write(item[layerdict['LINK']][index])
				file_out.write(item[layerdict['IP']][index])
				file_out.write(item[layerdict['TCP']][index])
				file_out.write(item[layerdict['DATA']][index])
			item[layerdict['MAXPKT']] = -1
			
			# 然后写入当前的数据包
			file_out.write(data)
			file_out.write(link)
			file_out.write(ipdata)
			file_out.write(tcpdata)
			file_out.write(skip)
			
			
	else:
		item = [0, 0, [], [], [], [], [], 0, 0]
		# 该四元组第一次被扫描到,创建字典元素,并缓存这第一个收到的数据包到List
		item[layerdict['HEAD']].append(data)
		item[layerdict['LINK']].append(link)
		item[layerdict['IP']].append(ipdata)
		item[layerdict['TCP']].append(tcpdata)
		item[layerdict['DATA']].append(skip)
		files4out[name] = item

	# read next packet
	data = http://blog.csdn.net/dog250/article/details/file.read(pkthdrlen)

file.close
for item in files4out.values():
	file_out = item[layerdict['FILE']]	
	if file_out <> 0:
		file_out.close()

我本来应该定义一些函数然后调用的,或者说让代码看起来更OO,但是我觉得那样不太直接,这一方面是因为我确实觉得那样不太直接,更重要的是因为我不会。
用Python的好处在于,它让你省去了设计数据结构并管理这些结构的精力,在Python中,如下定义一个List:
item = []
然后竟然可以把几乎所有东西都放进去,Python帮你维护类型和长度,你可以放进去一个数字:
item.append(1)
也可以放进去一块数据:
data = http://blog.csdn.net/dog250/article/details/file.read(...)
item.append(data)

如果用C语言,我想不得不用类似ASN.1那样的玩意儿或者万能的length+void*了。

4.版本三:用Python直接抓取HTTP流

可以用Python直接抓取HTTP流吗?
        考虑到如果我们先用tcpdump抓取全量的TCP流,然后再使用我上面的程序过滤,为什么不直接在抓包的时候就过滤掉呢?这一方面可以省时间,省去了两遍操作,另一方面可以节省空间,不该记录的数据流的数据包就不记录。幸运的是,Python完全有能力做到这些。
        和我上面的程序唯一不同的是,上面的程序在获得数据包的时候,其来源是来自于pcap文件,而如果直接抓包的话,其来源是来自于底层的pcap,对于Python而言,下面的代码可以完成抓包:
pc=pcap.pcap()
pc.setfilter('tcp port 80')
for ptime,pdata in pc:
    ...
将此代码替换从pcap文件中读取的逻辑即可。

5.版本四:使用归并排序

这个思路来自于一个面试题目。归并排序可以并行处理,在处理海量数据时比较有用,如果我们抓取的数据包特别大,要处理它就会很慢,此时如果能并行处理就会快很多,大致思路就是把一个大文件不断切分,然后再不断合并,局部的有序性逐渐蔓延到全局(背后有一个原理,那就是如果局部更有序了,全局也就有序了,修身,齐家,治国,平天下)。使用Python来完成这个流分离,需要完成两步:
逐渐切分文件;
小文件内按照四元组排序。

代码就不贴了,这个也不难。最后要说的是,如果一开始用C调用libpcap来实现这个,或者直接裸分析pcap格式,那么其中的内存分配,数据结构管理,NULL指针,越界等问题可能让我马上就干别的去了,然而Python并没有这类问题,它简直就像伪代码一样可以快速整理思路!Python代码实现的基础上,就可以轻而易举的将其变成任何其它语言了,昨天跟温州皮鞋厂老板交流,让他用go重构一下,把我拒绝了,然后想让王姐姐整成PHP的,也拒绝我了...我自己想把它翻译成Java(这是我唯一比较精通的语言),睡了一晚上,自己把自己拒绝了,Python已经可以工作,干嘛继续折腾呢?
赞 (0) 评论 分享 ()