反汇编系列——函数篇

Posted 牧秦丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了反汇编系列——函数篇相关的知识,希望对你有一定的参考价值。

汇编中的函数调用我们需要着重讲述一下。一般我们用 call 发起调用,最后需要用 ret/retn/retf 来返回。我们逐个来看汇编中的函数调用。


1、相关指令

  • call  ADDRESS:
发起调用,根据 ADDRESS 的地址决定是发起段内调用还是段间调用。当是段内调用时,将 eip 入栈,然后 jmp 到 ADDRESS 处;当是段间调用时,将 cs、eip 依次入栈,然后 jmp 到 ADDRESS 处。
  • enter:
进行堆栈准备工作,将 ebp 原来的内容入栈,将当前 esp 存入 ebp。相当于:
push	ebp
mov		ebp,	esp
  • leave:
恢复堆栈,是 enter 指令的逆向操作,如你所想:
mov		esp,	ebp
pop		ebp
  • retn  [NUMBER]:
段内返回,不带 [] 内的 NUMBER 时,表示是无参的函数调用或者 __cdecl 调用约定,当有 NUMBER 时,返回后将 esp 的值加 NUMBER,表示将入栈的参数全部出栈,一般是 __stdcall 调用约定。作用如下:
pop		eip
add		esp,	NUMBER
  • retf  [NUMBER]:
段间返回,后面的 NUMBER 含义和 retn 的一样,只是出栈时还要将 cs 出栈,如下:
pop		eip
pop		cs
add		esp,	NUMBER
  • ret   [NUMBER]:
是 leave 和 retn 的结合体,恢复堆栈后返回。NUMBER 的含义和 retn 的含义一样:
leave
retn	NUMBER

2、函数堆栈

当我们发起函数调用时,当函数准备好堆栈时堆栈的内容是什么样的呢?用个例子一探究竟。

HWND GetHWndByClassName(LPCWSTR lpszClsName)

	HWND hWnd = NULL;
	hWnd = ::FindWindowW(lpszClsName, NULL);
	return hWnd;


