利用vstruct解析二進位制資料

wyzsk發表於2020-08-19
作者: m6aa8k · 2015/09/23 11:45

原文地址: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的解析器更易於長期維護。

0x00 安裝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上去看看有沒有現成的答案。

0x01 Vstruct入門


下面的例子相當於大家學程式語言時的“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()這兩個方法,但是我們最好熟悉所有方法的使用方法。下面是對每種方法的簡單總結:

  • .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)    

原始碼

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

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

0x04 小結


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

  • Vstruct的定義,地址為https://github.com/vivisect/vivisect/tree/master/vstruct/defs
  • Python-cim,地址為https://github.com/fireeye/flare-wmi/blob/master/python-cim/cim/cim.pyhttps://github.com/fireeye/flare-wmi/blob/master/python-cim/cim/objects.py
  • Python-sdb,地址為https://github.com/williballenthin/python-sdb/blob/master/sdb/sdb.py
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章