注册 |登录

0GiNr技术社区论坛系统常规开发 › 查看主题

506

查看

5

回复
返回列表

Rank: 10Rank: 10

金钱
83 元 
经验
22 点 
威望
0 点 
贡献
0 点 
精华
go

[C/Java] 用 Thunk 实现 COM 的挂钩

1
发表于 2009-7-3 11:58 | 只看该作者 | 倒序看帖 | 打印
凡本论坛原创内容,其作者享有著作权,未经许可谢绝转载。
本帖最后由 李马 于 2009-7-3 11:59 编辑

原文链接:http://www.titilima.cn/show-557-1.html
COM 的挂钩其实已经是一个很古老的话题了,其核心技术就是替换 COM 对象虚表中相应位置的函数指针,从而达到挂钩的效果。顺便说一句,这个方法和内核的 SSDT 挂钩是十分类似的。其相应的实现代码也十分简单,如下所示:
  1. typedef struct _tagHookHelper {
  2.     PVOID* vptr;
  3. } HOOKHELPER, *PHOOKHELPER;

  4. PVOID WINAPI LSetComHook(
  5.     IUnknown* unk,
  6.     int index,
  7.     PVOID pfnHook)
  8. {
  9.     PHOOKHELPER p = (PHOOKHELPER)unk;
  10.     PVOID ret = p->vptr[index];

  11.     DWORD dwOldProtect;
  12.     VirtualProtect(&p->vptr[index], sizeof(PVOID), PAGE_READWRITE,
  13.         &dwOldProtect);
  14.     p->vptr[index] = pfnHook;
  15.     VirtualProtect(&p->vptr[index], sizeof(PVOID), dwOldProtect, NULL);
  16.     return ret;
  17. }
复制代码
需要指出的是,这里要使用 VirtualProtect 改变虚表的页面属性,就像挂钩 SSDT 时要改变 cr0 的保护属性一样。
整个的挂钩过程及使用类似于这个样子:
  1. typedef HRESULT (STDCALL * QIPtr)(IUnknown* This, REFIID riid, PVOID* ppv);

  2. QIPtr g_pfnQueryInterface = NULL;

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

  9. IUnknown* punk = NULL;
  10. // CoCreateInstance....
  11. g_pfnQueryInterface = (QIPtr)LSetComHook(punk, 0, HookQueryInterface);
  12. punk->QueryInterface(...);
复制代码
这种挂钩的方式有一个局限性,就是挂钩函数 HookQueryInterface 不能作为一个非 static 的类成员函数来实现。与之类似,Win32 的 WNDPROC 也无法使用非 static 的类成员函数来封装,实乃一大憾事。

当然,我们可以通过非常规的方法来解决这个问题,比如 thunk。
在开始实现我的 thunk 之前,先来看看一个 COM 方法调用的过程,考虑如下代码:
  1. class A
  2. {
  3. public:
  4.     virtual void WINAPI foo(int i);
  5.     int m_n;
  6. };

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

  11. A a;
  12. A* pa = &a;
  13. pa->m_n = 1;
  14. 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 定义一个结构:
  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)
复制代码
以及一个用于保存挂钩信息的结构:
  1. typedef struct _tagComHook {
  2.     HOOKTHUNK Thunk;
  3.     PVOID* vptr;
  4.     int index;
  5.     PVOID pfnOriginal;
  6. } COMHOOK;
复制代码
最后,就可以实现这个升级版的挂钩函数了,如下:
  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.     HCOMHOOK h = new COMHOOK;
  10.     // pop eax
  11.     h->Thunk.PopEax = 0x58;
  12.     // push this
  13.     h->Thunk.Push = 0x68;
  14.     h->Thunk.This = This;
  15.     // push eax
  16.     h->Thunk.PushEax = 0x50;
  17.     // jmp addr
  18.     h->Thunk.Jmp = 0xe9;
  19.     h->Thunk.Addr = (PBYTE)((int)pfnHook - (int)h - sizeof(HOOKTHUNK));
  20.     ::FlushInstructionCache(::GetCurrentProcess(), &h->Thunk,
  21.         sizeof(HOOKTHUNK));

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

  25.     *pfnOriginal = h->pfnOriginal;
  26.     return h;
  27. }
复制代码
测试代码如下,使用 B 类中的 hook_foo 挂钩了上文中的 A::foo。
  1. typedef void (WINAPI * ptr)(A* This, int i);

  2. class B
  3. {
  4. public:
  5.     void WINAPI hook_foo(A* This, int i);
  6.     ptr pfn;
  7. };

  8. void WINAPI B::hook_foo(A* This, int i)
  9. {
  10.     puts("hooked by B");
  11.     pfn(This, i);
  12. }

  13. B b;
  14. HCOMHOOK h = LSetComHook((IUnknown*)pa, 0, &b,
  15.     member_cast<PVOID>(&B::hook_foo), (PVOID*)&b.pfn);
  16. pa->foo(2);
复制代码
其中 member_cast 用于非 static 成员的类型转换,可以参考《获取成员函数的指针》一文,再次感谢 likunkun 所提供的优雅解决方案。
全部示例代码见附件。
附件: 您需要登录才可以下载或查看附件。没有帐号?注册

TOP

0GiNr核心团队

“花”指令

Rank: 12Rank: 12

金钱
1497 元 
经验
3248 点 
威望
365 点 
贡献
34 点 
精华
2
发表于 2009-7-3 15:37 | 只看该作者
好久不见李马。听说是旅游去了。

TOP

大牛

PH The Great

Rank: 4Rank: 4

金钱
1490 元 
经验
808 点 
威望
40 点 
贡献
13 点 
精华
3
发表于 2009-7-4 05:35 | 只看该作者
COM挂钩可以做什么呢?

TOP

Rank: 10Rank: 10

金钱
83 元 
经验
22 点 
威望
0 点 
贡献
0 点 
精华
4
发表于 2009-7-6 15:28 | 只看该作者
3# phthegreat


和 API 挂钩一样,监控或修改某个 COM 对象的行为。

TOP

0GiNr核心团队

“花”指令

Rank: 12Rank: 12

金钱
1497 元 
经验
3248 点 
威望
365 点 
贡献
34 点 
精华
5
发表于 2009-7-6 15:53 | 只看该作者
很多HIPS没有防御COM操作。也确实不好防。

TOP

0GiNr核心团队

“花”指令

Rank: 12Rank: 12

金钱
1497 元 
经验
3248 点 
威望
365 点 
贡献
34 点 
精华
6
发表于 2009-7-6 15:54 | 只看该作者
很多HIPS没防COM操作啊,也确实难防。

TOP

0GiNr安全门户 |联系我们

GMT+8, 2010-9-4 00:25.

Powered by Discuz! X1

© 2001-2010 Comsenz Inc.