自 20 世纪 90 年代初以来,组件对象模型 (COM) 始终是 Microsoft Windows 开发的基石,至今仍在现代 Windows 操作系统和应用程序中广泛存在。对 COM 组件的依赖以及多年来广泛的功能开发,形成了可观的攻击面。2025 年 2 月,Google Project Zero 团队的 James Forshaw (@tiraniddo) 发布博客文章,详细阐述了一种滥用分布式 COM (DCOM) 远程技术的新方法——可利用受困 COM 对象在服务器端 DCOM 进程上下文中执行 .NET 托管代码。Forshaw 重点列举了该技术在权限提升和受保护进程轻型 (PPL) 绕过方面的多种用例。
基于 Forshaw 的研究,Mohamed Fakroud (@T3nb3w) 于 2025 年 3 月初发布了利用该技术绕过 PPL 防护的实现方案。我和 Jimmy Bayne (@bohops) 在 2025 年 2 月进行了类似研究,由此开发出通过滥用受困 COM 对象实现的概念验证无文件横向移动技术。
COM 是一种二进制接口标准与中间件服务层,允许将独立的模块化组件暴露给彼此及应用程序进行交互,且不受底层编程语言限制。例如,使用 C++ 开发的 COM 对象可轻松与 .NET 应用程序对接,使开发者能有效集成多样化软件模块。DCOM 是一种远程技术,使 COM 客户端能够通过进程间通信 (IPC) 或远程过程调用 (RPC) 与 COM 服务器通信。许多 Windows 服务都实现了可本地或远程访问的 DCOM 组件。
COM 类通常注册并保存在 Windows 注册表中。客户端程序通过创建 COM 类的实例(即 COM 对象)与 COM 服务器交互。该对象提供指向标准化接口的指针。客户端利用该指针访问对象的方法与属性,从而实现客户端与服务器间的通信和功能交互。
COM 对象常作为评估漏洞暴露面和发掘可利用功能的研究目标。受困 COM 对象属于一类缺陷:当 COM 客户端在进程外 DCOM 服务器中实例化 COM 类时,客户端通过按引用封送的对象指针控制该 COM 对象。根据具体条件,这种控制向量可能引发与安全相关的逻辑缺陷。
Forshaw 的博客描述了一种 PPL 绕过用例:通过操控 WaaSRemediation COM 类暴露的 IDispatch 接口,实现受困 COM 对象滥用与 .NET 代码执行。WaaSRemediation 在 WaaSMedicSvc 服务中实现,该服务以 NT AUTHORITY\SYSTEM 身份在受保护的 svchost.exe 进程上下文中运行。Forshaw 详尽的演练为我们开展应用研究及开发概念验证无文件横向移动技术构建了基础。
我们的研究始于探索支持 IDispatch 接口的 WaaSRemediation COM 类。该接口允许客户端执行后期绑定。通常情况下,COM 客户端在编译时已定义其所用对象的接口和类型定义。而后期绑定则允许客户端在运行时动态发现并调用对象方法。IDispatch 接口包含 GetTypeInfo 方法,该方法返回 ITypeInfo 接口。ITypeInfo 提供的方法可用于发现实现该接口对象的类型信息。
若 COM 类使用类型库,客户端可通过 ITypeLib (从 ITypeInfo-> GetContainingTypeLib 获取)查询类型库以检索类型信息。此外,类型库也可能引用其他类型库以获取补充类型信息。
根据 Forshaw 的博客文章,WaaSRemediation 引用了类型库 WaaSRemediationLib, 而该库又引用了 stdole(OLE 自动化)。WaaSRemediationLib 使用了该库中的两个 COM 类:StdFont 和 StdPicture。通过修改 StdFont 对象的 TreatAs 注册表键实施 COM 劫持,可将其指向我们指定的另一个 COM 类,例如 .NET 框架中的 System.Object。值得注意的是,Forshaw 指出 StdPicture 不可行,因为该对象会检查进程外实例化,因此我们将注意力集中在使用 StdFont 上。
.NET 对象之所以引起我们关注,是因为 System.Object 的 GetType 方法。通过 GetType, 我们可以执行 .NET 反射,最终访问 Assembly.Load。虽然选择了 System.Object,但该类恰巧是 .NET 类型层次结构的根类。因此任何 .NET COM 对象都可被利用。
在完成初始阶段设置后,还需要修改 HKLM\Software\Microsoft\.NetFramework 项下的两个 DWORD 值,才能使预想的使用场景成为现实:
在初步测试中确认能够加载最新版本的 CLR 和 .NET 后,我们确信研究方向正确。
将关注点转向远程可编程层面后,我们首先利用远程注册表操作目标机器上的 .NetFramework 注册表键值,并劫持了 StdFont 对象。接着,我们使用 Cocreateinstance 替换 CocreateinstanceEx,在远程目标上实例化 WaaSRemediation COM 对象,并获取指向 IDispatch 接口的指针。
通过 IDispatch 指针调用 GetTypeInfo成员方法后,我们获得了受困在服务器中的 ITypeInfo 接口指针。此后调用的成员方法均在服务器端执行。在识别出目标类型库引用 (stdole) 并推导出后续目标类对象引用 (StdFont) 后,我们最终利用 ITypeInfo 接口上的“可远程操作” CreateInstance 方法,将 StdFont 对象链接流(通过预先的 TreatAs 操控)重定向以实例化 System.Object。
由于 AllowDCOMReflection 已正确设置,我们便能通过 DCOM 执行 .NET 反射来访问 Assembly.Load,从而将 .NET 程序集加载至 COM 服务器。由于通过 DCOM 调用 Assembly.Load,这种横向移动技术完全无文件化,因为程序集字节传输由 DCOM 远程处理的“魔法”机制实现。关于从对象实例化到反射的完整技术流程详解,请参见下图:
我们遇到的首个主要问题是通过 IDispatch->Invoke 调用 Assembly.Load_3。Invoke 向目标函数传递参数对象数组,而 Load_3 是接收单个字节数组的 Assembly.Load 重载版本。因此,我们需要将字节 SAFEARRAY 包装在另一个 VARIANT 类型 SAFEARRAY 中——最初我们始终尝试直接传递单个字节 SAFEARRAY。
另一难题在于定位正确的 Assembly.Load 重载方法。我们从 Forshaw 的 CVE-2014-0257 代码中提取了辅助函数,其中包括 GetStaticMethod 函数。该函数通过 DCOM 的 .NET 反射机制,根据类型指针、方法名及其参数数量查找静态方法。Assembly.Load 有两个接收单参数的重载版本。最终我们采用了一种取巧方案——发现第三个单参数 Load 实例正是所需版本。
我们观察到此技术最大的缺陷在于:生成的信标存活周期受限于 COM 客户端——在本案例中即取决于我们武器化二进制程序“ForsHops.exe”(这个名字当然取得很优雅)的应用程序生命周期。因此,若 ForsHops.exe 清理其 COM 引用或退出,在远程机器 svchost.exe 下运行的信标也会随之终止。我们尝试了多种方案,例如使 .NET 程序集的主线程无限挂起其主线程、在另一个线程中执行 shellcode,或让 ForsHops.exe 保持利用线程挂起,但均未实现优雅效果。
在当前状态下,ForsHops.exe 会持续运行直至信标退出,此时程序将清除其注册表操作。虽存在改进空间,但我们将其留作读者自行探索的练习。
在 Mohamed Fakroud 发布其实现方案后,Samir Bousseaden (@SBousseaden) 提出的检测指南同样适用于此横向移动技术:
此外,我们建议实施以下额外控制措施:
同时,可利用以下概念验证 YARA 规则检测标准的 ForsHops.exe 可执行文件:
rule Detect_Standard_ForsHops_PE_By_Hash
我们的实现方案在 Forshaw 博客所述 COM 滥用技术基础上稍作扩展,利用受困 COM 对象实现横向移动,而非仅用于本地 PPL 绕过。因此,该技术仍然容易受到与本地执行实现方案相同的检测手段影响。
您可在此处获取 ForsHops.exe 概念验证横向移动代码。
特别感谢 Dwight Hohnstein (@djhohnstein) 和 Sanjiv Kawa (@sanjivkawa) 对本研究提出的反馈及博文内容审阅。