S.l.e!ep.¢%

像打了激速一样,以四倍的速度运转,开心的工作
简单、开放、平等的公司文化;尊重个性、自由与个人价值;
posts - 1098, comments - 335, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

用 Thunk 实现 COM 的挂钩

Posted on 2010-02-06 17:05 S.l.e!ep.¢% 阅读(603) 评论(-1)  编辑 收藏 引用 所属分类: COM

用 Thunk 实现 COM 的挂钩

您可以任意转载这篇文章,但请在转载时注明原始链接和作者,谢谢。

本文链接: http://blog.titilima.com/show-557-1.html
相关分享: 收藏到QQ书签  添加到百度搜藏  新浪微博分享  提交天天网摘

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

C++代码
  1. typedef   struct  _tagHookHelper {   
  2.      PVOID * vptr;   
  3. } HOOKHELPER, *PHOOKHELPER;   
  4.   
  5. PVOID  WINAPI LSetComHook(   
  6.     IUnknown* unk,   
  7.      int  index,   
  8.      PVOID  pfnHook)   
  9. {   
  10.     PHOOKHELPER p = (PHOOKHELPER)unk;   
  11.      PVOID  ret = p->vptr[index];   
  12.   
  13.      DWORD  dwOldProtect;   
  14.     VirtualProtect(&p->vptr[index],  sizeof ( PVOID ), PAGE_READWRITE,   
  15.         &dwOldProtect);   
  16.     p->vptr[index] = pfnHook;   
  17.     VirtualProtect(&p->vptr[index],  sizeof ( PVOID ), dwOldProtect, NULL);   
  18.      return  ret;   
  19. }  

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

C++代码
  1. typedef   HRESULT  (STDCALL * QIPtr)(IUnknown* This, REFIID riid,  PVOID * ppv);   
  2.   
  3. QIPtr g_pfnQueryInterface = NULL;   
  4.   
  5. HREUSLT STDCALL HookQueryInterface(IUnknown* This, REFIID riid,  PVOID * ppv)   
  6. {   
  7.      HRESULT  hr = g_pfnQueryInterface(This, riid, ppv);   
  8.     OutputDebugString(_T( "HookQueryInterface.\n" ));   
  9.      return  hr;   
  10. }   
  11.   
  12. IUnknown* punk = NULL;   
  13. // CoCreateInstance....   
  14. g_pfnQueryInterface = (QIPtr)LSetComHook(punk, 0, HookQueryInterface);   
  15. punk->QueryInterface(...);  

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

当然,我们可以通过非常规的方法来解决这个问题,比如 thunk。
在开始实现我的 thunk 之前,先来看看一个 COM 方法调用的过程,考虑如下代码:

C++代码
  1. class  A   
  2. {   
  3. public :   
  4.      virtual   void  WINAPI foo( int  i);   
  5.      int  m_n;   
  6. };   
  7.   
  8. void  WINAPI A::foo( int  i)   
  9. {   
  10.     printf( "m_n = %d, i = %d\n" , m_n, i);   
  11. }   
  12.   
  13. A a;   
  14. A* pa = &a;   
  15. pa->m_n = 1;   
  16. pa->foo(2);  

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

反汇编代码
  1. push        2   
  2. mov         eax,dword ptr [pa]   
  3. ; vptr   
  4. mov         ecx,dword ptr [eax]   
  5. ; this   
  6. mov         edx,dword ptr [pa]   
  7. push        edx   
  8. mov         eax,dword ptr [ecx]   
  9. call        eax  

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

  1. 由右至左的各个参数,也就是 STDCALL 调用约定的压栈顺序;
  2. this 指针;
  3. 当然,还有 call 的返回地址,这个压栈是在 call 指令内部完成的。

从上面可以看出来,为了把一个 COM 调用重定向到我们自己的类成员函数中,需要做以下工作:

  1. 保留原 COM 方法的各个参数;
  2. 保留原 COM 对象的 this 指针;
  3. 加入我们自己类对象的 this 指针;
  4. 保留 call 原有的返回地址。

