译者:blueSky
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
在日常的工作中,使用hook操作有时可以大大提高我们的工作效率。其中,典型的例子有:为了便于对程序进行调试,我们可以对Windows API函数执行hook操作或者为了进行恶意软件检测,我们也可以对Windows API函数执行hook操作。在这些场景中,往往是一些DLL被注入到目标进程中,然后对相关的功能函数执行hook操作,有几种方法可以做到这一点,但这并不是本文要讨论的重点,感兴趣的读者可以在网上搜索与DLL相关的技术文章。
在组件对象模型(COM)的世界中,想要对COM组件接口函数执行hook操作并不是一件容易的事情。原因是因为COM组件是基于对象的,因此通常情况下我们是不可能获取COM组件接口函数地址的。并且,由于COM组件接口函数不直接导出,因此通过调用GetProcAddress或类似的方法也是无法找到这些函数地址的。此外,即使COM组件中的某些函数的地址被找到,执行hook操作也需要将一些代码注入到目标进程中,这种操作在某些情况下(例如受保护的进程)几乎是不可能实现的。
但经过我们的研究发现,COM组件提供了另一种“hooking”操作或者相当于重定向的机制,此机制能够将一个CLSID重定向到另一个CLSID,MSDN文档中把这种机制称之为“仿真”功能,就像一个类可以模拟另一个类的功能似的。这种机制在一定程度上打开了将一个类重定向到另一个类的可能性,而不需要注入代码到目标进程中或者对某些函数执行hook操作。
Hooking COM接口函数实例
下面让我们一起来看一个具体的例子。Windows中的后台智能传输服务(BITS)提供了异步下载/上传服务,该服务具有进度通知,网络自动恢复等功能。恶意软件可以通过BITS服务来下载其有效载荷,而不需要其直接下载有效载荷,直接下载会使恶意软件更容易暴露在反恶意软件检测工具中。通过BITS下载使得恶意软件与下载的有效载荷没有什么直接的关联,进一步加大了对恶意软件的检测难度。
由于BITS是基于COM组件来实现的,因此可以使用上述所说的“仿真”机制对任何需要获得BITS操作的函数执行hook操作,以获取BITS模块的执行结果。实现这个想法的关键是COM组件中的CoTreatAsClass 这个API函数,该函数将一个TreatAs键添加到原始键中,并将其值指向备用的CLSID。尽管如此,可能我们还是没有办法来对某些BITS操作执行hook操作,原因是由于任何COM激活请求(例如通过CoCreateInstance)都将重定向到TreatAs CLSID,因此我们没有办法只重定向某些特定的请求。
因此,对哪些CLSID应该执行hook操作是我们应该要好好考虑和研究的问题。在BITS的情况下,我们需要操作的对象是BackgroundCopyManager,它是使用BITS操作时肯定会创建的对象。或许你们会认为一个更好的CLSID目标对象是由BITS创建的实际Job对象,但是这样做将起不到任何的作用,因为没有这样的CLSID对象。像许多基于COM组件的API一样,只有几个对象实际上可以使用COM公共激活API创建并具有CLSIDs。通过函数间接创建的一些对象,是不需要通过COM激活机制的。
以下是创建“替代类”以拦截BITS请求所需的代码:
HRESULT hr = ::CoTreatAsClass(
__uuidof(BackgroundCopyManager),
__uuidof(FakeBitsManager));
上述代码中的FakeBitsManager是我创建的一个COM类,该类需要实现与原始对象(IBackgroundCopyManager)相同的接口;否则,机器上的所有BITS操作都将失败!由于访问权限的原因,上述CoTreatAsClass函数将调用失败,即使从高权限的进程中调用也是无用的。原因是HKCRCLSID {4991d34b-80a1-4291-83b6-3328366b9097}下的这个注册表键值由TrustedInstaller拥有,它不允许被篡改,甚至不允许系统帐户去修改这个键值! 然而,超级管理员用户可以通过执行Take Ownership特权,成为该注册表键值的新拥有者,因此我们还是有权更改该键值权限的,如下图所示,我们现在已经拥有对该键值的修改权限:
那么,我们在哪里调用CoTreatAsClass函数比较合适呢?经过一些实验我们发现该调用可以在一些安装程序中完成,安装程序可以是一个简单的批处理或PowerShell脚本,并且可以直接使用注册表函数进行正确的设置,具体如下图所示:
下面是一个使用ATL实现的COM类,通过实现IBackgroundCopyManager以及IUnknown以用来对BITS管理器类执行拦截操作,代码如下所示:
class ATL_NO_VTABLE CFakeBitsManager :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CFakeBitsManager, &CLSID_FakeBitsManager>,
public IBackgroundCopyManager
{
//...
BEGIN_COM_MAP(CFakeBitsManager)
COM_INTERFACE_ENTRY(IBackgroundCopyManager)
END_COM_MAP()
HRESULT FinalConstruct();
CComPtr<IBackgroundCopyManager> m_spRealBits;
public:
STDMETHOD(CreateJob)(
LPCWSTR DisplayName,
BG_JOB_TYPE Type,
__RPC__out GUID *pJobId,
IBackgroundCopyJob **ppJob);
STDMETHOD(GetJob)(
REFGUID jobID,
IBackgroundCopyJob **ppJob);
STDMETHOD(EnumJobs)(
DWORD dwFlags,
IEnumBackgroundCopyJobs **ppEnum);
STDMETHOD(GetErrorDescription)(
HRESULT hResult,
DWORD LanguageId,
LPWSTR *pErrorDescription);
};
OBJECT_ENTRY_AUTO(__uuidof(FakeBitsManager), CFakeBitsManager)
上述代码只是IBackgroundCopyManager类实现中的部分代码,需要我们重点关注的是IBackgroundCopyManager接口映射函数的声明以及从<bits.h>头文件复制的四个方法。另一个需要我们重点关注的是IBackgroundCopyManager(m_spRealBits)智能指针的另一个实现,这个成员应该会被初始化为真正的BITS管理器,由于我们不想让所有的BITS操作都失败,因此我们可以将请求转发给它。但是如何创建真正的BITS管理器呢?其实答案很简单,我们只需要关闭TreatAs,以便快速调用创建真正的BITS,然后重新启动,代码如下所示:
HRESULT CFakeBitsManager::FinalConstruct() {
auto hr = ::CoTreatAsClass(__uuidof(BackgroundCopyManager), CLSID_NULL);
ATLASSERT(hr);
hr = m_spRealBits.CoCreateInstance(__uuidof(BackgroundCopyManager));
ATLASSERT(SUCCEEDED(hr));
hr = ::CoTreatAsClass(__uuidof(BackgroundCopyManager), __uuidof(FakeBitsManager));
ATLASSERT(SUCCEEDED(hr));
return hr;
}
使用CLSID_NULL的第二个参数调用CoTreatAsClass会关闭“仿真”,机器上的其他客户端程序会有很小的机会可以获得“真正的”BITS。但是这个时间间隔很小,并没有一种简单的方法可以获取该BITS(基于注册表的回调和ETW事件会捕获它)。但即使这样,用户模式下的注册表回调可能还不够快,因此它可能在恢复之前注意不到该更改。当然,高权限的进程可以手动还原它,但这不是一个典型的客户端程序应该要考虑的事情,因为它可能没有足够的权限去这样做。
现在我们可以继续实现这些函数方法。任何“不友善”的函数方法都可以被简单地重定向到真正的BITS管理器。示例代码如下所示:
STDMETHODIMP CFakeBitsManager::GetJob(
REFGUID jobID,
IBackgroundCopyJob **ppJob) {
return m_spRealBits->GetJob(jobID, ppJob);
}
STDMETHODIMP CFakeBitsManager::EnumJobs(
DWORD dwFlags,
IEnumBackgroundCopyJobs **ppEnum) {
return m_spRealBits->EnumJobs(dwFlags, ppEnum);
}
其中,比较有趣的函数是CreateJob函数,因为我们可以使用该函数收集信息,注册事件通知或其他信息。示例代码如下所示:
STDMETHODIMP CFakeBitsManager::CreateJob(
LPCWSTR DisplayName, BG_JOB_TYPE Type,
GUID *pJobId, IBackgroundCopyJob **ppJob) {
if(Type != BG_JOB_TYPE_DOWNLOAD)
return m_spRealBits->CreateJob(DisplayName, Type, pJobId, ppJob);
auto hr = m_spRealBits->CreateJob(DisplayName, Type, pJobId, ppJob);
if (FAILED(hr))
return hr;
// handle the successful job creation
// register for notifications, log the download, ...
return hr;
}
该示例显示,对于非下载作业任务,我们只需将请求传递给真正的BITS管理器处理即可。在下载作业任务创建时,我们可以对该下载请求执行任何的监视操作,这样我们就完成了对COM接口函数的Hooking操作。
总结
理论上讲,对COM接口函数执行hook操作要比对Win32 API执行hook操作可能会简单一些。因为hook Win32 API还需要我们去懂得一些汇编语言相关的知识,以及机器码等底层知识。它们的共同之处都是修改目标程序的执行流程,以达到对数据流进行监控的目的。对COM接口执行hook操作是一个有趣的技术,因此还需要进一步研究和分析。