反汇编系列——函数篇
Posted 牧秦丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了反汇编系列——函数篇相关的知识,希望对你有一定的参考价值。
汇编中的函数调用我们需要着重讲述一下。一般我们用 call 发起调用,最后需要用 ret/retn/retf 来返回。我们逐个来看汇编中的函数调用。
1、相关指令
- call ADDRESS:
- enter:
push ebp
mov ebp, esp
- leave:
mov esp, ebp
pop ebp
- retn [NUMBER]:
pop eip
add esp, NUMBER
- retf [NUMBER]:
pop eip
pop cs
add esp, NUMBER
- ret [NUMBER]:
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 存储
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
- 返回值是结构体
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 来展示如何逆向分析。
以上是关于反汇编系列——函数篇的主要内容,如果未能解决你的问题,请参考以下文章