常见数据类型分析要点一步到"胃"
数据结构是计算机存储、组织数据的方式。在进行逆向分析时,确定数据结构以后,算法就很容易得到了。有些时候,事情也会反过来,即根据特定算法来判断数据结构。本节将讨论常见的数据结构及它们在汇编语言中的实现方式。
局部变量:
局部变量(Local Variables)是函数内部定义的一个变量,其作用域和生命周期局限于所在函数内。使用局部变量使程序模块化封装成为可能。从汇编的角度来看,局部变量分配空间时通常会使用栈和寄存器。
(1)利用栈存放局部变量
局部变量在栈中进行分配,函数执行后会释放这些栈,程序用“sub esp,8”语句为局部变量分配空间,用[ehp-xxxx]寻址调用这些变量,而参数调用相对于ebp偏移量 是正的,即[ebp+xxxx],因此在逆向时比较容易区分。编译器在优化模式时,通过esp寄存器直接对局部变量和参数进行寻址。当函数退出时,用“add esp,8”指令平衡栈, 以释放局部变量占用的内存。有些编译器(例如Delphi)通过给esp加一个负值来进行内存的分配。另外,编译器可能会用“push reg’”指令取代“sub esp,4”指令,以节省几 字节的空间。
局部变量分配与清除栈的形式如表所示。
下面这个实例是用“push reg””指令来取代“sub esp,4”指令的。
1 | int add(int x, int y); |
用Microsoft Visual C+6.0进行编译,不进行忧化,其汇编代码如下。
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 | int z; |
用Microsoft Visual C+6.0进行编译,但不进行优化,其汇编代码如下。
用PEID打开编译后的程序,查看区块,区块信息如图所示。全局变量004084C0h在.data区块中,该区块的属性为可读写。
使用这种对内存直接寻址的硬编码方式,比较容易识别出这是一个全局变量。一般编译器会将全局变量放到可读写的区块里(如果放到只读区块里,就是一个常量)。
与全局变量类似的是静态变量,它们都可以按直接方式寻址等。不同的是,静态变量的作用范围是有限的,仅在定义这些变量的函数内有效。
数组:
数组是相同数据类型的元素的集合,它们在内存中按顺序连续存放在一起。在汇编状态下访问数组一般是通过基址加变址寻址实现的。
请看下面这个数组访问实例。
1 |
|
用Microsoft Visual C+6.0进行编译,将优化选项设置为“Maximize Speed”,其汇编代码如下。
在内存中,数组可存在于栈、数据段及动态内存中。本例中的a[ ]数组就保存在数据段.data中,其寻址用“基址+编移量”实现。
1 | mov edi, dword_407030[eax] |
这种间接寻址一般出现在给一些数组或结构赋值的情况下,其寻址形式一般是[基址+偏移量]。基址可以是常量,也可以是寄存器,为定值。根据n值的不同,可以对结构中的相应单元赋值。
b[ ]数组放在栈中,这些栈在编译时分配。数组在声明时可以直接计算偏移地址,针对数组成员寻址是采用实际的偏移量完成的。