​ 数据结构是计算机存储、组织数据的方式。在进行逆向分析时,确定数据结构以后,算法就很容易得到了。有些时候,事情也会反过来,即根据特定算法来判断数据结构。本节将讨论常见的数据结构及它们在汇编语言中的实现方式。

局部变量:局部变量(Local Variables)是函数内部定义的一个变量,其作用域和生命周期局限于所在函数内。使用局部变量使程序模块化封装成为可能。从汇编的角度来看,局部变量分配空间时通常会使用栈和寄存器。

​ (1)利用栈存放局部变量

​ 局部变量在栈中进行分配,函数执行后会释放这些栈,程序用“sub esp,8”语句为局部变量分配空间,用[ehp-xxxx]寻址调用这些变量,而参数调用相对于ebp偏移量 是正的,即[ebp+xxxx],因此在逆向时比较容易区分。编译器在优化模式时,通过esp寄存器直接对局部变量和参数进行寻址。当函数退出时,用“add esp,8”指令平衡栈, 以释放局部变量占用的内存。有些编译器(例如Delphi)通过给esp加一个负值来进行内存的分配。另外,编译器可能会用“push reg’”指令取代“sub esp,4”指令,以节省几 字节的空间。
​ 局部变量分配与清除栈的形式如表所示。

image-20240716144036292

​ 下面这个实例是用“push reg””指令来取代“sub esp,4”指令的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int x, int y);
int main(void)
{
int a = 5, b = 6;
add(a, b);
return 0;
}

int add(int x, int y)
{
int z;
z = x + y;
return(z);
}

​ 用Microsoft Visual C+6.0进行编译,不进行忧化,其汇编代码如下。

image-20240716144329185

image-20240716144347269

​ add函数里不存在“sub esp,n”之类的指令,程序通过“push ecx’”指令来开辟一块栈,然后用[ebp-04]来访问这个局部变量。局部变量的起始值是随机的,是其他函数 执行后留在栈中的垃圾数据,因此需要对其进行初始化。初始化局部变量有两种方法:一种是通过mov指令为变量赋值,例如“mov[ebp-04],5”;另一种是使用push指令直 接将值压人栈,例如“push 5”。

​ (2)利用寄存器存放局部变量

​ 除了栈占用2个寄存器,编译器会利用剩下的6个通用寄存器尽可能有效地存放局部变量,这样可以少产生代码,提高程序的效率。如果寄存器不够用,编译就会将 变量放到栈中。在进行逆向分析时要注意,局部变量的生存周期比较短,必须及时确定当前寄存器的变量是哪个变量。

全局变量:全局变量作用于整个程序,它一直存在,放在全局变量的内存区中。局部变量则存在于函数的栈区中,函数调用结束后便会消失。在大多数程序中,常数一般放在全局变量中,例如一些注册版标记、测试版标记等。在大多数情况下,在汇编代码中识别全局变量比在其他结构中要容易得多。全局变量通常位于数据区块(.data)的一个固定地址处,当程序需要访问全局变量时,一般会用一个固定的硬编码地址直接对内存进行寻址,示例如下。

1
mov eax, dword ptr [40874c0h]

​ 全局变量可以被同一文件中的所有函数修改,如果某个函数改变了全局变量的值,就能影响其他函数(相当于函数间的传递通道),因此,可以利用全局变量来传递参数和函数返回值等。全局变量在程序的整个执行过程中占用内存单元,而不像局部变量那样在需要时才开辟内存单元。

​ 看一个利用全局变量传递参数的实例,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int z;

int add(int x, int y);
int main(void)
{
int a = 5, b = 6;
z = 7;
add(a, b);
return 0;
}

int add(int x, int y)
{
return(x + y + z);
}

​ 用Microsoft Visual C+6.0进行编译,但不进行优化,其汇编代码如下。

image-20240716151649239

image-20240716151700772

​ 用PEID打开编译后的程序,查看区块,区块信息如图所示。全局变量004084C0h在.data区块中,该区块的属性为可读写。

image-20240716153017410

​ 使用这种对内存直接寻址的硬编码方式,比较容易识别出这是一个全局变量。一般编译器会将全局变量放到可读写的区块里(如果放到只读区块里,就是一个常量)。
​ 与全局变量类似的是静态变量,它们都可以按直接方式寻址等。不同的是,静态变量的作用范围是有限的,仅在定义这些变量的函数内有效。

数组:数组是相同数据类型的元素的集合,它们在内存中按顺序连续存放在一起。在汇编状态下访问数组一般是通过基址加变址寻址实现的。
​ 请看下面这个数组访问实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main(void)
{
static int a[3] = {0x11, 0x22, 0x33};
int i, s = 0, b[3];
for(i = 0; i < 3; i++)
{
s = s + a[i];
b[i] = s;
}

for(i = 0; i < 3; i++)
{
printf("%d\n", b[i]);
}

return 0;
}

​ 用Microsoft Visual C+6.0进行编译,将优化选项设置为“Maximize Speed”,其汇编代码如下。

image-20240716153737093

​ 在内存中,数组可存在于栈、数据段及动态内存中。本例中的a[ ]数组就保存在数据段.data中,其寻址用“基址+编移量”实现。

1
mov     edi, dword_407030[eax]

​ 这种间接寻址一般出现在给一些数组或结构赋值的情况下,其寻址形式一般是[基址+偏移量]。基址可以是常量,也可以是寄存器,为定值。根据n值的不同,可以对结构中的相应单元赋值。
​ b[ ]数组放在栈中,这些栈在编译时分配。数组在声明时可以直接计算偏移地址,针对数组成员寻址是采用实际的偏移量完成的。