58、COM组件逆向:COM接口、IUnknown、IDispatch、COM劫持

COM组件,说实话,是Windows系统里最绕不开的一块硬骨头。很多安全研究员一听到COM就头疼,觉得太复杂。我刚开始接触的时候也是这个感觉——满屏的GUID、接口指针、引用计数,看得人眼花缭乱。

但说白了,COM就是个组件通信的规范。你写了一个DLL,我想用你的功能,咱们怎么约定?COM就是这套约定。搞逆向的人如果不懂COM,很多系统组件、Office插件、IE扩展都没法分析。

今天咱们就聊聊COM逆向的几个核心点:接口结构、IUnknown和IDispatch的底层原理,以及怎么利用COM劫持做点有意思的事。

COM接口的内存布局

COM接口在内存里其实就是一个虚函数表指针(vtable pointer)。每个COM对象开头都指向一个vtable,vtable里按顺序排列着接口的所有方法地址。

我举个例子,IUnknown接口有三个方法:QueryInterface、AddRef、Release。它们在vtable里的偏移分别是0、4、8(32位系统)。

// IUnknown vtable 布局(32位)
偏移 0: QueryInterface
偏移 4: AddRef
偏移 8: Release

你想想看,如果我们拿到一个COM对象的指针,直接读前4个字节,那就是vtable地址。再根据偏移调用对应方法,这就是COM逆向的基本操作。

实战技巧: 我个人习惯用WinDbg的dt命令查看COM对象布局。比如dt ole32!CStdMarshal就能看到标准marshal对象的内部结构。这在分析RPC调用时特别有用。

IUnknown:COM的基石

每个COM组件都必须实现IUnknown。为什么?因为COM组件是动态加载的,谁也不知道对方什么时候用完。所以需要引用计数来管理生命周期。

AddRef增加引用计数,Release减少。当计数归零时,组件自己销毁自己。我在逆向一个老旧播放器时遇到过内存泄漏,就是某个插件没调Release,导致播放器退出后进程还在后台挂着。

QueryInterface更关键。它用来查询组件是否支持某个接口。传入一个IID(接口GUID),返回对应的接口指针。逆向时,我们经常hook QueryInterface来监控组件创建了哪些接口。

// 典型的QueryInterface实现
HRESULT QueryInterface(REFIID riid, void **ppvObject) {
    if (riid == IID_IUnknown) {
        *ppvObject = static_cast<IUnknown*>(this);
        AddRef();
        return S_OK;
    }
    if (riid == IID_IMyInterface) {
        *ppvObject = static_cast<IMyInterface*>(this);
        AddRef();
        return S_OK;
    }
    *ppvObject = NULL;
    return E_NOINTERFACE;
}
注意: 逆向时如果看到QueryInterface返回E_NOINTERFACE,不代表组件有问题。可能是你传入的IID不对,或者组件确实不支持这个接口。我曾经花了一下午排查一个COM调用失败的问题,最后发现是IID写错了一个字节。

IDispatch:自动化接口的秘密

IDispatch是IUnknown的扩展,主要用于脚本语言调用COM组件。比如VBScript、JavaScript调用COM对象,走的就是IDispatch。

IDispatch多了四个方法:GetTypeInfoCount、GetTypeInfo、GetIDsOfNames、Invoke。其中最关键的是GetIDsOfNames和Invoke。

GetIDsOfNames把方法名或属性名转成DISPID(一个整数ID)。Invoke根据DISPID调用对应方法。说白了,这就是个名字到方法的映射表。

逆向IDispatch组件时,我一般先hook GetIDsOfNames,看看脚本调用了哪些方法和属性。然后hook Invoke,监控参数和返回值。这样就能搞清楚脚本和COM组件之间的交互逻辑。

// IDispatch的Invoke调用流程
HRESULT Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, 
               WORD wFlags, DISPPARAMS *pDispParams, 
               VARIANT *pVarResult, EXCEPINFO *pExcepInfo, 
               UINT *puArgErr) {
    switch(dispIdMember) {
        case 0x60010001: // 某个方法的DISPID
            // 处理调用
            break;
        case 0x60010002:
            // 处理属性获取
            break;
    }
}
调试技巧: 用OleView工具可以查看注册表中所有COM组件的接口信息。我经常用它来快速定位某个CLSID对应的组件路径和接口列表。比手动翻注册表快多了。

