COM 组件对象模型
COM (Component Object Model,即组件对象模型)它是微软提出的一套开发软件的方法与规范。它也代表了一种软件开发思想,那就是面向组件编程的思想
COM 是一个平台无关、分布式、面向对象的系统 , 用于创建可交互的二进制软件组件。COM 是微软 OLE(对象链接与嵌入)(复合文档)和 ActiveX(启用 Internet 的组件)技术的基础技术,COM 旨在促进软件互操作性;也就是说,即使两个或多个应用程序或“组件”是由不同的供应商在不同的时间用不同的编程语言编写的,或者运行在不同的机器和不同的操作系统上,它们也能轻松地相互交互
面向组件编程 (COP):
模块分隔。这里的分隔有两层含义,第一就是要将应用程序按功能划分成多个模块;第二就是每一个模块要有相当程度的独立性
COM 最初是为了使 Microsoft Office 应用程序能够在文档之间进行通信和交换数据而创建的,例如将 Excel 图表嵌入到 Word 文档或 PowerPoint 演示文稿中 <—这种功能称为 OLE
COM 组件组成
对象与接口
一个COM组件就是一个dll文件或者一个exe文件,一个组件可以包含多个COM对象,并且每个COM对象可以实现多个接口。
COM对象是通过接口来进行进程间通信的。

在进程通信中,COM规范采用的是客户/服务器模型。
核心原则:客户端永远无法直接访问 COM 对象的全部内容 。相反,客户端总是通过明确定义的契约 (即对象支持的接口)来访问对象。
接口:本质上是一个抽象类,只包含纯虚函数,没有数据成员。
抽象类是指只包含纯虚函数声明(“= 0”表示纯虚函数)而没有数据成员的类:
class IClassA |
抽象类是不能用于创建对象的类;但是,它可以被继承
只能通过指向虚函数表的指针来访问接口类,该虚函数表公开了接口中的方法 。 接口本身并不存在,它通常由一个继承类来实现。这种实现了接口公开方法的类通常被称为共类(派生类)。以下是一个共类的示例:
class ClassB: public IClassA |
这里, IClassA 是基类,由于它包含一个纯虚函数 ( func1 ), 因此它成为了一个抽象类。ClassB 派生自父类 IClassA。func1 定义在派生类中
我们可以使用全局方法来创建 co-class 的实例,也可以使用静态方法。使用创建 co-class 实例并返回指向其接口的指针的方法,这种技术通常被称为类分解
IClassA* CreateInstance() |
一般而言,服务器为COM组件,调用方为客户端。
COM 类用一个128位全局唯一标识符(GUID)来标识,称之为CLISID。 (class id) 该 ID 将类与文件系统中的特定部署关联起来,在 Windows 系统中,该部署是 DLL 或 EXE 文件
还可以使用程序标识符( ProgID )来标识 COM 类 ,它是一个可以与 CLSID 关联的注册表项。与 CLSID 类似,ProgID 也用于标识一个类(COM组件名称),但精度较低,因为它不能保证全局唯一。
COM接口也是用一个128位的GUID来标识,称为接口标识符 ( IID ) 。接口是强类型的。每个接口都有其唯一的IID,
客户通过CLISID来创建COM对象,得到一个指向对象某个接口的指针之后(在此处可以得到该对象的所有接口指针),然后再调用该指针,就可以调用该接口提供的所有服务。
COM 客户端仅与指向接口的指针进行交互: 当客户端获得对某个对象的访问权限时,它实际上只拥有一个指针,通过该指针可以访问接口中的函数。该指针是不透明的,这意味着它隐藏了所有内部实现细节。在 COM 中,客户端只能调用它所拥有指针指向的接口的函数。
客户可以同时拥有两个相同CLISID的COM对象。
为了通过多种不同的视图(例如按 CLSID、ProgID、服务器可执行文件)枚举 COM 对象,枚举对象的接口,然后创建实例并调用方法,可以使用多个工具:
-

Windows SDK 自带工具 oleview.exe (例如:C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\oleview.exe)

-

