现在的位置: 首页 > 综合 > 正文

thunk

2013年10月01日 ⁄ 综合 ⁄ 共 2554字 ⁄ 字号 评论关闭

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中的格式显示

 

 

其中

 

 

属于epilog部分,编译器为我们设置新的stack frame,为这个函数的本地变量分配栈空间,保存ebx, esi, edi等寄存器的值。

 

 

 

主要作用是将分配的栈空间全部设置成0xCC,而这个值是int 3的汇编码,也就是x86的软件中断,通常我们在debug时用的assert就会在条件为false时调用这个中断指令,CPU停止执行指令。在Win32 API中有个宏也可以达到这个目的:

设置成0xCC的好处可能出于某种原因EIP指向了这段区域,那么可以暂停程序的进行,方便DEBUG。 

 

这段是编译器生成的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)

  1. paramters
  2. function return address
  3. caller's EBP
  4. local variable
  5. saved registers
EDIT: 这里还有一种定义(2)是将EBP, local variable, saved register和被调函数的参数,函数地址认为是stack frame,这里的差别仅是在1, 2,即上面(1)给出的定义是将自己的参数和返回地址归入stack frame,而(2)中是没有这么做,而是将自己调用的其他函数时的参数和返回地址归入自己的stack frame。也就是如果我们认为ebp和esp之间是该函数的stack frame,那么(2)的定义跟准确一些。

当我们调用一个函数时,编译器就会生成call指令,而call指令会替我们在栈上压入返回地址。而当函数结束时,会调用ret指令,将保存的返回地址从栈上弹出到EIP中,CPU就从该地址得以继续执行。所以通常我们听说的由buffer overrun造成的攻击,就是通过利用程序没有检查输入参数的长度,而造成了栈空间被覆写,正好将return address的改变为攻击者自己编写的指令来完成的。所以在使用c函数 sprintf, strcpy等函数时需要小心。

现在有一个简单的代码,我们来看一下这个函数的Stack Frame

  

 

 

Coding

 

假设我们需要获得调用者的返回地址,那么我们该怎么做呢?我们知道,EBP+4肯定指向当前函数的返回地址(假设这个被调用函数没有被Inline,如果inlined,将不会生成该函数的stack frame)。

如果我们要将获得返回地址的函数封装一个函数,那么该怎么办?很显然,我们需要特殊处理,因为对于一个普通的函数,调用时编译器会为其建立一个stack frame,那么此时返回地址已经变化。我们要做到就是不让编译器生成Prolog和Epilog。我们可以编写自定义的汇编函数,或者对于VC这样的编译器,使用__declspec(naked)关键字。naked关键字告诉编译器,不要为该函数生成prolog & epilog。

x86 CPU使用eax来存储返回值,所以我们直接将返回值存储于eax中。

 

抱歉!评论已关闭.