STUBborn - 无需代理激活和调用 DCOM 对象

admin 2024年11月18日18:28:43评论16 views字数 23331阅读77分46秒阅读模式

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 位进程,在需要时可以跨越“天堂之门”。

STUBborn - 无需代理激活和调用 DCOM 对象

缺少代理 DLL,COM 缺乏某些功能

最终目标是将其应用于生产环境,因此我增加了另一个要求:在可能的情况下,让真正的 COM 机制完成工作,以减少处理从 Windows Server 2003Windows 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.exeIExaDemo_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][stringwchar_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.11PythonForWindows,这是我为在 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(0x457861000x11110x22220x330x330x440x550x000x000x000x01, 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(0x414141410x01010101, 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,它提供了关于RPCSSILocalObjectExporter提供的 RPC 接口的各种信息。

最后,我可以引用我之前关于 RPC&ALPC 的工作9和集成在 PythonForWindows 中的 RPC 客户端10,这在这项研究中得到了改进。

2.2 我们在寻找什么?

基于现状,我们已经可以建立一个操作清单,以便手动与远程IExaDemoIExaDemo_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 端点。

通过使用自定义调试器分析对NtAlpcCreatePortNtAlpcSendWaitReceive的调用,可以绘制出以下图像:

[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 确实使用ILocalObjectExporter13,由端点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

尽管这将是一个有前途的研究角度,并且最终可能会成功,但我有多个理由采取不同的方法。首先,hazmat514ILocalObjectExporter.Connect描绘的原型表明,制作[in]参数和解析[out]将是繁琐的。参数也让我们猜测这个接口自Windows XP以来很可能已经多次演变。

这一点很重要,因为我的最终目标是能够在我们的取证收集代理(法语链接)1中使用STUBborn,以针对一些具有有价值取证信息的 DCOM 端点。因此,我希望代码能够在最少修改的情况下跨越最广泛的 Windows 版本工作。这与我们之前提出的让 COM 运行时为我们完成对象创建的主要工作目标一致。

最后,这是探索combase.dll内部工作原理的一个好理由!

2.4 Combase.dll

combase.dll是实现大部分 COM 逻辑的 DLL,其内容和功能非常值得探索。据我所知,这个 DLL 加载在任何 COM 客户端或服务器中,特别是因为它实现了诸如CoCreateInstanceCoRegisterClassObject等重要功能。在旧版本的 Windows 中,这些逻辑可以在ole32.dll中找到。

2.4.1 gInternalClassObjects

COM 的一个众所周知的行为是,COM 服务器在注册表中注册,CLSID 在HKEY_CLASSES_ROOTCLSID中查找服务器路径。一个不太为人所知的行为是,combase.dll实现了一些类对象作为硬编码的 CLSID,绕过注册表,直接在combase中实现。

这表示为一个(CLSIDCreateInstance_MethodFlags)的数组,可以在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_ComActivator1718,它在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.SetClassInfoIComClassInfo 可以通过两种方法获得:

  • 实现一个自定义的 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() 期间获取实例化的接口指针,作为 MInterfacePointer21

根据文档,MInterfacePointer21 仅仅是一种描述包含 OBJREF22 的可变大小数组的方式,可能用于 NDR 编码。OBJREF22 是 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">

我们获取的接口标识符是IPID12。它代表远程 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", (00))
>>> ipid = interfaces[0][0].objref.std.ipid
>>> data = client.call(iid, 3b"x41x41x41x41x01x01x01x01", ipid=ipid)
>>> data
b'x01x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00BBBBx00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00'

调用成功,我们可以看到结果 0x42424242(即 BBBB)出现在响应缓冲区中,表明我们已经接近成功。同时,IExaDemo_server_64.exe 打印了 [IExaDemoImplem_add] 0x41414141 + 0x1010101 = 0x42424242,这是一个积极的信号。

与 ORPC 调用类似,响应包含 ORPCTHATLOCALTHAT,经过简单解析后,我们得到了结果。

>>> 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.py24 中完整展示了该技术的实现。

代码执行的步骤如下:

  • 实例化 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() 方法进行交互并手动实现 CreateInstance25IPrivActivationPropertiesIn 也提供了 DelegateGetClassObject() 的实现。在这种情况下,您可以请求 IID_IClassFactory,并像之前一样检索 ALPC 端点和 IPID

接下来,您可以使用 ORPC 客户端手动调用 IClassFactory.CreateInstance() 并解析其结果。与标准 COM 函数不同,远程版本的 IClassFactory.CreateInstance() 仅接受 IID 作为参数。

响应将包含一个带有新创建对象的 MInterfacePointer

remfactory = client.bind(gdef.IClassFactory.IID, (00))
# 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 调用。

需要使用 IRemUnknown26 接口和 RemQueryInterface27 函数:

  • 此接口的 IPID 可以在 IScmReplyInfo 中找到:PRIV_RESOLVER_INFO.OxidInfo.ipidRemUnknown
  • 此函数需要提供您想要调用 QueryInterface 的 IPID 和一个要查询的 IID 列表
  • 响应是一个包含 STDOBJREFREMQIRESULT28

以下是一个包含非常简陋的 NDR 解析代码的示例:

# iunk_ipid is a IPID on a IUnknown of our ExaDemoSrv Object.
remunk = client.bind(gdef.IRemUnknown.IID, (00))

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 对象

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月18日18:28:43
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   STUBborn - 无需代理激活和调用 DCOM 对象https://cn-sec.com/archives/3406301.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息