IUnknown接口
所有的COM接口都要继承IUnknown接口,所有的COM对象都需要实现IUnknown接口。IUnknown 是所有其他接口继承的基础接口
IUnknown接口提供了非常重要的两个特性:生存期控制和接口查询
客户端程序虽然只能通过接口与COM对象进行通信,但是也能控制COM对象的存在与否
IUnknown接口的定义:
class IUnknown |
QueryInterface用于查询接口,AddRef用于增加引用计数,Release用于减少引用计数
按照惯例,指向接口函数表的指针称为 pVtbl 指针。该表本身通常用 vtbl 表示,即“虚函数表”
查询接口: 一个COM对象可以绑定多个接口,QueryInterface函数返回客户端指定的接口类型指针
IClassFactory
COM 组件采用的工厂设计模式
我们不直接使用 COM 类来创建 COM 对象类型的实例。反而,使用称为类对象的中间对象来创建 COM 对象的实例。
类对象是一种特殊的 COM 对象,它知道如何创建特定类型的 COM 对象。类对象和 COM 对象之间存在一一对应的关系。
每种类型的类对象只知道如何创建一种类型的 COM 对象。例如,如果在 COM 服务器中实现了两个 COM 对象,分别称为 O1 和 O2 ,则也必须实现两个类对象,一个知道如何创建 O1 ,另一个知道如何创建 O2。
类对象可以被视为“创建者”对象。它的唯一目标是创建 COM 对象的实例。
客户端通过向 COM 子系统请求能够创建所需 COM 对象的特定类对象,来获取指向该类对象接口的指针。利用此接口指针,客户端可以指示该类对象创建一个或多个与其关联的 COM 对象的实例。
COM 规范定义了一个名为 IClassFactory 的 COM 对象创建接口:
class IClassFactory:public IUnkown |
实现了 IClassFactory 作为对象创建接口的类对象称为类工厂(ClassFactory)
IDispatch 接口
IDispatch 接口 继承IUnknown接口 , IDispatch 是 OLE Automation 的核心接口,用于“晚绑定(Late Binding) ”
晚绑定:运行时根据字符串名字来调用对象方法,而不是编译时通过 vtable 固定调用。
这便于 脚本语言(如 VBScript、JavaScript、Python COM 模块)中动态调用
所以此接口不是COM组件必须的实现的接口,而是方便其他脚本语言调用而需要实现的接口
class IDispatch : public IUnknown |
这些四个方法构成 OLE Automation 的反射和调用体系
实现 IDispatch 意味着组件必须:
- 提供类型信息(ITypeInfo)
- 提供名称到 DISPID 的映射表
- 封装参数的打包/解包(VARIANT)
系统级接口(DirectX、Shell、Audio、Networking 等)多数不实现 IDispatch,只提供 C++ 风格接口。
COM对象创建
COM库
COM库:使用 COM 的任何进程都必须初始化和取消初始化 COM 库,
COM 库在 Microsoft Windows 中作为一组 DLL 和 EXE(主要是 Ole32.dll 和 Rpcss.exe)提供,其中包括:
- 基本功能,有助于创建 COM 应用程序,包括客户端和服务器。 对于客户端,COM 提供了创建对象的基本功能。 对于服务器,COM 提供了公开其对象的方法。
- 实现定位器服务,COM 通过该服务从唯一的类标识符 (CLSID) 中确定哪个服务器实现该类以及该服务器所在的位置。 此服务包括对对象类的标识和实现的打包之间的间接级别(通常是系统注册表)的支持,以便客户端独立于打包,打包在未来可能会更改。
- 允许应用程序控制如何在其进程中分配内存
- 当对象在本地或远程服务器中运行时,透明远程过程调用。
learn.microsoft.com/zh-cn/windows/win32/com/the-com-library
调用COM库函数之前,必须调用COM库的初始化函数 (初始化com 线程模型)
// 本地 COM |
OLE 复合文档应用程序调用 OleInitialize 函数,该函数调用 CoInitializeEx
取消初始化库也很重要。 对于对 CoInitialize 或 CoInitializeEx 的每个调用,必须有对 CoUninitialize 的相应调用。 对于对 OleInitialize 的每个调用,必须有对 OleUninitialize 的相应调用
COM对象创建流程
当我们想要使用某个 COM 对象的特定接口时,我们不能直接创建该接口的实例,而是需要创建实现了该接口的 COM 类的实例。这个过程称为激活 。
给定一个 CLSID,客户端现在必须创建该类的对象才能使用其服务。激活过程分为:
- 获取 CLSID 的“类工厂”
- 请求类工厂实例化该类的对象
- 返回给客户端的接口指针

