通过分析汇编代码来理解其代码功能,然后用高级语言重新描述这段代码,逆向分析原始软件的思路,这就是逆向工程。
32位软件逆向技术
启动函数
首先被执行的是启动函数的相关代码,这段代码是由编译器生成的。启动函数就是对该运行库进行一个初始化。当所有的初始化操作结束后,启动函数会调用应用程序的进入点函数。进入点返回时,启动函数便调用C运行库的exit函数,将返回值传递给他,再推出。
函数
逆向工程中将重点放在函数的识别及参数的传递是明智的,这样可以将注意力集中在一段代码上。
1.函数的识别
程序通过调用程序调用函数,函数执行后又返回调用程序继续执行。实际上,调用函数的代码中保存了一个返回地址,该地址会与参数一起传递给被调用的函数。很多情况下,编译器都是用call与ret。
call给出的地址就是被调用函数的起始位置,ret命令则用于结束函数的执行。有一个例子。
也有例外,程序的调用函数通过间接调用,即通过寄存器传递函数地址或者动态计算函数地址调用,也有例子
2.函数的参数
参数传递有三种方式,栈放肆,寄存器方式及通过全局变量进行隐含函数参数传递。如果是栈方式,就需要定义参数在栈中的顺序并约定被调用后谁来平衡栈。如果是寄存器传递,就需要规定存放在哪个寄存器当中。
①栈传递
栈后进先出。在调用函数时,调用者依次把参数压入栈,然后调用函数。函数调用后,栈中取得数据并计算。当参数的个数多于一个时,以什么样的顺序压入栈有一个约定。这就是调用约定。不同的语言有不同的调用约定。
我们可以理解stdcall是缝合的约定协议。有cdecl的特点:参数传递由右到左(先压入最后面的),也有pascal的特点:由调用函数清除栈。
非优化编译器用一个专门的寄存器通常是ebp,对参数进行寻址。
对于函数传递,也通过对高级语言分析出了汇编语言。
此外enter和leave指令也可以进行栈的维护,诸如此类的可以看出编译器对程序进行优化,以此来减少代码提高运行速度。
②利用寄存器传递参数
利用寄存器传递参数没有标准。fastcall顾名思义也很快。
左边的2个不大于4个字节的参数放在ecx和edx寄存器中。左边的3个不大于4个字节的参数放在eax,edx和ecx寄存器中。
另外也有一款编译器总是通过寄存器来传递参数。第一个参数用eax,第二个edx第三个ebx第四个ecx。
③名称修饰约定
为了允许使用操作符和函数重载,C++编译器往往会按照某种规则改写每一个入口点的符号名,从而允许同一个名字有多个用法且不破坏现有的基于C的链接器。这项技术通常称为名称改变或者名称修饰。
C编译的函数名修饰约定规则如下
C++编译
3.函数的返回值
函数被调用执行后,将向调用者返回一个或者多个执行结果,称为函数返回值。
①return操作符
一般情况下,返回值存放在eax返回。如果超过规模那么高32位就会放在edx寄存器当中。
②通过参数按传引用方式返回值
函数参数传递方式有两种,分别是传值和传引用。传值调用时,会建立参数的一份副本,传给参数,在调用函数中修改副本不影响原来的值。传引用调用允许修改原始量。
数据结构
数据结构是计算机存储,组织数据的方式。本节将讨论厂家的呢数据结构以及他们在汇编中的实现方式。
1.局部变量
是函数内部定义的一个变量。作用域和生命周期局限于所在函数内。
①利用栈存放局部变量
程序用sub语句为局部变量分配空间,用【ebp-xxxx】寻址调用这些变量,而参数调用相对于ebp偏移量是正的,即【ebp+xxxx】。另外push reg用取代sub esp,4指令也可以节省几个字节
②利用寄存器存放局部变量
除了栈占用的两个寄存器,编译器还会利用6个通用寄存器尽可能有效的存放局部变量。如果寄存器不够了,编译就会将变量放在栈中。
2.全局变量
全局变量作用于整个程序,放在全局变量的内存区中。绝大部分情况中,汇编代码识别全局变量比在其他结构当中容易的多。全局变量位于.data的数据区块当中。调用全局变量的时候一般会用一个硬编码地址进行内存寻址
与全局变量类似的是静态变量都直接方式寻址,区别在静态变量仅在定义这些变量的函数中有效。
3.数组
数组是相同数据类型的元素的结合。在汇编状态下访问数组一般是基址加变址寻址实现的。例如下面这例子
虚函数
C++是一门支持面向对象的语言。它的核心概念不多,最重要的概念是虚函数。虚函数的地址不能在编译时确定,只能在调用时确定。所有对虚函数的引用通常放在一个专用数组,虚函数表。调用虚函数时先取出虚函数表指针,再得到虚函数表的地址,并通过这个地址取出虚函数。
有一个例子
虚函数表中有两组数据分别是add函数和sub函数
所以看出虚函数是函数表的指针间接加以调用的。
控制语句
识别关键跳转是软件解密的一项重要技能
1.if then else语句
cmp指令不会修改操作数。操作数相减会影响处理的几个标志,例如零标志,进位标志符号标志和溢出标志。jz等指令就是条件跳转指令。实际上,很多编译器用test或者or之类的短的逻辑指令替换cmp。一般是“test eax,eax”如果eax值为0,逻辑与运算结果为0,zf为1否则zf为0.
2.switch-case语句
实际上就是多个if then语句的嵌套组合。
不优化版本
优化后
优化时用dec eax代替了cmp,指令更短运行速度更快。
3.转移指令机器码的计算
分为短转移和长转移。短转移:无条件和条件转移都是2个字节。长转移:无条件为5字节,有条件为6字节。子程序调用指令:有两类,一类类似于长位移,一类是调用的参数设计寄存器,栈比较复杂。
①短转移指令机器码计算实例
有一段无条件转移指令
无条件转移的机器码格式为“EBxx”,EB00h~EB7Fh为后位移80h到FFh是向前位移。
②长位移指令机器码计算实例