拦截 COM 接口调用






4.91/5 (34投票s)
本文将介绍如何实现 COM 接口挂钩。
目录
引言
本文将介绍 如何实现 COM 接口挂钩。COM 挂钩在目标和方法上都与用户模式 API 挂钩有共同之处,但由于 COM 技术自身的特性,也存在一些显著的差异。我将展示两种最常用的方法,并强调它们的优缺点。代码示例已尽可能简化,以便我们能专注于问题的最重要部分。
COM 的一些基本概念
在开始拦截 COM 对象调用之前,我想先提及一些 COM 技术的基础概念。如果您对此非常了解,可以直接跳过这些枯燥的理论,直接进入实践部分。
所有 COM 类都实现一个或多个接口。所有接口都必须派生自 IUnknown
。它用于引用计数和获取对象实现的指向其他接口的指针。每个接口都有一个全局唯一的接口标识符 - IID
。客户端使用接口指针来调用 COM 对象的所有方法。
这一特性使得 COM 组件在二进制级别上是独立的。这意味着如果更改了 COM 服务器,不需要重新编译其客户端(只要新版本的服务器提供相同的接口)。甚至可以用您自己的实现来替换 COM 服务器。
所有 COM 接口方法的调用都是通过虚方法表(或简称为 vtable
)实现的。指向 vtable
的指针始终是每个 COM 类的第一个字段。简而言之,此表是一个指针数组 - 指向类方法的指针(按声明顺序)。当客户端调用方法时,它会通过相应的指针进行调用。
COM 服务器在客户端进程的上下文中运行,或者在某个其他进程的上下文中运行。在第一种情况下,服务器是一个加载到客户端进程中的 DLL。在第二种情况下,服务器作为另一个进程(可能甚至在另一台计算机上)执行。为了与服务器通信,客户端会加载所谓的代理/存根 DLL。它将客户端的调用重定向到服务器。
为了便于访问,COM 服务器应在系统注册表中注册。客户端有几个函数可用于创建 COM 实例,但通常使用的是 CoGetClassObject
、CoCreateInstanceEx
或(最常见的)CoCreateInstance
。
如果您想获取更详细的信息,可以使用 MSDN 或“参考文献”部分中的某个来源。
实际示例
让我们看看如何拦截对 COM 接口的调用。解决这个问题有几种不同的方法。例如,我们可以修改注册表或使用 CoTreatAsClass
或 CoGetInterceptor
函数。本文介绍了两种最常用的方法:使用代理对象和修补虚拟方法表。每种方法都有其优点和缺点,因此选择哪种方法取决于任务。
本文的代码包含了一个最简单的 COM 服务器 DLL、客户端应用程序以及两个演示我将要介绍的方法的 COM 挂钩示例的实现。
让我们在不安装挂钩的情况下运行客户端应用程序。首先,通过执行命令 regsvr32 ComSample.dll 注册 COM 服务器。然后运行 ComSampleClient.exe 或 SrciptCleint.js 来查看示例服务器的客户端是如何工作的。现在是时候设置一些挂钩了。
方法一:代理对象
COM 主要涉及二进制封装。客户端通过接口使用任何 COM 服务器,并且可以更改服务器的一个实现而无需重新编译客户端。此特性可用于拦截对 COM 服务器的调用。
此方法的主要思想是拦截 COM 对象创建请求,并用我们自己的代理对象替换新创建的实例。此代理对象是具有与原始对象相同接口的 COM 对象。客户端代码将其视为原始对象进行交互。代理对象通常存储指向原始对象的指针,以便它可以调用原始对象的某些方法。
如前所述,代理对象必须实现目标对象的所有接口。在我们的示例中,它只有一个接口:ISampleObject
。代理类使用 ATL 实现。
class ATL_NO_VTABLE CSampleObjectProxy :
public ATL::CComObjectRootEx<ATL::CComMultiThreadModel>,
public ATL::CComCoClass<CSampleObjectProxy, &CLSID_SampleObject>,
public ATL::IDispatchImpl<ISampleObject, &IID_ISampleObject,
&LIBID_ComSampleLib, 1, 0>
{
public:
CSampleObjectProxy();
DECLARE_NO_REGISTRY()
BEGIN_COM_MAP(CSampleObjectProxy)
COM_INTERFACE_ENTRY(ISampleObject)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
DECLARE_PROTECT_FINAL_CONSTRUCT()
public:
HRESULT FinalConstruct();
void FinalRelease();
public:
HRESULT static CreateInstance(IUnknown* original, REFIID riid, void **ppvObject);
public:
STDMETHOD(get_ObjectName)(BSTR* pVal);
STDMETHOD(put_ObjectName)(BSTR newVal);
STDMETHOD(DoWork)(LONG arg1, LONG arg2, LONG* result);
...
};
STDMETHODIMP CSampleObjectProxy::get_ObjectName(BSTR* pVal)
{
return m_Name.CopyTo(pVal);
}
STDMETHODIMP CSampleObjectProxy::DoWork(LONG arg1, LONG arg2, LONG* result)
{
*result = 42;
return S_OK;
}
STDMETHODIMP CSampleObjectProxy::put_ObjectName(BSTR newVal)
{
return m_OriginalObject->put_ObjectName(newVal);
}
请注意,即使您不关心某些方法(例如 put_ObjectName
),也必须在代理中实现它们。
现在,我们需要拦截目标对象的创建,以便用我们的代理替换它。有几个 Windows API 函数能够创建 COM 对象,但通常使用 CoCreateInstance
。
为了拦截目标对象创建,我使用了 mhook 库来挂钩 CoCreateInstance
和 CoGetClassObject
。设置 API 挂钩的技术是一个被广泛讨论的话题。如果您想获取更详细的信息,可以参考,例如,Sergey Podobry 的文章 设置全局 API 挂钩的简便方法。
这是 CoCreateInstance
挂钩函数的实现:
HRESULT WINAPI Hook::CoCreateInstance
(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID* ppv)
{
if (rclsid == CLSID_SampleObject)
{
if (pUnkOuter)
return CLASS_E_NOAGGREGATION;
ATL::CComPtr<IUnknown> originalObject;
HRESULT hr = Original::CoCreateInstance(rclsid, pUnkOuter,
dwClsContext, riid, (void**)&originalObject);
if (FAILED(hr))
return hr;
return CSampleObjectProxy::CreateInstance(originalObject, riid, ppv);
}
return Original::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, riid, ppv);
}
要查看代理对象示例方法的运行情况,请在 AppInit_DLLs
注册表值(HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows)中指定 ComInterceptProxyObj.dll 的完整名称。现在您可以运行 ComSampleClient.exe 或 ScriptClient.js,并看到对目标对象方法的调用已被拦截。
方法二:Vtable 补丁
拦截 COM 对象调用的另一种方法是修改对象的虚拟方法表。它包含 COM 对象所有 public
方法的指针,因此可以将它们替换为挂钩函数的指针。
与前一种方法不同,此方法不需要在客户端获取到目标对象指针之前设置挂钩。只要可以访问对象指针的任何地方,都可以设置挂钩。
这是设置 COM 方法挂钩的 HookMethod
函数代码:
HRESULT HookMethod(IUnknown* original, PVOID proxyMethod,
PVOID* originalMethod, DWORD vtableOffset)
{
PVOID* originalVtable = *(PVOID**)original;
if (originalVtable[vtableOffset] == proxyMethod)
return S_OK;
*originalMethod = originalVtable[vtableOffset];
originalVtable[vtableOffset] = proxyMethod;
return S_OK;
}
使用 InstallComInterfaceHooks
函数来为 ISampleObject
接口方法设置挂钩。
HRESULT InstallComInterfaceHooks(IUnknown* originalInterface)
{
// Only single instance of a target object is supported in the sample
if (g_Context.get())
return E_FAIL;
ATL::CComPtr<ISampleObject> so;
HRESULT hr = originalInterface->QueryInterface(IID_ISampleObject, (void**)&so);
if (FAILED(hr))
return hr; // we need this interface to be present
// remove protection from the vtable
DWORD dwOld = 0;
if(!::VirtualProtect(*(PVOID**)(originalInterface),
sizeof(LONG_PTR), PAGE_EXECUTE_READWRITE, &dwOld))
return E_FAIL;
// hook interface methods
g_Context.reset(new Context);
HookMethod(so, (PVOID)Hook::QueryInterface, &g_Context->m_OriginalQueryInterface, 0);
HookMethod(so, (PVOID)Hook::get_ObjectName, &g_Context->m_OriginalGetObjectName, 7);
HookMethod(so, (PVOID)Hook::DoWork, &g_Context->m_OriginalDoWork, 9);
return S_OK;
}
虚拟方法表可能位于写保护区域,因此在设置挂钩之前,我们必须使用 VirtualProtect
移除保护。
变量 g_Context
是一个结构,其中包含与目标对象相关的数据。为了简化示例,我将 g_Context
设置为全局变量,尽管它仅支持同时存在一个目标对象。
这是挂钩函数代码:
typedef HRESULT (WINAPI *QueryInterface_T)
(IUnknown* This, REFIID riid, void **ppvObject);
STDMETHODIMP Hook::QueryInterface(IUnknown* This, REFIID riid, void **ppvObject)
{
QueryInterface_T qi = (QueryInterface_T)g_Context->m_OriginalQueryInterface;
HRESULT hr = qi(This, riid, ppvObject);
return hr;
}
STDMETHODIMP Hook::get_ObjectName(IUnknown* This, BSTR* pVal)
{
return g_Context->m_Name.CopyTo(pVal);
}
STDMETHODIMP Hook::DoWork(IUnknown* This, LONG arg1, LONG arg2, LONG* result)
{
*result = 42;
return S_OK;
}
查看挂钩函数定义。它们的原型与目标接口方法原型完全相同,只是它们是自由函数(而非类方法),并且多一个参数 - this
指针。这是因为 COM 方法通常声明为 stdcall
,this
作为隐式堆栈参数传递。
使用此方法时,您需要注意几点。首先,当您设置一个方法挂钩时,它不仅对当前 COM 对象实例有效。它对同一类的所有对象都有效(但不是对实现被挂钩接口的所有类)。如果有几个类实现了同一个接口,并且您想拦截该接口所有实例的调用,那么您需要修补所有这些类的 vtables
。
如果您需要存储特定于每个对象的数据,您必须在静态内存区域中存储一个可按目标对象指针值访问的上下文集合。您还必须注意目标对象的生命周期。如果您期望目标对象的多线程访问,则必须为静态集合提供同步。
如果您需要从挂钩函数调用目标对象的某个方法,您必须小心。您不能仅仅通过接口指针调用已挂钩的方法,因为它会访问 vtable
并调用挂钩函数(这不是您想要的)。因此,您必须保存指向原始方法的指针,并直接使用它来调用该方法。
这是另一个棘手的问题。设置挂钩时,请务必小心,不要重复挂钩同一个方法。如果您保存了指向原始方法的指针,在第二次尝试挂钩时它将被覆盖。
好消息是,在这种方法中,您不必为您不需要拦截的方法实现挂钩。也不需要拦截对象的创建。
要查看此示例部分如何工作,请像以前一样在 AppInit_DLLs
注册表值中指定 ComInterceptVtablePatch.dll 的完整名称,然后运行客户端。
结论
本文介绍的两种方法都有优缺点。代理对象方法更容易实现,特别是当您需要在代理中实现复杂的逻辑时。但是,您必须在客户端获取到原始对象指针之前用代理替换目标对象,这在某些情况下可能很困难,甚至是不可能的。此外,即使您只需要拦截少数几个方法,也必须在代理中实现与目标对象相同的接口。而实际的 COM 接口可能非常大。如果目标对象有多个接口,您很可能需要全部实现它们。
vtable
补丁方法需要更仔细的实现,并要求开发人员记住很多事情。它需要一些额外的代码来处理同一接口的多个目标对象实例或调用目标的某些方法。但是,它不需要直接在目标创建后设置挂钩,挂钩可以在任何时候设置。它还允许仅为您实际需要拦截的方法实现挂钩。
哪个方法在特定时刻更方便,通常取决于具体情况。
参考文献
- MSDN COM 文档
- Don Box. Essential COM
- Galen C. Hunt, Michael L. Scott. Intercepting and Instrumenting COM Applications
- Sergey Podobry. Easy way to set up global API hooks (简便方法设置全局 API 挂钩)
- Mhook, an API hooking library, v 2.2 (Mhook,一个 API 挂钩库,v 2.2)
- Working with the AppInit_DLLs registry value (使用 AppInit_DLLs 注册表值)
历史
- 2011 年 2 月 2 日:初始发布