Gabriel Landau

介绍一种新的漏洞类别:虚假文件不可变性

本文介绍了一种以前未命名的 Windows 漏洞类别,该类别展示了假设的危险,并描述了一些意想不到的安全后果。

阅读时间 28 分钟安全研究, 漏洞更新
Introducing a New Vulnerability Class: False File Immutability

简介

本文将讨论 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_READFILE_SHARE_WRITEFILE_SHARE_DELETE。您可以将共享模式视为调用者声明“我允许其他人在我使用此文件时对它执行 X 操作”,其中 X 可以是读取、写入或重命名。例如,传递 FILE_SHARE_WRITE 的调用者允许其他人在他们使用文件时写入该文件。

当打开文件时,调用者的 dwDesiredAccess 会针对所有现有文件句柄的 dwShareMode 进行测试。同时,调用者的 dwShareMode 会针对该文件所有现有句柄的先前授予的 dwDesiredAccess 进行测试。如果其中任何一个测试失败,则 CreateFile 将失败并出现共享冲突。

共享不是强制性的。调用者可以传递零的共享模式以获得独占访问权限。根据 Microsoft 文档

未共享(dwShareMode 设置为零)的打开文件无法再次打开,无论是打开它的应用程序还是其他应用程序,直到其句柄已关闭。这也称为独占访问。

共享强制执行

在内核中,共享由文件系统驱动程序强制执行。当打开文件时,文件系统驱动程序有责任调用 IoCheckShareAccessIoCheckLinkShareAccess 以查看所请求的 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 想要使用数据节将文件映射到内存中。首先,它使用诸如 ZwCreateFile 之类的 API 打开该文件,该 API 返回文件句柄。接下来,它将此文件句柄传递给诸如 ZwCreateSection 之类的 API,该 API 创建一个描述文件如何映射到内存中的节对象;这将产生一个节句柄。然后,进程使用节句柄将该节的“视图”映射到进程地址空间中,完成内存映射。

成功映射文件后,进程 A 可以关闭文件句柄和节句柄,从而使该文件没有打开的句柄。如果进程 B 稍后想要使用该文件而没有被外部修改的风险,它将在打开文件时省略 FILE_SHARE_WRITEIoCheckLinkShareAccess 查找打开的文件句柄,但由于句柄先前已关闭,因此它不会使操作失败。

这会给文件共享带来问题。进程 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 中,在内核中验证签名。如果 虚拟机监控程序保护的代码完整性 正在运行,安全内核将使用 skci.dll 来验证 Authenticode。本文将重点介绍常规内核中的代码完整性 (ci.dll)。

代码完整性提供了内核模式代码完整性和用户模式代码完整性,每种都服务于不同的功能集。

内核模式代码完整性 (KMCI)

用户模式代码完整性 (UMCI)

KMCI 和 UMCI 针对不同的场景实施不同的策略。例如,受保护进程的策略与 INTEGRITYCHECK 的策略不同。

不正确的假设

Microsoft 文档 暗示,成功打开且没有写入共享的文件不能被其他用户或进程修改。

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 绕过立即提升为管理员权限,然后尝试终止系统上的任何安全软件,以便它可以不受阻碍地运行。反恶意软件服务以 受保护的轻量进程 (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)
受保护的轻量进程 (PPL)

去年在 Black Hat Asia 2023 (摘要幻灯片录音),我们公开了 Windows 内核中的一个漏洞,展示了如何利用分页中的错误假设将代码注入 PPL,从而击败诸如 LSA反恶意软件进程保护 等安全功能。该攻击利用了 PPL 中 DLL 的错误的文件不变性假设,正如我们刚才描述的那样,尽管我们尚未命名漏洞类别。

在演示文稿的同时,我们发布了 PPLFault 漏洞利用,该漏洞通过转储受保护 PPL 的内存来演示该漏洞。我们还发布了 GodFault 漏洞利用链,该漏洞利用链将 PPLFault 从管理员到 PPL 的漏洞利用与 AngryOrchard 从 PPL 到内核的漏洞利用相结合,以实现从用户模式对物理内存的完全读/写控制。我们这样做是为了促使 Microsoft 对 MSRC 拒绝修复的漏洞采取行动,因为它不符合他们的 服务标准。值得庆幸的是,微软的 Windows Defender 团队站了出来,在 2024 年 2 月 发布了一个修复程序,该程序强制对通过网络重定向器加载的可执行映像执行动态页面哈希,从而破坏 PPLFault。

新研究

上面,我们讨论了嵌入在 PE 文件中的 Authenticode 签名。除了嵌入签名外,Windows 还支持一种称为 安全编录 的分离签名形式。安全编录(.cat 文件)本质上是已签名身份验证哈希的列表。该列表中具有身份验证哈希的每个 PE 都被认为是由该签名者签名的。Windows 在 C:\Windows\System32\CatRoot 中保留大量编录文件,CI 会加载、验证和缓存这些文件。

