COM 的挂钩其实已经是一个很古老的话题了,其核心技术就是替换 COM 对象虚表中相应位置的函数指针,从而达到挂钩的效果。顺便说一句,这个方法和内核的 SSDT 挂钩是十分类似的。其相应的实现代码也十分简单,如下所示:
C++代码
-
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 的保护属性一样。
整个的挂钩过程及使用类似于这个样子:
C++代码
-
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;
-
-
g_pfnQueryInterface = (QIPtr)LSetComHook(punk, 0, HookQueryInterface);
-
punk->QueryInterface(...);
这种挂钩的方式有一个局限性,就是挂钩函数 HookQueryInterface 不能作为一个非 static 的类成员函数来实现。与之类似,Win32 的 WNDPROC 也无法使用非 static 的类成员函数来封装,实乃一大憾事。
当然,我们可以通过非常规的方法来解决这个问题,比如 thunk。
在开始实现我的 thunk 之前,先来看看一个 COM 方法调用的过程,考虑如下代码:
C++代码
-
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]
-
-
mov ecx,dword ptr [eax]
-
-
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 对象的虚表中。
汇编代码
-
-
pop eax
-
-
push this
-
-
push eax
-
-
jmp addr
相应地,我们为这个 thunk 定义一个结构:
C++代码
-
#pragma pack(push, 1)
-
typedef
struct
_tagHookThunk {
-
BYTE
PopEax;
-
BYTE
Push;
-
PVOID
This;
-
BYTE
PushEax;
-
BYTE
Jmp;
-
PBYTE
Addr;
-
} HOOKTHUNK, *PHOOKTHUNK;
-
#pragma pack(pop)
以及一个用于保存挂钩信息的结构:
C++代码
-
typedef
struct
_tagComHook {
-
HOOKTHUNK Thunk;
-
PVOID
* vptr;
-
int
index;
-
PVOID
pfnOriginal;
-
} COMHOOK;
最后,就可以实现这个升级版的挂钩函数了,如下:
C++代码
-
HCOMHOOK WINAPI LSetComHook(
-
IUnknown* unk,
-
int
index,
-
PVOID
This,
-
PVOID
pfnHook,
-
PVOID
* pfnOriginal)
-
{
-
PHOOKHELPER p = (PHOOKHELPER)unk;
-
-
HCOMHOOK h =
new
COMHOOK;
-
-
h->Thunk.PopEax = 0x58;
-
-
h->Thunk.Push = 0x68;
-
h->Thunk.This = This;
-
-
h->Thunk.PushEax = 0x50;
-
-
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。
C++代码
-
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 所提供的优雅解决方案。
全部示例代码见附件。