滥用 VBS 飞地创建规避型恶意软件
目录
前言
基于虚拟化的安全性 (VBS) 是最近安全领域引人注目的一大进步。VBS 可以隔离操作系统关键组件,使 Microsoft 能够通过 Credential Guard 和受虚拟机监控程序保护的代码完整性 (HVCI) 等功能实现显著的安全改进。
在 VBS 启用的功能中, VBS 飞地 功能经常被忽视,这项技术可以隔离进程的某个区域,使其他进程、该进程本身甚至内核都无法访问该区域。
VBS 飞地可以承载各种安全应用程序,Microsoft 使用该技术来实现多项知名服务,包括备受争议的 Recall 功能。此外,Microsoft 支持第三方开发 VBS 飞地并积极推动该技术的采用。
虽然飞地有助于确保系统安全,但是它们对攻击者也很有吸引力, 基于内存的检测和取证可能无法发现在飞地中运行的恶意软件。
我们已经着手研究 VBS 飞地,了解它们如何被用于恶意目的,本博文将详细介绍我们的主要发现。我们将深入探讨 VBS 飞地,探索以前未记录的行为,描述攻击者在飞地中运行恶意代码的不同场景,并研究“飞地恶意软件”可能使用的各种技术。
此外,我们还将介绍 Mirage,这是一种基于新方法的内存规避技术,我们称之为“存在漏洞的自带飞地”。我们将探讨攻击者如何使用存在漏洞的旧版本合法飞地来实施这种隐蔽的规避技术。
虚拟信任级别
传统上,Windows 依靠处理器 Ring 级别来防止用户应用程序篡改操作系统。此硬件功能支持将操作系统与用户应用程序分隔开来,即内核在 ring0 中运行,与 ring3 用户模式应用程序隔离。这种方法的问题在于,它为攻击者提供了一种相对容易入侵操作系统的途径:内核漏洞。
Windows 内核暴露了非常丰富的攻击面。大量的第三方驱动程序以及内核本身暴露的各种服务导致内核漏洞层出不穷。 利用这种漏洞,攻击者可以控制操作系统的方方面面。事实证明, 用户/内核 模式的安全边界是不够的。
为了弥补这一差距,Microsoft 以虚拟信任级别 (VTL) 的形式在操作系统中引入了额外的安全边界。VTL 权限基于内存访问,每个信任级别为在该级别下运行的实体提供不同的物理内存访问权限。除此之外,这些权限可以确保较低 VTL 无法访问较高 VTL 的内存。
与传统的处理器 Ring 架构类似,VTL 将操作系统分为不同的执行“模式”,从 VTL0 开始,到(可能的)VTL16 为止。与处理器 Ring(其中 ring0 具有最高权限)不同,较高的 VTL 比较低的 VTL 具有更高的权限。
Windows 目前主要使用两种信任级别:VTL0 和 VTL1(也使用 VTL2,但不在本博文的讨论范围内)。VTL0 用于运行传统操作系统组件,包括内核和用户模式应用程序。VTL1 比 VTL0 具有更高的权限,可创建两种新的执行模式: 安全内核模式 和 隔离用户模式。
安全内核模式
安全内核模式是指 ring0 VTL1 执行模式。该模式用于运行安全内核,即在 VTL1 中运行的内核,因此比常规内核具有更高的权限。利用这些权限,安全内核可以对常规内核实施策略,并限制对敏感内存区域的访问。
正如我们提到过的,内核暴露了很大的攻击面,容易受到攻击。通过剥离内核的某些权限并将这些权限授予安全内核,我们可以减少内核遭到入侵的影响。
从理论上讲,安全内核遭到入侵后,攻击者仍然可以彻底入侵系统。尽管如此,这种情况发生的可能性则要小得多,因为安全内核非常狭小,不支持任何第三方驱动程序,大大减少了攻击面。
隔离用户模式
VTL1 还创建了另一种值得关注的执行模式,称为隔离用户模式 (IUM),指的是 ring3 VTL1 执行模式。IUM 用于执行安全进程,这是一种使用 VTL1 内存隔离功能的特殊类型用户模式进程。任何 VTL0 代码(包括常规内核)都无法访问 IUM 的内存。此执行模式是 Credential Guard等基于隔离的功能的基础。
总而言之,VTL0/1 的引入产生了四种执行模式(图 1)。
- Ring0 VTL0 — 常规内核模式
- Ring0 VTL1 — 安全内核模式
- Ring3 VTL0 — 常规用户模式
- Ring3 VTL1 — 隔离用户模式
什么是 VBS 飞地?
通过 IUM 启用的另一个功能是 VBS 飞地。VBS 飞地是驻留在 IUM 的用户模式进程的一部分,我们可以向其中加载称为“飞地模块”的 DLL。
向飞地中加载模块后,飞地就变成了“受信任执行环境”,在 VTL0 中运行的任何程序都无法访问飞地中的数据和代码,因此也就无法篡改或窃取(图 2)。此功能有助于将敏感操作与能够入侵系统的攻击者隔离开来。
用户模式进程可以调用 专用的 Windows API 来创建飞地、向其中加载模块并对其进行初始化。飞地模块以 DLL 的形式出现,专门为此目的所编译。飞地初始化后,托管的用户模式进程将无法访问其内存,只能通过使用 CallEnclave API 调用其模块导出的函数来与其交互。
图 3 是实现此过程的代码示例( Microsoft提供了更详细的示例)。
由于 Microsoft 旨在尽可能限制对 VTL1 的访问,因此向飞地中加载 DLL 需要使用 Microsoft 颁发的特殊证书对其进行正确签名。如果没有此类签名,任何加载模块的尝试都将失败。对飞地模块进行签名的选择权仅委托给受信任的第三方。耐人寻味的是,对于可以加载这些模块的 人员 没有任何限制, 只要经过签名,任何进程都可以向飞地中加载任意模块。
飞地模块旨在充当小型“计算单元”,与系统交互或影响系统的能力非常有限。因此,它们只能使用 一组最少的 API,无法访问大多数操作系统组件。
飞地中可用的 API 是从加载到 VTL1 的专用库中导入的。例如,常规进程依赖 ntdll.dll 库从操作系统请求服务,而飞地模块则使用 vertdll.dll ,一种“替代”ntdll,用于通过系统调用与安全内核进行通信。
飞地恶意软件
飞地恶意软件的概念肯定会吸引攻击者,因为它有两个显著优势:
在隔离的内存区域中运行:在 VTL0 中运行的任何程序(包括 EDR 和分析工具)都无法访问飞地的地址空间,这使得检测变得更加复杂。
无法追踪的 API 调用:EDR 无法监测飞地内部进行的 API 调用。EDR 通过在系统库中放置钩子来监控用户模式下的 API,并使用驱动程序来监控内核中的活动。这些技术无法检测到从飞地触发的 API 调用,因为飞地调用是从 VTL1 执行的,并且不会通过任何“挂钩”VTL0 组件。
图 4 描述了这一优势:可以通过 NTDLL 或内核本身中的钩子来监控调用 Windows API 的常规进程。但是,飞地模块会通过驻留在 VTL1 的 vertdll 并调用安全内核,而这两者 EDR 都无法访问。
认识到这种可能性后,我们开始研究飞地恶意软件的概念。要利用飞地实现此目的,必须解决两个问题:
- 攻击者如何在飞地中执行恶意代码?
- 一旦在飞地中运行,攻击者可以采用哪些技术?
攻击者如何在飞地中执行恶意代码?
如前所述,飞地模块必须使用 Microsoft 颁发的证书进行签名才能加载,这意味着只有获得 Microsoft 批准的实体才能在飞地中执行自己的代码。尽管如此,攻击者仍有一些选择。
首先,攻击者可以依赖操作系统漏洞。例如,Alex Ionescu (为 Winsider Seminars & Solutions 工作)发现的 CVE-2024-49706漏洞,可以使攻击者将未签名的模块加载到飞地中。Microsoft 已修补该漏洞,但活跃的攻击者可能会在未来发现类似漏洞。
第二种直接的方法是获取合法签名,这种方法可行是因为 Microsoft 通过 受信任签名 平台向第三方公开了飞地签名。当然,这并非易事,但高级攻击者也有可能获得对受信任签名实体的访问权限,并为他们自己的飞地进行签名。
除了这两种方法之外,我们还研究了另外两种可能支持攻击者在 VBS 飞地中运行代码的技术:滥用可调试飞地和利用存在漏洞的飞地。
滥用可调试飞地模块
创建 VBS 飞地模块时,开发人员可以将其配置为 可调试。使用此设置编译模块支持对模块进行调试。飞地模块在 VTL1 下运行,因此通常无法对其进行调试,因为调试程序无法访问飞地内存来检索数据或设置断点。图 5 显示了调试程序无法从飞地中的内存地址进行读取的示例。
值得关注的是,当执行可调试的飞地模块时,该模块仍会加载到 VTL1。为了启用调试,安全内核实施了一些异常情况,应用于可调试的飞地模块。例如,当尝试读取此类模块的内存时,常规内核会发出对 SkmmDebugReadWriteMemory 安全内核调用的调用,在执行请求的操作之前验证目标模块是否确实可调试。
如图 6 中的示例所示,加载可调试的飞地模块后,调试程序可成功读取飞地内存。
类似地,VTL0 进程也可以修改可调试飞地模块的内存权限( SkmmDebugProtectVirtualMemory 安全内核调用中实施的异常情况)。
Microsoft 强烈呼吁开发人员不要发布可调试的飞地模块,因为这样做会破坏飞地的核心目的,即将一部分内存与 VTL0 隔离(图 7)。使用可调试模块意味着它处理的数据很容易被泄露。
生产级应用程序运行可调试飞地模块的风险显而易见,但攻击者实际上可以将其用于其他目的,即在 VTL1 中执行未签名的代码。 如果攻击者获得 任何 可调试的已签名飞地模块,他们可以通过执行以下四个步骤来实现 VTL1 代码执行:
- 使用以下函数获取飞地模块中例程的地址: GetProcAddress
- 将例程内存保护更改为 RWX
- 使用任意 shellcode 覆盖例程代码
- 使用以下函数触发例程 CallEnclave
图 8 描述了实现此过程的代码。
从攻击者的角度来看,问题显而易见,这是一把双刃剑,正如攻击者可以访问飞地内存一样,EDR 也可以访问。尽管如此,它确实具有逃避 API 监控的优势,飞地模块执行的 API 调用仍将通过 VTL1 DLL 和安全内核,从而限制了 EDR 的监测能力。
总而言之,这种方法可能有助于创建隐蔽的“半 VTL1”植入程序,利用在飞地中运行的一些好处。
我们曾尝试使用 VirusTotal 和其他源来识别可调试的已签名飞地模块,但截至撰写本文时,我们尚未成功。不过,我们相信,只要有足够的时间,飞地技术得到更广泛的采用,最终必定会发生数据泄露。
存在漏洞的自带飞地
正如我们所讨论的,Windows 使用签名来防止将不受信任的飞地加载到 VTL1。这种方法并非飞地所独有, 驱动程序强制签名 (DSE) 中也出现了这一概念,DSE 可以防止不受信任的驱动程序在 Windows 内核中运行。
为了突破这种强制机制,攻击者开始使用 存在漏洞的自带驱动程序 (BYOVD) 技术,即攻击者无法加载自己的驱动程序,因此他们改为加载具有已知漏洞的合法已签名驱动程序。然后,他们可以利用此漏洞在内核中运行未签名代码。
我们希望在飞地的上下文中探索这种方法:我们能否滥用存在漏洞的已签名飞地模块在 IUM 中执行代码?
第一步是找到这样的飞地,这让我们很快就找到了 CVE-2023-36880 ,这是 Microsoft Edge 使用的 VBS 飞地模块中的一个漏洞。该漏洞使攻击者能够在飞地中读取和写入任意数据。虽然 Microsoft 将该漏洞标记为信息泄露漏洞,但说明中也指出该漏洞可能导致受限的代码执行(图 9)。
Chrome 安全团队的 Alex Gough 发现了该漏洞,他还分享了利用该漏洞的 概念验证 。在 VirusTotal中发现存在漏洞的此飞地版本后,我们开始尝试利用该漏洞来执行代码。
我们的想法是滥用读取/写入基元,用 ROP 链覆盖飞地堆栈,最终在飞地中执行 shellcode。在探索此方法时,我们发现了一个值得关注的情况,飞地使用 任意代码防护 (ACG)防止未签名的代码执行。
ACG 是一种安全缓解措施,旨在阻止动态生成代码的执行,即代码是在运行时所创建,而非原始进程可执行文件或其 DLL 的一部分。ACG 通过强制执行两个规则来实现以下两点:
- 在初始加载进程后无法生成新的可执行页面
- 已经可执行的页面无法变为可写页面
默认情况下,ACG 在常规内核上强制执行,但在用户模式下,仅适用于配置为可使用它的进程。我们的研究表明,对于包括飞地在内的 IUM 进程,ACG 似乎会自动应用,就像在常规内核上一样。
我们尝试使用 VirtualAlloc 在飞地中分配新的 RWX 页面来观察这一点,操作失败,错误代码为 0x677, STATUS_DYNAMIC_CODE_BLOCKED (图 10)。尝试使用 VirtualProtect 修改可执行页面权限或将页面变为可执行文件将导致相同的结果。
为了理解这种行为,我们可以检查 SecureKernel!NtAllocateVirtualMemoryEx,这是处理 IUM 中动态内存分配的安全内核函数。该函数评估请求的保护掩码,如果请求可执行页面,则返回 ACG 错误 STATUS_DYNAMIC_CODE_BLOCKED。在 SkmmProtectVirtualMemory 中实施了类似的检查,以防止对现有 IUM 页面进行更改(图 11)。
我们还没有找到一种在飞地中绕过 ACG 并将未签名代码加载到其中的方法。理论上,完全 ROP 攻击是可行的,例如,可以使攻击者调用 VTL1 的任意 API,但我们没有朝这个方向深究。尽管如此,我们仍然设法为存在漏洞的飞地找到了另一种值得关注的应用,我们将 在本博文的稍后部分中进行讨论。
一旦在飞地中运行,攻击者可以采用哪些技术?
了解到飞地可能存在大量恶意活动,并且活跃的攻击者可能会加以利用,我们接下来要解决的问题是,此类恶意软件可以使用哪些技术。
最直接的使用方法是按照飞地的本意来使用。正如飞地可以保护敏感数据免遭攻击者窃取一样,攻击者也可以利用飞地向 VTL0 实体隐藏自己的“秘密”。
这在很多场景下都很有用,例如将有效负载存储在 EDR 检测不到的位置,将加密密钥密封起来不让分析师发现,或将敏感的恶意软件配置放置在内存转储之外。
至于更高级的方法,许多传统恶意软件技术都无法在飞地中实现。由于飞地仅限于受限的系统 API 子集,因此无法与文件、注册表、网络、其他进程等关键操作系统组件交互。
尽管如此,仍有许多技术可以在飞地中执行,让我们能够利用在 IUM 中运行所获得的好处。
访问 VTL0 用户模式内存
尽管飞地对系统的访问权限有限,但它们仍然可以访问一个关键资源:进程内存。飞地可以在整个进程地址空间(包括 VTL0)内执行读取/写入操作。
这种访问有一些限制,在飞地中运行的代码必须遵循内存权限,并且不能更改这些权限。这意味着飞地将无法写入不可写内存,也无法将不可执行内存变为可执行内存。图 12 描述了在飞地中运行的代码,并演示了不同的可能性和限制。
通过访问 VTL0 用户模式内存,我们可以实现各种有用的技术。 通过将恶意飞地加载到目标进程中,我们可以隐蔽地监控和窃取敏感信息,或修补进程中的值以更改其行为。
如前所述,利用飞地实现这些技术有一个显著优势,从飞地触发 API 可以逃避 EDR 的监控。由于在这些技术下进行的内存访问由飞地执行,因此仍然无法监测。
执行 VTL0 用户模式代码
虽然飞地可以在获得适当权限的情况下读取/写入 VTL0 用户模式内存,但存储在 VTL0 的代码永远无法在飞地中执行,即使该代码具有执行权限也是如此。
尽管无法在飞地中运行 VTL0 代码,但飞地可以选择“远程”触发该代码。通过使用带有 VTL0 地址的 CallEnclave API,飞地可以触发常规用户模式线程中 VTL0 代码的执行(图 13)。
通过让用户模式进程“代其行事”,飞地可以间接访问系统,而通常情况下,飞地无法访问系统。例如,飞地可以触发可读取文件、创建套接字等的 VTL0 例程。
请务必注意,以这种方式运行用户模式代码在规避方面没有优势,因为此类代码的运行方式与任何其他用户模式代码一样,所以 EDR 可以监测得到。
反调试
飞地恶意软件另一个值得关注的应用是反调试。事实上,VTL0 应用程序(包括调试程序)无法访问在飞地中运行的代码,这为恶意软件提供了显著的优势。
暴露给飞地的 API 减少意味着并非所有传统的反调试技术都适用于飞地。例如, NtQueryInformationProcess 或 IsDebuggerPresent API 以及所有日期和时间 API 都不适用于飞地。尽管如此,我们仍有一些选择。
首先,可以依赖飞地对进程的 VTL0 地址空间的访问权限,该权限支持其手动读取进程 PEB 并检查“BeingDebugged”标志的值。如果检测到调试程序,飞地可以终止该进程。
另一种方法是实施基于时间的反调试技术(图 14)。虽然飞地无法使用日期和时间 API,但仍可使用 rdtsc 汇编指令。该指令返回自启动以来的处理器时钟周期数。利用此数据,我们可以测量不同飞地调用之间所花费的时间,并在检测到显著延迟时终止该进程。
通过将代码的关键部分与反调试检查一起移至飞地, 我们可以制作一个几乎完全不受动态分析影响的恶意软件。 恶意软件依靠飞地正常运行,而用户模式进程却无法修补飞地中运行的检查。如果实施得当,这种方法只能通过调试 Hyper-V 或安全内核才能破解。
BYOVE — 第二轮
在上一节中,我们尝试利用存在漏洞的飞地模块在 IUM 中实现代码执行。在意识到这可能无法实现时,我们想看看 BYOVE 的概念是否还可以有其他应用,决定进一步探索存在漏洞的飞地模块。
该漏洞 (CVE-2023-36880) 源自 SealSettings 和 UnsealSettings 函数(由 飞地模块导出)。其中, SealSettings 函数接收指向数据缓冲区的指针,对其进行加密,并将结果写入调用方提供的目标地址。 UnsealSettings 的运行方式类似,解密提供的缓冲区并将其写入指定地址。
这两个函数的问题在于,它们不会验证目标地址或源缓冲区地址,因此可以指向进程中的任何地址, 包括飞地本身内部的地址。图 15 描述了存在漏洞的代码,其中显示了 UnsealSettings 对用户提供的任意地址执行 memcpy 。
该漏洞为攻击者实现了两种功能(图 16)。
在飞地中任意写入::攻击者可以调用 SealSettings 来加密任意数据,然后调用 UnsealSettings 指向飞地中的目标地址。这将导致原始数据被写入飞地内存。
在飞地中任意读取::攻击者可以调用 SealSettings,同时提供飞地中的地址作为源缓冲区指针。这将导致飞地加密来自飞地内存的数据并将其写入攻击者控制的位置。然后,攻击者可以通过调用 UnsealSettings来解密这些数据,从而实现从飞地中读取任意数据。
Mirage — 基于 VTL1 的内存规避
尽管它无法让我们在 VTL1 中执行代码,但任意写入基元确实提供了两种独特的功能:
将任意数据存储在 VTL1 中,而 VTL0 实体无法访问该数据
从 VTL1将任意数据写入常规进程地址空间 (VTL0),阻止 VTL0 实体监控此操作
此外,由于这些功能通过加载已签名飞地模块来实现, 因此任何攻击者都可以加以利用,而无需自己签名。
为了展示这些功能的潜力,我们提出了一种内存扫描规避技术,称之为“Mirage”。Mirage 的灵感来自于 Gargoyle,一种规避技术,可用于创建在良性状态和武器化状态之间不断切换的有效负载(图 17)。
Gargoyle 通过在可执行和不可执行内存之间切换来实现这一点,而 Mirage 旨在通过在 VTL1 和 VTL0 内存之间转换来实现类似的效果,将 shellcode 存储在 VTL1 飞地内存中,定期使用漏洞将其传输回 VTL0,执行该代码,然后迅速将其从 VTL0 内存中删除(图 18)。
这种方法有两个主要优势。首先,由于有效负载大部分时间都隐藏在 VTL1 中,因此可抵御内存扫描和转储。这比 Gargoyle 技术更有优势,因为在休眠阶段,我们的有效负载不仅“隐蔽”,而且 无法访问。
第二个优势是由飞地来执行将 shellcode 写入 VTL0。如前所述,EDR 无法监控在 VTL1 中执行的代码,这意味着典型的 EDR 钩子无法在将 shellcode 写入内存时进行拦截。
我们为 Mirage 执行了 PoC (图 19)。该 PoC 仅用于演示该技术背后的想法,因此并未完全武器化。
检测
截至目前,只有极少数应用程序使用飞地。即使飞地得到更广泛的采用,也仍将仅由特定进程加载,而不是由任意进程加载。例如, calc.exe 可能永远不会加载 VBS 飞地。
因此,异常的飞地使用可能是一个很好的检测机会。防御者应该加以利用,建立已知的、合法的 VBS 飞地使用基线,并对任何偏离该基线的情况进行标记。可以通过两种方式识别飞地的使用情况:监控飞地 API 和检测已加载的飞地 DLL。
飞地 API
以下 API 用于由托管进程管理 VBS 飞地,并可能表明正在加载飞地:
- CreateEnclave
- LoadEnclaveImageW
- InitializeEnclave
- CallEnclave
- DeleteEnclave
- TerminateEnclave
已加载的飞地 DLL
另一种检测飞地异常使用的方法是检测其通常所用环境 DLL 的加载情况,即 Vertdll.dll 和 ucrtbase_enclave.dll。由于这些 DLL 不应由除飞地之外的任何进程使用,因此它们的存在将表明该进程可能使用了一个飞地。
结论
VBS 飞地为开发人员提供了保护其应用程序敏感部分的绝佳工具,但是正如我们刚才所演示的,攻击者也可以使用飞地来“保护”其恶意软件。虽然目前这一概念还处于理论阶段,但未来高级攻击者确实有可能开始将 VBS 飞地用于恶意目的。
鸣谢
我们对 Matteo Malvica (Offsec) 和 Cedric Van Bockhaven (Outflank) 所做的贡献深表谢意。他们最近进行了一项与此非常相似的研究项目。请阅读他们两部分系列文章中的 第一篇博文 ,并继续关注第二篇,该博文 将于 2025 年度 Insomni’Hack 大会召开后发布。