首先来看一个简单的函数调用对应的汇编代码
#include <stdio.h>
int __stdcall temp(int m , int n)
{
m++;
return m;
}
int main()
{
temp(1,2);
getchar();
getchar();
return 0;
}
main 函数对应汇编
push ebp
mov ebp, esp
push 2
push 1
call sub_401000
call ds:getchar
call ds:getchar
xor eax, eax
pop ebp
retn
temp 函数对应汇编
push ebp
mov ebp, esp
mov eax, [ebp+arg_0]
add eax, 1
mov [ebp+arg_0], eax
mov eax, [ebp+arg_0]
pop ebp
retn 8
0x00CFFB6C b0 fb cf 00 push ebp ,mov ebp,esp
0x00CFFB68 02 00 00 00 push 2
0x00CFFB64 01 00 00 00 push 1
0x00CFFB60 2c 10 91 00 00911027 E8 D4 FF FF FF call temp (911000h)
0x00CFFB5C 6c fb cf 00 push ebp ,mov ebp,esp
pop ebp
ret 8
X86 函数调用有很多调用约定,这里以被调用者回收栈区为例,栈回溯的操作与函数调用中最常见的指令---- PUSH EBP,MOV EBP,ESP.SUB ESP… 相关。这两步汇编指令经常出现在函数入口。在执行call 指令,将参数压栈之后,代码将call 指令的下一个指令eip 压栈,并跳转到call 指令的目标地址,之后执行上面的两条指令,通过当前函数的ebp 既可以得到当前函数的返回地址,又可以得到上一个ebp,即上一层的调用堆栈。
据此,我们不难写出如下代码,得到本函数的调用堆栈:
#include <stdio.h>
#include <Windows.h>
PVOID g_ImageBase;
void Show(PVOID dwAddress)
{
printf("返回地址 EIP:%p\r\n",dwAddress);
}
void Test3()
{
__try
{
__asm
{
mov edx,ebp
agin:
mov eax,edx
mov edx,[eax] //上一个ebp
mov ecx,[eax+4] //函数返回地址
mov eax,ecx
push edx
push eax
call Show
add esp,4
pop edx
jmp agin
}
}__except(1)
{
printf("嘿嘿\r\n");
getchar();
getchar();
}
}
void Test2(int a)
{
Test3();
}
void Test1(int a)
{
Test2(0x12345678);
}
int main()
{
Test1(0x12345670);
return 0;
}
返回地址 EIP:009410B8
返回地址 EIP:009410CD
返回地址 EIP:009410ED
返回地址 EIP:00941267
返回地址 EIP:759D8744
返回地址 EIP:77E9587D
返回地址 EIP:77E9584D
返回地址 EIP:00000000
使用windbg 简单验证如下
0:001> u 0x009410B8 - 5
Show!Test2+0x3 [f:\program\show\show\show.cpp @ 38]:
009410b3 e868ffffff call Show!Test3 (00941020)
009410b8 5d pop ebp
0:001> u 0x009410CD - 5
Show!Test1+0x8 [f:\program\show\show\show.cpp @ 43]:
009410c8 e8e3ffffff call Show!Test2 (009410b0)
009410cd 83c404 add esp,4
009410d0 5d pop ebp
009410d1 c3 ret
0:001> u 0x77E9584D - 5
ntdll!_RtlUserThreadStart+0x16:
77e95848 e801000000 call ntdll!__RtlUserThreadStart (77e9584e)
77e9584d cc int 3
ntdll!__RtlUserThreadStart:
77e9584e 6a1c push 1Ch
77e95850 682801f377 push offset ntdll!ResCSegmentValidateHeader+0x1122 (77f30128)
77e95855 e81efd0100 call ntdll!_SEH_prolog4_GS (77eb5578)
77e9585a 8bf9 mov edi,ecx
77e9585c 8365fc00 and dword ptr [ebp-4],0
77e95860 8b35f478f477 mov esi,dword ptr [ntdll!Kernel32ThreadInitThunkFunction (77f478f4)]
如此,我们已经掌握了X86 栈回溯的基本原理及实现。