[系统安全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

posted @ 2017-10-14 18:30  17bdw  阅读(677)  评论(0编辑  收藏  举报