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 所提供的優雅解決方案。
全部示例代碼見附件。