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逆向的基本操作。
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;
}
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;
}
}
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;
}
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组件可能被劫持!"
}
总结
COM逆向的核心就三件事:理解vtable布局、掌握IUnknown和IDispatch的调用机制、学会注册表劫持。这三样搞明白了,大部分COM相关的逆向工作都能应付。
我个人觉得,COM劫持是最实用的技术之一。它不需要漏洞,不需要提权,只要你有注册表写入权限就能实现持久化。当然,这也意味着防御方必须重视注册表的完整性监控。
嗯,今天就聊到这儿。COM组件逆向的门道其实还有很多,比如跨进程COM的marshal/unmarshal机制、COM+的事件系统等等。这些咱们后面有机会再细说。