2023年9月1日,微软发布了 Windows 预览体验计划 Canary 版本的新构建,版本号为 25941。预览体验计划版本是 Windows 的预发布版本,包含实验性功能,这些功能可能最终会发布到正式版 (GA),也可能不会。构建 25941 包含对代码完整性 (CI) 子系统的改进,从而减轻了一个长期存在的问题,该问题使攻击者能够将未签名的代码加载到受保护进程轻量级 (PPL) 进程中。
PPL 机制在 Windows 8.1 中引入,允许使用特殊签名程序以这样一种方式运行:即使是管理进程也无法篡改或终止它们。其目标是防止恶意软件肆虐——篡改关键系统进程并终止反恶意软件应用程序。PPL 存在一个“级别”层次结构,较高权限的级别不受较低权限级别的篡改,反之则不然。大多数 PPL 进程由微软管理,但微软病毒计划 的成员被允许在其不太受信任的反恶意软件 PPL 级别运行其产品。
一些核心 Windows 组件在最高级别的 PPL 中运行,称为 Windows 可信计算基 (WinTcb-Light)。由于这些组件的保护及其有限的功能范围,它们被认为比大多数用户模式代码更可信。这些进程(例如csrss.exe)及其复杂的内核模式对应部分(例如win32k.sys)中的大多数是在几十年前编写的,当时的内核-用户边界甚至比现在还要弱。微软没有重写所有这些组件,而是将这些用户模式进程设为WinTcb-Light,从而减轻了篡改和注入攻击。Alex Ionescu 在 2013 年清楚地阐述了这一点。
由于 Win32k.sys 开发人员没有预料到本地代码注入攻击会成为问题(毕竟这需要管理员权限),因此许多这些 API 甚至没有 SEH,或者存在其他假设和错误。也许最著名的是,其中一个由j00ru 发现,并且至今仍未修复,已被用作 Windows 8 RT 越狱的唯一基础。在Windows 8.1 RT 中,这种越狱被“修复”了,因为代码无法再注入到 Csrss.exe 中进行攻击。类似的依赖于 Csrss.exe 的 Win32k.sys 漏洞也通过这种方式得到缓解。
为了减少攻击面,微软运行其大部分 PPL 代码的权限低于WinTcb-Light。
微软不认为 PPL 是一个安全边界,这意味着他们不会优先处理在此发现的代码执行漏洞的安全补丁,但他们历来处理过一些此类漏洞,但优先级较低。
将代码加载到 PPL 进程中
要将代码加载到 PPL 进程中,它必须由特殊的证书签名。这适用于可执行文件(进程创建)和库(DLL 加载)。为简单起见,我们将重点关注 DLL 加载,但 CI 验证过程对于两者都非常相似。本文重点关注 PPL,因此我们将不讨论内核模式代码完整性。
可移植可执行文件 (PE) 文件有多种扩展名,包括 EXE、DLL、SYS、OCX、CPL 和 SCR。虽然扩展名可能有所不同,但它们在二进制级别都非常相似。对于 PPL 进程加载并执行 DLL,必须采取以下几个步骤。请注意,这些步骤已简化,但对于本文应该足够了。
- 应用程序调用LoadLibrary,传递要加载的 DLL 的路径。
- LoadLibrary 调用 NTDLL 中的加载程序(例如ntdll!LdrLoadDll),该加载程序使用诸如NtCreateFile之类的 API 打开文件句柄。
- 然后,加载程序将此文件句柄传递给NtCreateSection,要求内核内存管理器创建一个节对象,该对象描述如何将文件映射到内存中。节对象在较高的抽象层(例如 Win32)中也称为文件映射对象,但由于我们关注的是内核,因此我们将继续称其为节对象。Windows 加载程序始终使用一种称为可执行映像(又称SEC_IMAGE)的特定类型的节,该节只能从 PE 文件创建。
- 在将节对象返回到用户模式之前,内存管理器会检查文件上的数字签名,以确保它满足给定 PPL 级别要求。内部内存管理器函数MiValidateSectionCreate依赖于代码完整性模块ci.dll来处理必要的加密和PKI策略。
- 内存管理器重新构造 PE,以便可以将其映射到内存中并执行。此步骤涉及创建多个子节,每个子节对应 PE 文件的不同部分,这些部分必须以不同的方式映射。例如,全局变量可能是读写的,而代码可能是可执行的。为了实现这种粒度,生成的内存区域必须具有不同的页表条目,这些条目具有不同的页权限。此处可能还会应用其他更改,例如应用重定位,但它们不在本研究报告的范围内。
- 内核将新的节句柄返回到 NTDLL 中的加载程序。
- 然后,NTDLL 加载程序要求内核内存管理器通过NtMapViewOfSection系统调用将节的视图映射到进程地址空间。内存管理器会执行此操作。
- 映射视图后,加载程序将完成创建内存中功能性 DLL 所需的处理。这方面的详细信息不在本研究报告的范围内。
页面哈希值
在上一步中,我们可以看到在节创建期间会验证 PE 的数字签名,但还有一种方法可以将代码加载到 PPL 进程的地址空间——分页。
属于文件支持节(包括SEC_IMAGE)的未修改页面可以在系统内存不足时快速丢弃,因为磁盘上存在该数据的精确副本。如果以后触摸该页面,CPU 将发出页面错误,而内存管理器的页面错误处理程序将从磁盘重新读取该数据。由于SEC_IMAGE节只能从不可变的文件数据创建,并且签名已验证,因此数据被认为是可信的。
PE 文件可以选择使用 /INTEGRITYCHECK 标记进行构建。这会在 PE 头部设置一个标记,该标记除其他事项外,还会指示内存管理器为 PE 的每个页面(也称为“页面哈希”)创建和存储哈希值,这些页面是从 PE 中创建的节。从磁盘读取页面后,页面错误处理程序会调用 MiValidateInPage 来验证自签名最初验证以来页面哈希是否已更改。如果页面哈希已更改,则处理程序将引发异常。此功能可用于检测页面文件中的 位腐烂 和几种类型的攻击。除了 /INTEGRITYCHECK 映像之外,对于加载到完全受保护的进程(非 PPL)中的所有模块以及加载到内核中的驱动程序,也会 启用 页面哈希。
注意:可以使用具有 用户可写引用 的文件创建 SEC_IMAGE 节,这是诸如 Process Herpaderping 之类的技术所采用的策略。用户可写引用的存在意味着可以在创建映像节后修改文件。当程序尝试使用此类可变文件时,内存管理器首先将文件内容复制到页面文件,为映像节创建一个不可变的备份以防止篡改。在这种情况下,节将不会由原始文件支持,而是由页面文件支持。有关用户可写引用的更多信息,请参阅 这篇 Microsoft 文章。
漏洞利用
2022 年 9 月,来自 Elastic Security 的 Gabriel Landau 向 MSRC 提交了 VULN-074311,通知他们 Windows 中存在两个 零日 漏洞:一个管理员到 PPL 漏洞和一个 PPL 到内核漏洞。提供了针对这些漏洞的两个漏洞利用程序,分别命名为 PPLFault 和 GodFault,以及它们的源代码。这些漏洞利用程序允许恶意软件 绕过 LSA 保护、终止或屏蔽 EDR 软件以及修改内核内存以篡改核心操作系统行为——所有这些都不需要任何易受攻击的驱动程序。有关其影响的更多详细信息,请参阅 这篇文章。
管理员到 PPL 漏洞利用程序 PPLFault 利用了这样一个事实:PPL 的页面哈希未经验证,并使用 云过滤器 API 来违反支持 SEC_IMAGE 节的文件的不可变性假设。PPLFault 使用分页将代码注入到以 WinTcb-Light(PPL 的最高权限形式)运行的 PPL 进程中加载的 DLL 中。PPL 到内核漏洞利用程序 GodFault 首先使用 PPLFault 获取 WinTcb-Light 代码执行,然后利用内核对 WinTcb-Light 进程的信任来修改内核内存,从而授予自身对物理内存的完全读写访问权限。
尽管 MSRC 拒绝 对这些漏洞采取任何行动,但 Windows Defender 团队已 表示关注。PPLFault 和 GodFault 于 2023 年 5 月在 Black Hat Asia 上发布,同时还发布了一个名为 NoFault 的缓解措施来阻止这些漏洞利用程序。
缓解措施
2023 年 9 月 1 日,微软发布了 Windows 预览体验计划 Canary 版本 25941。此版本在内存管理器函数 MiValidateSectionCreate 中添加了一个新的检查,该检查为驻留在远程设备上的所有映像启用页面哈希。通过比较 25941 与其前身 25936,我们可以看到以下两个新的基本块。
反编译成 C 代码后,新的代码如下所示。
运行 PPLFault 时,Windows 错误报告会生成一个事件日志,指示分页操作期间发生故障。
PPLFault 需要其有效负载 DLL 通过 SMB 网络重定向器加载才能实现所需的分页行为。通过强制对这种网络托管的 DLL 使用页面哈希,漏洞利用程序将无法再注入其有效负载,因此漏洞已修复。在 Black Hat 上发布的上述 NoFault 缓解措施也针对网络重定向器,完全阻止此类 DLL 加载到 PPL 中。Elastic Defend 8.9.0 及更高版本阻止 PPLFault——如果您尚未更新,请立即更新。
在内核调试器中追踪故障的确切点,我们可以看到页面错误处理程序调用 CI 来验证页面哈希,这会失败并返回 STATUS_INVALID_IMAGE_HASH (0xC0000428)。这稍后会被转换为 STATUS_IN_PAGE_ERROR (0xC0000006)。
0: kd> g
Breakpoint 1 hit
CI!CiValidateImagePages+0x360:
0010:fffff805`725028b4 b8280400c0 mov eax,0C0000428h
7: kd> k
# Child-SP RetAddr Call Site
00 fffff508`1b4a6dc0 fffff805`72502487 CI!CiValidateImagePages+0x360
01 fffff508`1b4a6f90 fffff805`6f2f1bbd CI!CiValidateImageData+0x27
02 fffff508`1b4a6fd0 fffff805`6ee35de5 nt!SeValidateImageData+0x2d
03 fffff508`1b4a7020 fffff805`6efa167b nt!MiValidateInPage+0x305
04 fffff508`1b4a70d0 fffff805`6ef9fffe nt!MiWaitForInPageComplete+0x31b
05 fffff508`1b4a71d0 fffff805`6ef68692 nt!MiIssueHardFault+0x3fe
06 fffff508`1b4a72e0 fffff805`6f0a784b nt!MmAccessFault+0x3b2
07 fffff508`1b4a7460 00007fff`ccf71500 nt!KiPageFault+0x38b
08 000000b6`776bf1b8 00007fff`d5500ac0 0x00007fff`ccf71500
09 000000b6`776bf1c0 00000000`00000000 0x00007fff`d5500ac0
7: kd> !error C0000428
Error code: (NTSTATUS) 0xc0000428 (3221226536) - Windows cannot verify the
digital signature for this file. A recent hardware or software change
might have installed a file that is signed incorrectly or damaged, or
that might be malicious software from an unknown source.
行为比较
使用在版本 25941 中引入的修复程序,最终的易受攻击版本为 25936。在内核调试器下运行这两个版本的 PPLFault,我们可以使用以下 WinDbg 命令查看 CI 为其计算页面哈希的文件。
bp /w "&CI!CipValidatePageHash == @rcx" CI!CipValidateImageHash
"dt _FILE_OBJECT @r8 FileName; g"
此命令为修复之前的版本 25936 生成以下 WinDbg 输出。
以下是包含修复程序的版本 25941 的 WinDbg 输出。
结论
尽管花费的时间 可能比预期要长,但看到微软采取措施保护 PPL 进程(包括反恶意软件)免受以管理员身份运行的恶意软件的攻击,这令人兴奋,如果此改进很快进入 GA 版本,用户将从中受益。Insider 版本中的许多功能,甚至是安全功能,在(可能永远不会)GA 版本中不可用。微软在可能影响稳定性、兼容性或性能的更改方面非常保守;内存管理器更改属于风险较高的更改类型。例如,去年 11 月在 Insider 版本中发现的 PreviousMode 内核漏洞利用缓解措施 至今仍未进入 GA 版本,即使已经过去了至少 10 个月。
特别感谢 Grzegorz Tworek 帮助我们反向工程一些内核函数。