简单说来,这个重定向的过程是将堆栈中插入另外一个 this 指针,仅此而已。
明确了这个操作的步骤,我们可以写出如下的 thunk 代码,这段代码将被放到目标 COM 对象的虚表中。

汇编代码
  1. ; 弹出 call 的返回地址   
  2. pop eax   
  3. ; 加入自己的 this 指针   
  4. push this   
  5. ; 重新压入 call 的返回地址   
  6. push eax   
  7. ; 跳至挂钩函数之中   
  8. jmp addr  

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

C++代码
  1. #pragma pack(push, 1)   
  2. typedef   struct  _tagHookThunk {   
  3.      BYTE  PopEax;   // 0x58   
  4.      BYTE  Push;     // 0x68   
  5.      PVOID  This;   
  6.      BYTE  PushEax;  // 0x50   
  7.      BYTE  Jmp;      // 0xe9   
  8.      PBYTE  Addr;   
  9. } HOOKTHUNK, *PHOOKTHUNK;   
  10. #pragma pack(pop)   

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

C++代码
  1. typedef   struct  _tagComHook {   
  2.     HOOKTHUNK Thunk;   
  3.      PVOID * vptr;   
  4.      int  index;   
  5.      PVOID  pfnOriginal;   
  6. } COMHOOK;  

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

C++代码
  1. HCOMHOOK WINAPI LSetComHook(   
  2.     IUnknown* unk,   
  3.      int  index,   
  4.      PVOID  This,   
  5.      PVOID  pfnHook,   
  6.      PVOID * pfnOriginal)   
  7. {   
  8.     PHOOKHELPER p = (PHOOKHELPER)unk;   
  9.   
  10.     HCOMHOOK h =  new  COMHOOK;   
  11.      // pop eax   
  12.     h->Thunk.PopEax = 0x58;   
  13.      // push this   
  14.     h->Thunk.Push = 0x68;   
  15.     h->Thunk.This = This;   
  16.      // push eax   
  17.     h->Thunk.PushEax = 0x50;   
  18.      // jmp addr   
  19.     h->Thunk.Jmp = 0xe9;   
  20.     h->Thunk.Addr = ( PBYTE )(( int )pfnHook - ( int )h -  sizeof (HOOKTHUNK));   
  21.     ::FlushInstructionCache(::GetCurrentProcess(), &h->Thunk,   
  22.          sizeof (HOOKTHUNK));   
  23.   
  24.     h->vptr = p->vptr;   
  25.     h->index = index;   
  26.     h->pfnOriginal = LSetComHook(unk, index, &h->Thunk);   
  27.   
  28.     *pfnOriginal = h->pfnOriginal;   
  29.      return  h;   
  30. }  

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

C++代码
  1. typedef   void  (WINAPI * ptr)(A* This,  int  i);   
  2.   
  3. class  B   
  4. {   
  5. public :   
  6.      void  WINAPI hook_foo(A* This,  int  i);   
  7.     ptr pfn;   
  8. };   
  9.   
  10. void  WINAPI B::hook_foo(A* This,  int  i)   
  11. {   
  12.     puts( "hooked by B" );   
  13.     pfn(This, i);   
  14. }   
  15.   
  16. B b;   
  17. HCOMHOOK h = LSetComHook((IUnknown*)pa, 0, &b,   
  18.     member_cast< PVOID >(&B::hook_foo), ( PVOID *)&b.pfn);   
  19. pa->foo(2);  

其中 member_cast 用于非 static 成员的类型转换,可以参考《获取成员函数的指针》一文,再次感谢 likunkun 所提供的优雅解决方案。
全部示例代码见附件。

附件: comhook.zip (3.52 K, 下载次数:210)

Tags: c/c++, win32, com


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理