用 Thunk 实现 COM 的挂钩

COM 的挂钩其实已经是一个很古老的话题了,其核心技术就是替换 COM 对象虚表中相应位置的函数指针,从而达到挂钩的效果。顺便说一句,这个方法和内核的 SSDT 挂钩是十分类似的。其相应的实现代码也十分简单,如下所示:

view plaincopy to clipboardprint?
typedef struct _tagHookHelper {   
    PVOID* vptr;   
} HOOKHELPER, *PHOOKHELPER;   
  
PVOID WINAPI LSetComHook(   
    IUnknown* unk,   
    int index,   
    PVOID pfnHook)   
{   
    PHOOKHELPER p = (PHOOKHELPER)unk;   
    PVOID ret = p->vptr[index];   
  
    DWORD dwOldProtect;   
    VirtualProtect(&p->vptr[index], sizeof(PVOID), PAGE_READWRITE,   
        &dwOldProtect);   
    p->vptr[index] = pfnHook;   
    VirtualProtect(&p->vptr[index], sizeof(PVOID), dwOldProtect, NULL);   
    return ret;   
}  
typedef struct _tagHookHelper {
    PVOID* vptr;
} HOOKHELPER, *PHOOKHELPER;

PVOID WINAPI LSetComHook(
    IUnknown* unk,
    int index,
    PVOID pfnHook)
{
    PHOOKHELPER p = (PHOOKHELPER)unk;
    PVOID ret = p->vptr[index];

    DWORD dwOldProtect;
    VirtualProtect(&p->vptr[index], sizeof(PVOID), PAGE_READWRITE,
        &dwOldProtect);
    p->vptr[index] = pfnHook;
    VirtualProtect(&p->vptr[index], sizeof(PVOID), dwOldProtect, NULL);
    return ret;
}

需要指出的是,这里要使用 VirtualProtect 改变虚表的页面属性,就像挂钩 SSDT 时要改变 cr0 的保护属性一样。
整个的挂钩过程及使用类似于这个样子:

view plaincopy to clipboardprint?
typedef HRESULT (STDCALL * QIPtr)(IUnknown* This, REFIID riid, PVOID* ppv);   
  
QIPtr g_pfnQueryInterface = NULL;   
  
HREUSLT STDCALL HookQueryInterface(IUnknown* This, REFIID riid, PVOID* ppv)   
{   
    HRESULT hr = g_pfnQueryInterface(This, riid, ppv);   
    OutputDebugString(_T("HookQueryInterface.\n"));   
    return hr;   
}   
  
IUnknown* punk = NULL;   
// CoCreateInstance....   
g_pfnQueryInterface = (QIPtr)LSetComHook(punk, 0, HookQueryInterface);   
punk->QueryInterface(...);  
typedef HRESULT (STDCALL * QIPtr)(IUnknown* This, REFIID riid, PVOID* ppv);

QIPtr g_pfnQueryInterface = NULL;

HREUSLT STDCALL HookQueryInterface(IUnknown* This, REFIID riid, PVOID* ppv)
{
    HRESULT hr = g_pfnQueryInterface(This, riid, ppv);
    OutputDebugString(_T("HookQueryInterface.\n"));
    return hr;
}

IUnknown* punk = NULL;
// CoCreateInstance....
g_pfnQueryInterface = (QIPtr)LSetComHook(punk, 0, HookQueryInterface);
punk->QueryInterface(...);

这种挂钩的方式有一个局限性,就是挂钩函数 HookQueryInterface 不能作为一个非 static 的类成员函数来实现。与之类似,Win32 的 WNDPROC 也无法使用非 static 的类成员函数来封装,实乃一大憾事。

当然,我们可以通过非常规的方法来解决这个问题,比如 thunk。

在开始实现我的 thunk 之前,先来看看一个 COM 方法调用的过程,考虑如下代码:

view plaincopy to clipboardprint?
class A   
{   
public:   
    virtual void WINAPI foo(int i);   
    int m_n;   
};   
  
void WINAPI A::foo(int i)   
{   
    printf("m_n = %d, i = %d\n", m_n, i);   
}   
  
A a;   
A* pa = &a;   
pa->m_n = 1;   
pa->foo(2);  
class A
{
public:
    virtual void WINAPI foo(int i);
    int m_n;
};

void WINAPI A::foo(int i)
{
    printf("m_n = %d, i = %d\n", m_n, i);
}

A a;
A* pa = &a;
pa->m_n = 1;
pa->foo(2);

这个调用过程所对应的汇编代码为:

push        2
mov         eax,dword ptr [pa]
; vptr
mov         ecx,dword ptr [eax]
; this
mov         edx,dword ptr [pa]
push        edx
mov         eax,dword ptr [ecx]
call        eax

也就是说,一个 COM 方法调用的压栈顺序为:

由右至左的各个参数,也就是 STDCALL 调用约定的压栈顺序; 
this 指针; 
当然,还有 call 的返回地址,这个压栈是在 call 指令内部完成的。 
从上面可以看出来,为了把一个 COM 调用重定向到我们自己的类成员函数中,需要做以下工作:

保留原 COM 方法的各个参数; 
保留原 COM 对象的 this 指针; 
加入我们自己类对象的 this 指针; 
保留 call 原有的返回地址。 
简单说来,这个重定向的过程是将堆栈中插入另外一个 this 指针,仅此而已。

