1.裸函数
c语言的普通函数中即使什么都不写,编译器在编译时也会给它加上一些汇编代码;比如开栈、返回等;
裸函数就是编译器什么都不管,一切都需要自己来处理;
裸函数的优点是自由度高,可以应用在钩子程序等地方;
例如:一个裸函数
#include "stdio.h"
void __declspec(naked) method(){
}
int main(int argc, char* argv[])
{
method();
return 0;
}
空的裸函数由于编译器不会给它添加返回指令,所以运行时会报错,可以自己在其中添加汇编指令;
void __declspec(naked) method(){
__asm{
ret
}
}
然后反汇编时就有了ret指令:
1)裸函数实现加法运算
#include "stdio.h"
//实现1+2+3+4+5+6
int __declspec(naked) method(int x, int y, int z){
__asm{
//保留之前的栈底
push ebp
//开栈 ->注意十六进制数前最好加上0x防止被当成十进制数
mov ebp,esp
sub esp,0x40
//保护现场
push ebx
push esi
push edi
//填充缓冲区
lea edi,[ebp-0x40]
mov ecx,0x10
mov eax,0xcccccccc
rep stos dword ptr [edi]
//往缓冲区存放局部变量
mov dword ptr [ebp-0x04],4
mov dword ptr [ebp-0x08],5
mov dword ptr [ebp-0x0c],6
//计算
mov eax,dword ptr [ebp+0x08]
add eax,dword ptr [ebp+0x0c]
add eax,dword ptr [ebp+0x10]
add eax,dword ptr [ebp-0x04]
add eax,dword ptr [ebp-0x08]
add eax,dword ptr [ebp-0x0c]
//恢复现场
pop edi
pop esi
pop ebx
//恢复堆栈
mov esp,ebp
pop ebp
//返回
ret
}
}
//同效果的函数
int plus(int x, int y, int z){
int a = 4;
int b = 5;
int c = 6;
return x+y+z+a+b+c;
}
int main(int argc, char* argv[])
{
method(1,2,3);
//plus(1,2,3);
return 0;
}
2.调用约定
常用的调用约定:
调用约定
|
参数压栈顺序
|
平衡堆栈
|
__cdecl
|
从右至左入栈
|
调用者清理栈
|
__stdcall
|
从右至左入栈
|
自身清理堆栈
|
__fastcall
|
ECX/EDX传送前两个
剩下:从右至左入栈
|
自身清理堆栈
|
1)外平栈
默认调用约定;
int __cdecl Plus(int a, int b)
{
return a+b;
}
push 2
push 1
call @ILT+15(Plus) (00401014)
add esp,8
2)内平栈
ret后面的参数为平衡堆栈所需的内存单元数;
int __stdcall Plus(int a, int b)
{
return a+b;
}
push 2
push 1
call @ILT+10(Plus) (0040100f)
函数内部:
ret 8
3)快速调用
用寄存器来保存参数;
最多两个参数可保存在寄存器;
不需要平衡堆栈;
比较适合用在参数多次传递的地方,比如循环;
int __fastcall Plus(int a, int b)
{
return a+b;
}
mov edx,2
mov ecx,1
call @ILT+0(Plus) (00401005)
函数内部:
ret
4)超过两个参数的快速调用
其它参数保持在栈中,用内平栈的方式来平衡堆栈;
int __fastcall Plus4(int a, int b,int c,int d)
{
return a+b+c+d;
}
push 4
push 3
mov edx,2
mov ecx,1
call @ILT+5(Plus) (0040100a)
函数内部:
ret 8
3.函数入口
(1) main 或WinMain 是“语法规定的用户入口”,而不是“应用程序入口”。应用程序入口通常是启动函数。
例如:vc6中修改入口函数名
右键单击项目-》Setting
(2) mainCRTStartup 和 wmainCRTStartup 是控制台环境下多字节编码和Unicode 编码的启动函数.
而WinMainCRTStartup 和wWinMainCRTStartup 是windows 环境下多字节编码和Unicode 编码的启动函数.
可以在vc6的调用栈中看到:
3)找main函数
main 函数被调用前要先调用的函数如下:
GetVersion() ->获得当前操作系统的版本
_heap_init() ->初始化堆空间的大小
GetCommandLineA() ->获取命令行参数
_crtGetEnvironmentStringsA() ->获取环境变量
_setargv()
_setenvp()
_cinit()
查看mainCRTStartup()可知main函数在经过编译器处理后有3个参数:
这些函数调用结束后就会调用main 函数,根据main 函数调用的特征,将3 个参数压入栈内作为函数的参数。
也就是说找到上面的函数后,它们后面如果有传入3个参数的函数被调用,那么这个函数很可能就是main函数;
4.逆向实例
1)找到程序入口main
用od打开目标程序进行分析:
2)进入mian函数分析
这里有个两个函数,只分析第一个;
注意:参数的入栈顺序是从右向左的;
目前可以确定的程序代码:
#include "stdio.h"
int __fastcall plus(int a, int b, int c, int d, int e){
}
int main(int argc, char* argv[]){
plus(1,3,4,6,7);
return 0;
}
3)分析第一个函数
可以确定的代码:
#include "stdio.h"
int __stdcall plus1(int x, int y, int z){
}
int plus2(int m, int n){
}
int __fastcall plus(int a, int b, int c, int d, int e){
int j = plus1(a,b,c);
int k = plus2(a,b);
return plus2(j,k);
}
int main(int argc, char* argv[]){
plus(1,3,4,6,7);
return 0;
}
3)分析函数的子函数
用同样的方法分析另一个子函数;
代码:
#include "stdio.h"
int __stdcall plus1(int x, int y, int z){
return x+y+z;
}
int plus2(int m, int n){
return m+n;
}
int __fastcall plus(int a, int b, int c, int d, int e){
int j = plus1(a,b,c);
int k = plus2(a,b);
return plus2(j,k);
}
int main(int argc, char* argv[]){
plus(1,3,4,6,7);
return 0;
}
4)查看程序结果
接着运行到函数调用完,查看结果,结果保存在eax中;
换算成十进制:
0x0c=12
也就是结果为:12
(1+3+4)+(1+3)=12;
和分析的结果一样;