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比较简单,如果有人有跟我一样的需求,看到这个代码,也可以拿去用,仅此而已。
起初,我认为将一个偌大的包含N多个TCP流的pcap文件分解成一个个的包含单独TCP流的pcap文件,这是一件简单的事情。
把整个pcap文件看作是一个数据集合,每一个数据包当作一个数据项,这个任务就是执行一次按照五元组排序的过程,此时我也再一次印证了最基础的排序算法是多么重要。需要强调的是,这个排序过程必须是稳定排序,也就是说排序过后,数据包的相对顺序不能发生改变,这是为了保证同一个流数据包的时间序。
这么简单的想法以至于我真想马上就做!
然而当我想到各种在处理期间必须要面对的问题是,我就退缩了,比如要处理文件解析,字符串匹配,内存管理,内存比对...把这些加起来都是一个巨大的工程了(我在此奉劝那些眼高手低的博学之士或者那些没有做过一线coder的经理,不要再说”这个实现起来有什么困难吗?真的就那么难吗“,千万别再说这话,有本事你自己试一下就知道了,光说不练假把式)...
于是,我想到了Python,号称可以不必处理内存分配之类,毕竟解释语言嘛,你写出语句表达你的处理逻辑即可,至于编程,交给解释器吧,这就是解释语言代码写起来就跟写600字作文一样,异常轻松,而诸如C语言,则更像是面对计算机的”兽语“,能信手拈来的,都是猛士。
看情况吧,如果还有点时间,我会在最后给出Python版本的基于归并排序(其实嘛,只要是稳定排序均可)的数据流分离的实现,如果没有时间,就算了,这里仅仅作为一些个Tips。
虽然我知道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调用在底层帮你完成了查找不成便创建这类操作。
首先来个简单的脚本,这个也比较容易看,它无法识别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)吗?这要看你有没有把底层文件系统的操作算在内。
然后,我们来看如何识别并分离特定的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(...)
如果用C语言,我想不得不用类似ASN.1那样的玩意儿或者万能的length+void*了。
可以用Python直接抓取HTTP流吗?
考虑到如果我们先用tcpdump抓取全量的TCP流,然后再使用我上面的程序过滤,为什么不直接在抓包的时候就过滤掉呢?这一方面可以省时间,省去了两遍操作,另一方面可以节省空间,不该记录的数据流的数据包就不记录。幸运的是,Python完全有能力做到这些。
和我上面的程序唯一不同的是,上面的程序在获得数据包的时候,其来源是来自于pcap文件,而如果直接抓包的话,其来源是来自于底层的pcap,对于Python而言,下面的代码可以完成抓包:
pc=pcap.pcap() pc.setfilter('tcp port 80') for ptime,pdata in pc: ...
将此代码替换从pcap文件中读取的逻辑即可。
这个思路来自于一个面试题目。归并排序可以并行处理,在处理海量数据时比较有用,如果我们抓取的数据包特别大,要处理它就会很慢,此时如果能并行处理就会快很多,大致思路就是把一个大文件不断切分,然后再不断合并,局部的有序性逐渐蔓延到全局(背后有一个原理,那就是如果局部更有序了,全局也就有序了,修身,齐家,治国,平天下)。使用Python来完成这个流分离,需要完成两步:
逐渐切分文件;
代码就不贴了,这个也不难。最后要说的是,如果一开始用C调用libpcap来实现这个,或者直接裸分析pcap格式,那么其中的内存分配,数据结构管理,NULL指针,越界等问题可能让我马上就干别的去了,然而Python并没有这类问题,它简直就像伪代码一样可以快速整理思路!Python代码实现的基础上,就可以轻而易举的将其变成任何其它语言了,昨天跟温州皮鞋厂老板交流,让他用go重构一下,把我拒绝了,然后想让王姐姐整成PHP的,也拒绝我了...我自己想把它翻译成Java(这是我唯一比较精通的语言),睡了一晚上,自己把自己拒绝了,Python已经可以工作,干嘛继续折腾呢?