ClassFactory类工厂是由DllGetClassObject来创建的。
DllGetClassObject:
HRESULT DllGetClassObject(const CLSID& clsid,const IID& iid,(void**) ppv); |
纵观不同的编程语言,我们发现有很多函数可以帮助我们创建实例,例如:
- C / C++ - CreateInstance | CoCreateInstance | CoCreateInstanceEx
- VBScript / JScript - CreateObject | ActiveXObject
- Powershell - New-Object -ComObject
C/C++:
在COM库中, 有三个API可用于创建对象:
HRESULT CoGetClassObject( |
如果定位和连接到指定的类对象成功,则此函数返回 S_OK
你不应该直接调用 DllGetClassObject 。当对象在 DLL 中定义时, CoGetClassObject 调用 CoLoadLibrary 加载 DLL的函数,该函数进而调用 DllGetClassObject
在PowerShell 中 :
# Create Instance from CLSID |
COM组件注册
Windows 注册表是系统上安装的 COM 类信息的主要位置,每个 COM 对象在安装过程中都会将自身及其相关信息注册到注册表中。
注册 COM 服务器通常是强制性的,需要修改注册表,但是也可以使用 regsvr32.exe 等工具在不修改注册表的情况下注册 COM 对象。
注册表路径:
HKEY_CLASSES_ROOT 用于存储一些文档类型、类、类的关联属性 HKCR |
COM组件需要在注册表内进行注册才可进行调用。通常情况下:
- 系统预定义组件注册于 *HKEY_LOCAL_MACHINE\SOFTWARE\Classes*
- 用户组件注册于*HKEY_CURRENT_USER\SOFTWARE\Classes*
- HKEY_CLASSES_ROOT为二者合并后的视图
- 在系统服务角度等同于*HKEY_LOCAL_MACHINE\SOFTWARE\Classes*
以下注册表项是与 COM 相关的主要关注点:
- HKEY_LOCAL_MACHINE\SOFTWARE\Classes
- HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Ole
- HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
其中HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\
https://learn.microsoft.com/zh-cn/windows/win32/com/clsid-key-hklm
重要的
| Key Name | 说明 |
|---|---|
| InprocHandler32 | 指定应用程序使用的自定义处理程序 |
| InprocServer32 | 注册32位进程所需要的模块、线程属性配置 |
| LocalServer32 | 指定 32 位本地服务器应用程序的完整路径。 |
com组件从注册表读取这些属性来查找二进制文件,这也是后面com劫持所需要用到的
COM 服务器支持自注册。 对于进程内服务器,这意味着 DLL 必须导出以下函数:
- DllRegisterServer
- DllUnregisterServer
当我们提供 CLSID 或 ProgID 作为参数时,控制流将传递给 SCM(Windows 服务管理器),由其处理客户端的请求。
SCM 会查询注册表中的相关位置,并搜索我们传递的标识符。
包含我们传递的标识符的注册表项的值映射到 COM 服务器的位置。例如,如果我们传递 CLSID - {72c57034-02c4-4e9f-bf9c-ca711031757e},我们会发现映射到该 CLSID 的 COM 服务器(二进制文件)位于 | SystemRoot%\system32\windows.storage.dll

服务管理器请求处理顺序:
- 客户端向 COM 库请求指向 COM 对象的接口指针。 (ole32.dll)通过调用诸如 CoCreateInstance 之类的函数。 使用 COM 对象的 CLSID。
- COM 库查询 SCM 以查找与请求的 CLSID 对应的服务器
- SCM 定位服务器,并请求从服务器提供的类工厂创建 COM 对象
- 如果成功,COM 库将返回一个指向客户端的接口指针
COM 服务器类型
注册 COM 服务器时(即添加与服务器及其相关配置的注册表项),可以指定如何激活此服务器。
激活方式有以下几种:
进程内(InprocServer32)
进程外/本地 -(LocalServer32)
进程外/远程
In-Process Server
当 COM 对象配置为本地服务器时,意味着服务器是一个 EXE 文件,它将作为与实例化 COM 对象的客户端不同的进程执行。SCM 启动本地可执行文件,该文件在启动时注册一个类工厂,并且其接口指针可供系统和客户端使用。
进程内服务器是一种 COM 服务器,它以 DLL 的形式加载到客户端进程中:

有以下特点:
无需封送处理- 由于它只是一个已加载的 DLL,因此客户端和服务器之间的通信无需封送处理
EDR 不可见- 大多数 EDR / XDR 解决方案利用 Hook 来监控某些 API 或接口,通常是针对常见的目标进程,但是要实现这一点,它们需要将自己的 DLL / 保护机制注入到它们想要保护的进程中。 因此,除非向每个生成的进程注入代码,否则他们可能对进程内 COM 对象在接口方法监视方面的活动一无所知。
不使用 RPCSS - 当使用进程外(本地服务器)COM 或 DCOM 时,通信是通过驻留在 svchost.exe 进程之一上的 RPCSS 服务进行中转的。
示例COM组件: Wscript.Shell
微软提供了各种 COM 对象,其中一些是为自动化目的而设计的,Wscript.Shell 类就是其中之一
使用这个 COM 类,我们可以实例化一个对象,从而允许我们在 Jscript 或其他可执行文件中执行任意命令
- CLSID : {72C24DD5-D70A-438B-8A42-98424B88AFB8}
- ProgID : WScript.Shell
我们可以查询注册表中的 COM 类 ID (CLSID) 并查看其配置, 来查看Wscript.Shell 是一个进程内服务器
Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{72C24DD5-D70A-438B-8A42-98424B88AFB8}" |
其结果如下:

我们知道它指的是一个 DLL 服务器组件,然后我们可以看到 Wscript.Shell 类服务器 DLL 是 wshom.ocx
“.ocx” 扩展名表示该 DLL 是一个 ActiveX 控件对象
使用 CLSID 激活 COM 类:
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("72C24DD5-D70A-438B-8A42-98424B88AFB8")) |
或者我们可以使用 ProgID 来实现
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromProgID("WScript.Shell")) |