典型的 Windows 系统具有一千多个编录文件,其中许多包含数十个或数百个身份验证哈希。

要使用安全编录,代码完整性必须首先加载它。这发生在几个离散的步骤中。首先,CI 使用 ZwOpenFileZwCreateSectionZwMapViewOfSection 将文件映射到内核内存中。映射后,它使用 CI!MinCrypK_VerifySignedDataKModeEx 验证编录的数字签名。如果签名有效,它将使用 CI!I_MapFileHashes 解析哈希。

分解开来,我们看到了一些关键的见解。首先,ZwCreateSection(SEC_COMMIT) 告诉我们 CI 正在创建一个数据段,而不是映像段。这很重要,因为数据段没有页面哈希的概念。

接下来,该文件在没有 FILE_SHARE_WRITE 的情况下打开,这意味着拒绝写入共享。 这是为了防止在处理过程中修改安全编录。 然而,正如我们在上面展示的那样,这是一个错误的假设,并且是文件不可变性的另一个错误示例。 理论上,应该可以对安全编录处理执行 PPLFault 风格的攻击。

攻击计划

攻击的总体流程如下

  1. 攻击者将在他们控制的存储设备上植入一个安全编录。 他们将在 CatRoot 目录中安装指向此编录的符号链接,以便 Windows 知道在哪里找到它。
  2. 攻击者请求内核加载一个恶意的未签名内核驱动程序。
  3. 代码完整性尝试验证驱动程序,但它找不到签名或受信任的 authentihash,因此它会重新扫描 CatRoot 目录并找到攻击者的新编录。
  4. CI 将编录映射到内核内存中并验证其签名。 这会生成页面错误,这些错误将发送到攻击者的存储设备。 存储设备返回一个合法的微软签名的编录。
  5. 攻击者清空系统工作集,强制丢弃所有先前获取的编录页面。
  6. CI 开始解析编录,生成新的页面错误。 这次,存储设备注入其恶意驱动程序的 authentihash。
  7. CI 在编录中找到恶意驱动程序的 authentihash 并加载驱动程序。 此时,攻击者已在内核中实现任意代码执行。

实现和注意事项

计划是使用 PPLFault 风格的攻击,但这种情况有一些重要的区别。 PPLFault 使用 机会锁 (oplock) 来确定性地冻结受害者进程的初始化。 这给了攻击者时间切换到有效负载并刷新系统工作集。 不幸的是,我们在这里找不到任何使用 oplocks 的好机会。 相反,我们将采用一种概率方法:在恶意版本和良性版本之间快速切换安全编录。

验证步骤会触及编录的每个页面,这意味着在解析开始时,所有这些页面都将驻留在内存中。 如果攻击者更改其存储设备上的编录,则只有在发生后续页面错误后,它才会反映在内存中。 要将这些页面从内核内存中逐出,攻击者必须在 MinCrypK_VerifySignedDataKModeExI_MapFileHashes 之间清空工作集。

这种方法本质上是一种竞争条件。 签名验证和编录解析之间没有内置延迟 - 这是一场激烈的竞争。 我们需要采用多种技术来扩大我们的机会窗口。

系统上的大多数安全编录都很小,只有几 KB。 通过选择一个大的 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 LoadLibrary1. 启用页面哈希
数据节MapViewOfFile ZwMapViewOfSection1. 避免双重读取\ 2. 在处理之前将文件复制到堆缓冲区\ 3. 通过 MmProbeAndLockPages/VirtualLock 阻止分页
常规 I/OReadFile ZwReadFile1. 避免双重读取\ 2. 在处理之前将文件复制到堆缓冲区

还有哪些可能存在漏洞?

在 NT 内核中查找可能存在漏洞的 ZwMapViewOfSection 调用会产生许多有趣的函数

如果我们将搜索范围扩大到常规文件 I/O,我们会发现更多的候选者。 然而,一个重要的警告是,ZwReadFile 可能不仅仅用于文件。 只有对文件的使用(或那些可以被强制操作文件的使用)才可能存在漏洞。

在 NT 内核之外查找,我们可以找到其他要调查的驱动程序

不要忘记用户模式

到目前为止,我们主要讨论的是内核,但重要的是要注意,任何在攻击者可控制的文件上调用 ReadFileMapViewOfFileLoadLibrary 的用户模式应用程序,拒绝写入共享以实现不可变性,都可能存在漏洞。 以下是一些假设的示例。

MapViewOfFile

想象一个应用程序分为两个组件 - 一个具有网络访问权限的低权限工作进程和一个安装更新的特权服务。 工作进程下载更新并将其暂存到特定文件夹。 当特权服务看到新的更新已暂存时,它首先验证签名,然后再安装更新。 攻击者可能会滥用 FFI 在签名检查后修改更新。

ReadFile

