在文章如何Hook Windows API中,我们讨论了如何Hook Windows API。此种方式的结果是每个DLL都会跳转到相同的函数,所以不便实现针对每个DLL的函数使用信息。如果希望得到基于每个DLL的信息,可以通过修改栈的结构的方式实现。
假设现在要Hook的函数是: void __stdcall Func(int, int);
其调用时的栈结构如图1所示:
图1: Thunk对栈结构的调整
现在我们把Hook的函数替换成如下的thunk代码:
#pragma pack(push,1)
struct _stdcallThunk
{
BYTE m_push[3];// push dword ptr [esp]
DWORD m_mov; // move dword ptr [esp+0x4],pThis
DWORD m_this; // pThis, which serves as the operand for mov instruction
BYTE m_jmp; // jmp proc
DWORD m_relproc; // proc, which serves as the operand for jmp instruction
VOID Init(INT_PTR proc, INT_PTR pThis, INT_PTR pThunk)
{
m_push[0] = 0xFF;
m_push[1] = 0x34;
m_push[2] = 0x24;
m_mov = 0x042444C7;
m_this = (DWORD)pThis;
m_jmp = 0xE9;
m_relproc = (DWORD)(proc - (pThunk + sizeof(_stdcallThunk)));
FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallThunk));
}
};
那么当函数调用转到Hook之后,其栈结构会被调整,如图1所示。调整之后的栈结构正好是一个对C++对象的成员函数调用之后的栈结构。所以只要我们定义一个C++类,使其中的一个非静态成员函数拥有和被Hook函数相同的声明,我们就可以利用上面的Thunk把函数的调用分发给为每个DLL创建的该C++类的对象,从而记录每个DLL中对该函数的使用信息。
使用thunk的步骤如下:
- 定义一个C++类,使其一个非静态成员函数拥有和被Hook函数相同的声明;
- 创建该类的一个对象;
- 创建一个_stdcallThunk对象,调用其Init()函数,参数设置如下表所示.
- 把Hook函数的地址设成thunk对象的首地址。
参数名 | 取值 |
proc | 步骤1中定义的非静态成员函数的函数指针 |
pThis | 步骤2中创建对象的指针 |
pThunk | 步骤3中创建的对象的指针 |
这里有如下几个问题需要注意:
1. 如何取得非静态C++函数的地址
C++标准不允许用提取静态成员函数的方式提取非静态函数的地址,可以使用如下的方式:
a) 定义如下共用体:
union {
INT_PTR dwFunc;
RETURN_TYPE (CLASS::*pfn)(ARGS);
} pfn;
b)把非静态函数指针赋值给pfn.pfn,然后通过pfn.dwFunc取出值。
2. 防止DEP设置导致程序crash
由于thunk是堆上的对象,所以如果OS打开DEP,那么程序可能会crash。解决方法是使用VirtualAlloc()创建一个具有read/write/execute属性的内存地址,把thunk分到该地址空间中。
3. 对齐地址防止cache不同步
需要对thunk的起始地址进行对齐,以防止代码被加载到指令缓存的时候出现不同步,从而导致程序crash。