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;
}
二叉树也是一样的,只是会使用两个不同的指针(右分支指针和左分支指针)。