Thunk
所谓thunk就是一小段汇编码,通常有点hack的意思。比如VC的虚函数的调用入口就是一小段thunk,用来调整this指针,所以我们直接取vc中的virtual成员函数的地址获得的其实是一小段thunk的地址,调用时会先经过这个thunk,调整this指针,再到真正的函数入口。
Implementation
而在进行api的hook时,我们也需要使用thunk,将被调用函数的前几个字节替换成一个thunk,通常是一个jmp指令,经这个jmp指令跳转到我们的hook函数后,再返回到正常的函数中进行执行。在x86下,我们通常用relative jmp来达到这个目的。用relative jmp是因为对于动态链接库dll,或者共享对象so而言,加载到进程的地址空间的后的函数地址是不固定的,用absolute jmp就会出现问题,而我们只要计算了正确的偏移,使用relative jmp就不会出错。
Function Prolog & Epilog
我们编写的函数,除了我们自己写的指令意外,通常编译器会为我们添加额外的代码,比如保存寄存器的值,还原寄存器的值等。对于C++,编译器将会做更多的工作,因为在抛出异常或者函数返回时,需要保证一个具有non-trivial destructor的对象的destructor始终会被调用。我们来看一看VC中编译后一个函数的反汇编码。这里以VC的disassembler中的格式显示
int main() { // prolog push ebp mov ebp,esp sub esp,0C0h push ebx push esi push edi lea edi,[ebp-0C0h] mov ecx,30h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] f(); call f (0DE10EBh) } xor eax,eax // epilog pop edi pop esi pop ebx add esp,0C0h
其中
push ebp mov ebp,esp sub esp,0C0h push ebx push esi push edi
属于epilog部分,编译器为我们设置新的stack frame,为这个函数的本地变量分配栈空间,保存ebx, esi, edi等寄存器的值。
而
lea edi,[ebp-0C0h] mov ecx,30h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi]
主要作用是将分配的栈空间全部设置成0xCC,而这个值是int 3的汇编码,也就是x86的软件中断,通常我们在debug时用的assert就会在条件为false时调用这个中断指令,CPU停止执行指令。在Win32 API中有个宏也可以达到这个目的:
void WINAPI DebugBreak(void);
设置成0xCC的好处可能出于某种原因EIP指向了这段区域,那么可以暂停程序的进行,方便DEBUG。
// epilog pop edi pop esi pop ebx add esp,0C0h
这段是编译器生成的Epilog,其中编译器恢复了保存的寄存器,并回收了分配的栈空间。
由于C/C++函数默认的calling convention(调用约定)是__cdecl,这就意味值栈的清理工作由调用者来完成,所以esp的设置就由main来完成了。还有其他一些convention,比如__fastcall、__stdcall等,可以参考http://msdn.microsoft.com/en-us/library/k2b2ssfy.aspx。
Stack Frame
一个典型的stack frame由一下5部分组成:(1)
当我们调用一个函数时,编译器就会生成call指令,而call指令会替我们在栈上压入返回地址。而当函数结束时,会调用ret指令,将保存的返回地址从栈上弹出到EIP中,CPU就从该地址得以继续执行。所以通常我们听说的由buffer overrun造成的攻击,就是通过利用程序没有检查输入参数的长度,而造成了栈空间被覆写,正好将return address的改变为攻击者自己编写的指令来完成的。所以在使用c函数 sprintf, strcpy等函数时需要小心。
现在有一个简单的代码,我们来看一下这个函数的Stack Frame
void f(int a) { } void g() { f(1); }
Coding
假设我们需要获得调用者的返回地址,那么我们该怎么做呢?我们知道,EBP+4肯定指向当前函数的返回地址(假设这个被调用函数没有被Inline,如果inlined,将不会生成该函数的stack frame)。
// 假设我们要获得f的返回地址 void f() { __asm { mov eax, [ebp+4] ; eax存储返回地址 } }
如果我们要将获得返回地址的函数封装一个函数,那么该怎么办?很显然,我们需要特殊处理,因为对于一个普通的函数,调用时编译器会为其建立一个stack frame,那么此时返回地址已经变化。我们要做到就是不让编译器生成Prolog和Epilog。我们可以编写自定义的汇编函数,或者对于VC这样的编译器,使用__declspec(naked)关键字。naked关键字告诉编译器,不要为该函数生成prolog & epilog。
__declspec(naked) DWORD get_return_address() { __asm { mov eax, [ebp+4] ret } }
x86 CPU使用eax来存储返回值,所以我们直接将返回值存储于eax中。