可以看到powershell 加载了目标dll
现在我们有了 WScript.Shell 的一个实例,可以使用它的一个方法“Run”来执行命令
$comobj.Run("notepad.exe") |

notepad.exe 是我们加载 COM 对象的进程的子进程,而不是通常的 wscript.exe 或 cscript.exe 的子进程。
Out-of-Process Local Server
当 COM 对象配置为本地服务器时,意味着服务器是一个 EXE 文件,它将作为与实例化 COM 对象的客户端不同的进程执行。SCM 启动本地可执行文件,该文件在启动时注册一个类工厂,并且其接口指针可供系统和客户端使用
对于这种类型,我将以 MMC20 应用为例,因为它也是一个非常常见的例子,同时也是一种常见的横向移动技术
注意,此 COM 对象既可以在本地使用,也可以在远程(远程计算机)使用
在进程外激活中,会进行“网络调用”,因此需要使用封送和代理存根。要了解更多信息,请查阅微软的文档。(遇到时再在另外一篇文章分析)
MMC20 应用程序:
- CLSID : {49B2791A-B1AE-4C90-9B8E-E860BA07F889}
查询注册表,LocalServer32 指定了对应的可执行文件,以证明它是一个进程外 COM 服务器,并了解要查找哪个进程
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889")) |

激活COM对象 (此组件激活需要管理员权限):
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889")) |
我们可以看到 COM 服务器是 mmc.exe,它位于 svchost.exe 之下,svchost.exe 生成了它

