目录
1.逆向为什么必须要了解入参顺序(调用约定)?
2.X86平台不同的调用约定
3.X64平台系统调用约定_fastcall
4.C++类入参约定_thiscall
了解这部分,可以了解汇编功能代码前(序言部分)后(结束部分)的参数传递,堆栈的入(push)出(pop)栈顺序,这是逆向代码分析的根基,在浩如烟海的汇编代码中,有这些知识和信息,会增加确定度,利于总体代码的识别。
C语言转为汇编,因为有不同的调用约定,会有三种参数入栈顺序和栈平衡的方法,分别是:
在32位的体系结构中,有3种调用约定,分别是cdecl
,stdcall
,fastcall
,GCC的默认调用约定为cdecl
。参数从右向左压栈,调用者负责在调用后清理堆栈,返回值保存在eax寄存器中,非易失寄存器为ebp
,esp
,ebx
,esi
,edi
。
C++还有一种叫thiscall的传参模式,导出类的函数,也是类似用寄存器eax来传递参数:
1.IDA分析的时候,默认使用cdecl来解析:
2.初始化局部变量的初始值用的是0XCCCCC,因为CC是汇编中断指令,如果不小心跑到这里,会立即终止。不会乱操作。
3.eax寄存器是放返回值的。有一种爆破的方法是:进入call调用的过程后做如下修改:
"mov eax,1"指令的作用是将eax的al赋值为01,retn则是跳出call,程序接着执行call后面的test指令(相当于c++中的return),这样的操作是因为函数retn之后,执行了一个:
4.edi,esi,edx三个寄存器是出入栈时要保留的。
5.其他寄存器的用途
(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
(3)EIP寄存器,用来存储CPU要读取指令的地址的,通过修改这个寄存器的值,可以让程序跑到一个别的地方去,比如我们自己的代码。
X64默认使用_fastcall模式,rcx,rdx,r8,r9四个寄存器用于存放参数的最后前面的四个参数,但在栈上还是会预留4个寄存器的对应空间(shadow space),调用者负责栈的平衡:调用者来管理变量的初始化和管理堆栈指针(来支持可变参数),这和之前的约定不一样,参数入栈,会对齐到8个字节,栈的大小是16的整数倍。
下面是一个选择了X64平台编译的函数。
int mix(int a,int b,int c,int d,int e,int f, int g){
return a | b | c | d | e | f |g;
}
int addAll(int a,int b,int c,int d,int e,int f, int g){
return a+b+c+d+e+f+g+mix(a,b,c,d,e,f,g);
}
int main(int argc,char **argv){
int total;
total = addAll(1,2,3,4,5,6,7);
printf("result is %d\n",total);
return 0;
}
对应的汇编指令为:
(gdb) disassemble *main
Dump of assembler code for function main:
0x0000000000400534 : push rbp
0x0000000000400535 : mov rbp,rsp
0x0000000000400538 : sub rsp,0x30
0x000000000040053c : mov DWORD PTR [rbp-0x14],edi
0x000000000040053f : mov QWORD PTR [rbp-0x20],rsi
0x0000000000400543 : mov DWORD PTR [rsp],0x7
0x000000000040054a : mov r9d,0x6
0x0000000000400550 : mov r8d,0x5
0x0000000000400556 : mov ecx,0x4
0x000000000040055b : mov edx,0x3
0x0000000000400560 : mov esi,0x2
0x0000000000400565 : mov edi,0x1
0x000000000040056a : call 0x4004c7
0x000000000040056f : mov DWORD PTR [rbp-0x4],eax
0x0000000000400572 : mov esi,DWORD PTR [rbp-0x4]
0x0000000000400575 : mov edi,0x400688
0x000000000040057a : mov eax,0x0
0x000000000040057f : call 0x400398
0x0000000000400584 : mov eax,0x0
0x0000000000400589 : leave
0x000000000040058a : ret
对应的addALL里面,极有可能会有这样的汇编语句:
move dword ptr p[rsp+20h],r9d ;将四个寄存器的值放回到堆栈中
move dword ptr p[rsp+18h],r8d
move dword ptr p[rsp+10h],edx
move dword ptr p[rsp+8h],ecx
__thiscall是关于类的一种调用方式,它与其他调用方式的最大区别是:
__thiscall对每个函数都增加了一个类指针参数
class aa
{
void bb(int cc);
};
实际上bb的函数原形是void bb(aa &this, int cc);
这就是__thiscall的调用方式。
以C++打印函数为例:
cout << "Hello World! " << endl;
其编译出来的代码,反编译出来,看到是使用__thiscall调用约定的
std::ostream *__thiscall std::operator<<>(std::ostream *_Ostr)
{
int v2; // eax
int v3; // edi
unsigned int v4; // esi
int v5; // edi
unsigned int v6; // esi
int v7; // ecx
int v8; // ecx