天天看點

利用vstruct解析二進制資料

  Vstruct是一個純粹由Python語言編寫的子產品,可用于二進制資料的解析和序列化處理。實際上,Vstruct是隸屬于vivisect項目的一個子子產品,該項目是由[Invisig0th Kenshoto](

  visi.kenshoto/viki/MainPage)發起的,專門用來處理二進制分析。 Vstruct的開發和測試已經有許多年頭了,并且已經內建到了許多生成環境下的系統中了。此外,這個子產品不僅簡單易學,而且重要的是,它還非常有趣!

  您還在使用struct子產品火急火燎地手工編寫腳本嗎?太苦逼了,不如使用vstruct吧!利用vstruct開發的代碼,往往更具有陳述性或聲明性,更加簡明易懂,這是因為在編寫二進制解析代碼時通常會帶有大量樣闆代碼,而vstruct卻不會出現這種情況。聲明性代碼強調的是二進制分析的下列重要方面:偏移,大小和類型。這使得基于vstruct的解析器更易于長期維護。

  ##0x00 安裝vstruct

  [Vstruct](

  github/vivisect/vivisect/tree/master/vstruct)子產品是[vivisect](

  github/vivisect/vivisect)項目的一個組成部分,目前該項目與Python 2.7保持相容,當然,面向Python 3.x的vivisect分支目前正在開發之中。

  由于vivisect的子項目不是用相容setuptools的setup.py檔案分發的,是以你需要自己下載下傳vstruct的源代碼目錄,并将其放入你的Python路徑目錄中,比如目前目錄下:

  

`

  $ git clone github/vivisect/vivisect.git vivisect

  $ cd vivisect

  $ python

  In [1]: import vstruct

  In [2]: vstruct.isVstructType(str)

  Out[2]: False

`

  當然,通過setup.py來聲明vstruct依賴的Python子產品是非常麻煩的事情,是以為友善起見,我提供了一個[PyPI](

  pypi.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

`

  我已經對這個鏡像進行了更新,現在它既支援Python 2.7也支援Python 3.0的解釋程式,以便于讀者在将來的工程中繼續使用vivisect-vstruct-wb。另外,遇到問題時,千萬不要忘了到Visi的GitHub上去看看有沒有現成的答案。

  ##0x01 Vstruct入門

  下面的例子相當于大家學程式設計語言時的“Hello World !”程式,它使用vstruct來解析位元組串中的小端模式的32位無符号整數:

`

  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()這兩個方法,但是我們最好熟悉所有方法的使用方法。下面是對每種方法的簡單總結:

  - .vsParse()——從位元組串解析執行個體。

  - .vsParseFd()——從檔案類型的對象中解析執行個體(必然用到.read()方法)。

  - .vsEmit()——将執行個體序列化為位元組串。

  - .vsSetValue()——利用原生Python執行個體為執行個體指派。

  - .vsGetValue()——複制執行個體的資料,并将其作為原生Python執行個體。

  - .vsSetLength()——設定數組類型如v_str的長度。

  - .vsIsPrim()——如果執行個體為簡單的primitive類型,則傳回True。

  - .vsGetTypeName()——取得存放執行個體類型名稱的字元串。

  - .vsGetEnum()——取得v_number執行個體關聯的v_enum執行個體,如果存在的話。

  - .vsSetMeta()——(内部方法)。

  - .vsCalculate()——(内部方法)。

  - .vsGetMeta()——(内部方法)。

  目前為止,vstruct看上去就像是struct.unpack的轉基因克隆,是以,接下來我們有必要介紹它更酷的功能。

  ##0x02 Vstructs的進階特性

  Vstruct解析器通常是基于類的。這個子產品提供了一組基本資料類型(例如v_uint32和v_wstr分别用于DWORD和寬字元串),以及一個相應的機制來将這些類型組合成更加進階的資料類型(VStructs)。首先,我們先來介紹基本的資料類型:

  - Vstruct.primitives.v_int8——有符号整數。

  - vstruct.primitives.v_int16

  - vstruct.primitives.v_int24

  - vstruct.primitives.v_int32

  - vstruct.primitives.v_int64

  -

  Vstruct.primitives.v_uint8 -無符号整數。

  - vstruct.primitives.v_uint16

  - vstruct.primitives.v_uint24

  - vstruct.primitives.v_uint32

  - vstruct.primitives.v_uint64

  - vstruct.primitives.long

  - vstruct.primitives.v_float

  - vstruct.primitives.v_double

  - vstruct.primitives.v_ptr

  - vstruct.primitives.v_ptr32

  - vstruct.primitives.v_ptr64

  - vstruct.primitives.v_size_t

  Vstruct.primitives.v_bytes——有确定長度的原始位元組序列。

  - Vstruct.primitives.v_str——有明确長度的ASCII字元串。

  - Vstruct.primitives.v_wstr——有明确長度的寬字元串。

  - Vstruct.primitives.v_zstr——以NULL為終止符的ASCII碼字元串。

  Vstruct.primitives.v_zwstr——以NULL為終止符的寬字元串。

  - vstruct.primitives.GUID

  - Vstruct.primitives.v_enum——用來說明整數類型。

  Vstruct.primitives.v_bitmask——用來說明整數類型。

  複雜的解析器可以通過定義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)

`

  [源代碼](

  github/vivisect/vivisect/blob/master/vstruct/defs/pe.py#L130)

  在這個例子中,我們使用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…

  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.

  self.Signature=v_bytes(size=4)

  self.FileHeader=IMAGE_FILE_HEADER()

`

  當Vstruct執行個體解析二進制資料遇到複雜的成員變量時,可以通過遞歸方式用子解析器來解決。在本例中,成員變量.FileHeader就是一種複合的類型,其定義見[這裡](

  github/vivisect/vivisect/blob/master/vstruct/defs/pe.py#L80)。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)

`

  ##0x03 Vstruct進階主題

  條件性的成員

  由于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):

  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):

  super(BlipBlop, self).__init__()

  self.aaa=v_uint32()

  self.bbb=v_uint32()

  selfc=v_uint32()

  def pcb_aaa(self):

  print("pcb_aaa: aaa: %s

  " % hex(self.aaa))

  def pcb_bbb(self):

  print("pcb_bbb: aaa: %s" % hex(self.aaa))

  print("pcb_bbb: bbb: %s

  " % 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

  " % hex(selfc))

  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):

  super(BazDataRegion2, self).__init__()

  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),需要讀者自己在實踐中摸索一下,但通常來說,如果你想要解析一個具體值的話,就應該避免使用方括号。

  ##0x04 小結

  在我們開發可維護的二進制代碼解析器的時候,vstruct子產品是一個強大的助手。它能夠去除開發過程帶來的大量樣本代碼。我特别喜歡使用vstruct解析惡意軟體的C2協定、資料庫索引和二進制的XML檔案。如果讀者感興趣的話,我建議大家通過下列項目來學習vstruct解析器:

  - Vstruct的定義,位址為

  github/vivisect/vivisect/tree/master/vstruct/defs。

  - Python-cim,位址為

  github/fireeye/flare-wmi/blob/master/python-cim/cim/cim.py和

  github/fireeye/flare-wmi/blob/master/python-cim/cim/objects.py。

  - Python-sdb,位址為

  github/williballenthin/python-sdb/blob/master/sdb/sdb.py。