Out-of-Process Local Server
此处的“远程”指的是分布式 COM (DCOM) ,目前可以将其理解为基于网络的 COM (DCE-RPC) 。它指的是在远程计算机上激活或访问 COM 对象。本地 SCM 从运行在远程计算机上的 SCM 获取类工厂接口指针。
远程激活过程对用户来说应该是完全透明的。这里深入探讨 DCOM。
唯一改变的是我添加了目标 IP,请注意,这是在使用这些 .NET 包装器时的情况,在使用 Win32 API 时, CoCreateInstance 创建本地实例,而 CoCreateInstanceEx 用于创建远程实例。
本地服务管理器从远程计算机上运行的服务管理器获取类工厂接口指针,然后,如果它拥有适当的权限,它将使用该指针在远程计算机上创建实例。
注意的是:防火墙规则(rpc 135) 和缺乏足够的安全权限将阻止您激活 COM 对象,甚至无法访问它们
COM组件开发使用
一个典型的自注册COM组件需要提供4个必需的导出函数(dll):
- DllGetClassObject:用于获得类工厂指针
- DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载COM组件
- DllRegisterServer:将COM组件注册到注册表中
- DllUnregisterServer:删除注册表中的COM组件的注册信息
DLL还有一个可选的入口函数DllMain,可用于初始化和释放全局变量
DllMain:DLL的入口函数,在LoadLibrary和FreeLibrary时都会调用
C++ 开发
示例:
进程内COM组件
接口定义:
// MyCom.h |
或者 使用IDL :
// MyCom.idl |
实现类:
|
ClassFactory 实现:
|
注册(dll导出):
|
上面是服务端代码
编译后使用 regsvr32 进行注册
regsvr32 xxx.dll |
客户端调用:
|
C语言调用COM
C 中没有面向对象 ,所以实现COM开发比C++ 要更加复杂
C++ 中接口如下定义
class ISomeInterface{ |
该接口对应的 C 声明如下所示:
typedef struct ISomeInterface{ |
我们所做的,是用纯 C 代码重新创建了一个 C++ 类。ISomeInterface 结构体实际上就是一个 C++ 类。C++ 类本质上就是一个结构体,它的第一个成员始终是指向其虚函数表(一个函数指针数组)的指针 ——该数组包含指向该类中所有函数的指针。 传递给对象函数的第一个参数是指向对象(结构体)自身的指针。(这被称为隐藏的“ this ”指针。)
主要是调用进程内COM组件
步骤:
加载指定dll ,调用 dll中DllGetClassObject 获取ClassFactory
调用ClassFactory接口类型变量中的CreateInstance 创建对象
通过IDispatch 接口中 GetIDsOfNames 获取 dispid
初始化函数参数,并调用目标函数
需要注意的是 接口查询 IDispatch 后 调用Invoke 函数的参数传递:
在Invoke中第5个参数是 DISPPARAMS结构体指针 ,包含了参数数组,命名参数的 DISPID 数组以及数组中元素数量的个数的结构。
typedef struct tagDISPPARAMS { |
BSTR(基本字符串或二进制字符串) 是一种字符串数据类型,供 COM、自动化和互操作函数使用。所有需要通过脚本访问的接口都应使用 BSTR 数据类型。
字符串初始化:
|
最终代码:
|
Golang 调用COM
使用go-ole 库实现与COM组件交互
go get github.com/go-ole/go-ole |
如果目标COM组件 实现了IDispatch 接口
package main |
假如目标COM组件 没有实现,需要像c那样定义结构体 虚函数表
package main |
COM 利用
枚举COM对象
gwmi Win32_COMSetting | ? {$_.progid } | sort | ft ProgId,Caption,InprocServer32 |
COM接口里枚举出来的函数(如果是微软公开的话)可以到:https://docs.microsoft.com/en-us/search/?dataSource=previousVersions&terms= 搜索
利用系统提供的COM组件
命令执行
使用mmc 来执行命令(管理员权限)
$handle = [activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application.1")) |
另外一种com 执行命令:
$hb = [activator]::CreateInstance([type]::GetTypeFromCLSID("9BA05972-F6A8-11CF-A442-00A0C90A8F39")) |
WScript.Shell
$shell = [Activator]::CreateInstance([type]::GetTypeFromCLSID("72C24DD5-D70A-438B-8A42-98424B88AFB8")) |
计划任务
通过调用ITaskFolder::registerTask 来注册计划任务
进程注入
利用com实现进程注入,没有调用CreateProcess等常规api,而是调用oleacc.dll!GetProcessHandleFromHwnd(),利用 IRundown::DoCallback()执行命令,并且该接口需要一个IPID和OXID值来执行代码。该接口也不是公开的方法,需要手动去逆,来实现武器化
相关项目
https://github.com/mdsecactivebreach/com_inject
COM 劫持
com组件从注册表中获取二进制文件路径,加载过程如下:
- HKCU\Software\Classes\CLSID
- HKCR\CLSID
- *HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\shellCompatibility\Objects*
看到HKCU的优先级高于HKCR高于HKLM:
当前 用户COM > 系统COM > 兼容性映射COM
那我们的目标就很明显了,劫持目标选择 HKCU\Software\Classes\CLSID 用户注册COM组件,这样就会先加载我们的恶意dll
与dll劫持不同的是,dll劫持只能劫持dll,com劫持可以劫持 com文件、pe文件、api文件等
步骤就是修改注册表的路径,指向我们的恶意路径,和白加黑一样
与DLL劫持原理相近,但是COM组件的劫持可以拓展很多东西,劫持的目标不一定是一个进程,劫持所需的文件不一定是一个DLL,它可以是一个.com文件、二进制PE文件、DLL文件,劫持的目标也可以是一个Windows API。
利用缺失的CLSID
条件: 管理员权限
因为修改InprocServer32下的dll需要一定权限,所以该方法需要管理员权限
使用Process Monitor 收集,
添加filter:
- RegOpenKey 注册表读取操作
- 注册表路径 包含 InprocServer32
- 结果是
NAME NOT FOUND

能找到很多

覆盖COM键
原理:在HKCU注册表中添加键值后,当com对象被调用,HKLM中的键值就会被覆盖(并且添加到HKCR)中
思路: 查找低权限可调用的COM组件 ,修改注册表项 ,修改加载的dll为恶意dll
该劫方法不需要高权限,可以看到本来的注册表项键值,但是需要找到一个通用的COM组件
先使用oleview.net来过滤程序启动权限为空的id

找一个查看属性,CLSID: 275AF033-1C37-48ED-91F7-8E23C5D9B382

HKEY_CLASSES_ROOT\CLSID{275AF033-1C37-48ED-91F7-8E23C5D9B382}\InProcServer32

将其修改为恶意dll路径
COM注册表滥用利用
查找注册的COM 中 注册表中指定可执行文件不存在的 进行利用
LocalServer32
枚举所有LocalServer32键值
$inproc = gwmi Win32_COMSetting | ?{ $_.LocalServer32 -ne $null } |
寻找File not Found
$paths = gc .\values.txt |
找exe的文件夹路径

找可用exe路径的效率很低
InprocServer32
枚举所有InprocServer32中的键值
$inproc = gwmi Win32_COMSetting | ?{ $_.InprocServer32 -ne $null } |

$paths = gc .\comDll.txt |
找文件夹的权限路径,如果everyone可写,可以替换恶意dll,然后使用rundll32加载
rundll32.exe -sta {CLSID} |
COM 挖掘
手动挖掘
首先需要遍历系统中所有COM对象的CLSID,于是编写powershell脚本,将CLSID输出到txt文本中:
New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR |
接着利用这些clsid通过powershell创建对应的COM对象,并且使用Get-Member方法获取对应的方法和属性,并最终输出到文本中,pwoershell脚本如下:
$Position = 1 |
自动化挖掘:https://github.com/nickvourd/COM-Hunter
Reference
- https://mohamed-fakroud.gitbook.io/red-teamings-dojo/windows-internals/playing-around-com-objects-part-1
- https://www.221bluestreet.com/offensive-security/windows-components-object-model/demystifying-windows-component-object-model-com
- http://diff3.com/b7ae/
- https://tttang.com/archive/1824/