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 所提供的优雅解决方案。