逆向知识十一讲,识别函数的调用约定,函数参数,函数返回值.
在反汇编中,我们常常的会看到各种的函数调用,或者通过逆向的手段,单独的使用这个函数,那么此时,我们就需要认识一下怎么识别函数了.
一丶识别__cdecl 函数(俗称C Call),函数参数,函数返回值
首先写一个C Call的函数
1.返回值 int类型, 参数int 类型
高级代码:
int __cdecl MyAdd(int a,int b) { return a + b; } int main(int argc, char* argv[]) { MyAdd(3,4); return 0; }
main函数调用我们的自己写的
Debug下的汇编代码
在Debug版本下的调用处,我们会看到这种代码,没有流水线优化,没有任何优化
看到了,两个push,紧接着一个Call,然后平栈在外面
识别参数
有经验的可能会说两个push 就是两个参数,其实不然,我们要进入函数内部,看内部的代码用了几个参数,要通过这个来识别.
有两处使用,所以是两个参数. 而且直接给eax反回了,此时我们就可以在main函数位置,调用此函数的位置往上数几个push了,这些push才是属于自己这个函数的.
识别参数类型:
参数类型还是很好识别了,使用参数的地方用的直接是4个字节的寄存器,那么我们可以暂定为int类型
识别调用约定
如果在函数外面平栈,那么就是C调用约定,从识别参数来看,函数内部的 retn并没有平栈.
识别返回值
从上面的识别参数我们看到,eax重新写入了,那么返回值就是int类型
Release版本下的汇编
Release版本和Debug版本差不多一样,优化了少许代码,但是核心代码不变
总结:
1.识别参数,看其函数内部使用了几个参数,然后在函数调用的地方往上数几个push这些是属于自己函数的.
2.识别参数类型,看其参数是怎么使用.
3.识别调用约定,看其函数内部是否平栈
4.识别返回值类型,看其eax是否是被重写,如果被重写,则是返回值是int类型
2.返回值 __int64 C调用约定,参数是浮点和double的情况下
高级代码:
__int64 __cdecl MyAdd(float a,double b) { return a + b; } int main(int argc, char* argv[]) { MyAdd(3,4); return 0; }
Debug下的汇编代码
1.main 函数调用处
2.函数内部
3.函数内部调用的__ftol
讲解:
1.识别C约定和上面一样,外面平栈
2.识别参数,看其我们的的函数调用处,发现有三个push,如果不知道,则会陷入坑,直接认为是三个参数.,但是跟随到函数内部,我们发现只有两个参数,而第二个参数是double,所以在32位下要push 两个四字节,其中高位是0,低位是常量(4)的浮点编码.
3.识别参数类型,在MyAdd内部,发现了两处使用参数的地方,用的指令分别是 fld 和fadd指令,这些都是浮点相关的.
识别技巧.
fld指令 将实数压入浮点协处理器,那么此时我们看下汇编指令,(使用IDA的K命令,可以不是符号显示,也就是下方贴出的汇编指令)
FLD 第一个是一个Dword 那么可以确定为是一个32位浮点
Fadd指令,使用Fadd指令的时候,发现是第二个QWORD,难么可以确定是一个double类型的浮点.功能的还原和汇编逆向前10讲一样,里面都是各种流程和指令
4.识别返回值,在识别返回值的时候,我们发现调用了一个_ftol函数,看到这个函数可以确定返回的是一个__int64,当然我们进入函数内部看到了
下方使用eax 和edx了,而且直接反会了,那么我们知道,在32位系统下,返回一个64位数字,在汇编中的表现形式就是edx.eax的存储方式.
Release版本下的汇编
熟悉总结的四句话,以不变应万变即可,因为类型都不一样.
二丶识别stdcall 函数参数,返回值,参数类型
stdcall比较简单.但是和fastcall还是有区别的.因为fastcall会有寄存器传参,所以把两个的区别搞明白就可以了.
高级代码:
__int64 __stdcall MyAdd(float a,double b,int c) { return a + b + c; } int main(int argc, char* argv[]) { MyAdd(3.0f,4.0f,6); return 0; }
直接一次性的把各种参数类型,以及返回值设置不一样.观看汇编
Debug下的汇编代码
1.函数调用处
2.MyAdd函数内部
1.识别函数的参数,发现了push 4个进去,但是不要被骗了,在MyAdd内部 分别得出了使用三个参数的位置,所以得出第一个参数为 float 第二个参数类型是 double,第三个参数 是int,又因为其中有一个double参数,所以在调用外面可以看到4个push,因为double是8个字节
2.识别参数个数,stdcall最好的就是它是内部平栈,也就是retn 10h,当然也可以通过这个来判断函数参数的个数
3.识别函数返回值类型
函数返回值类型,在MyAdd中调用了_ftol函数,其内部则是返回__int64,返回值是 edx.eax
Release下的汇编
和Debug汇编一样,有少许优化.
总结:
1.识别参数类型: 识别参数类型可以通过函数内部使用参数的时候用的指令,比如第一个 float,使用的是fld指令,fld系列指令就是操作浮点的,而又因为它是一个dword,所以判断是float,第三个参数是一个int,使用的是fixxx指令,fixxx指令就是操作的整数,因为它也是一个dword所以判断是int(当然可以看函数参数使用过程中其指令使用的时候表明这个参数是什么类型的)
2.识别参数个数, 识别参数个数在stdcall中有两种方式,第一种,直接看内部指令使用参数的地方,第二种,看平栈的时候平了多少.比如上面的例子, retn 10h(16),也就是4个参数的大小,但因为double是8字节,所以判断是三个参数
3.识别返回值,识别返回值 如果是int指令,那么返回值则放在eax中,如果是__int64指令,返回值则是在 edx.eax中,如果是浮点返回值,返回值则是在浮点协处理器中.
4.识别调用约定,函数内部平栈,如果没有寄存器传参则是stdcall,如果有寄存器传参,则是fastcall
三丶识别 fastcall 函数,参数个数,参数类型,返回值
高级代码:
double __fastcall MyAdd(float a,double b,int c) { return a + b + c; } float __fastcall MySub(__int64 a,int b) { return a - b; } int main(int argc, char* argv[]) { MyAdd(3.0f,4.0f,6); MySub(4,3); return 0; }
Debug下的汇编代码
1.main函数调用的时候的汇编代码
2. MyAdd函数内部
3.MySub函数内部
1.识别调用约定, 我们看MyAdd内部,还是MySub内部,里面都是用了外面传入的ecx,并且没有保存.那么fastcall就是ecx传参了.平栈和stdcall一样,函数内部平栈
2.识别函数的个数,识别函数的个数也有两种方法,第一种,看retn的时候,然后加上寄存器, 我们看myadd内部,retn 0ch,平了3个参数,但外面更改了ecx,里面使用了ecx,那么就是4个参数,但因为其中一个参数类型是double,所以还是三个参数.
3.识别参数类型,看指令来判断是什么类型,fld指令是浮点,fixxx指令则是使用的int,如果看edx.eax并且符号扩展了,则是__int64
4.上面返回值类型么有更改为doubLe和float,可以看出,在main函数下面是用浮点的出栈指令 fstp指令,从浮点协处理器出栈,浮点协处理器是64位的,所以返回double
总结:
1.调用约定,如果是c call那么外面平栈,stdcall函数内部平栈,fastcall函数内部平栈,但是会使用外面的寄存器.
2.识别参数个数,类型,同上
3.识别返回值,同上.