逆向分析-之深入理解函数
Posted godoforange
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向分析-之深入理解函数相关的知识,希望对你有一定的参考价值。
程序都是由具有不同功能的函数组成的,
因此在逆向分析中将重点放在 函数的识别 和 参数的传递 上是明智的,
这样做可以将注意力集中在某一段代码上。函数是一个程序模块,用来实现一个特定的功能。一个函数包括函数名,入口参数,返回值,函数功能等部分。
1. 函数的识别
程序通过调用程序来调用函数,在函数执行后又返回调用程序继续执行。函数如何知道要返回的函数的地址呢?
实际上,调用函数的代码中保存了一个返回地址,该地址会与参数一起传递给被调用的函数。有多种方法可以实现这个功能,在绝大多数情况下,编译器都使用call和ret 指令来调用函数及返回调用指令。
call指令与跳转指令类似。
不同的是,call指令保存返回信息,即将其之后的指令地址压入栈顶,当遇到ret指令时返回这个地址。
也就是说,call指令给出的地址就是被调用函数的起始地址。ret指令则用于结束函数的执行(当然,不是所有的ret指令都标志着函数的结束)。
通过这一机制可以很容易地把函数调用和其他跳转指令区分开来。
因此,可以通过定位call机器指令或利用ret指令结束的标志来识别函数。call指令的操作数就是所调用函数的首地址。
这种函数直接调用方式使程序变得很简单。
2.函数的参数
函数传递参数有3种方式,栈方式,寄存器方式以及通过全局变量进行隐含参数传递方式。如果参数是通过栈传递的,就需要定义参数在栈中的顺序,并约定函数被调用后由谁来平衡栈。如果参数是通过寄存器传递的,就要确定参数存放在哪个寄存器中。
每种机制都有优缺点,并且和使用的编译语言有关。
(1)栈方式
栈是一种先进后出的存储区,栈顶指针esp 指向栈中第一个可用的数据项。在调用函数时,调用者依此把参数压入栈,然后调用函数,函数被调用后,在栈中取得数据并进行计算。函数计算结束后,由调用者或函数本身修改栈,使栈恢复原样。即平衡。
在参数的传递中有俩个很重要的问题:
1.当参数个数多于1个时,按照什么顺序把参数压入栈?
2.函数结束后,由谁来平衡栈?这些都必须有约定。这种在程序设计语言中为了实现函数调用而建立的协议称为调用约定。这种协议规定了函数中的参数传送方式,参数是否可变和由谁处理栈问题等。
C举例子。C的参数传递顺序为从右到左,平衡栈者是调用者。
push param3//参数从右到左 push param2 push param1 call test1 add esp,0C;//平衡栈
函数对参数的存取及局部变量都是通过栈定义的,非优化编译器用一个专门的寄存器(通常是 ebp)对参数进行寻址。C,C++,pascal等高级语言的函数执行过程基本一致情况如下:
调用者将函数(子程序)执行完毕时应返回的地址,参数压入栈。
子程序使用 ”ebp指针+偏移量“对栈中的参数进行寻址并取出,完成操作
子程序使用ret或retf返回 。此时CPU将eip置为栈中保存的地址,并继续执行它。
栈在整个过程中发挥着非常重要的作用。栈是一个先进后出的区域,只有一个出口,即当前栈顶。栈操作的对象只能说双操作数(占4字节) 。
在许多时候,编译器会按优化方式来编译程序,栈寻址稍有不同。这时,编译器为了节省ebp寄存器或尽可能减少代码以提高速度,会直接通过esp对参数进行寻址。esp的值在函数执行期间会发生变化,该变化出现在每次有数据进出栈时。要想确定对那个变量进行寻址,就要知道程序当前位置的esp的值,为此必须从函数的开始部分进行跟踪。
(2)利用寄存器传递参数
寄存器传递参数的方式没有标准,所有与平台相关的方式都是由编译器开发人员制定的。尽管没有标准,但绝大多数编译器提供商都在不对兼容性进行声明的情况下遵循相应的规范,即fastcall规范。fastcall 顾名思义,特点就是快(因为它是靠寄存器传递参数的)
不同编译器实现的Fastcall 稍有不同。Microsoft Visual C++ 编译器在采用Fastcall 规范传递参数时,左边的2个不大于4字节的参数分别放在ecx和edx寄存器中,寄存器用完后就要使用栈,其余参数仍按照从左到右的顺序压入栈,被调用的函数在返回前清理传送参数的栈,浮点值,远指针和__int64 类型总是通过栈来传递的。而Borland Delphi/C++ 编译器在采用Fastcall 规范传递参数时候,左边的3个不大于4字节(dword)的参数分别放在eax,edx,和ecx寄存器中,寄存器使用完之后,其余参数按照从左到右的PASCAL方式压入栈。
另有一款编译器Watcom C 总是通过寄存器来传递参数,它严格为每一个参数分配一个寄存器,默认情况下第一个参数用eax,第二个参数用edx,第三个参数用ebx,第四个阐述用ecx。如果寄存器用完,就会用栈来传递参数。因此,其参数实际上可能通过任何寄存器进行传递。
(3)名称修饰约定
为了允许使用操作符和函数重载,C++编译器往往会按照某种规则改写每一个入口点的符号名,从而允许同一个名字(具有不同的参数类型或者不同的作用域) 有多个用法且不会破坏现有的基于C的链接器。这项技术通常称为名称改编或者名称修饰。许多C++编译器厂商都制定了自己的名称修饰方案。
在VC++中,函数修饰名由变异类型(C或C++),函数名,类名,调用约定,返回类型,参数等共同决定。关于名称的内容很多,
stdcall调用约定在输出函数名前面加一个下划线前缀,在后面加一个”@“符号以及参数的字节数,格式为”[email protected]“
_cdecl调用约定仅在输出函数名前面加一个下划线前缀,格式为”_functionname“
Fastcall 调用约定在输出函数名前面加一个”@“符号,在后面加一个”@“符号及其参数的字节数,格式为”@[email protected]“
3.函数的返回值
函数被调用执行后,将向调用者返回1个或多个执行结果,称为函数返回值。返回值最常见的形式是return操作符,还有通过参数按传引用的方式返回值,通过全局变量返回值等。
(1) 用return 操作符返回值
在一般情况下,函数的返回值放在eax寄存器中返回,如果处理结果的大小超过eax寄存器的容量,其高32位就会放到edx寄存器中。
(2)通过参数按照引用方式返回值
给参数传递的方式有俩种,分别是传值和传引用。进行传值调用时,会建立参数的一份复本,并把它传给调用函数,在调用函数中修改参数的复本不会影响原始的变量值。
传引用调用允许调用参数修改原始变量的值。调用某个函数,当把变量的地址传递给函数时,可以在函数中用间接运算符修改调用函数内存单元该变量的值。
以上是关于逆向分析-之深入理解函数的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段