明确了这个操作的步骤,我们可以写出如下的 thunk 代码,这段代码将被放到目标 COM 对象的虚表中。

; 弹出 call 的返回地址
pop eax
; 加入自己的 this 指针
push this
; 重新压入 call 的返回地址
push eax
; 跳至挂钩函数之中
jmp addr

相应地,我们为这个 thunk 定义一个结构:

view plaincopy to clipboardprint?
#pragma pack(push, 1)   
typedef struct _tagHookThunk {   
    BYTE PopEax;  // 0x58   
    BYTE Push;    // 0x68   
    PVOID This;   
    BYTE PushEax; // 0x50   
    BYTE Jmp;     // 0xe9   
    PBYTE Addr;   
} HOOKTHUNK, *PHOOKTHUNK;   
#pragma pack(pop)  
#pragma pack(push, 1)
typedef struct _tagHookThunk {
    BYTE PopEax;  // 0x58
    BYTE Push;    // 0x68
    PVOID This;
    BYTE PushEax; // 0x50
    BYTE Jmp;     // 0xe9
    PBYTE Addr;
} HOOKTHUNK, *PHOOKTHUNK;
#pragma pack(pop) 

以及一个用于保存挂钩信息的结构:

view plaincopy to clipboardprint?
typedef struct _tagComHook {   
    HOOKTHUNK Thunk;   
    PVOID* vptr;   
    int index;   
    PVOID pfnOriginal;   
} COMHOOK;  
typedef struct _tagComHook {
    HOOKTHUNK Thunk;
    PVOID* vptr;
    int index;
    PVOID pfnOriginal;
} COMHOOK; 

最后,就可以实现这个升级版的挂钩函数了,如下:

view plaincopy to clipboardprint?
HCOMHOOK WINAPI LSetComHook(   
    IUnknown* unk,   
    int index,   
    PVOID This,   
    PVOID pfnHook,   
    PVOID* pfnOriginal)   
{   
    PHOOKHELPER p = (PHOOKHELPER)unk;   
  
    HCOMHOOK h = new COMHOOK;   
    // pop eax   
    h->Thunk.PopEax = 0x58;   
    // push this   
    h->Thunk.Push = 0x68;   
    h->Thunk.This = This;   
    // push eax   
    h->Thunk.PushEax = 0x50;   
    // jmp addr   
    h->Thunk.Jmp = 0xe9;   
    h->Thunk.Addr = (PBYTE)((int)pfnHook - (int)h - sizeof(HOOKTHUNK));   
    ::FlushInstructionCache(::GetCurrentProcess(), &h->Thunk,   
        sizeof(HOOKTHUNK));   
  
    h->vptr = p->vptr;   
    h->index = index;   
    h->pfnOriginal = LSetComHook(unk, index, &h->Thunk);   
  
    *pfnOriginal = h->pfnOriginal;   
    return h;   
}  
HCOMHOOK WINAPI LSetComHook(
    IUnknown* unk,
    int index,
    PVOID This,
    PVOID pfnHook,
    PVOID* pfnOriginal)
{
    PHOOKHELPER p = (PHOOKHELPER)unk;

    HCOMHOOK h = new COMHOOK;
    // pop eax
    h->Thunk.PopEax = 0x58;
    // push this
    h->Thunk.Push = 0x68;
    h->Thunk.This = This;
    // push eax
    h->Thunk.PushEax = 0x50;
    // jmp addr
    h->Thunk.Jmp = 0xe9;
    h->Thunk.Addr = (PBYTE)((int)pfnHook - (int)h - sizeof(HOOKTHUNK));
    ::FlushInstructionCache(::GetCurrentProcess(), &h->Thunk,
        sizeof(HOOKTHUNK));

    h->vptr = p->vptr;
    h->index = index;
    h->pfnOriginal = LSetComHook(unk, index, &h->Thunk);

    *pfnOriginal = h->pfnOriginal;
    return h;

测试代码如下,使用 B 类中的 hook_foo 挂钩了上文中的 A::foo。

view plaincopy to clipboardprint?
typedef void (WINAPI * ptr)(A* This, int i);   
  
class B   
{   
public:   
    void WINAPI hook_foo(A* This, int i);   
    ptr pfn;   
};   
  
void WINAPI B::hook_foo(A* This, int i)   
{   
    puts("hooked by B");   
    pfn(This, i);   
}   
  
B b;   
HCOMHOOK h = LSetComHook((IUnknown*)pa, 0, &b,   
    member_cast<PVOID>(&B::hook_foo), (PVOID*)&b.pfn);   
pa->foo(2);  
typedef void (WINAPI * ptr)(A* This, int i);

class B
{
public:
    void WINAPI hook_foo(A* This, int i);
    ptr pfn;
};

void WINAPI B::hook_foo(A* This, int i)
{
    puts("hooked by B");
    pfn(This, i);
}

B b;
HCOMHOOK h = LSetComHook((IUnknown*)pa, 0, &b,
    member_cast<PVOID>(&B::hook_foo), (PVOID*)&b.pfn);
pa->foo(2); 

其中 member_cast 用于非 static 成员的类型转换,可以参考《获取成员函数的指针》一文,再次感谢 likunkun 所提供的优雅解决方案。
posted on 2010-06-02 00:00  carekee  阅读(226)  评论(0编辑  收藏  举报