原文地址:http://williballenthin.com/blog/2015/09/08/parsing-binary-data-with-%60vstruct%60/
Vstruct是一個純粹由Python語言編寫的模塊,可用于二進制數據的解析和序列化處理。實際上,Vstruct是隸屬于vivisect項目的一個子模塊,該項目是由Invisig0th Kenshoto發起的,專門用來處理二進制分析。 Vstruct的開發和測試已經有許多年頭了,并且已經集成到了許多生成環境下的系統中了。此外,這個模塊不僅簡單易學,而且重要的是,它還非常有趣!
您還在使用struct模塊火急火燎地手工編寫腳本嗎?太苦逼了,不如使用vstruct吧!利用vstruct開發的代碼,往往更具有陳述性或聲明性,更加簡明易懂,這是因為在編寫二進制解析代碼時通常會帶有大量樣板代碼,而vstruct卻不會出現這種情況。聲明性代碼強調的是二進制分析的下列重要方面:偏移,大小和類型。這使得基于vstruct的解析器更易于長期維護。
Vstruct模塊是vivisect項目的一個組成部分,目前該項目與Python 2.7保持兼容,當然,面向Python 3.x的vivisect分支目前正在開發之中。
由于vivisect的子項目不是用兼容setuptools的setup.py文件分發的,所以你需要自己下載vstruct的源代碼目錄,并將其放入你的Python路徑目錄中,比如當前目錄下:
$ git clone https://github.com/vivisect/vivisect.git vivisect
$ cd vivisect
$ python
In [1]: import vstruct
In [2]: vstruct.isVstructType(str)
Out[2]: False
當然,通過setup.py來聲明vstruct依賴的Python模塊是非常麻煩的事情,因此為方便起見,我提供了一個PyPI鏡像包,名為vivisect-vstruct-wb,這樣的話,大家就可以直接利用pip命令來安裝vstruct了:
$ mkdir /tmp/env
$ virtualenv -p python2 /tmp/env
$ /tmp/env/bin/pip install vivisect-vstruct-wb
$ /tmp/env/bin/python
In [1]: import vstruct
In [2]: vstruct.isVstructType(str)
Out[2]: False
我已經對這個鏡像進行了更新,現在它既支持Python 2.7也支持Python 3.0的解釋程序,以便于讀者在將來的工程中繼續使用vivisect-vstruct-wb。另外,遇到問題時,千萬不要忘了到Visi的GitHub上去看看有沒有現成的答案。
下面的例子相當于大家學編程語言時的“Hello World !”程序,它使用vstruct來解析字節串中的小端模式的32位無符號整數:
In [1]: import vstruct
In [2]: u32 = vstruct.primitives.v_uint32()
In [3]: u32.vsParse(b"\x01\x02\x03\x04")
In [4]: hex(u32)
Out[4]: '0x4030201'
請注意觀察上面代碼是如何創建v_uint32類型實例、如何使用.vsParse()方法解析字節串,以及如何像處理原生Python類型實例那樣來處理最后的結果的。為了更安全起見,我要顯式地將解析后的對象轉換成一個純Python類型:
In [5]: type(u32)
Out[5]: vstruct.primitives.v_uint32
In [6]: python_u32 = int(u32)
In [7]: type(python_u32)
Out[7]: int
In [8]: hex(python_u32)
Out[8]: '0x4030201'
事實上,每個vstruct操作都被定義為一個以vs為前綴的方法,幾乎在所有由vstruct派生的解析器中,都能找到這些方法的身影。雖然我最常用的是.vsParse()和.vsSetLength()這兩個方法,但是我們最好熟悉所有方法的使用方法。下面是對每種方法的簡單總結:
目前為止,vstruct看上去就像是struct.unpack的轉基因克隆,所以,接下來我們有必要介紹它更酷的功能。
Vstruct解析器通常是基于類的。這個模塊提供了一組基本數據類型(例如v_uint32和v_wstr分別用于DWORD和寬字符串),以及一個相應的機制來將這些類型組合成更加高級的數據類型(VStructs)。首先,我們先來介紹基本的數據類型:
復雜的解析器可以通過定義vstruct.VStruct類的子類來開發,因為vstruct.VStruct類可以包含眾多變量,而這些變量可以是vstruct基本類型或高級類型的實例。好吧,我承認這句話有點繞口,那就一點一點來逐步消化吧!
Complex parsers are developed by defining subclasses of the `vstruct.VStruct`
class…
class IMAGE_NT_HEADERS(vstruct.VStruct):
def __init__(self):
vstruct.VStruct.__init__(self)
在這個例子中,我們使用vstruct定義了一個Windows可執行文件的PE頭部。我們的解析器名為IMAGE_NT_HEADERS,它是從類vstruct.VStruct那里派生出來的。我們必須在init()方法中顯式調用父類的構造函數,具體形式可以是vstruct.VStruct.init(self)或者super(IMAGE_NT_HEADERS, self).init()。
…that contain member variables that are instances of `vstruct` primitives…
class IMAGE_NT_HEADERS(vstruct.VStruct):
def __init__(self):
vstruct.VStruct.__init__(self)
self.Signature = vstruct.pimitives.v_bytes(size=4)
IMAGE_NT_HEADERS實例的第一個成員變量是一個v_bytes實例,它可以存放4字節內容。v_bytes通常用來存放無需進一步解析的原始字節序列。在本例中,成員變量.Signature的作用是,在解析有效PE文件時存放魔法序列“PE\x00\x00”。
在定義這個類的時候,還可以添加其他的成員變量,以用于解析二進制數據中不同部分的序列。類VStruct會記錄成員變量的聲明順序,并處理其他相關的記錄工作。唯一需要你去做的事情就是決定以哪種順序來使用這些類型。夠簡單吧!
當某種結構在各種子結構中都要用到的時候,你可以將它們抽象成可重用的Vstruct類型,之后就可以像使用vstruct基本類型那樣來使用它們了。
[Complex parsers are developed by defining classes that contain] other complex `VStruct` types.
class IMAGE_NT_HEADERS(vstruct.VStruct):
def __init__(self):
vstruct.VStruct.__init__(self)
self.Signature = v_bytes(size=4)
self.FileHeader = IMAGE_FILE_HEADER()
當Vstruct實例解析二進制數據遇到復雜的成員變量時,可以通過遞歸方式用子解析器來解決。在本例中,成員變量.FileHeader就是一種復合的類型,其定義見這里。IMAGE_NT_HEADERS解析器首先會遇到.Signature字段的四個字節,然后,它把解析控制權傳遞給復合解析器IMAGE_FILE_HEADER。我們需要檢查這個類的定義,以便確定其大小和布局情況。
我的建議是,開發多個Vstruct類,每個類負責文件格式的一小部分,然后使用一個更高級別的VStruct將它們組合起來。這樣做的話,調試起來會更加容易一些,因為解析器的每一部分都可以單獨進行檢驗。無論用什么方法,一旦定義好了一個Vstruct,你就可以通過文檔開頭部分描述的模式來解析數據了。
In [9]:
with open("kernel32.dll", "rb") as f:
bytez = f.read()
In [10]: hexdump.hexdump(bytez[0xf8:0x110])
Out[10]:
00000000: 50 45 00 00 4C 01 06 00 62 67 7D 53 00 00 00 00 PE..L...bg}S....
00000010: 00 00 00 00 E0 00 0E 21 .......!
In [11]: pe_header = IMAGE_NT_HEADERS()
In [12]: pe_header.vsParse(bytez[0xf8:0x110])
In [13]: pe_header.Signature
Out[13]: b'PE\x00\x00'
In [14]: pe_header.FileHeader.Machine
Out[14]: 332
在執行第9條命令的時候,我們打開了一個PE樣本文件,并將其內容讀入到了一個字節串中。在執行第10條命令的時候,我們用十六進制的形式展示了PE頭部開頭部分的一些內容。在執行第11條命令的時候,我們創建了一IMAGE_NT_HEADERS類的實例,但是需要注意的是,它還沒有包含任何解析過的數據。此后,我們利用第12條命令顯式解析了一個存放PE頭部的字節串。通過第13和14條命令,我們展示了解析實例的成員的內容。需要注意的是,當我們訪問一個嵌入的復合Vstruct時,我們可以繼續進一步索引其內部內容,但是當我們訪問一個基本類型成員時,我們得到的是原生Python的數據形式。說句實在話,這真是太方便了!
在進行調試的時候,我們可以通過.tree()方法把被解析數據以人類可讀的形式打印出來:
In [15]: print(pe_header.tree())
Out[15]:
00000000 (24) IMAGE_NT_HEADERS: IMAGE_NT_HEADERS
00000000 (04) Signature: 50450000
00000004 (20) FileHeader: IMAGE_FILE_HEADER
00000004 (02) Machine: 0x0000014c (332)
00000006 (02) NumberOfSections: 0x00000006 (6)
00000008 (04) TimeDateStamp: 0x537d6762 (1400727394)
0000000c (04) PointerToSymbolTable: 0x00000000 (0)
00000010 (04) NumberOfSymbols: 0x00000000 (0)
00000014 (02) SizeOfOptionalHeader: 0x000000e0 (224)
00000016 (02) Characteristics: 0x0000210e (8462)
條件性的成員
由于Vstruct的布局是在該類型的init()構造函數中定義的,所以,它能夠對這些參數進行交互,并能夠選擇性的包含某些成員。舉例來說,一個Vstruct在32位平臺和64位平臺上可以有不同的行為,如下所示:
class FooHeader(vstruct.VStruct):
def __init__(self, bitness=32):
super(FooHeader, self).__init__(self)
if bitness == 32:
self.data_pointer = v_ptr32()
elif bitness == 64:
self.data_pointer = v_ptr64()
else:
raise RuntimeError("invalid bitness: {:d}".format(bitness))
這是一種非常強大的技術,盡管要想正確使用需要一點點小技巧。重要的是要了解它們的布局是何時最終確定下來的,何時用于估計,何時用于二進制數據的解析。當init()被調用時,這個實例并不會訪問待解析的數據。只有當.vsParse()被調用時,成員變量中才會填上待解析的數據。因此,VStruct構造函數無法通過引用成員實例的內容來決定如何繼續下面的解析工作。舉例來說,下面的代碼是行不通的:
class BazDataRegion(vstruct.VStruct):
def __init__(self):
super(BazDataRegion, self).__init__()
self.data_size = v_uint32()
# NO! self.data_size doesn't contain anything yet!!!
self.data_data = v_bytes(size=self.data_size)
回調函數
為了正確地處理動態解析器,我們需要使用vstruct的回調函數。當一個VStruct實例完成了一個成員區段的解析時,它會檢查這個類是否具有一個前綴為pcb_(解析器的回調函數)的同名方法,如果有的話,就會調用這個方法。同時,這個方法名稱的其他部分就是剛才解析的區段的名稱;舉例來說,一旦BazDataRegion.data_size被解析完,名為BazDataRegion.pcb_data_size的方法就會被調用,當然,前提是這個方法確實存在。
這一點非常重要,因為當回調函數被調用時,VStruct實例已經被待解析的數據填充了一部分了,舉例來說:
In [16]:
class BlipBlop(vstruct.VStruct):
def __init__(self):
super(BlipBlop, self).__init__()
self.aaa = v_uint32()
self.bbb = v_uint32()
self.ccc = v_uint32()
def pcb_aaa(self):
print("pcb_aaa: aaa: %s\n" % hex(self.aaa))
def pcb_bbb(self):
print("pcb_bbb: aaa: %s" % hex(self.aaa))
print("pcb_bbb: bbb: %s\n" % hex(self.bbb))
def pcb_ccc(self):
print("pcb_ccc: aaa: %s" % hex(self.aaa))
print("pcb_ccc: bbb: %s" % hex(self.bbb))
print("pcb_ccc: ccc: %s\n" % hex(self.ccc))
In [17]: bb = BlipBlop()
In [18]: bb.vsParse(b"AAAABBBBCCCC")
Out[18]:
pcb_aaa: aaa: 0x41414141
pcb_bbb: aaa: 0x41414141
pcb_bbb: bbb: 0x42424242
pcb_ccc: aaa: 0x41414141
pcb_ccc: bbb: 0x42424242
pcb_ccc: ccc: 0x43434343
這就意味著,我們可以推遲一個類的布局的最終初始化,直到某些二進制數據解析完成為止。下面是實現一個規定大小的緩沖區的正確方法:
In [19]:
class BazDataRegion2(vstruct.VStruct):
def __init__(self):
super(BazDataRegion2, self).__init__()
self.data_size = v_uint32()
self.data_data = v_bytes(size=0)
def pcb_data_size(self):
self["data_data"].vsSetLength(self.data_size)
In [20]: bdr = BazDataRegion2()
In [21]: bdr.vsParse(b"\x02\x00\x00\x00\x99\x88\x77\x66\x55\x44\x33\x22\x11")
In [22]: print(bdr.tree())
Out[22]:
00000000 (06) BazDataRegion2: BazDataRegion2
00000000 (04) data_size: 0x00000002 (2)
00000004 (02) data_data: 9988
在第19條命令中,我們聲明了一個結構,它具有一個頭字段(.data_size),指示隨后的原始數據(.data_data )的大小。因為當時我們還沒有這個待解析的頭部的值。之后,init()被調用,我們使用了一個名為.pcb_data_size()的回調函數,它將在解析.data_size區段時被調用。當這個回調函數執行時,會更新.data_data字節數組的大小,以便使用正確的字節數量。在執行第20條命令的時候,我們創建了一個解析器的實例,然后,利用第21條命令對一個字符串進行了解析處理。雖然我們傳入了13個字節,但是我們希望只用其中的6個字節:4字節用于uint32型變量.data_size,2字節用于字節數組.data_data。而其余的字節則不做處理。在執行第22條命令的時候,結果表明我們的解析器對二進制數據進行了正確的解析。
請注意,在執行回調函數.pcb_data_size()期間,我們使用方括號訪問了Vstruct實例中名為.data_data的對象。之所以這樣做,是因為我們既想要修改子實例本身,但是又不想從子實例中取得待解析的具體值的緣故。要想弄清楚到底應該使用哪種技術 (self.field0.xyz 或 self["field0"].xyz),需要讀者自己在實踐中摸索一下,但通常來說,如果你想要解析一個具體值的話,就應該避免使用方括號。
在我們開發可維護的二進制代碼解析器的時候,vstruct模塊是一個強大的助手。它能夠去除開發過程帶來的大量樣本代碼。我特別喜歡使用vstruct解析惡意軟件的C2協議、數據庫索引和二進制的XML文件。如果讀者感興趣的話,我建議大家通過下列項目來學習vstruct解析器:
https://github.com/vivisect/vivisect/tree/master/vstruct/defs
。https://github.com/fireeye/flare-wmi/blob/master/python-cim/cim/cim.py
和https://github.com/fireeye/flare-wmi/blob/master/python-cim/cim/objects.py
。https://github.com/williballenthin/python-sdb/blob/master/sdb/sdb.py
。