天天看點

使用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 = 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已經可以工作,幹嘛繼續折騰呢?