[系统安全10]反汇编-函数调用约定、Main函数查找
0x1 准备工作
1.1、准备工具
- IDA:交互式反汇编工具
- OllyDbg:用户层调试工具
- Visual Studio:微软开发工具
1.2、基础知识
-
C++开发
-
汇编语言
0x2 查找真正的main()函数
入口点开始到Main()函数之间的代码都是编译器加进去用于初始化环境用的。
main()函数其实是有3个参数的,这取决于Windows系统的机制。
查找方法:
1、字符串搜索法
2、栈回溯法
3、逐步分析法
4、小例子
C源代码:
程序执行的时候会将路径保存在argv字符数组中,因此argc的值始终是等于1的。程序未经处理会显示”Helllo world“。
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
if (argc)
{
printf("Hello world!\r\n");
}
else
{
printf("Hello everybody!\r\n");
}
return 0;
}
反汇编后的代码:
ESP是栈指针,也就是当前栈的所在位置。+4就是在当前堆栈+4的地方取内容。
CMP指令是对两个操作数做减法操作,仅影响标志位。
00041000 >/$ 55 push ebp
00041001 |. 8BEC mov ebp,esp
00041003 |. 837D 08 00 cmp [arg.1],0x0
00041007 |. 74 11 je X9-3.0004101A
00041009 |. 68 48640500 push 9-3.00056448 ; /Hello world!\r\n
0004100E |. E8 2D000000 call 9-3.printf ; \printf
00041013 |. 83C4 04 add esp,0x4 ; esp+4的意思就是在当前堆栈+4的地方取其内容
00041016 |. 33C0 xor eax,eax
00041018 |. 5D pop ebp
00041019 |. C3 retn
0004101A |> 68 58640500 push 9-3.00056458 ; /Hello everybody!\r\n
0004101F |. E8 1C000000 call 9-3.printf ; \printf
00041024 |. 83C4 04 add esp,0x4
00041027 |. 33C0 xor eax,eax
00041029 |. 5D pop ebp
0004102A \. C3 retn
0x3 函数识别初探
1、函数调用约定的参数入栈书序、回收堆栈的规则
- C规范 _cdecl 参数入栈顺序从右到左 调用者负责回收堆栈
- pascal规范 pascal 参数入站从左到右 被调用者负责回收堆栈
- 快速调用规范 __fastcall 从右到左 被调用者负责回收堆栈
- 标准调用规范 __stdcall 从右到左 被调用者负责回收堆栈
2、调用方式示例
调用约定 | 关键字 | 参数入栈顺序 | 回收堆栈 |
---|---|---|---|
C规范 | _cdecl | 从右到左 | 调用者负责 |
Pascal规范 | pascal | 从左到右 | 被调用者负责 |
快速调用规范 | _fastcall | 从右到左,使用寄存器传参 | 被调用者负责 |
标准调用规范 | _stdcall | 从右到左 | 被调用者负责 |
示例程序:
#include "stdafx.h"
int __cdecl fun_a(int nNumA, int nNumB, int nNumC) // C规范
{
return nNumA+nNumB+nNumC;
}
int __fastcall fun_b(int nNumA, int nNumB, int nNumC)// 快速调用
{
return nNumA+nNumB-nNumC;
}
int __stdcall fun_c(int nNumA, int nNumB, int nNumC) // 标准调用
{
return nNumA-nNumB-nNumC;
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("11111111111111111111111111111");
fun_a(argc, 1, 2); //func_a,入栈顺序从右向左,调用者平衡堆栈
fun_b(argc, 1, 2); //fun_b,入栈顺序从右向左,被调用者平衡堆栈
fun_c(argc, 1, 2);//fun_c,入栈顺序从右向左,被调用者平衡堆栈
return 0;
}
反汇编程序:
Main()函数
.text:004016A0 main__ proc near ; CODE XREF: j_main__j
.text:004016A0
................
.text:00401707 push 2 ; 参数3入栈
.text:00401709 push 1 ; 参数2入栈
.text:0040170B mov eax, [ebp+arg_0]
.text:0040170E push eax ; 参数1入栈
.text:0040170F call j_fun_a__ ; func_a,cdecl调用方式,入栈顺序从右向左,调用者平衡堆栈
.text:00401714 add esp, 0Ch ; 由Main()函数销毁fun_a用到的局部变量平衡堆栈
.text:00401714 ;
.text:00401717 push 2 ; 参数3入栈
.text:00401719 mov edx, 1 ; 参数2入栈
.text:0040171E mov ecx, [ebp+arg_0] ; 参数1传递给ecx
.text:00401721 call j_fun_b__ ; fun_b,fastcall调用方式,入栈顺序从右向左,被调用者平衡堆栈
.text:00401726 push 2 ; 参数3入栈
.text:00401728 push 1 ; 参数2入栈
.text:0040172A mov eax, [ebp+arg_0]
.text:0040172D push eax ; 参数1入栈
.text:0040172E call j_fun_c__ ; fun_c,stdcall调用方式,入栈顺序从右向左,被调用者平衡堆栈
.text:00401733 xor eax, eax
.text:00401735 pop edi
.text:00401736 pop esi
.text:00401737 pop ebx
.text:00401738 add esp, 0C0h ; 销毁局部变量,平衡堆栈
.text:0040173E cmp ebp, esp ; 比较esp的值是否正常
.text:00401740 call sub_4010B4 ; 调用检查esp的函数
.text:00401745 mov esp, ebp
.text:00401747 pop ebp
.text:00401748 retn
.text:00401748 main__ endp
fun_a()函数是cdecl调用,所以没有堆栈平衡,由调用者main()函数进行堆栈平衡
.text:00401480 fun_a__ proc near ; CODE XREF: j_fun_a__j
.text:00401480
.text:00401480 var_C0 = byte ptr -0C0h
.text:00401480 arg_0 = dword ptr 8
.text:00401480 arg_4 = dword ptr 0Ch
.text:00401480 arg_8 = dword ptr 10h
.text:00401480
.text:00401480 push ebp ; EBP入栈保存
.text:00401481 mov ebp, esp ; 然后将堆栈指针ESP的值传递给EBP
.text:00401481 ; 如此一来在这个函数内只需要使用EBP就可对栈进行操作了。
.text:00401481 ; 这样做的好处是不需要对ESP做过多的操作
.text:00401483 sub esp, 0C0h ; 将ESP减0xC0
.text:00401489 push ebx
.text:0040148A push esi
.text:0040148B push edi ; 保存EBX、ESI、EDI
.text:0040148C lea edi, [ebp+var_C0]
.text:00401492 mov ecx, 30h
.text:00401497 mov eax, 0CCCCCCCCh
.text:0040149C rep stosd
.text:0040149E mov eax, [ebp+arg_0] ; 将参数1传递给eax
.text:004014A1 add eax, [ebp+arg_4] ; 将eax与参数2相加
.text:004014A4 add eax, [ebp+arg_8] ; 将eax与参数3相加
.text:004014A7 pop edi
.text:004014A8 pop esi
.text:004014A9 pop ebx
.text:004014AA mov esp, ebp
.text:004014AC pop ebp
.text:004014AD retn
.text:004014AD fun_a__ endp
fun_b()函数是fastcall调用,参数是由ECX与EDX这两个寄存器完成的,超出部分的参数依然采用压栈方式传递。被调用者fun_b()负责堆栈平衡。
.text:004014C0 fun_b__ proc near ; CODE XREF: j_fun_b__j
.text:004014C0
.text:004014C0 var_D8 = byte ptr -0D8h
.text:004014C0 var_14 = dword ptr -14h
.text:004014C0 var_8 = dword ptr -8
.text:004014C0 arg_0 = dword ptr 8
.text:004014C0
.text:004014C0 push ebp
.text:004014C1 mov ebp, esp
.text:004014C3 sub esp, 0D8h
.text:004014C9 push ebx
.text:004014CA push esi
.text:004014CB push edi
.text:004014CC push ecx
.text:004014CD lea edi, [ebp+var_D8]
.text:004014D3 mov ecx, 36h
.text:004014D8 mov eax, 0CCCCCCCCh
.text:004014DD rep stosd
.text:004014DF pop ecx
.text:004014E0 mov [ebp+var_14], edx ; 将参数2的值传递给局部变量2
.text:004014E3 mov [ebp+var_8], ecx ; 将参数1的值传递给局部变量1
.text:004014E6 mov eax, [ebp+var_8] ; 将局部变量1的值传递给eax
.text:004014E9 add eax, [ebp+var_14] ; 将eax与局部变量2相加
.text:004014EC sub eax, [ebp+arg_0] ; 将eax与参数3相减
.text:004014EC ;
.text:004014EC ; 采用快速调用的函数参数是由exc与edx这两个寄存器完成的,
.text:004014EC ; 而超出部分的参数则依然要使用传统的压栈方式传递,以下就是本函数的参数与局部变量的结构
.text:004014EC ;
.text:004014EC ; 参数1:ecx
.text:004014EC ; 参数2:edx
.text:004014EC ; 参数3:ebp+0x8
.text:004014EC ; 局部变量1:ebp-0x8
.text:004014EC ; 局部变量2:ebp-0x1
.text:004014EF pop edi
.text:004014F0 pop esi
.text:004014F1 pop ebx
.text:004014F2 mov esp, ebp
.text:004014F4 pop ebp
.text:004014F5 retn 4
.text:004014F5 fun_b__ endp
fun_c函数是stdcall调用,函数返回时销毁局部变量,平衡堆栈。
.text:00401510 fun_c__ proc near ; CODE XREF: j_fun_c__j
.text:00401510
.text:00401510 var_C0 = byte ptr -0C0h
.text:00401510 arg_0 = dword ptr 8
.text:00401510 arg_4 = dword ptr 0Ch
.text:00401510 arg_8 = dword ptr 10h
.text:00401510
.text:00401510 push ebp
.text:00401511 mov ebp, esp
.text:00401513 sub esp, 0C0h
.text:00401519 push ebx
.text:0040151A push esi
.text:0040151B push edi
.text:0040151C lea edi, [ebp+var_C0]
.text:00401522 mov ecx, 30h
.text:00401527 mov eax, 0CCCCCCCCh
.text:0040152C rep stosd
.text:0040152E mov eax, [ebp+arg_0] ; 将参数1传递给eax
.text:00401531 sub eax, [ebp+arg_4] ; 将eax与参数2相减
.text:00401534 sub eax, [ebp+arg_8] ; 将eax与参数3相减
.text:00401537 pop edi
.text:00401538 pop esi
.text:00401539 pop ebx
.text:0040153A mov esp, ebp
.text:0040153C pop ebp
.text:0040153D retn 0Ch ; 函数返回时销毁局部变量,平衡堆栈。
.text:0040153D fun_c__ endp
3、汇编改变特征小技巧
并不是所有的函数都只有用call指令才能调用,使用lea、push加jmp的组合也可以达到相同的目的。
例如将call Demo.013A1127可以转换为以下形式:
lea esi,return_addr ; 取到jmp Demo.013A1127指令后面的地址
push esi ; 将这个地址压入栈
jmp Demo.013A1127 ; 跳转到Demo.013A1127处执行函数代码
4、裸函数
C++使用naked标识创建的裸函数将不包含任何用户代码以外的指令,即便是函数末尾的retn也要用户自己来实现。
代码如下:
#include "stdafx.h"
__declspec(naked) int fun(int nNumA, int nNumB, int nNumC)
{
__asm
{
push ebp
mov ebp, esp
sub esp, 0x4
}
nNumA += (nNumB+nNumC); // 注意,此行为c语句。
__asm
{
mov eax, nNumA
add esp, 0x4
mov esp, ebp
pop ebp
retn
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("fun=%d", fun(argc,1,2));
return 0;
}
5、小结
采用不同的调用方式,反汇编代码不同。
-
a) 几乎全部函数调用方式都会用栈来传递参数,只有使用快速调用约定后且参数少于等于2时才会全部采用寄存器传参。
-
b) 函数起始部分:以push ebp和mov ebp,esp汇编指令开始
-
c) 每个函数由call指令调用,且以retn指令结尾。
-
d) 裸函数进行内联汇编可以改变以上的某些规律。
注:裸函数是指编译器生成汇编代码时不添加任何额外的指令,包括retn。
0x5 参考文章
《黑客免杀攻防》 软件逆向工程1-3
http://blog.csdn.net/dalerkd/article/details/41173623