HWND hTrayWnd = GetHWndByClassName(L"Shell_TrayWnd);
比如我们有这样一段简单的代码,那么它未优化的汇编代码如下:

.data
__str_Shell_Tray_Wnd	db	'Shell_TrayWnd', 0


.code

; ....
; HWND GetHWndByClassName(LPCWSTR)

GetHWndByClassName proc near

loc_hWnd	= dword ptr -4h
lpszClsName	= dword ptr 8

	; 准备堆栈
	push	ebp
	mov		ebp,	esp
	
	; 为局部变量分配内存
	sub		esp,	4
	
	; 保存寄存器
	push	ebx
	push	esi
	push	edi
	xor		esi,	esi
	
	; 调用 FindWindowW
	mov		eax,	[ebp + lpszClsName]
	push	eax
	call	ds:FindWindowW
	
	; 将 FindWindowW 返回值保存到局部变量
	mov		[ebp + loc_hWnd],	eax
	
	; 清场
	leave
	retn	4
GetHWndByClassName endp


; 调用 GetHWndByClassName 的地方
push	offset	__str_Shell_Tray_Wnd
call	GetHwndByClassName
mov		[ebp + loc_hTrayWnd],	eax

在函数 GetHWndByClassName中,堆栈明细如下:




如你所见,一个__stdcall 调用约定下的汇编函数一般的结构都是这样:

__My_Proc proc near

loc_var_n	dword ptr -LOC_BYTES
..
loc_var_2	dword ptr -8h
loc_var_1	dword ptr -4h
param_1		dword ptr 8h
param_2		dword ptr 0Ch
...
param_n		dword ptr PARAM_BYTES

	; 函数体开始
	; 准备堆栈
	push	ebp
	mov		ebp,	esp
	
	; 为局部变量分配内存
	sub		esp,	LOC_BYTES
	
	; 保存寄存器
	push	ebx
	push	esi
	push	edi
	xor		esi,	esi
	
	; 函数正文
	; ...
	; ...
	
	; 函数清场
	leave
	retn	PARAM_BYTES
__My_Proc endp

函数的第一个参数的偏移是  8h,第二个参数的偏移是 (8h + 第一个参数字节大小)……依次类推。函数的第一个局部变量的偏移是 -4h,第二个局部变量的偏移是 (-4h - 第一个局部变量的字节大小)……依次类推。

3、__cdecl 调用约定

当函数是__cdecl 调用约定时,函数会怎么样呢?比如我们有一个函数:

void __cdecl foo(int a, int b)

	// ...


foo(89, 24);

那么,它的汇编代码将如下:

_foo proc near

param_b	= dword ptr -8
param_a	= dword ptr -4

	enter

	; ...
	
	leave
	retn
_foo endp


; 调用 foo
push	24
push	89
call	_foo

; 调用者平衡堆栈
add		esp,	8

可以看到,对于 __cdecl 调用约定的函数 foo,最后的返回 retn 后不加任何数值,在调用房执行完 call 后,再将 esp 加上所有传递参数的总字节数大小。所以,在逆向过程中,如果你看到一个 call 后紧接着有 add esp, NUMBER 的操作,基本可以断定被调的这个函数是 __cdecl 调用约定。

4、函数返回值

那么,当函数具有返回值时,堆栈又是如何呢?对于具有返回值的函数,一般有两种情况。

  • 返回值小——可以用 eax 存储
对于这种情况,将直接将返回值存储到 eax 中。调用完函数后,调用者直接从 eax 中取值。如:
int getInteger()

	int retVal = 0;
	scanf("%d", &retVal);
	return retVal;


int myAge = getInteger();
printf("%d\\n", myAge);
它的未经优化的汇编代码将如下所示:
.data
__str_Scanf_d	db	'%d', 0
__str_Printf_d	db	'%d\\n', 0


.code

getInteger proc near

loc_retVal	= dword ptr -4

	; 准备堆栈
	push	ebp
	mov		ebp,	esp
	sub		esp,	4
	
	; retVal 置0
	xor		eax,	eax
	mov		[ebp + loc_retVal],	eax

	; 调用 scanf
	lea		eax,	[ebp + loc_retVal]
	push	eax
	push	offset __strScanf_d
	call	_scanf
	
	; 平衡 __cdecl 调用约定的 scanf 函数堆栈
	add		esp,	8
	
	; 将返回值保存在 eax 中
	mov		eax,	[ebp + loc_retVal]
	
	; 返回
	retn
getInteger endp


; 调用 getInteger
loc_myAge	= dword ptr -4

	call	getInteger
	mov		[ebp + loc_myAge],	eax
	
	; 执行 printf
	mov		eax,	[ebp + loc_myAge]
	push	eax
	push	offset __str_Printf_d
	call	_printf
	
	; 平衡 __cdecl 调用堆栈
	add		esp,	8
  • 返回值是结构体
对于返回值是结构体或类的函数,处理过程可能不一样。一般来说,汇编代码会将取返回值的指针加到参数中最后入栈,最后将这个返回值指针保存到 eax 中返回。如下:
struct CFoo

	int		m_val;
	HICON	m_hIcon;
	char*	m_szName;
;


CFoo getFoo(char* lpszName)

	CFoo tmp;
	scanf("%d", &tmp.m_val);
	tmp.m_hIcon = 0;
	tmp.m_szName = lpszName;
	
	return tmp;


int main()

	CFoo foo = getFoo("foo_Name");
	printf("%d, %s", foo.m_val, foo.m_szName);
	return 0;
未优化汇编代码如下:
.data

__str_Foo_Name	db	'foo_name', 0
__str_Scanf_d	db	'%d', 0
__str_Printf	db	'%d, %s', 0


.code

getFoo proc near

tmp_offset		= dword ptr -0Ch
tmp_m_val		= dword ptr -0Ch
tmp_m_hIcon		= dword ptr -8
tmp_m_szName	= dword ptr -4
ptr_retValue	= dword ptr 8
param_lpszName	= dword ptr 0Ch

	push	ebp
	mov		ebp,	esp
	sub		esp,	0ch
	
	push	esi
	push	ecx
	
	lea		eax,	[ebp + tmp_m_val]
	push	eax
	push	offset __str_Scanf_d
	call	_scanf
	add		esp,	8
	
	mov		[ebp + tmp_m_hIcon], 0
	mov		eax,	[ebp + tmp_m_va;]
	mov		ecx,	[ebp + param_lpszName]
	mov		esi,	[ebp + tmp_m_hIcon]
	
	; 将结果存入返回值
	mov		edx,	[ebp + ptr_retValue]
	mov		[edx],	eax
	mov		[edx + 4],	esi
	mov		[edx + 8], ecx
	
	; 将返回值地址存入eax
	mov		eax,	[ebp + ptr_retValue]
	
	pop		ecx
	pop		esi
	mov		esp,	ebp
	pop		ebp
	retn
getFoo endp


main proc near

loc_foo		= dword ptr -0Ch

	; ...
	
	; 参数入栈
	push	offset	__str_Foo_Name
	
	; 返回值结构体地址入栈
	lea		eax, [ebp + loc_foo]
	push	eax
	
	; 函数调用
	call	getFoo
	add		esp,	8
	
	; 其他操作
	; ...
	
main endp
通过示例可以看到,对于返回值是结构体的函数,先将参数入栈,最后将存放返回值的结构体地址入栈,最终在该函数中赋值完毕后,将该返回值存入 eax 并返回。
当然,这只是未经优化的汇编代码,比较容易理解。在大部分的逆向过程中,你得面对经过优化的代码,相信我,那里面的逻辑将非常复杂,所以,你必须具有非常扎实的汇编基础。下一篇将通过一个实际示例从头到尾利用 IDA Pro 来展示如何逆向分析。













以上是关于反汇编系列——函数篇的主要内容,如果未能解决你的问题,请参考以下文章

反汇编系列——堆栈篇

挂茶馆_VIP其他杂项系列教程

C++反汇编第二讲,反汇编中识别虚表指针,以及指向的虚函数地址

linux驱动系列之程序反汇编

dll文件如何反汇编成源码,C++语言编写

如何反汇编获知dll中函数的参数