STUBborn Activate and call DCOM objects without proxy
近年来,本地 RPC (LRPC) 和 ALPC 一直是 Windows 内部爱好者和漏洞研究人员的关注焦点。
在本文中,我们将更进一步,探讨如何处理 LocalServer DCOM 对象,如何实例化它们并直接连接到它们的接口,而不依赖于传统的 COM 代理客户端。
这将为我们提供一个机会,深入探索一些 COM 的内部结构,理解 combase DLL 的部分内容,并编写更多有趣的 Python 代码。
1 目标
本文的目标是通过自定义 RPC 客户端,直接与本地计算机远程进程中的 DCOM 对象进行交互。
这个挑战的起源是什么?在我们开发的 Windows 取证收集代理(法语链接)1 中,我有时会遇到一些新的取证工件或信息,这些信息可能难以获取。在这种情况下,我将其视为深入研究 Windows 内部结构和探索新事物的契机。
我曾遇到一个 COM 服务器,它能够提供一些有价值的取证信息,而其代理仅以 64 位 DLL 的形式存在,我当时的目标是能够从 32 位进程中查询 COM 接口的特定方法。我们的取证代理是一个独特的 32 位进程,在需要时可以跨越“天堂之门”。
缺少代理 DLL,COM 缺乏某些功能
最终目标是将其应用于生产环境,因此我增加了另一个要求:在可能的情况下,让真正的 COM 机制完成工作,以减少处理从 Windows Server 2003 到 Windows 11 的 Windows 版本所需的更改。
本文依赖于一些 COM 的基本知识2;理解 IID、CLSID、CoCreateInstance、IUnknown 和 IUnknown::QueryInterface 的概念将有助于理解。正如往常,我推荐观看 James Forshaw 的 COM in Sixty Seconds!3 演讲(至少是前半部分)。
1.1 设置
1.1.1 IExaDemo
为满足需求,我们将使用我实现并开源的一个玩具 COM LocalServer:https://github.com/ExaTrack/COM_IExaDemo。关于如何编译和安装这两个二进制文件(IExaDemo_server_64.exe
和 IExaDemo_proxy_64.dll
)的详细信息,请参阅仓库中的 README.md。
在我们的示例中,我们将编译 64 位的 COM 服务器和代理 DLL,并尝试从 32 位 Python 解释器进行访问。
我们主要交互的接口是 IExaDemo
,其 IDL 描述如下:
[
uuid (45786100-1111-2222-3333-445500000001),
version(1.0),
] interface IExaDemo : IUnknown
{
HRESULT add(
[in] unsigned int x,
[in] unsigned int y,
[out] unsigned int *res
);
HRESULT print(
[in][string] wchar_t* msg);
};
// IDL description of the server CLSID
[
uuid(45786100-4343-4343-4343-434343434343),
version(1.0)
]
coclass ExaDemoSrv
{
interface IExaDemo;
}
因此,在缺少代理 DLL 的情况下,我们将尝试执行两个数字的加法运算并获取结果。如果您对 add()
方法的实现细节感兴趣,可以在此处查看代码。
在我们的配置中,服务器的 CLSID 为 45786100-4343-4343-4343-434343434343
,而我们需要交互的接口 IID 为 45786100-1111-2222-3333-445500000001
,即 IID_IExaDemo
。
1.1.2 PythonForWindows
在这个项目中,我将专门使用 Python3.11
和 PythonForWindows
,这是我为在 Windows 环境下工作而开发的库。我的大部分研究成果已提交到该项目中,因此部分解释和代码将直接引用该库。
一个简单的 Python 客户端,利用代理 DLL 和完整的 COM 机制,可以在 COM_IExaDemo
仓库中找到,文件名为 client.py
。
以下是关键部分的解释:
import ctypes
import windows.com
import windows.generated_def as gdef
# General PFW definition for the COM class, normally generated from the IDL c output
class IExaDemo(gdef.COMInterface):
IID = gdef.generate_IID(0x45786100, 0x1111, 0x2222, 0x33, 0x33, 0x44, 0x55, 0x00, 0x00, 0x00, 0x01, name="IExaDemo",
strid="45786100-1111-2222-3333-445500000001")
[ctypes interface definition]
EXA_DEMO_SRV_CLSID = "45786100-4343-4343-4343-434343434343"
windows.com.init() # CoInitialize
iunk = gdef.IUnknown()
print("CreateInstance {0}".format(EXA_DEMO_SRV_CLSID))
# Wrapper around CoCreateInstance which is more permissiv with GUID & strings
windows.com.create_instance(EXA_DEMO_SRV_CLSID, iunk, gdef.IUnknown.IID)
print(" OK: Got an IUnknown: {0}".format(iunk))
print("QueryInterface: IExaDemo.IID: {0}".format(IExaDemo.IID))
# A wrapper around IUnknown.QueryInterface that can be read as
# iunk.QueryInterface(IExaDemo.IID, IExaDemo())
iexademo = iunk.query(IExaDemo)
print(" OK: {0}".format(iexademo))
print("Adding stuff:")
result = gdef.DWORD()
iexademo.add(0x41414141, 0x01010101, result)
print(" iexademo.add(0x41414141, 0x01010101) == {0:#x}".format(result.value))
该脚本的输出结果如下所示
PS> py -3.11 .client.py
CreateInstance 45786100-4343-4343-4343-434343434343
OK: Got an IUnknown: <IUnknown at 0x1dc36053750>
QueryInterface: IExaDemo.IID: 45786100-1111-2222-3333-445500000001
OK: <IExaDemo at 0x1dc360539d0>
Adding stuff:
iexademo.add(0x41414141, 0x01010101) == 0x42424242
ExaDemo_server_64.exe
控制台输出应如下所示:
CALL:IExaDemoImplem_QueryInterface
[IExaDemoImplem_QueryInterface] * riid: {45786100-1111-2222-3333-445500000001} !
[IExaDemoImplem_QueryInterface] * Asking for IID_IExaDemo: I CAN DO THAT !
[...]
CALL:IExaDemoImplem_add
[IExaDemoImplem_add] 0x41414141 + 0x1010101 = 0x42424242
1.2 问题
首先,我们来探讨在缺少代理 DLL 的情况下,典型的 COM 客户端会如何表现。为此,我们将使用 32 位的 Python 运行 client.py
,在这种环境下代理 DLL 未安装。
PS> py -3.11-32 .client.py
CreateInstance 45786100-4343-4343-4343-434343434343
OK: Got an IUnknown: <IUnknown at 0x1b12440>
QueryInterface: IExaDemo.IID: 45786100-1111-2222-3333-445500000001
Traceback (most recent call last):
File "COM_IExaDemoclient.py", line 34, in <module>
iexademo = iunk.query(IExaDemo)
^^^^^^^^^^^^^^^^^^^^
File "pythonforwindowswindowsgenerated_definterfaces.py", line 62, in query
self.QueryInterface(interface_iid, target)
File "pythonforwindowswindowsgenerated_definterfaces.py", line 28, in _default_errcheck
ctypes._check_HRESULT(result)
OSError: [WinError -2147221164] Class not registered
客户端出现了故障,更有趣的是,故障并不是在创建 IUnknown
实例时发生的,而是在我们查询由代理处理的接口 IID_IExaDemo
时发生的。
在 WinDBG 中,我们可以看到错误发生在以下堆栈中:
0:000> k
# ChildEBP RetAddr
...
02 (Inline) -------- combase!CStdMarshal::GetPSFactory+0xa9 [onecorecomcombasedcomremmarshal.cxx @ 6664]
03 00beecd8 772f369b combase!CStdMarshal::CreateProxy+0x1da [onecorecomcombasedcomremmarshal.cxx @ 6745]
...
09 (Inline) -------- combase!CStdMarshal::QueryRemoteInterfaces+0x71 [onecorecomcombasedcomremmarshal.cxx @ 5759]
0b 00beeef8 73834018 combase!CStdIdentity::CInternalUnk::QueryInterface+0x1f0 [onecorecomcombasedcomremstdid.cxx @ 428]
这表明我们的代理实际上是在查询特定接口时被搜索和加载的,而不是在实例化时。
这意味着我们可以将一些繁重的工作委托给 COM 运行时。我们可以让 COM 为我们执行实例创建,然后连接到 COM 服务器及其创建的对象。
设置完成,问题定义清楚后,让我们开始探索。
2 STUBBorn
2.1 现状
目前,关于 RPC、ORPC 和 DCOM 的研究已经相当深入。
查找信息的首选资源是MS-DCOM 开放规范4,这是一份包含大量关于 DCOM 详细信息的权威文档。
我还可以引用这篇空中客车的文章5,作为关于OXIDResolver
从网络角度的可靠信息来源。这为我们提供了预期的预览。
接下来,我显然需要引用 James Forshaw 的各种作品。我想到的是这篇关于 DCOM 认证中继的文章6和andbox-attacksurface-analysis-tools GitHub 仓库7。这为我们提供了关于OBJREF
和接口封送的更多信息。
当然,在讨论 Windows 内部的现状时,很难不提到 Alex Ionescu,这次是关于hazmat5 GitHub 仓库8,它提供了关于RPCSS
和ILocalObjectExporter
提供的 RPC 接口的各种信息。
最后,我可以引用我之前关于 RPC&ALPC 的工作9和集成在 PythonForWindows 中的 RPC 客户端10,这在这项研究中得到了改进。
2.2 我们在寻找什么?
基于现状,我们已经可以建立一个操作清单,以便手动与远程IExaDemo
和IExaDemo_server_64.exe
进行交互。
我们需要在IExaDemo_server_64.exe
中找到一个 RPC 端点,基于之前的研究10,我们可以评估该端点将是一个 ALPC 端口。我们可以列出进程的句柄,但那样做就不够严谨 😁。我们需要将我们的 RPC 知识升级到 ORPC(对象远程过程调用)。关于ORPC 调用11的微软文档还告诉我们,我们将需要一个IPID12,它代表远程对象的给定接口。
操作清单:
-
ALPC 端点 -
IPID -
ORPC 能力
2.3 我们不会做什么
我研究的第一步是分析一个工作客户端的 ALPC-RPC 交互以及它在实例化、接口查询和 ORPC 调用期间与之交互的各种 ALPC-RPC 端点。
通过使用自定义调试器分析对NtAlpcCreatePort
和NtAlpcSendWaitReceive
的调用,可以绘制出以下图像:
[ALPC-CONNECT] <RPC Controlepmapper>
[RPC-BIND]: RPC Controlepmapper (<RPC_IF_ID "E60C73E6-88F9-11CF-9AF1-0020AF6E72F4" (2, 0)>) (name=ILocalObjectExporter)
...
[RPC-CALL]: RPC Controlepmapper (<RPC_IF_ID "E60C73E6-88F9-11CF-9AF1-0020AF6E72F4" (2, 0)>) (method 0) (name=Connect)
...
[RPC-CALL]: RPC Controlepmapper (<RPC_IF_ID "E60C73E6-88F9-11CF-9AF1-0020AF6E72F4" (2, 0)>) (method 6) (name=ServerAllocateOXIDAndOIDs)
[RPC-BIND]: RPC Controlepmapper (<RPC_IF_ID "00000136-0000-0000-C000-000000000046" (0, 0)>) (name=ISCMLocalActivator)
[RPC-CALL]: RPC Controlepmapper (<RPC_IF_ID "00000136-0000-0000-C000-000000000046" (0, 0)>) (method 4) (name=CreateInstance)
[ALPC-CONNECT] <RPC ControlOLE1C6126F89B815E927A6AF617D45E>
[RPC-BIND]: RPC ControlOLE1C6126F89B815E927A6AF617D45E (<RPC_IF_ID "00000134-0000-0000-C000-000000000046" (0, 0)>) (name=)
基于这些信息,我们可以确认:
-
DCOM 使用 ALPC -
DCOM 确实使用 ILocalObjectExporter
13,由端点RPC Controlepmapper
提供服务 -
相关信息可在这里14,这里15和这里16找到 -
该端点属于 RPCSS
服务 -
COM 使用 IID 00000136-0000-0000-C000-000000000046
,公开称为ISCMLocalActivator
-
确认 ISCMLocalActivator
是一个 DCOM 对象,这让我们对 ORPC 与 RPC 绑定/调用逻辑共享了很多逻辑有了一些了解 -
绑定发生在 DCOM 接口目标的接口 IID 上。
所有这些,给了我们明确的方向:
-
手动连接到 epmapper
-
绑定到 ILocalObjectExporter
并调用Connect
-
绑定到 ISCMLocalActivator
并调用CreateInstance
尽管这将是一个有前途的研究角度,并且最终可能会成功,但我有多个理由采取不同的方法。首先,hazmat514为ILocalObjectExporter.Connect
描绘的原型表明,制作[in]
参数和解析[out]
将是繁琐的。参数也让我们猜测这个接口自Windows XP
以来很可能已经多次演变。
这一点很重要,因为我的最终目标是能够在我们的取证收集代理(法语链接)1中使用STUBborn
,以针对一些具有有价值取证信息的 DCOM 端点。因此,我希望代码能够在最少修改的情况下跨越最广泛的 Windows 版本工作。这与我们之前提出的让 COM 运行时为我们完成对象创建的主要工作目标一致。
最后,这是探索combase.dll
内部工作原理的一个好理由!
2.4 Combase.dll
combase.dll
是实现大部分 COM 逻辑的 DLL,其内容和功能非常值得探索。据我所知,这个 DLL 加载在任何 COM 客户端或服务器中,特别是因为它实现了诸如CoCreateInstance
或CoRegisterClassObject
等重要功能。在旧版本的 Windows 中,这些逻辑可以在ole32.dll
中找到。
2.4.1 gInternalClassObjects
COM 的一个众所周知的行为是,COM 服务器在注册表中注册,CLSID 在HKEY_CLASSES_ROOTCLSID
中查找服务器路径。一个不太为人所知的行为是,combase.dll
实现了一些类对象作为硬编码的 CLSID,绕过注册表,直接在combase
中实现。
这表示为一个(CLSID
,CreateInstance_Method
,Flags
)的数组,可以在combase!gInternalClassObjects
中找到:
CLSID | CreateInstance 函数 |
---|---|
combase!CLSID_StdEvent |
combase!CStdEventCF_CreateInstance |
combase!CLSID_ManualResetEvent |
combase!CManualResetEventCF_CreateInstance |
combase!CLSID_SynchronizeContainer |
combase!CSynchronizeContainerCF_CreateInstance |
combase!CLSID_StdGlobalInterfaceTable |
combase!CGIPTableCF_CreateInstance |
combase!CLSID_DCOMAccessControl |
combase!CAccessControlCF_CreateInstance |
combase!CLSID_ErrorObject |
combase!CErrorObjectCF_CreateInstance |
combase!GUID_00000346_0000_0000_c000_000000000046 |
combase!CComCatalogCF_CreateInstance |
combase!CLSID_RpcHelper |
combase!CRpcHelperCF_CreateInstance |
combase!CLSID_ObjectContext |
combase!CObjectContextCF_CreateInstance |
combase!CLSID_InProcFreeMarshaler |
combase!CFreeThreadedMarshalerCF_CreateInstance |
combase!CLSID_ActivationPropertiesIn |
combase!CActivationPropertiesInCF_CreateInstance |
combase!CLSID_ActivationPropertiesOut |
combase!CActivationPropertiesOutCF_CreateInstance |
combase!CLSID_InprocActpropsUnmarshaller |
combase!CInprocActpropsUnmarshallerCF_CreateInstance |
combase!CLSID_ComActivator |
combase!CComActivatorCF_CreateInstance |
combase!CLSID_AddrControl |
combase!CAddrControlCF_CreateInstance |
combase!CLSID_LocalMachineNames |
combase!CLocalMachineNamesCF_CreateInstance |
combase!CLSID_GlobalOptions |
combase!CGlobalOptionsCF_CreateInstance |
combase!CLSID_ContextSwitcher |
combase!CContextSwitcherCF_CreateInstance |
combase!CLSID_RestrictedErrorObject |
combase!CRestrictedErrorObjectCF_CreateInstance |
combase!CLSID_RegisterSuspendNotify |
combase!CSuspendMonitorCF_CreateInstance |
combase!CLSID_MachineGlobalObjectTable |
combase!CMgotCF_CreateInstance |
combase!CLSID_ActivationCapabilities |
combase!CActivationCapabilitiesCF_CreateInstance |
这给了我们一个 CLSID 列表来探索、实例化和测试。其中一些已经被使用和知道,例如CLSID_ComActivator
1718,它在combase!CoCreateInstance
的底层使用。
问题是,我们想要探索哪个类来实现我们的目标?通过探索对象创建和 ALPC 数据传输的ntdll!NtAlpcSendWaitReceivePort
,我们可以遇到以下类型的调用栈:
:000> kc
# Call Site
00 ntdll!NtAlpcSendWaitReceivePort
[...]
07 combase!ServerAllocateOXIDAndOIDs
08 combase!CRpcResolver::ServerRegisterOXID
[...]
11 combase!CRpcResolver::CreateInstance
12 combase!CClientContextActivator::CreateInstance
13 combase!ActivationPropertiesIn::DelegateCreateInstance
14 combase!ICoCreateInstanceEx
15 combase!CComActivator::DoCreateInstance
16 combase!CoCreateInstanceEx
17 combase!CoCreateInstance
对象 ActivationPropertiesIn
位于已知的 CComActivator
下,似乎参与了实例创建,并且可以通过 gInternalClassObjects
访问,因此是进一步探索的理想候选对象。
另一个有力的指示是 MS-DCOM IRemoteSCMActivator:: RemoteGetClassObject 页面19,该页面指出该函数将 ActivationPropertiesIn
作为参数。非常有前景!
2.4.2 ActivationPropertiesIn
CLSID_ActivationPropertiesIn
确实允许我们在 combase 中实例化一个对象,该对象至少提供以下接口:
-
IActivationProperties -
IActivationPropertyIn -
IInitActivationPropertiesIn -
IPrivActivationPropertiesIn -
IActivationStageInfo
每个接口都允许初始化 ActivationPropertiesIn
结构的关键部分,以触发自定义的 CreateInstance
。
这一步代表了识别出成功模拟 CoCreateInstance
的最小代码。
-
IActivationPropertiesIn.AddRequestedIIDs(1, IID)
允许填充请求的 IID (riid
) -
IInitActivationPropertiesIn.SetClassInfo(IComClassInfo)
允许填充请求的 CLSID (rclsid
) -
IInitActivationPropertiesIn.SetClsctx(CLSCTX_LOCAL_SERVER)
允许填充服务器上下文 (dwClsContext
) -
IActivationStageInfo.SetStageAndIndex(CLIENT_CONTEXT_STAGE, 0)
表示我们是一个客户端激活器(并防止Catastrophic failure
错误) -
IPrivActivationPropertiesIn.DelegateCreateInstance([out] IActivationPropertiesOut)
触发实际的CreateInstance
需要传递给 IInitActivationPropertiesIn.SetClassInfo
的 IComClassInfo
可以通过两种方法获得:
-
实现一个自定义的 IComClassInfo
-
唯一似乎被调用的方法是 IComClassInfo.GetConfiguredClsid
,它需要返回我们请求的 CLSID -
通过具有 CLSID GUID_00000346_0000_0000_c000_000000000046
的 combase 对象,对应于CComCatalog
-
它实现了 IComCatalog.GetClassInfo
,接受一个 CLSID 并返回一个IComClassInfo
可能会问的问题是:使用 ActivationPropertiesIn
进行实例创建相比于 CoCreateInstance
的优势是什么?答案是返回值,这种方法返回一个 ActivationPropertiesOut
,它是信息的宝库!
2.4.3 ActivationPropertiesOut
由 IPrivActivationPropertiesIn.DelegateCreateInstance()
返回的对象是一个 ActivationPropertiesOut
,它至少提供以下接口:
-
IActivationProperties -
IActivationPropertiesOut -
IPrivActivationPropertiesOut -
IScmReplyInfo
第一个值得注意的函数是 IScmReplyInfo.GetResolverInfo()
,它允许我们检索一个结构,其中包含关于远程 COM 服务器的信息,位于 PRIV_RESOLVER_INFO。
2.4.3.1 PRIV_RESOLVER_INFO
据我所知,这个结构是唯一在不同 Windows 版本中发生变化的元素。旧版本的 PRIV_RESOLVER_INFO 可以追溯到 Windows XP
一直到 Windows Server 2016
(至少在某些安装 ISO 上)。在我的测试计算机(10.0.22631.4317
)以及 Windows Server 2019
的 ISO 安装中发现了新版本的 PRIV_RESOLVER_INFO。
如果有人找到了定义更改的确切更新/版本,我将很高兴获得该信息!对于本文的其余部分,我将使用最新版本。
这个结构包含了完成我们任务的许多有用信息,让我们看看 IScmReplyInfo.GetResolverInfo()
返回了什么。
>>> resolver_info
<windows.generated_def.winstructs._PRIV_RESOLVER_INFO object at 0x000001F2BA31AFD0>
>>> windows.utils.sprint(resolver_info)
struct.OxidServer -> 0x47b7459170ebf6dd
struct.pServerORBindings -> NULL
struct.OxidInfo.dwTid -> 0x2f94
struct.OxidInfo.dwPid -> 0x56fc
struct.OxidInfo.dwAuthnHint -> 0x5
struct.OxidInfo.dcomVersion.MajorVersion -> 0x5
struct.OxidInfo.dcomVersion.MinorVersion -> 0x7
struct.OxidInfo.containerVersion.version -> 0x3
struct.OxidInfo.containerVersion.capabilityFlags -> 0x0
struct.OxidInfo.containerVersion.extensions -> NULL
struct.OxidInfo.ipidRemUnknown -> <GUID "00009400-56FC-2F94-4A97-D41AAB84F489">
struct.OxidInfo.dwFlags -> 0x4000000
struct.OxidInfo.psa<deref>.wNumEntries -> 0x3d
struct.OxidInfo.psa<deref>.wSecurityOffset -> 0x2b
struct.OxidInfo.psa<deref>.aStringArray -> <windows.generated_def.winstructs.c_ushort_Array_1 object at 0x000002215903F050>
struct.OxidInfo.guidProcessIdentifier -> <GUID "36A580E5-366E-4E9D-81E7-3FAC865D240A">
struct.OxidInfo.processHostId -> 0x0
struct.OxidInfo.clientDependencyBehavior -> 0x0
struct.OxidInfo.packageFullName -> NULL
struct.OxidInfo.userSid -> NULL
struct.OxidInfo.appcontainerSid -> NULL
struct.OxidInfo.primaryOxid -> 0xc81a618f9842fdf3
struct.OxidInfo.primaryIpidRemUnknown -> <GUID "00004C01-56FC-FFFF-2994-EED6240E08FE">
struct.LocalMidOfRemote -> 0x57d71877bf3b4fe2
struct.FoundInROT -> 0x0
关键信息在于 priv_resolver_info.OxidInfo.psa
,它是一个指向 tagDUALSTRINGARRAY20 的指针,需要进行一定的解析来理解其含义。
>>> resolver_info.OxidInfo.psa[0].bidings
['ncalrpc:[OLE85D5A9520394209954DA2E2595EF]']
这为我们提供了由 COM 服务器 IExaDemo_server_64.exe
暴露的 ALPC 端口。端口的实际路径可以推断为 RPC ControlOLE85D5A9520394209954DA2E2595EF
。
掌握这些信息后,我们可以通过 ALPC 直接连接并开始交互:
>>> client = windows.rpc.RPCClient(r"RPC ControlOLE85D5A9520394209954DA2E2595EF")
>>> iid = client.bind("45786100-1111-2222-3333-445500000001", (0, 0))
>>> client.call(iid, 0, b"Hello !")
Traceback (most recent call last):
[...]
ValueError: RPC Response error 2147549457 (RPC_E_INVALID_HEADER(0x80010111))
完成了一个步骤后,让我们更新任务清单并继续探索如何实现直接调用。
任务清单:
-
ALPC 端点 -
IPID -
ORPC 功能
2.4.3.2 IPrivActivationPropertiesOut
我们的 ActivationPropertiesOut
提供了另一个值得关注的接口,即 IPrivActivationPropertiesOut
。具体来说,是 IPrivActivationPropertiesOut.GetMarshalledResults()
方法。此方法允许在 IPrivActivationPropertiesIn.DelegateCreateInstance()
期间获取实例化的接口指针,作为 MInterfacePointer
21。
根据文档,MInterfacePointer
21 仅仅是一种描述包含 OBJREF
22 的可变大小数组的方式,可能用于 NDR 编码。OBJREF
22 是 COM/DCOM 专业人员熟悉的结构,并且已被 James Forshaw 在 Windows Exploitation Tricks: Relaying DCOM Authentication6 中使用和描述。
那么在我们的场景中,OBJREF
包含了什么呢?
# Call to IPrivActivationPropertiesOut.GetMarshalledResults
>>> propout_as_priv.GetMarshalledResults(nb_interface, iids, results, interfaces)
0
# Explore the interface
>>> interfaces[0].contents.objref.flags
1
>>> interfaces[0].contents.objref.flags == gdef.OBJREF_STANDARD
True
>>> interfaces[0].contents.objref.std
<windows.generated_def.winstructs.tagSTDOBJREF object at 0x000002B3C0723E50>
>>> windows.utils.sprint(interfaces[0].contents.objref.std)
struct.flags -> 0x0
struct.cPublicRefs -> 0x5
struct.oxid -> 0x6a74afc5319c3f21
struct.oid -> 0x9883dc2f214bf1a4
struct.ipid -> <GUID "00001006-4A14-4DF4-173F-4799F71C9689">
我们获取的接口标识符是IPID
12。它代表远程 COM 服务器中的接口,客户端利用它进行ORPC 调用11。这是实现直接调用对象的关键部分。
所需元素清单:
-
ALPC 端点 -
IPID -
ORPC 功能
2.5 LORPC
拥有以下元素,我们几乎具备了直接与IExaDemo_server_64.exe
通信所需的所有工具和信息:
-
IExaDemo_server_64.exe
的 ALPC 端口 -
IExaDemo
的 IPID -
大量来自MS-DCOM4的文档 -
关于本地 RPC 和 ALPC 的一些背景知识
将 RPC 客户端转换为 ORPC 客户端所需的大部分信息可以在MS-DCOM: ORPC Calls11和MS-DCOM: ORPC Invocations23页面中找到。
如前所述,我们可以连接到 ALPC 端口并像正常的本地 RPC 调用一样绑定到我们想要的IID
。
唯一未记录的添加内容是:
-
RPC_HEADER.Flag
必须设置为值1
以使其成为 RPC 调用 -
IPID
必须作为RPC_HEADER
的最后一个值填写,我在PythonForWindows
中将其命名为ALPC_RPC_CALL.orpc_ipid
-
ORPCTHIS
必须将其flags
设置为ORPCF_LOCAL(1)
,version
设置为(5, 7)
-
在我们的本地 ORPC 情况下,另一个结构 combase!_LOCALTHIS
存在于ORPCTHIS
和方法参数之间 -
LOCALTHIS.callTraceActivity
,带有一个随机 GUID -
LOCALTHIS.dwClientThread
-
唯一需要的字段是: -
参数的 NDR 封送不变
对PythonForWindows LRPC客户端
所做的改进可以在这里找到。
注意,combase!_LOCALTHIS
的布局也会随着版本的变化而发生多次变化。目前在PythonForWindows
中仅存在结构的最新版本。
基于此,我们可以编写以下代码来请求我们的IExaDemo
执行0x41414141 + 0x01010101
的加法:
>>> client = windows.rpc.RPCClient(target_alpc_server)
>>> iid = client.bind("45786100-1111-2222-3333-445500000001", (0, 0))
>>> ipid = interfaces[0][0].objref.std.ipid
>>> data = client.call(iid, 3, b"x41x41x41x41x01x01x01x01", ipid=ipid)
>>> data
b'x01x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00BBBBx00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00'
调用成功,我们可以看到结果 0x42424242
(即 BBBB
)出现在响应缓冲区中,表明我们已经接近成功。同时,IExaDemo_server_64.exe
打印了 [IExaDemoImplem_add] 0x41414141 + 0x1010101 = 0x42424242
,这是一个积极的信号。
与 ORPC 调用类似,响应包含 ORPCTHAT
和 LOCALTHAT
,经过简单解析后,我们得到了结果。
>>> addstream = ndr.NdrStream(addrep)
>>> orpcthat = gdef.ORPCTHAT32.from_buffer_copy(bytearray(addstream.read(ctypes.sizeof(gdef.ORPCTHAT32))))
>>> localthat = gdef.LOCALTHAT32.from_buffer_copy(bytearray(addstream.read(ctypes.sizeof(gdef.LOCALTHAT32))))
>>> result = addstream.partial_unpack("<I")[0]
>>> print(hex(result))
0x42424242
>>> hex(result)
'0x42424242'
2.6 STUBborn_client.py
IExaDemo 仓库 在 stubborn_client.py
24 中完整展示了该技术的实现。
代码执行的步骤如下:
-
实例化 IComCatalog
-
在 CLSID_ExaDemoSrv
上获取IComClassInfo
-
实例化 ActivationPropertiesIn
并查询所需接口 -
通过本文档中的接口填充各种必要信息 -
调用 IPrivActivationPropertiesIn.DelegateCreateInstance()
以获取ActivationPropertiesOut
-
查询并提取信息所需的不同接口 -
使用 IScmReplyInfo.GetResolverInfo()
获取PRIV_RESOLVER_INFO
-
从该结构中提取 COM 服务器的 ALPC 端点 -
使用 IPrivActivationPropertiesOut.GetMarshalledResults()
获取接口IPID
-
使用 PythonForWindows
ORPC 客户端调用IExaDemo.add(0x41414141, 0x01010101)
-
解析结果并验证输出参数是否为 0x42424242
-
确认 iexademo_proxy
DLL 未在进程中加载
基于我们的目标,我们还可以验证在未注册代理 DLL 的情况下,32 位 Python 是否能够成功调用我们的接口。
# Real COM client.py still does not work :(
PS COM_IExaDemo> py -3.11-32 .client.py
CreateInstance 45786100-4343-4343-4343-434343434343
OK: Got an IUnknown: <IUnknown at 0x20d2440>
QueryInterface: IExaDemo.IID: 45786100-1111-2222-3333-445500000001
Traceback (most recent call last):
File "client.py", line 34, in <module>
iexademo = iunk.query(IExaDemo)
^^^^^^^^^^^^^^^^^^^^
File "pythonforwindowswindowsgenerated_definterfaces.py", line 62, in query
self.QueryInterface(interface_iid, target)
File "rojetspythonforwindowswindowsgenerated_definterfaces.py", line 28, in _default_errcheck
ctypes._check_HRESULT(result)
OSError: [WinError -2147221164] Class not registered
# Stubborn client successfuly makes the call!
PS COM_IExaDemo> py -3.11-32 .stubborn_client.py
Addition is OK : 0x42424242 !
Proxy DLL was never loaded !
通过这些步骤,我们已经具备了使用自定义 ORPC 客户端与任意 DCOM 服务器进行交互所需的全部条件。
3 进一步探索
为了完成这次“COM 之旅”,我还利用这种新方法探索了一些更为常见的机制。
3.1 GetClassObject
如果您希望与 IClassFactory.GetClassObject()
方法进行交互并手动实现 CreateInstance
25,IPrivActivationPropertiesIn
也提供了 DelegateGetClassObject()
的实现。在这种情况下,您可以请求 IID_IClassFactory
,并像之前一样检索 ALPC 端点和 IPID
。
接下来,您可以使用 ORPC 客户端手动调用 IClassFactory.CreateInstance()
并解析其结果。与标准 COM 函数不同,远程版本的 IClassFactory.CreateInstance()
仅接受 IID 作为参数。
响应将包含一个带有新创建对象的 MInterfacePointer
!
remfactory = client.bind(gdef.IClassFactory.IID, (0, 0))
# 3 -> CreateInstance
params = bytearray(IID_IExaDemo) # The remote version of CreateInstance() only take an IID
createi_resp = client.call(remfactory, 3, params, ipid=ifactory_objref.std.ipid)
createi_stream = ndr.NdrStream(createi_resp)
xorpcthat = gdef.ORPCTHAT32.from_buffer_copy(createi_stream.read(ctypes.sizeof(gdef.ORPCTHAT32)))
xlocalthat = gdef.LOCALTHAT32.from_buffer_copy(createi_stream.read(ctypes.sizeof(gdef.LOCALTHAT32)))
createi_stream.read(4) # Manual NDR Parsing: UNIQU
createi_stream.read(4) # Manual NDR Parsing: Conformant size
tmpbuffer = ctypes.create_string_buffer(createi_stream.data)
mifptr = gdef.MInterfacePointer.from_buffer(tmpbuffer) # Parse tmpbuffer as a MInterfacePointer and extract the objref
obj_ipid = mifptr.objref.std.ipid
3.2 RemQueryInterface
您还可以在远程接口上调用 QueryInterface
。为此,不能使用普通的 ORPC 方法 0
调用。
需要使用 IRemUnknown
26 接口和 RemQueryInterface
27 函数:
-
此接口的 IPID
可以在IScmReplyInfo
中找到:PRIV_RESOLVER_INFO.OxidInfo.ipidRemUnknown
-
此函数需要提供您想要调用 QueryInterface
的 IPID 和一个要查询的IID
列表 -
响应是一个包含 STDOBJREF
的REMQIRESULT
28
以下是一个包含非常简陋的 NDR 解析代码的示例:
# iunk_ipid is a IPID on a IUnknown of our ExaDemoSrv Object.
remunk = client.bind(gdef.IRemUnknown.IID, (0, 0))
target_id = target_id = IID_IExaDemo
params = bytearray(iunk_ipid)[:] + struct.pack("<I", 12) + struct.pack("<I", 1) + struct.pack("<I", 1) + bytearray(target_id)
rem_queryi_response = client.call(remunk, 3, params, ipid=ipidRemUnknown) # Works !
# Parse reponse to RemoteQueryInterface
stream = ndr.NdrStream(rem_queryi_response)
orpcthat = gdef.ORPCTHAT32.from_buffer_copy(stream.read(ctypes.sizeof(gdef.ORPCTHAT32)))
localthat = gdef.LOCALTHAT32.from_buffer_copy(stream.read(ctypes.sizeof(gdef.LOCALTHAT32)))
sream = stream.read(8)
assert sream == b"x00x00x02x00x01x00x00x00", repr(sream)
reminterface = gdef.REMQIRESULT.from_buffer_copy(stream.data)
new_ipid = reminterface.std.ipid
此代码展示了进一步研究的潜力,因为使用不同的 IPID(例如 iunk_ipid
)调用 client.call(remunk, 3, params, ipid=ipid)
可能导致服务器崩溃并引发空指针解引用错误。
4 结论
通过对 combase.dll
的探索,我们理解了其暴露的内部 COM 类并与之交互,找到了访问一些通常被 COM 抽象和隐藏的信息的方法。利用这些信息和 MS-DCOM 开放规范4,我们能够实例化一个 COM 对象并与远程 COM 接口交互,同时绕过部分 COM 运行时逻辑。
此代码已在 Windows 10.0.22631.4317
上测试,并通过使用 _PRIV_RESOLVER_INFO_LEGACY
在 Windows 7 (6.1.7601.0
) 上进行了适配,尽管这需要对 RPC-CALL 缓冲区(LOCALTHIS
)的布局进行调整。
这种技术及其提供的能力可能为探索 Windows 内部机制开辟新的途径,以更好地理解系统、开发新工具并发现潜在的漏洞。
对于 ExaTrack,这将使我们能够改进我们的取证收集代理,并提高我们在 Windows 上识别恶意行为的能力。
我非常欢迎您对文章和代码的反馈,代码已分享在 https://github.com/ExaTrack/COM_IExaDemo 和 https://github.com/hakril/PythonForWindows。如有任何问题或意见,请随时与我们联系!
脚注和参考
https://exatrack.com/recherche_compromission.html
https://learn.microsoft.com/en-us/windows/win32/com/com-technical-overview
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/4a893f3d-bd29-48cd-9f43-d9777a4415b0
https://www.cyber.airbus.com/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
https://googleprojectzero.blogspot.com/2021/10/windows-exploitation-tricks-relaying.html
https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools
https://github.com/ionescu007/hazmat5/tree/main
https://hakril.net/slides/A_view_into_ALPC_RPC_pacsec_2017.pdf
https://github.com/hakril/PythonForWindows?tab=readme-ov-file#alpc-rpc
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/db1d5ce1-a783-4f3d-854c-dc44308e78fb
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/ba4c4d80-ef81-49b4-848f-9714d72b5c01#gt_74540339-daab-46ea-a8f9-fe8fca3b150c
https://github.com/ionescu007/hazmat5/blob/main/lclor.idl#L186
https://gist.github.com/enigma0x3/2e549345e7f0ac88fad130e2444bb702#file-rpc_dump_rs5-txt-L1107
https://github.com/samba-team/samba/blob/master/librpc/idl/dcom.idl#L133
-
https://github.com/tongzx/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/com/ole32/idl/internal/objsrv.idl#L81
-
https://cicada-8.medium.com/process-injection-is-dead-long-live-ihxhelppaneserver-af8f20431b5d
https://github.com/antonioCoco/RemotePotato0/blob/main/RemotePotato0.cpp#L268
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/c5917c4f-aaf5-46de-8667-bad7e495abf9
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/8ad7d21d-5c34-4649-9bc7-5be6fe568245
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/21781a97-cb45-4655-82b0-02c4a1584603
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/fe6c5e46-adf8-4e34-a8de-3f756c875f31
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/98c08086-d94a-443c-b7c6-167e82652885
https://github.com/ExaTrack/COM_IExaDemo/blob/master/stubborn_client.py
https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance#remarks
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/7f621d16-8448-4f9a-9567-793845db2bc7
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/11fd5e3a-f5ef-41cc-b943-45217efdb054
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/1d6a8a54-b115-4148-815a-af0258931948
原文始发于微信公众号(securitainment):STUBborn - 无需代理激活和调用 DCOM 对象
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论