x86 反彙編/資料結構
很少有程式可以透過使用簡單的記憶體儲存來工作;大多數程式需要利用複雜的資料物件,包括指標、陣列、結構和其他複雜型別。本章將討論編譯器如何實現複雜的資料物件,以及逆向工程師如何識別這些物件。
陣列僅僅是用於儲存相同型別多個數據物件的儲存方案。資料物件按順序儲存,通常作為指向陣列開頭的指標的偏移量。考慮以下 C 程式碼
x = array[25];
它與以下彙編程式碼相同
mov ebx, $array
mov eax, [ebx + 25]
mov $x, eax
現在,考慮以下示例
int MyFunction1()
{
int array[20];
...
這(大致)轉換為以下彙編虛擬碼
:_MyFunction1
push ebp
mov ebp, esp
sub esp, 80 ;the whole array is created on the stack!!!
lea $array, [esp + 0] ;a pointer to the array is saved in the array variable
...
整個陣列在堆疊上建立,指向陣列底部的指標儲存在變數“array”中。最佳化編譯器可能會忽略最後一條指令,並簡單地透過 esp 的 +0 偏移量(在本例中)引用陣列,但我們將詳細地進行處理。
同樣,考慮以下示例
void MyFunction2()
{
char buffer[4];
...
這將轉換為以下彙編虛擬碼
:_MyFunction2
push ebp
mov ebp, esp
sub esp, 4
lea $buffer, [esp + 0]
...
看起來沒什麼問題。但是,如果程式無意中訪問了 buffer[4]?buffer[5]呢?buffer[8]呢?這是緩衝區溢位漏洞的雛形,將在後面的部分討論(可能)。但是,本節不討論安全問題,而是隻關注資料結構。
要在堆疊上發現數組,請查詢在堆疊上分配的大量本地儲存(例如,“sub esp, 1000”),並查詢該資料的大部分被 esp 的不同暫存器偏移量訪問。例如
:_MyFunction3
push ebp
mov ebp, esp
sub esp, 256
lea ebx, [esp + 0x00]
mov [ebx + 0], 0x00
是堆疊上建立陣列的一個好跡象。當然,最佳化編譯器可能只是想從 esp 偏移,所以你需要小心。
記憶體中的陣列,如全域性陣列,或具有初始資料的陣列(請記住,初始化資料是在記憶體中的 .data 部分建立的),並將作為記憶體中硬編碼地址的偏移量訪問
:_MyFunction4
push ebp
mov ebp, esp
mov esi, 0x77651004
mov ebx, 0x00000000
mov [esi + ebx], 0x00
需要記住,結構和類可能會以類似的方式訪問,因此逆向工程師需要記住,陣列中的所有資料物件都是相同型別的,它們是連續的,並且通常會在某種迴圈中處理。此外,(這可能是最重要的部分),陣列中的每個元素都可以透過相對於基地址的變數偏移量訪問。
由於大多數情況下陣列是透過計算的索引訪問的,而不是透過常數訪問的,因此編譯器可能會使用以下方法訪問陣列的元素
mov [ebx + eax], 0x00
如果陣列儲存的元素大小大於 1 位元組(對於 char),則需要將索引乘以元素的大小,從而產生類似於以下程式碼的程式碼
mov [ebx + eax * 4], 0x11223344 # access to an array of DWORDs, e.g. arr[i] = 0x11223344
...
mul eax, $20 # access to an array of structs, each 20 bytes long
lea edi, [ebx + eax] # e.g. ptr = &arr[i]
此模式可用於區分對陣列的訪問和對結構資料成員的訪問。
所有 C 程式設計師都會熟悉以下語法
struct MyStruct
{
int FirstVar;
double SecondVar;
unsigned short int ThirdVar;
}
它被稱為結構(Pascal 程式設計師可能知道類似的概念,稱為“記錄”)。
結構可以非常大或非常小,並且可以包含各種不同的資料。在記憶體中,結構可能看起來非常像陣列,但需要記住一些關鍵點:結構不需要包含所有相同型別的資料欄位,結構欄位通常是 4 位元組對齊的(不是連續的),並且結構中的每個元素都有自己的偏移量。因此,透過相對於基地址的變數偏移量引用結構元素毫無意義。
看看以下結構定義
struct MyStruct2
{
long value1;
short value2;
long value3;
}
假設指向此結構基地址的指標載入到 ebx 中,我們可以透過以下兩種方案之一訪問這些成員
;data is 32-bit aligned
[ebx + 0] ;value1
[ebx + 4] ;value2
[ebx + 8] ;value3
|
;data is "packed"
[ebx + 0] ;value1
[ebx + 4] ;value2
[ebx + 6] ;value3
|
第一種排列是最常見的,但它顯然在偏移量 +6 處留出了一個完整的記憶體字(2 個位元組),而它根本沒有使用。編譯器偶爾允許程式設計師手動指定每個資料成員的偏移量,但這並不總是這樣。第二個示例也有一個好處,即逆向工程師可以很容易地識別出結構中的每個資料成員都有不同的尺寸。
現在考慮以下函式
:_MyFunction
push ebp
mov ebp, esp
lea ecx, SS:[ebp + 8]
mov [ecx + 0], 0x0000000A
mov [ecx + 4], ecx
mov [ecx + 8], 0x0000000A
mov esp, ebp
pop ebp
該函式顯然以指向資料結構的指標作為其第一個引數。此外,每個資料成員的大小都相同(4 個位元組),那麼我們如何判斷這是一個數組還是一個結構呢?要回答這個問題,我們需要記住結構和陣列之間的一個重要區別:陣列中的元素都是相同型別的,結構中的元素不需要是相同型別的。根據這條規則,很明顯該結構中的一個元素是一個指標(它指向結構本身的基地址!),另外兩個欄位載入了十六進位制值 0x0A(十進位制為 10),這當然不是我在任何系統上使用過的有效指標。然後,我們可以部分重新建立結構和函式程式碼如下
struct MyStruct3
{
long value1;
void *value2;
long value3;
}
void MyFunction2(struct MyStruct3 *ptr)
{
ptr->value1 = 10;
ptr->value2 = ptr;
ptr->value3 = 10;
}
順便說一句,請注意,此函式沒有將任何內容載入到 eax 中,因此它不返回值。
假設我們在函式中遇到以下情況
:MyFunction1
push ebp
mov ebp, esp
mov esi, [ebp + 8]
lea ecx, SS:[esi + 8]
...
這裡發生了什麼?首先,esi 載入了函式第一個引數的值(ebp + 8)。然後,ecx 載入了指向 esi 的偏移量 +8 的指標。看起來我們有兩個指標訪問同一個資料結構!
所討論的函式可以很容易地是以下兩種原型之一
struct MyStruct1
{
DWORD value1;
DWORD value2;
struct MySubStruct1
{
...
struct MyStruct2
{
DWORD value1;
DWORD value2;
DWORD array[LENGTH];
...
結構中一個指標相對於另一個指標的偏移量通常意味著一個複雜的資料結構。然而,結構和陣列的組合太多了,因此這本華夏公益教科書不會花費太多時間在這個主題上。
陣列元素和結構欄位都是作為陣列/結構指標的偏移量訪問的。反彙編時,我們如何區分這些資料結構?以下是一些提示
- 陣列元素不應被單獨訪問。陣列元素通常使用變數偏移量訪問
- 陣列經常在迴圈中訪問。由於陣列通常包含一系列類似的資料項,因此訪問它們的最佳方式通常是迴圈。具體來說,
for(x = 0; x < length_of_array; x++)型別的迴圈通常用於訪問陣列,儘管也可能存在其他迴圈。 - 陣列中的所有元素都具有相同的資料型別。
- 結構欄位通常使用常量偏移量訪問。
- 結構體欄位通常不是按順序訪問的,也不是使用迴圈訪問的。
- 結構體欄位通常不是全部相同的型別,或者不是相同的寬度。
程式設計中常用的兩種結構是連結串列和二叉樹。這兩種結構又可以以多種方式變得更加複雜。下面的圖片顯示了連結串列結構和二叉樹結構的示例。
連結串列或二叉樹中的每個節點都包含一定數量的資料,以及指向其他節點的指標(或指標)。考慮以下彙編程式碼示例
loop_top:
cmp [ebp + 0], 10
je loop_end
mov ebp, [ebp + 4]
jmp loop_top
loop_end:
在每次迴圈迭代中,[ebp + 0] 處的資料值與值 10 進行比較。如果兩者相等,則迴圈結束。但是,如果兩者不相等,則 ebp 中的指標會使用 ebp 偏移處的指標進行更新,並且迴圈繼續。這是一種經典的連結串列迴圈搜尋技術。這類似於以下 C 程式碼
struct node
{
int data;
struct node *next;
};
struct node *x;
...
while(x->data != 10)
{
x = x->next;
}
二叉樹也是一樣的,只是會使用兩個不同的指標(右分支指標和左分支指標)。