COM劫持的原理与实战

COM劫持,说白了就是让系统加载我们自己的DLL,而不是原始的COM组件。怎么做?修改注册表里的CLSID路径。

每个COM组件在注册表里都有个InprocServer32键,指向DLL路径。我们把这个路径改成自己的DLL,系统就会加载我们的DLL。我们的DLL再转发调用到原始DLL,实现中间人攻击。

我曾经在渗透测试中用过这招。目标系统有个老旧的COM组件,每次启动时加载。我把它的注册表路径改成了我的劫持DLL,成功注入了代码。而且因为COM组件是系统信任的,杀软基本不会拦截。

// 劫持DLL的DllGetClassObject实现
HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv) {
    // 1. 加载原始DLL
    HMODULE hOriginal = LoadLibrary(L"original.dll");
    // 2. 获取原始DllGetClassObject
    DllGetClassObjectProc pOriginal = 
        (DllGetClassObjectProc)GetProcAddress(hOriginal, "DllGetClassObject");
    // 3. 调用原始函数
    HRESULT hr = pOriginal(rclsid, riid, ppv);
    // 4. 包装返回的接口指针(可选)
    if (SUCCEEDED(hr)) {
        *ppv = new CMyClassFactory((IUnknown*)*ppv);
    }
    return hr;
}
避坑指南: 我曾经在劫持一个64位COM组件时,忘了处理32位和64位注册表的区别。Windows有注册表重定向,32位组件的路径在Wow6432Node下。如果搞混了,劫持根本不会生效。嗯,这个坑我踩过,你们别踩了。

COM逆向的常用工具

工具 用途 我的评价
OleView 浏览注册表COM组件信息 必备,快速定位CLSID和接口
WinDbg 调试COM对象内存布局 查看vtable和接口调用最方便
Process Monitor 监控注册表和文件访问 排查COM加载失败的神器
API Monitor Hook COM接口调用 可以监控QueryInterface和Invoke

COM劫持的检测与防御

作为安全研究员,我们不光要会攻击,还得知道怎么防。COM劫持的检测其实不难:定期检查注册表中CLSID的InprocServer32路径是否被篡改。

我建议用PowerShell脚本定期扫描关键COM组件的注册表路径,计算DLL文件的哈希值,和原始值对比。如果发现不一致,基本就是被劫持了。

# 检测COM劫持的PowerShell脚本片段
$clsid = "{00024500-0000-0000-C000-000000000046}"  # 某个关键CLSID
$path = "HKLM:\SOFTWARE\Classes\CLSID\$clsid\InprocServer32"
$dllPath = (Get-ItemProperty -Path $path)."(default)"
$hash = (Get-FileHash -Path $dllPath -Algorithm SHA256).Hash
# 对比原始哈希值
if ($hash -ne $originalHash) {
    Write-Warning "COM组件可能被劫持!"
}
防御建议: 对于高安全环境,可以启用Windows的AppLocker策略,限制只有签名的DLL才能加载。这样即使注册表被改,未签名的劫持DLL也无法运行。

总结

COM逆向的核心就三件事:理解vtable布局、掌握IUnknown和IDispatch的调用机制、学会注册表劫持。这三样搞明白了,大部分COM相关的逆向工作都能应付。

我个人觉得,COM劫持是最实用的技术之一。它不需要漏洞,不需要提权,只要你有注册表写入权限就能实现持久化。当然,这也意味着防御方必须重视注册表的完整性监控。

嗯,今天就聊到这儿。COM组件逆向的门道其实还有很多,比如跨进程COM的marshal/unmarshal机制、COM+的事件系统等等。这些咱们后面有机会再细说。

COM组件逆向知识体系 COM接口 IUnknown IDispatch IClassFactory 核心方法 QueryInterface AddRef / Release GetIDsOfNames / Invoke COM劫持 注册表路径篡改 DLL转发劫持 接口包装与监控 核心思路 理解vtable布局 → 掌握IUnknown/IDispatch → 实施COM劫持

公众号:蓝海资料掘金营,微信deep3321