安全路透社
当前位置:安全路透社 > 安全客 > 正文

【技术分享】见招拆招:详谈COM接口函数Hook技术


前言


在日常的工作中,使用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函数将调用失败,即使从高权限的进程中调用也是无用的。原因是HKCR\CLSID {4991d34b-80a1-4291-83b6-3328366b9097}下的这个注册表键值由TrustedInstaller拥有,它不允许被篡改,甚至不允许系统帐户去修改这个键值! 然而,超级管理员用户可以通过执行Take Ownership特权,成为该注册表键值的新拥有者,因此我们还是有权更改该键值权限的,如下图所示,我们现在已经拥有对该键值的修改权限: 

http://p5.qhimg.com/t013b283ed3d5ca0639.png

那么,我们在哪里调用CoTreatAsClass函数比较合适呢?经过一些实验我们发现该调用可以在一些安装程序中完成,安装程序可以是一个简单的批处理或PowerShell脚本,并且可以直接使用注册表函数进行正确的设置,具体如下图所示:

http://p8.qhimg.com/t01762654b4d894f693.png

下面是一个使用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操作是一个有趣的技术,因此还需要进一步研究和分析。


原文链接:http://blogs.microsoft.co.il/pavely/2017/08/07/hooking-com-classes/

未经允许不得转载:安全路透社 » 【技术分享】见招拆招:详谈COM接口函数Hook技术

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册