标题 | 简介 | 类型 | 公开时间 | ||||||||||
|
|||||||||||||
|
|||||||||||||
详情 | |||||||||||||
[SAFE-ID: JIWO-2025-3501] 作者: ShYLie 发表于: [2025-02-10]
本文共 [3] 位读者顶过
引言防病毒软件 (AV) 和终端检测响应 (EDR) 产品在企业环境或个人设置中的系统安全中起着关键作用。这些产品旨在保护设备,但它们的广泛使用——特别是在企业中——意味着这些产品中的漏洞可能会对整体安全产生重大影响。我们之前分析了 Wazuh 并发现了可能允许在组织网络中进行横向移动的漏洞。在本系列中,我们将讨论如何在多个安全产品中识别漏洞,这些漏洞理论上可以允许在获得初始访问权限的情况下,在数百万台设备上将权限提升到 SYSTEM。我们将介绍目标安全产品的总体设计,为您提供一些关于允许我们提升权限的机制的背景信息。
我们还在德国汉堡的 38c3 会议上就这篇博客内容进行了演讲。您可以在本博客文章的最后找到演讲 (德语) 和幻灯片 (英语)。 在此研究中我们发现的 CVE 概览
技术背景我们检查的所有安全产品都包含一个用户界面,通常允许用户执行诸如触发文件系统扫描、启动更新或修改设置 (如排除文件) 等操作。例如,设置排除项应该需要较高权限,以防止恶意软件将自己从扫描中排除。然而,用户界面通常在执行它的用户上下文中运行。特别是在企业环境中,这个用户通常缺乏高权限,因为授予此类权限会违反良好的安全实践。 低权限用户如何更改设置?由于用户界面无法直接执行特权操作 (如设置排除项),因此需要一个具有更高权限的单独系统进程来代表用户界面执行这些更改。在我们的分析中,我们将:
为了协调操作,前端进程必须与后端进程通信。根据产品的不同,这种通信通过命名管道、远程过程调用 (RPC) 或组件对象模型 (COM) 接口进行。在我们检查的所有产品中,后端进程都以 SYSTEM 权限运行。 后端通信中的安全风险自然会产生一个担忧:恶意软件是否可能滥用这种通信来执行特权操作?如果恶意软件可以直接与后端进程交互,它可能会利用这个途径来修改注册表或其他敏感设置。 为了缓解这种情况,安全产品通常会验证后端进程发起的操作是否来自可信源。例如,它们可能会检查发起通信的可执行文件的签名。 然而,这种保护措施本身是不够的,因为 Windows 在同一用户账户下运行的进程之间缺乏严格的边界。进程可以读取或写入同一用户上下文中其他进程的内存。它甚至可以在这些进程中执行代码。因此,恶意软件可能会劫持可信进程来滥用其与后端进程的连接。 防止代码注入的保护措施为了解决这个风险,安全供应商实施了额外的保护措施来保护前端进程:
这些防御措施显著降低了不受信任代码注入的风险。 前端和后端进程之间的通信下图说明了前端和后端进程之间通信涉及的组件:
EDR 不同进程之间典型通信涉及的组件概览 与后端进程的通信仍然是一个有吸引力的攻击面。例如,攻击者可以利用它从非特权上下文触发特权操作,如修改注册表。制造商意识到这些风险,并已实施保护措施以防止直接与后端进程通信。然而,之前发现的漏洞,如 Avast 中的漏洞[1,2],已经证明绕过这些保护是可能的。 利用后端通信要滥用后端通信,攻击者必须首先建立与后端进程交互的方式。主要有两种方法:
在我们的研究中,我们采用了第二种方法。使用 COM 劫持,我们成功地向前端进程注入了代码,使我们能够从可信前端内部与后端进程通信。 组件对象模型 (COM) 接口为应用程序提供额外功能,提供了进程间通信和对象重用的框架。例如,Windows Runtime (WinRT) 就是基于 COM 实现的。COM 的一个关键优势是其抽象性:使用 COM 接口的开发人员不需要理解底层实现,这可能是用另一种语言编写的,在单独的进程中执行,或者在分布式 COM (DCOM) 的情况下甚至可能位于远程服务器上。 一些 COM 接口通过在调用接口时动态加载到调用进程中的 DLL 来实现其功能。劫持这样的 COM 接口允许将自定义 DLL 注入到调用进程中,从而能够在进程上下文中执行代码。 要使用 COM 接口,开发人员调用带有 GUID 的 CoCreateInstance,然后这会导致搜索正确的 COM 接口,如果找到接口则返回 COM 对象。下图给出了 TaskScheduler 接口的这种工作方式的高级概述: ![]()
ITaskScheduler COM 对象的 COM 查找示例 COM 劫持的核心思想是利用注册表对 COM 接口定义的搜索顺序。当访问 COM 接口时,系统首先在 HKEY_CURRENT_USER (HKCU) 注册表配置单元中查找其定义,然后再检查 HKEY_LOCAL_MACHINE (HKLM) 配置单元。如果 COM 接口使用 DLL 来提供其功能,注册表条目将包含实现 DLL 的路径。由于 HKCU 配置单元属于当前用户,因此可以由具有该用户权限的进程修改。这意味着在用户上下文中运行的任何进程——包括在我们的非特权用户上下文中运行的 EDR 产品的前端进程——都会优先考虑 HKCU 配置单元中的 COM 定义,并在找到匹配项后停止搜索。下图显示了 COM 劫持前后的注册表访问:
涉及组件的概览 COM 劫持最常被讨论为一种持久性技术。例如,攻击者可以劫持已知会被调用的 COM 接口,确保执行他们的有效载荷。然而,在我们的研究中,我们以不同的方式使用 COM 劫持。我们不是仅仅将其用于持久性,而是专门针对 EDR 产品的前端进程来加载自定义 DLL。这使我们能够在进程上下文中执行代码,在通信期间利用后端进程的提升权限。有趣的是,这种方法对许多 EDR 产品都证明是有效的。过去也有类似的研究,滥用 COM 劫持来绕过类似产品的自我防御[5]。此外,James Forshaw 之前展示了它对 VirtualBox 的使用[3]。 在我们检查的所有 EDR 产品中,前端进程都使用了 COM 接口。这些接口大多位于 HKLM 配置单元下,因此无需覆盖任何数据。但是,覆盖 HKCU 配置单元中的接口也是可能的。 在劫持 COM 接口后,用户上下文中对目标接口的每次调用都会触发我们劫持的 COM 接口。就我们的目的而言,这使我们能够在执行特定操作时 (如在用户界面中打开文件对话框) 将自定义 DLL 加载到前端进程中。 现在我们已经在理论上讨论了 COM 劫持,下一个问题是我们如何识别前端进程中感兴趣的 COM 接口。 我们发现的所有漏洞中,初始步骤都涉及通过 COM 劫持在前端进程中实现代码执行。由于这在我们分析的所有产品中都类似,我们将在这里概述一般过程,而不是为每个特定产品重复。 我们可以看到,每个 COM 查找都是通过与 CLSID(类 ID) 匹配的 GUID 执行的。现在我们可以搜索这些 GUID 并找出产品使用的 COM 对象。 对于每个产品,第一个任务是识别前端进程使用的 COM 接口。 这需要考虑几个因素:
我们使用 SysInternals 套件中的 Process Monitor 来识别相关的 COM 接口。我们首先识别要目标进程。然后,我们使用过滤器只查看此进程触发的事件。接下来,我们创建一个注册表事件过滤器,其中路径包含 CLSID 和 InProcServer32,表明该进程试图加载用于 COM 接口的 DLL。 以下截图演示了 explorer.exe 如何查询相关注册表键,提供了它访问的 COM 接口的洞察:
explorer.exe 对 COM 接口的访问 在识别潜在的 COM 接口后,下一步是确认前端进程是否加载了引用的 DLL。为此,我们监控文件交互并过滤包含 DLL 名称的路径。如果 DLL 被加载,它将触发注册表中指定的 DLL 的加载事件:
加载与 COM 相关的 DLL 一旦识别出合适的接口,下一步就是劫持它。 我们在多个产品中针对的一个注册表键是: Computer\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8} 这个 COM 接口会将 dataexchange.dll 加载到调用进程中。为了劫持这个 DLL,我们首先将其导出: reg export "HKLM\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8}" .\export.reg /reg:64 然后,我们在文本编辑器中打开导出的文件 export.reg,将路径修改为 HKEY_CURRENT_USER。我们还将文件路径更改为指向我们的自定义 DLL: Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8}] [HKEY_CURRENT_USER\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8}\\InProcServer32] @="C:\\\\poc\\\\dataxchange.dll" "ThreadingModel"="Both" 接下来,我们导入修改后的注册表导出文件: reg import .\export.reg /reg:64 通过这些修改,来自我们非特权用户上下文的所有对该 COM 接口的调用都会调用我们的自定义 DLL。这可能会导致其他进程出现问题,因此在完成利用后,我们应该移除这个劫持。 我们的 DLL 必须导出原始 COM DLL 会暴露的函数,以确保操作顺利进行。这可以通过使用以下模板代理调用原始 DLL 来实现: #include <windows.h> #include <combaseapi.h> #pragma comment( linker, "/export:DllGetClassObject" ) #define ORIGINAL_COM_DLL_PATH "C:\\Windows\\System32\\dataxchange.dll" void Go(void) { // Our payload } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return TRUE; } typedef HRESULT(WINAPI * tDllGetClassObject)(REFCLSID rclsid, REFIID riid, LPVOID* ppv); STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID FAR* ppv) { // Start our payload Go(); // Load the original DLL and proxy the function call to it tDllGetClassObject pDllGetClassObject; HMODULE hOrigDLL = LoadLibrary(ORIGINAL_COM_DLL_PATH); pDllGetClassObject = (tDllGetClassObject) GetProcAddress(hOrigDLL, "DllGetClassObject"); if (!pDllGetClassObject) return S_FALSE; HRESULT hRes = pDllGetClassObject(rclsid, riid, ppv); return hRes; } 此时,我们已经在目标产品的上下文中实现了代码执行。因此,下一步是分析前端、和后端进程之间的通信,以了解如何利用这个原语。 Named pipe 通信Named pipe 是服务器和一个或多个客户端之间通信的常用方法。它们可以通过唯一的名称(顾名思义)访问,通常作为安全产品的前端和后端进程之间的通信通道。
通过 WinAPI 进行的典型 Named Pipe 通信 我们发现,确定产品是否使用 named pipe 的最简单方法是使用 IONinja 的 Pipe Monitor 功能。为此,点击"New Session",选择"Pipe Monitor"并启用"Run as administrator"。你可以点击右上角的"Capture"按钮开始捕获 named pipe 流量: ![]()
启动 IONinja
使用 IONinja 监听 named pipe
通过这种方式,你可以与产品的用户界面交互以生成 pipe 流量,并观察与交互相对应的捕获到的 named pipe 流量。根据我们的经验,在原始系统上应该很少有 named pipe 通信,因此如果你在专用系统上安装了产品,识别相关通信应该很简单。
在 IONinja 中识别通信后,我们就有了 pipe 名称和打开或写入 named pipe 的进程。现在我们需要识别逻辑。为此,我们可以查找以 \\.\pipe\ 开头的字符串,这些字符串在创建 named pipe 时使用。与 named pipe 交互的逻辑很可能会引用这个字符串。你还会看到对 CreateNamedPipe 和 ConnectNamedPipe 函数的调用。
对于我们的初始目标,这一切都是不必要的:在通过 named pipe 捕获数据时,我们观察到明文通信,包括看起来像是注册表项的内容:
Named pipe 流量中的注册表路径
下一节将详细介绍我们如何利用这种通信获得高权限。
如上面的截图所示,我们第一个目标的 named pipe 流量包含一个注册表路径,且未经混淆。每次我们打开前端进程时都会发送这条消息。
使用 Process Monitor,我们观察到后端进程以 SYSTEM 权限访问注册表项。这看起来很有希望,因为以 SYSTEM 权限写入注册表项可能导致权限提升...
为了验证这个理论,我们实施了以下步骤:
写入修改后的注册表项
我们发现,我们写入注册表项的能力仅限于制造商指定的注册表路径下。这个限制使我们无法写入像 RunOnce 这样可以实现权限提升的项。
然而,我们发现了一个名为 Application Path 的有希望的注册表项。这个项指向 C:\Program Files (x86) 下的一个应用程序文件夹。通过将此路径修改为我们可写入的路径,我们推测任何从该路径加载的高权限进程都可以执行我们的文件,从而获得高权限。
因此,我们再次修改消息,选择一个无需修改任何偏移量就能适应消息的路径。在将我们的 DLL 注入进程后,我们重放修改后的消息以覆盖 Application Path。系统重启后,我们观察到一个特权 EDR 进程从修改后的 Application Path 执行文件。通过将我们的 payload 放在这个目录中,我们成功获得了 SYSTEM 权限:
从修改后的路径以 SYSTEM 权限启动的进程
这篇博文探讨了与 AV/EDR 前端和后端进程交互相关的攻击面。主要要点是:
|