引言
本文将讨论 Windows 中一个以前未命名的漏洞类别,展示在核心 Windows 功能设计中长期存在的错误假设如何导致未定义的行为和安全漏洞。我们将演示如何利用 Windows 11 内核中的此类漏洞来实现具有内核权限的任意代码执行。
Windows 文件共享
当应用程序在 Windows 上打开文件时,它通常使用某种形式的 Win32 CreateFile API。
HANDLE CreateFileW(
[in] LPCWSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
CreateFile 的调用者在 dwDesiredAccess 中指定他们想要的访问权限。例如,调用者将传递 FILE_READ_DATA 以能够读取数据,或者传递 FILE_WRITE_DATA 以能够写入数据。完整的访问权限集在 Microsoft Learn 网站上有文档记录。
除了传递 dwDesiredAccess 之外,调用者还必须在 dwShareMode 中传递“共享模式”,该模式由零个或多个 FILE_SHARE_READ、FILE_SHARE_WRITE 和 FILE_SHARE_DELETE 组成。您可以将共享模式视为调用者声明“我同意其他人在我使用此文件时对其执行 X 操作”,其中 X 可以是读取、写入或重命名。例如,传递 FILE_SHARE_WRITE 的调用者允许其他人在他们使用文件时写入文件。
当打开文件时,调用者的 dwDesiredAccess 将针对所有现有文件句柄的 dwShareMode 进行测试。同时,调用者的 dwShareMode 将针对所有现有文件句柄先前授予的 dwDesiredAccess 进行测试。如果这些测试中的任何一个失败,则 CreateFile 将因共享冲突而失败。
共享不是强制性的。调用者可以传递零的共享模式以获得独占访问权限。根据 Microsoft 文档
未共享的打开文件(dwShareMode 设置为零)无法再次打开,无论是打开它的应用程序还是其他应用程序,都无法打开,直到其句柄已关闭。这也被称为独占访问。
共享强制执行
在内核中,共享由文件系统驱动程序强制执行。当打开文件时,文件系统驱动程序有责任调用 IoCheckShareAccess 或 IoCheckLinkShareAccess 来查看请求的 DesiredAccess/ShareMode 元组是否与打开文件的任何现有句柄兼容。 NTFS 是 Windows 上的主要文件系统,但它是闭源的,因此出于说明目的,我们将改为查看 Microsoft 的 FastFAT 示例代码执行 相同的检查。与 IDA 反编译不同,它甚至附带注释!
//
// Check if the Fcb has the proper share access.
//
return IoCheckShareAccess( *DesiredAccess,
ShareAccess,
FileObject,
&FcbOrDcb->ShareAccess,
FALSE );
除了传统的读/写文件操作之外,Windows 还允许应用程序将文件映射到内存中。在我们深入探讨之前,重要的是要了解 节对象 是内核术语,指代 文件映射;它们是同一件事。本文重点关注内核,因此主要将它们称为节对象。
节对象有两种类型 - 数据节和可执行映像节。数据节是文件到内存的直接 1:1 映射。文件的内容在内存中将与它们在磁盘上的内容完全相同。数据节对于整个内存范围也具有统一的内存权限。关于底层文件,数据节可以是只读的,也可以是读写的。文件的读写视图使进程能够通过读取/写入其自身地址空间内的内存来读取或写入文件的内容。
可执行映像节(有时缩写为映像节)准备 PE 文件 以便执行。映像节必须从 PE 文件创建。PE 文件的示例包括 EXE、DLL、SYS、CPL、SCR 和 OCX 文件。内核专门处理 PE 以准备执行它们。不同的 PE 区域将根据其元数据以不同的页面权限映射到内存中。映像视图是 写时复制,这意味着内存中的任何更改都将保存到进程的私有工作集中 — 永远不会写入支持 PE。
假设应用程序 A 想要使用数据节将文件映射到内存中。首先,它使用 API(例如 ZwCreateFile)打开该文件,该 API 返回文件句柄。接下来,它将此文件句柄传递给 API(例如 ZwCreateSection),该 API 创建一个节对象,该对象描述文件将如何映射到内存中;这会产生一个节句柄。然后,进程使用节句柄将该节的“视图”映射到进程地址空间中,从而完成内存映射。
成功映射文件后,进程 A 可以关闭文件和节句柄,使文件没有打开的句柄。如果进程 B 稍后想要使用该文件而不必担心它被外部修改,它将在打开文件时省略 FILE_SHARE_WRITE。IoCheckLinkShareAccess 查找打开的文件句柄,但由于句柄先前已关闭,因此它不会使操作失败。
这会给文件共享带来问题。进程 B 认为它打开了一个文件而没有外部修改的风险,但进程 A 可以通过内存映射来修改它。为了解决这个问题,文件系统还必须调用 MmDoesFileHaveUserWritableReferences。这将检查是否存在对给定文件的任何活动的、可写的文件映射。我们可以在 FastFAT 示例 此处 看到此检查
//
// Do an extra test for writeable user sections if the user did not allow
// write sharing - this is neccessary since a section may exist with no handles
// open to the file its based against.
//
if ((NodeType( FcbOrDcb ) == FAT_NTC_FCB) &&
!FlagOn( ShareAccess, FILE_SHARE_WRITE ) &&
FlagOn( *DesiredAccess, FILE_EXECUTE | FILE_READ_DATA | FILE_WRITE_DATA | FILE_APPEND_DATA | DELETE | MAXIMUM_ALLOWED ) &&
MmDoesFileHaveUserWritableReferences( &FcbOrDcb->NonPaged->SectionObjectPointers )) {
return STATUS_SHARING_VIOLATION;
}
Windows 要求 PE 文件在运行时是不可变的(不可修改的)。这可以防止 EXE 和 DLL 在内存中运行时在磁盘上被更改。文件系统驱动程序必须使用 MmFlushImageSection 函数来检查在允许 FILE_WRITE_DATA 访问之前是否存在 PE 的任何活动映像映射。我们可以在 FastFAT 示例代码 和 Microsoft Learn 上看到这一点。
//
// If the user wants write access access to the file make sure there
// is not a process mapping this file as an image. Any attempt to
// delete the file will be stopped in fileinfo.c
//
// If the user wants to delete on close, we must check at this
// point though.
//
if (FlagOn(*DesiredAccess, FILE_WRITE_DATA) || DeleteOnClose) {
Fcb->OpenCount += 1;
DecrementFcbOpenCount = TRUE;
if (!MmFlushImageSection( &Fcb->NonPaged->SectionObjectPointers,
MmFlushForWrite )) {
Iosb.Status = DeleteOnClose ? STATUS_CANNOT_DELETE :
STATUS_SHARING_VIOLATION;
try_return( Iosb );
}
}
另一种思考此检查的方式是,只要视图存在,ZwMapViewOfSection(SEC_IMAGE) 就意味着禁止写入共享。
Authenticode
Windows Authenticode 规范 描述了一种使用密码术对 PE 文件进行“签名”的方法。“数字签名”以密码方式证明 PE 是由特定实体生成的。数字签名具有防篡改功能,这意味着对已签名文件的任何重大修改都应该是可检测到的,因为数字签名将不再匹配。数字签名通常附加到 PE 文件的末尾。
在这种情况下,Authenticode 不能应用传统的哈希算法(例如 sha256sum),因为附加签名的行为会更改文件的哈希值,从而破坏刚刚生成的签名。相反,Authenticode 规范描述了一种算法,用于跳过 PE 文件中将在签名过程中更改的特定部分。此算法称为 authentihash。您可以将 authentihash 与任何哈希算法一起使用,例如 SHA256。当对 PE 文件进行数字签名时,实际签名的是文件的 authentihash。
代码完整性
Windows 有几种不同的方法来验证 Authenticode 签名。用户模式应用程序可以调用 WinVerifyTrust 在用户模式下验证文件的签名。代码完整性 (CI) 子系统驻留在 ci.dll
中,它在内核中验证签名。如果 Hypervisor-Protected Code Integrity 正在运行,安全内核将使用 skci.dll
验证 Authenticode。本文将重点介绍常规内核中的代码完整性 (ci.dll
)。
代码完整性提供内核模式代码完整性和用户模式代码完整性,它们各自提供一组不同的功能。
内核模式代码完整性 (KMCI)
- 强制执行 驱动程序签名强制执行 和 易受攻击的驱动程序阻止列表
用户模式代码完整性 (UMCI)
- CI 在允许加载 EXE 和 DLL 之前验证它们的签名
- 强制执行 受保护进程和受保护进程 Light 的签名要求
- 强制执行 ProcessSignaturePolicy 缓解措施 (SetProcessMitigationPolicy)
- 为 FIPS 140-2 模块 强制执行 INTEGRITYCHECK。
- 向消费者公开为 Smart App Control
- 向企业公开为 App Control for Business(以前称为 WDAC)
KMCI 和 UMCI 为不同的场景实施不同的策略。例如,受保护进程的策略与 INTEGRITYCHECK 的策略不同。
错误的假设
微软的 文档 暗示,成功打开且没有写入共享的文件不能被其他用户或进程修改。
FILE_SHARE_WRITE
0x00000002
Enables subsequent open operations on a file or device to request write access. Otherwise, other processes cannot open the file or device if they request write access.
如果未指定此标志,但文件或设备已打开以进行写入访问或具有具有写入访问权限的文件映射,则函数将失败。
上面,我们讨论了文件系统如何强制执行共享,但是如果文件系统不知道文件已被修改,会发生什么情况?
与大多数用户模式内存一样,内核中的内存管理器 (MM) 可能会在它认为必要时(例如,当系统需要更多可用物理内存时)分页出文件映射的部分内容。数据和可执行映像映射都可能被分页出。可执行映像部分永远不会修改支持文件,因此它们在支持 PE 文件方面实际上被视为只读。如前所述,映像部分是写时复制的,这意味着任何内存中的更改都会立即创建给定页面的私有副本。
当内存管理器需要从映像部分分页出一页时,它可以使用以下决策树
- 从未修改过?丢弃它。我们可以从磁盘上的不可变文件中读取内容。
- 修改过?将其私有副本保存到页面文件中。
- 示例:如果安全产品挂钩了
ntdll.dll
中的某个函数,则 MM 将创建每个修改页面的私有副本。在分页出时,私有页面将被写入页面文件。
- 示例:如果安全产品挂钩了
如果稍后接触到这些分页出的页面,CPU 将发出页面错误,MM 将恢复这些页面。
- 页面从未修改过?从磁盘上的不可变文件中读取原始内容。
- 页面是私有的?从页面文件中读取它。
请注意以下例外情况:内存管理器可能会将 PE 重定位的页面视为未修改的,在页面错误期间动态地重新应用重定位。
页面哈希
页面哈希是 PE 文件中每个 4KB 页面的哈希列表。由于页面大小为 4KB,因此页面错误通常发生在一次 4KB 的数据上。完整的 Authenticode 验证需要整个连续的 PE 文件,这在页面错误期间不可用。页面哈希允许 MM 在页面错误期间验证单个页面的哈希。
页面哈希有两种类型,我们称之为静态和动态。如果开发人员将 /ph
传递给 signtool
,则静态页面哈希将存储在 PE 的数字签名中。通过预先计算这些哈希,它们在模块加载时立即可供 MM 和 CI 使用。
CI 还可以 在签名验证期间动态计算它们,我们将这种机制称为动态页面哈希。动态页面哈希使 CI 可以灵活地强制执行页面哈希,即使对于从未使用它们签名的文件也是如此。
页面哈希不是免费的 - 它们会占用 CPU 并减慢页面错误的速度。在大多数情况下,它们不会被使用。
攻击代码完整性
想象一下这样一种情况:勒索软件运营商想要勒索一家医院,因此他们向医院员工发送了一封钓鱼邮件。员工打开电子邮件附件并启用宏,运行勒索软件。勒索软件利用 UAC 绕过立即提升到管理员权限,然后尝试终止系统上的任何安全软件,以便它可以不受阻碍地运行。反恶意软件服务作为 受保护进程 Light (PPL) 运行,保护它们免受具有管理员权限的恶意软件的篡改,因此勒索软件无法终止反恶意软件服务。
如果勒索软件也可以作为 PPL 运行,它就可以终止反恶意软件产品。勒索软件无法直接以 PPL 身份启动自身,因为 UMCI 会阻止签名不正确的 EXE 和 DLL 加载到 PPL 中,正如我们上面讨论的那样。勒索软件可能会尝试通过修改已在运行的 EXE 或 DLL 将代码注入 PPL,但前面提到的 MmFlushImageSection 确保正在使用的 PE 文件保持不可变,因此这是不可能的。
我们之前讨论了文件系统如何负责共享检查。*如果攻击者将文件系统移动到另一台机器上,会发生什么情况?*
网络重定向器 允许在任何接受文件路径的 API 中使用网络路径。这非常方便,允许用户和应用程序轻松地通过网络打开和内存映射文件。任何产生的 I/O 都将透明地重定向到远程机器。如果程序是从网络驱动器启动的,则 EXE 及其 DLL 的可执行映像将透明地从网络中提取。
当使用网络重定向器时,管道另一端的服务器不必是 Windows 机器。它可以是运行 Samba 的 Linux 机器,甚至可以是 Python impacket 脚本,该脚本“说” SMB 网络协议。这意味着服务器不必遵守 Windows 文件系统共享语义。
攻击者可以利用网络重定向器在服务器端修改 PPL 的 DLL,绕过共享限制。这意味着支持可执行映像部分的 PE 被错误地假设为不可变的。这是一类我们称之为**错误文件不可变性** (FFI) 的漏洞。
分页利用
如果攻击者成功利用错误文件不可变性将代码注入正在使用的 PE 中,页面哈希难道不会捕获到这样的攻击吗?答案是:有时会。如果我们查看下表,我们可以看到对内核驱动程序和受保护进程强制执行页面哈希,但对 PPL 不强制执行,所以让我们假设我们是一个针对 PPL 的攻击者。
Authenticode | 页面哈希 | |
---|---|---|
内核驱动程序 | ✅ | ✅ |
受保护进程 (PP-Full) | ✅ | ✅ |
受保护进程 Light (PPL) | ✅ | ❌ |
去年在 2023 年亚洲黑帽大会上(摘要、幻灯片、录音),我们披露了 Windows 内核中的一个漏洞,展示了分页中的错误假设如何被利用将代码注入 PPL,从而破坏诸如 LSA 和 反恶意软件进程保护 等安全功能。正如我们刚才描述的那样,攻击利用了 PPL 中 DLL 的错误文件不可变性假设,尽管我们还没有为此类漏洞命名。
除了演示文稿之外,我们还发布了 PPLFault 利用程序,它通过转储受保护 PPL 的内存来演示该漏洞。我们还发布了 GodFault 利用链,它将 PPLFault Admin-to-PPL 利用程序与 AngryOrchard PPL-to-kernel 利用程序相结合,以实现从用户模式对物理内存的完全读/写控制。我们这样做是为了促使微软对 MSRC 拒绝修复 的漏洞采取行动,因为它不符合他们的 服务标准。值得庆幸的是,微软的 Windows Defender 团队挺身而出,在 2024 年 2 月 发布了一个修复程序,该修复程序对通过网络重定向器加载的可执行映像强制执行动态页面哈希,从而破坏了 PPLFault。
新研究
上面,我们讨论了嵌入在 PE 文件中的 Authenticode 签名。除了嵌入式签名之外,Windows 还支持一种称为 安全目录 的分离式签名。安全目录(.cat 文件)本质上是一个已签名身份验证哈希列表。该列表中具有身份验证哈希的每个 PE 都被视为由该签名者签名。Windows 在 C:\Windows\System32\CatRoot
中保存了大量目录文件,CI 会加载、验证和缓存这些文件。
一个典型的 Windows 系统有超过一千个目录文件,其中许多文件包含数十或数百个身份验证哈希。
要使用安全目录,代码完整性必须首先加载它。这发生在几个离散的步骤中。首先,CI 使用 ZwOpenFile、ZwCreateSection 和 ZwMapViewOfSection 将文件映射到内核内存中。一旦映射,它就会使用 CI!MinCrypK_VerifySignedDataKModeEx 验证目录的数字签名。如果签名有效,它将使用 CI!I_MapFileHashes 解析哈希。
将其分解,我们看到了一些关键的见解。首先,ZwCreateSection(SEC_COMMIT) 告诉我们 CI 正在创建数据部分,而不是映像部分。这很重要,因为数据部分没有页面哈希的概念。
接下来,文件在没有 FILE_SHARE_WRITE 的情况下打开,这意味着拒绝写入共享。这是为了防止在处理期间修改安全目录。然而,正如我们上面所示,这是一个错误的假设,也是错误文件不可变性的另一个例子。理论上,应该可以对安全目录处理执行类似 PPLFault 的攻击。
计划攻击
攻击的一般流程如下
- 攻击者会在其控制的存储设备上植入安全目录。他们会在
CatRoot
目录中安装指向此目录的符号链接,以便 Windows 知道在哪里找到它。 - 攻击者要求内核加载一个恶意的未签名内核驱动程序。
- 代码完整性尝试验证驱动程序,但找不到签名或受信任的 Authentihash,因此它重新扫描 CatRoot 目录并找到攻击者的新目录。
- CI 将目录映射到内核内存并验证其签名。这会生成页面错误,这些错误会被发送到攻击者的存储设备。存储设备返回一个合法的 Microsoft 签名目录。
- 攻击者清空系统工作集,强制丢弃所有先前获取的目录页面。
- CI 开始解析目录,生成新的页面错误。这次,存储设备注入了其恶意驱动程序的 Authentihash。
- CI 在目录中找到恶意驱动程序的 Authentihash 并加载驱动程序。此时,攻击者已在内核中实现了任意代码执行。
实现和注意事项
计划是使用 PPLFault 样式的攻击,但在这种情况下有一些重要的区别。PPLFault 使用了机会锁 (oplock) 来确定性地冻结受害者进程的初始化。这给了攻击者时间切换到有效负载并刷新系统工作集。不幸的是,我们在这里找不到任何使用机会锁的好机会。相反,我们将采用一种概率方法:在恶意版本和良性版本之间快速切换安全目录。
验证步骤会触及目录的每一页,这意味着在开始解析时,所有这些页面都将驻留在内存中。如果攻击者更改了存储设备上的目录,则在随后的页面错误之后,内存才会反映此更改。要从内核内存中逐出这些页面,攻击者必须在 MinCrypK_VerifySignedDataKModeEx 和 I_MapFileHashes 之间清空工作集。
这种方法本质上是一种竞争条件。签名验证和目录解析之间没有内置的延迟 - 这是一场激烈的竞争。我们需要采用多种技术来扩大我们的机会窗口。
系统上的大多数安全目录都很小,只有几千字节。通过选择一个 4MB 的大目录,我们可以大大增加 CI 花费在解析上的时间。假设目录解析是线性的,我们可以选择目录末尾附近的 Authentihash,以最大限度地延长签名验证和 CI 到达我们篡改页面之间的时间。此外,我们将为系统上的每个 CPU 创建线程,其唯一目的是消耗 CPU 周期。这些线程以比 CI 更高的优先级运行,因此 CI 将缺乏 CPU 时间。将有一个线程专门用于反复从系统工作集中刷新页面,还有一个线程反复尝试加载未签名的驱动程序。
这种攻击有两个主要的失败模式。首先,如果在签名检查期间读取了有效负载 Authentihash,则签名将无效,目录将被拒绝。
其次,如果在签名验证和解析之间发生偶数次切换(包括零次),则 CI 将解析良性哈希并拒绝我们的驱动程序。
如果 CI 验证了一个良性目录,然后解析了一个恶意目录,则攻击者获胜。
漏洞利用演示
我们将漏洞利用命名为 ItsNotASecurityBoundary,以向 MSRC 的“管理员到内核不是安全边界”策略致敬。代码在 GitHub 这里。
演示视频 这里。
了解这些漏洞
为了正确防御这些漏洞,我们首先需要更好地了解它们。
当受害者代码多次从攻击者控制的缓冲区中读取相同的值时,可能会发生双重读取(也称为双重获取)漏洞。攻击者可能会在读取之间更改此缓冲区的值,从而导致意外的受害者行为。
假设有两个进程之间共享一个内存页面用于 IPC 机制。客户端和服务器使用以下结构来回发送数据。要发送 IPC 请求,客户端首先将请求结构写入共享内存页面,然后发出事件信号以通知服务器有待处理的请求。
struct IPC_PACKET
{
SIZE_T length;
UCHAR data[];
};
双重读取攻击可能如下所示
首先,攻击客户端将数据包的结构长度字段设置为 16 字节,然后向服务器发出信号,指示数据包已准备好进行处理。受害者服务器唤醒并使用 malloc(pPacket->length)
分配一个 16 字节的缓冲区。之后,攻击者立即将长度字段更改为 32。接下来,受害者服务器尝试通过调用 memcpy(pBuffer, pPacket->data, pPacket->length)
将数据包的内容复制到新缓冲区中,重新读取 pPacket->length
中的值,该值现在为 32。受害者最终将 32 字节复制到 16 字节的缓冲区中,导致缓冲区溢出。
双重读取漏洞经常出现在共享内存场景中。它们通常发生在对用户可写缓冲区进行操作的驱动程序中。由于文件不可变性的错误,开发人员需要注意,它们的范围实际上要宽得多,包括攻击者可写的所有文件。拒绝写入共享并不一定能阻止文件修改。
受影响的操作
哪些类型的操作会受到文件不可变性错误的影响?
操作 | API | 缓解措施 |
---|---|---|
映像节 | CreateProcess LoadLibrary | 1. 启用页面哈希 |
数据节 | MapViewOfFile ZwMapViewOfSection | 1. 避免双重读取\ 2. 在处理之前将文件复制到堆缓冲区\ 3. 通过 MmProbeAndLockPages/VirtualLock 防止分页 |
常规 I/O | ReadFile ZwReadFile | 1. 避免双重读取\ 2. 在处理之前将文件复制到堆缓冲区 |
还有什么可能容易受到攻击?
在 NT 内核中查找对 ZwMapViewOfSection 的潜在易受攻击的调用会产生很多有趣的函数
如果我们将搜索范围扩展到常规文件 I/O,我们会发现更多的候选对象。但是,一个重要的警告是,ZwReadFile 的用途可能不仅仅是文件。只有在文件上使用(或可以强制在文件上操作)才可能容易受到攻击。
在 NT 内核之外,我们可以找到其他驱动程序来调查
不要忘记用户模式
到目前为止,我们主要讨论的是内核,但需要注意的是,任何对攻击者可控文件调用 ReadFile、MapViewOfFile 或 LoadLibrary 的用户模式应用程序,拒绝写入共享以获得不可变性,都可能容易受到攻击。以下是一些假设的例子。
MapViewOfFile
想象一个应用程序,它被分成两个组件 - 一个具有网络访问权限的低权限工作进程和一个安装更新的特权服务。工作进程下载更新并将它们暂存到特定文件夹。当特权服务看到暂存的新更新时,它会在安装更新之前首先验证签名。攻击者可能会滥用 FFI 在签名检查后修改更新。
ReadFile
由于文件容易受到双重读取漏洞的攻击,因此任何解析复杂文件格式的内容都可能容易受到攻击,包括防病毒引擎和搜索索引器。
LoadLibrary
某些应用程序依赖 UMCI 来防止攻击者将恶意 DLL 加载到其进程中。正如我们在 PPLFault 中展示的那样,FFI 可以击败 UMCI。
阻止漏洞利用
根据其官方服务指南,MSRC 默认情况下不会修复管理员 -> 内核漏洞。用这种说法,服务意味着“通过安全更新修复”。然而,这种类型的漏洞允许恶意软件绕过反恶意软件服务保护,使反恶意软件和 EDR 容易受到即时杀死攻击。
作为第三方,我们无法修补代码完整性,那么我们能做些什么来保护我们的客户呢?为了缓解 ItsNotASecurityBoundary,我们创建了 FineButWeCanStillEasilyStopIt,这是一个文件系统微过滤器驱动程序,可防止代码完整性通过网络重定向器打开安全目录。你可以在 GitHub 这里找到它。
FineButWeCanStillEasilyStopIt 必须克服一些障碍才能正确识别问题行为,同时最大限度地减少误报。理想情况下,CI 本身可以通过一些小的更改来修复。让我们看看这需要什么。
如上文“受影响的操作”部分所述,应用程序可以通过将文件内容从文件映射复制到堆中,并专门使用该堆副本进行所有后续操作来缓解双重读取漏洞。内核堆称为池,相应的分配函数是 ExAllocatePool。
另一种打破此类漏洞利用的缓解策略是使用 MmProbeAndLockPages 等 API 将文件映射的页面固定到物理内存中。这可以防止攻击者清空工作集时逐出这些页面。
最终用户检测和缓解
幸运的是,最终用户有一种方法可以在无需 Microsoft 更改的情况下缓解此漏洞利用 – 虚拟机监控程序保护的代码完整性 (HVCI)。如果启用了 HVCI,则 CI.dll 根本不会进行目录解析。相反,它会将目录内容发送到安全内核,该内核在同一主机上的单独虚拟机中运行。安全内核将其接收到的目录内容存储在其自己的堆中,签名验证和解析将从此堆中执行。就像上面描述的 ExAllocatePool 缓解措施一样,该漏洞利用得到了缓解,因为文件更改不会影响堆副本。
这种攻击的概率性质意味着很可能有很多次失败的尝试。Windows 会在 Microsoft-Windows-CodeIntegrity/Operational 事件日志中记录这些失败。用户可以检查此日志以查找漏洞利用的证据。
披露
披露时间线如下
- 2024-02-14:我们将 ItsNotASecurityBoundary 和 FineButWeCanStillEasilyStopIt 作为 VULN-119340 报告给 MSRC,建议将 ExAllocatePool 和 MmProbeAndLockPages 作为简单且低风险的修复方法
- 2024-02-29:Windows Defender 团队联系我们以协调披露
- 2024-04-23:Microsoft 发布了带有 MmProbeAndLockPages 修复的KB5036980 预览版
- 2024-05-14:修复程序作为KB5037771 进入 Windows 11 23H2 的正式版;我们尚未测试任何其他平台(Win10、Server 等)。
- 2024-06-14:MSRC 结案,声明“我们已完成调查,并确定该案例目前不符合我们的服务标准。因此,我们已为此问题打开了一个下一版本候选错误,并将评估其在即将发布的版本中的情况。再次感谢您与我们分享此报告。”
修复代码完整性
查看 CI!I_MapAndSizeDataFile 的原始实现,我们可以看到旧代码调用了 ZwCreateSection 和 ZwMapViewOfSection
与之形成对比的是新的 CI!CipMapAndSizeDataFileWithMDL,它随后调用了 MmProbeAndLockPages
总结与结论
今天,我们讨论并命名了一个错误类别:文件虚假不变性。我们知道有两个公开的漏洞利用了它,PPLFault 和 ItsNotASecurityBoundary。
PPLFault:管理员 -> PPL [-> 通过 GodFault 提权到内核]
- 利用 CI/MM 中关于映像段的不正确不变性假设
- 报告时间:2022 年 9 月
- 修补时间:2024 年 2 月(约 510 天后)
ItsNotASecurityBoundary:管理员 -> 内核
- 利用 CI 中关于数据段的不正确不变性假设
- 报告时间:2024 年 2 月
- 修补时间:2024 年 5 月(约 90 天后)
如果您正在编写操作文件的 Windows 代码,您需要注意以下事实:即使您拒绝写入共享,这些文件也可能在您处理它们时被修改。请参阅上述“受影响的操作”部分,了解如何保护您自己和您的客户免受此类攻击。
ItsNotASecurityBoundary 并不是文件虚假不变性的终结。还有其他可利用的文件虚假不变性漏洞存在。我和我在 Elastic Security Labs 的同事将继续探索和报告文件虚假不变性及其他问题。我们鼓励您在 X 上关注 @GabrielLandau 和 @ElasticSecLabs。