thunk 在网络词典上解释为:形实转换程序或替换程序。那么到底如何转换?如何替换呢?
其实可以把 thunk 理解为一小段代码,但这段代码并不是静态编译在程序的代码段中的,而是在程序运行过程中自动生成的一段代码,然后让程序在合适的时机去执行这段代码。
下面是一个替换函数参数的 thunk 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#include <windows.h> #include <iostream> // 定义一个函数指针。原型和 API 函数 MessageBoxA 函数的原型完全一样 typedef int (WINAPI *TEST)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType); // 按字节对齐,因为此结构中保存的是一段机器码 #pragma pack(push,1) // 此 thunk 代码的功能是用新的字符串的地址替换原先的字符串的地址 // 并跳转到 MessageBoxA 函数处执行 struct Thunk { DWORD m_mov; DWORD m_pStr; BYTE m_jmp; DWORD m_relproc; // 初始化后 thunk 代码执行以下功能 // 1. 把地址 [esp + 0x08] 处的内容替换为参数 pStr 的值 // 2. 跳转到 MessageBoxA 函数 BOOL Init( char * pStr) { m_mov = 0x082444C7; // mov dword ptr [esp+0x8] esp+8处是函数参数 lpText m_pStr = PtrToUlong(pStr); m_jmp = 0xe9; // jmp m_relproc = DWORD (( INT_PTR )::MessageBoxA - (( INT_PTR ) this + sizeof (Thunk))); // 不调用此函数程序也能正常执行,但最好是调用一下 ::FlushInstructionCache(::GetCurrentProcess(), this , sizeof (Thunk)); return TRUE; } }; #pragma pack(pop) CHAR love[] = "I love you!" ; int main() { // 在堆栈中生成 thunk 代码 Thunk thunk; thunk.Init(love); TEST MyMessageBox = (TEST)&thunk; // 注意 "I hate you!" 会被 thunk 替换为 "I love you!" // 此时并不会弹出"I hate you!"而是弹出"I love you!" MyMessageBox(NULL, "I hate you!" ,NULL,0); } |
要彻底理解以上代码的实现原理,必须对汇编语言有所了解。
首先看第47行代码。MyMessageBox 是一个函数指针,此行代码在编译后会产生汇编代码把四个参数从右向左(stdcall) push 到堆栈中然后再跳转到 MyMessageBox 所指向的内存位置处继续执行。在把四个参数 push 到堆栈后,内存地址[ESP + 8](ESP为栈顶指针) 处保存的就是字符串"I hate you!"的地址。而 MyMessageBox 所指向的就是我们的 thunk 代码。
跳转到 thunk 代码处后,首先用字符串"I love you!"的地址替换掉了堆栈中保存的"I hate you!"的地址,然后跳转到 MessageBoxA 函数中执行。注意这里执行函数 MessageBoxA 的代码使用的是“跳转”而不是“调用”。在 MessageBoxA 函数中有 ret 指令,当执行到此指令后程序将返回到第 48 行代码处继续往下执行。
再看第26行代码。此行代码是计算要跳转到的地址相对于当前地址的偏移量,格式是:目标地址 - EIP 中的当前地址。目标地址当然就是函数 MessageBoxA 的地址,而EIP中当前的地址是紧跟结构变量 thunk 之后的那个字节的地址,即:(INT_PTR)this+sizeof(Thunk)。