由于文件会受到双重读取漏洞的影响,因此任何解析复杂文件格式的内容都可能存在漏洞,包括防病毒引擎和搜索索引器。

LoadLibrary

某些应用程序依靠 UMCI 来防止攻击者将恶意 DLL 加载到其进程中。 正如我们在 PPLFault 中显示的那样,FFI 可以击败 UMCI。

阻止利用

根据他们的官方服务指南,MSRC 默认情况下不会处理管理员 -> 内核漏洞。 在这种说法中,服务意味着“通过安全更新进行修复”。 但是,这种类型的漏洞允许恶意软件绕过 AV 进程保护,使 AV 和 EDR 容易受到即时杀死攻击。

作为第三方,我们无法修补代码完整性,那么我们如何保护我们的客户呢? 为了缓解 ItsNotASecurityBoundary,我们创建了 FineButWeCanStillEasilyStopIt,这是一个文件系统微型筛选器驱动程序,可防止代码完整性通过网络重定向器打开安全编录。 你可以在 GitHub 上 这里找到它。

FineButWeCanStillEasilyStopIt 必须经过一些步骤才能正确识别有问题的行为,同时最大限度地减少误报。 理想情况下,可以通过一些小的更改来修复 CI 本身。 让我们看看这需要什么。

如上面受影响的操作部分所述,应用程序可以通过将文件内容从文件映射复制到堆中,并专门将该堆副本用于所有后续操作来缓解双重读取漏洞。 内核堆称为 ,相应的分配函数是 ExAllocatePool

打破这些类型的漏洞利用的另一种缓解策略是使用 MmProbeAndLockPages 等 API 将文件映射的页面固定到物理内存中。 这可以防止当攻击者清空工作集时逐出这些页面。

最终用户检测和缓解

幸运的是,最终用户无需微软进行更改即可缓解此漏洞 - 虚拟机监控程序保护的代码完整性 (HVCI)。 如果启用了 HVCI,CI.dll 根本不进行编录解析。 相反,它会将编录内容发送到安全内核,安全内核在同一主机上的单独虚拟机中运行。 安全内核将其接收到的编录内容存储在其自己的堆中,从中执行签名验证和解析。 就像上面描述的 ExAllocatePool 缓解一样,该漏洞得到了缓解,因为文件更改对堆副本没有影响。

此攻击的概率性质意味着可能存在许多失败的尝试。 Windows 会将这些失败记录在 Microsoft-Windows-CodeIntegrity/Operational 事件日志中。 用户可以检查此日志以查找漏洞利用的证据。

披露

披露时间表如下

  • 2024-02-14:我们向 MSRC 报告了 ItsNotASecurityBoundary 和 FineButWeCanStillEasilyStopIt,作为 VULN-119340,建议使用 ExAllocatePoolMmProbeAndLockPages 作为简单的低风险修复
  • 2024-02-29:Windows Defender 团队联系以协调披露
  • 2024-04-23:微软发布了 KB5036980 预览版,其中包含 MmProbeAndLockPages 修复
  • 2024-05-14:针对 Windows 11 23H2 的修复程序已正式发布,版本号为 KB5037771;我们尚未测试任何其他平台(Win10、Server 等)。
  • 2024-06-14:MSRC 关闭了此案例,并声明:“我们已完成调查,并确定该案例目前不符合我们的服务标准。因此,我们已针对此问题开设了下一个版本的候选 bug,并将在即将发布的版本中进行评估。再次感谢您与我们分享此报告。”

修复代码完整性

查看 CI!I_MapAndSizeDataFile 的原始实现,我们可以看到旧代码调用了 ZwCreateSectionZwMapViewOfSection

对比之下,新的 CI!CipMapAndSizeDataFileWithMDL 随后调用了 MmProbeAndLockPages

总结与结论

今天,我们讨论并命名了一个 bug 类:错误的文件不可变性。我们知道有两个公开的漏洞利用利用了它,即 PPLFault 和 ItsNotASecurityBoundary。

PPLFault:管理员 -> PPL [-> 通过 GodFault 进入内核]

  • 利用 CI/MM 中关于镜像分区的错误不可变性假设
  • 2022 年 9 月报告
  • 2024 年 2 月修复(大约 510 天后)

ItsNotASecurityBoundary:管理员 -> 内核

  • 利用 CI 中关于数据分区的错误不可变性假设
  • 2024 年 2 月报告
  • 2024 年 5 月修复(大约 90 天后)

如果您正在编写 Windows 代码来操作文件,则需要注意,即使您拒绝写入共享,这些文件也可能会在您操作它们时被修改。请参阅上面的“受影响的操作”部分,了解如何保护自己和您的客户免受此类攻击。

ItsNotASecurityBoundary 不是 FFI 的终点。还有其他可利用的 FFI 漏洞。我和 Elastic Security Labs 的同事将继续探索和报告 FFI 及其他内容。我们鼓励您在 X 上关注 @GabrielLandau@ElasticSecLabs