在逆向工程汇编分析过程中,我们很关心关键函数的功能及其参数的意义。总会遇到很多的函数调用。我们需要关注的是:参数的传递,函数返回,函数调用,参数引用,返回值,函数返回,堆栈平衡这些问题。下面我们用实例来分析一下函数整个调用过程。
首先给一张函数调用全景图
为了方便说明,写一个简单测试用例。C代码实例如下:
int funWith4args(int A, int B, int C,int D)
{
int sum = A + B + C + D;
return sum;
}
int __stdcall fun_stdcall_5args(int A, int B, int C, int D, int E)
{
int sum = A + B + C + D+ E;
return sum;
}
int __cdecl fun_cdecl_5args(int A, int B, int C, int D, int E)
{
int sum = A + B + C + D + E;
return sum;
}
int __fastcall fun_fastcall_5args(int A, int B, int C, int D, int E)
{
int sum = A + B + C + D + E;
return sum;
}
int _tmain(int argc, _TCHAR* argv[])
{
int A,B,C,D,E;
A = 0x100;
B = 0x200;
C = 0x300;
D = 0x400;
E = 0x500;
int sum1 = fun_stdcall_5args(A, B, C, D, E);
printf("sum = %d\r\n", sum1);
int sum2 = fun_cdecl_5args(A, B, C, D, E);
printf("sum = %d\r\n", sum2);
int sum3 = fun_fastcall_5args(A, B, C, D, E);
printf("sum = %d\r\n", sum3);
return 0;
}
函数调用协议可以参考文章:
函数调用协议汇总X86/X64/ARM/ARM64
如图1所示,参数由右向左依次入栈
图1
call 指令可以分写为两个动作,push 和jump。Push当前函数返回地址,jump到函数处。我们在IDA中F7进入函数,栈中情况如图2所示。
图2
0x791586指向函数的返回地址,如图3
图3
这里看看被代用函数是如何使用参数的。
首先了解C代码,很简单的求和。那么我们在汇编代码中学习一下如何引用A,B,C,D,E五个参数
int __stdcall fun_stdcall_5args(int A, int B, int C, int D, int E)
{
int sum = A + B + C + D+ E;
return sum;
}
如图4所示,ebp是本函数的栈底,从上面的分析我们知道,ebp+4保存的函数返回地址,从ebp+8开始保存着函数调用所使用的参数。
函数返回
当函数将返回值设置好后,函数开始准备返回。
图5中,返回关键代码参见方框中。首先恢复函数体内使用的寄存器,恢复函数体临时申请栈空间,弹出调用者的ebp,最后调用retn函数返回。
图5
retn指令表示取出当前栈顶值 作为返回地址 将eip修改为该地址。
看看函数返回栈的情况。如图6所以,retn指令后eip指向被调用函数的下面一条指令。esp和ebp恢复到了代用函数前的状态。
图6
根据规则进行堆栈平衡,可能是函数体内负责堆栈平衡,也可能是调用者负责堆栈平衡。
本例汇编如图7
图7
堆栈恢复平衡后如图8,调用函数之前将参数压栈使用的栈空间,释放了。达到调用的平衡。
至此,一个函数的代用过程结束。我们在回头看看开篇给出的函数调用全景图,回顾整个调用过程。