• 热门专题

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

作者:  发布日期:2016-08-08 21:07:12
Tag标签:文件  
  • 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 = 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 = 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 = 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 = 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 = 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已经可以工作,干嘛继续折腾呢?
About IT165 - 广告服务 - 隐私声明 - 版权申明 - 免责条款 - 网站地图 - 网友投稿 - 联系方式
本站内容来自于互联网,仅供用于网络技术学习,学习中请遵循相关法律法规