-
-
[原创]第三方MiniFilter中常见的一种TOCTOU漏洞
-
发表于: 2026-4-19 20:28 1984
-
TOCTOU作为一种广泛存在的漏洞类型,在第三方Windows MiniFilter中也存在,本文介绍一种典型的第三方MiniFilter TOCTOU漏洞。
为了了解为什么会有TOCTOU,我们需要一并介绍Windows MiniFilter的基础架构。
MiniFilter(文件系统微筛选器驱动)是微软推出的新一代文件系统过滤驱动模型,旨在解决传统文件系统过滤驱动(Legacy Filter Driver)开发复杂度高、维护困难、加载顺序难以控制等问题。MiniFilter 基于 Filter Manager(过滤器管理器,fltmgr.sys)驱动框架运行,借助系统提供的过滤器管理器支持,显著简化了过滤驱动的开发。
Filter Manager 的角色。 Filter Manager(FltMgr.sys)是 Windows 系统提供的内核模式驱动,它实现了文件系统筛选驱动中普遍需要的通用功能,并将其暴露给 MiniFilter 驱动使用。Filter Manager 随 Windows 一同安装,但仅在 MiniFilter 驱动被加载时才会激活。它将自身绑定到目标卷的文件系统栈上,而 MiniFilter 驱动则通过向 Filter Manager 注册所需过滤的 I/O 操作类型,间接地绑定到文件系统栈。
MiniFilter 的实例与加载顺序。 MiniFilter 在特定卷上以特定高度(Altitude)进行的绑定操作称为实例(Instance)。Altitude 是一个数值字符串,它决定了 MiniFilter 在文件系统 I/O 栈中的加载位置——Altitude 值越高,实例在栈中的位置越靠上。操作系统通过负载顺序组(Load Order Groups)和 Altitude 共同确定多个 MiniFilter 之间的附着顺序,Altitude 同时也决定了 Filter Manager 在 I/O 操作处理过程中调用各个 MiniFilter 的顺序。
I/O 操作过滤机制。 MiniFilter 能够过滤三种类型的操作:基于 IRP 的 I/O 操作、快速 I/O 操作以及文件系统筛选器回调操作(FSFilter)。对于每种需要过滤的 I/O 操作,MiniFilter 可以注册一个“过滤前”(preoperation)回调函数、一个“过滤后”(postoperation)回调函数,或者两者同时注册。
Preoperation 回调函数类似于传统过滤驱动模型中的派发函数。当 Filter Manager 处理某个 I/O 操作时,它会按照 Altitude 从高到低的顺序,依次调用所有为该操作类型注册了 preoperation 回调函数的 MiniFilter 实例。每个 MiniFilter 可以在其 preoperation 回调中执行检查、修改 I/O 参数、完成 I/O 操作、挂起操作或将操作传递至下一层处理。当所有 MiniFilter 都处理完成后,Filter Manager 将 I/O 请求向下发送至传统过滤驱动和文件系统。
Postoperation 回调函数则类似于传统过滤驱动模型中的完成例程。当 I/O 操作完成并由 I/O 管理器向上返回时,Filter Manager 会按照 Altitude 从低到高的顺序,依次调用各个 MiniFilter 的 postoperation 回调函数。这种对称的设计使得每个 MiniFilter 能够在 I/O 操作到达文件系统之前和之后分别获得处理机会。
我们可以先看一个图片,该图片展示了IRP的传递
该图展示了用户态 I/O 请求(如 CreateFile、ReadFile、WriteFile)在经过 I/O 管理器、Filter Manager、不同 Altitude 的 MiniFilter、最终到达文件系统驱动,再原路返回的完整过程。图中特别强调了 MiniFilter 框架的两个核心回调:PreOperation(操作前)和 PostOperation(操作后),以及它们按 Altitude 顺序调用的规则。
FLT_PREOP_COMPLETE :该 MiniFilter 认为操作已完成,Filter Manager 不再继续向下传递 IRP,直接返回 I/O 管理器。最终结果返回给应用程序(图中左侧分支)。FLT_PREOP_SUCCESS_NO_CALLBACK 继续传递,且不需要对该 I/O 做 PostOperation 回调,Filter Manager 将 IRP 传给下一个 MiniFilter 或文件系统。FLT_PREOP_SUCCESS_WITH_CALLBACK : 继续传递,但需要在完成时调用该 MiniFilter 的 PostOperation 回调。 同上,但 Filter Manager 会记录该 MiniFilter,在 IRP 返回时调用其 PostOperation。
以下是一个存在 TOCTOU 漏洞的简易 MiniFilter 驱动示例。相信有人会认为这是正确的,但,我们需要注意的是,文件操作在本质上是异步的,也就是说,我们不能保证执行流是线性的。如果执行流不是线性的,那么,我们可以通过并发来修改同一个状态,也就是在检查通过后通过并发线程马上修改目标,使其指向受保护目标。下面的代码展示了一个存在漏洞的 PreCreate 回调。
上述示例代码展示了一个典型的Time-of-Check to Time-of-Use(TOCTOU)漏洞。该漏洞的核心原因在于:文件路径的检查时刻(T1)与实际文件打开/使用时刻(T2)之间存在一个时间窗口,攻击者能够利用并发操作在这个窗口内改变目标文件的身份,从而绕过安全检查。
在 PreCreateCallback 中,驱动首先调用 FltGetFileNameInformation 获取文件的路径,然后使用 wcsstr 判断路径中是否包含 C:\Protected\target.txt。如果匹配,则返回 STATUS_ACCESS_DENIED,阻止打开。
问题在于:从 FltGetFileNameInformation 返回(T1)到文件系统真正打开并返回句柄(T2)之间,Windows 文件系统并不保证路径所指向的对象保持不变。攻击者可以在 T1 之后、T2 之前,通过另一个线程(或另一个进程)修改文件系统的命名空间。
由于 MiniFilter 的 PreCreate 回调只是整个 I/O 路径中的“观察者”,它无法原子化地锁定路径解析结果,因此上述竞态条件(race condition)是可能发生的。
Windows I/O 系统本质上是一个异步、多线程、可抢占的环境。PreCreate 回调运行在任意线程上下文中(通常是发起 I/O 的线程),而其他 CPU 核心或更高优先级的线程可以同时修改文件系统状态。检查与使用并非原子操作,因此竞态条件不可避免,除非驱动主动引入锁机制或利用系统提供的原子操作原语。
在 MiniFilter 框架中,Filter Manager 不会自动为路径解析过程加锁,因为这会严重影响并发性能,且容易导致死锁。因此,必须意识到:即使在内核模式,也不能假设两次查询之间文件状态不变。
如图,是笔者在进行测试时发现的一个TOCTOU小问题,以下是对所给伪代码中 TOCTOU 漏洞的注释分析。 
如图,笔者利用以下代码绕过了该软件的自定义文件保护

原理:将路径检查延迟到文件已经打开之后(PostCreate 阶段),使用 FLT_FILE_NAME_OPENED 标志获取文件系统最终解析并打开的文件名,此时文件名已经不会受后续符号链接或重命名影响。若检查发现不应被打开,则立即关闭该文件句柄。
改进代码:
获取的文件名是文件系统已经锁定并打开的对象,攻击者无法在 T1~T2 窗口内改变。性能较好,仅在文件成功打开后做一次额外检查。
原理:文件对象的唯一标识符(FileReferenceNumber)在文件生命周期内不会改变,即使文件被重命名、移动或修改路径,其 ID 保持不变。预先获取受保护文件的 ID,然后在 PreCreate 或 PostCreate 中比较 ID 而非路径。
步骤:
文件 ID 在重命名、移动等操作后不变,从根本上避免路径 TOCTOU。检查原子性较高(ID 在一次调用中获取并比较)。需要预先知道受保护文件的 ID(可在驱动加载时动态查询)。对于硬链接,所有链接共享同一个 ID,需注意策略一致性。
使用 FltGetFileNameInformation 的 FLT_FILE_NAME_OPENED 标志 + 同步检查或者结合对象属性与句柄验证
MiniFilter 驱动中的 TOCTOU 漏洞源于路径检查与文件打开之间的竞态条件。典型错误是在 PreCreate 回调中使用 FltGetFileNameInformation 获取一次路径后直接决策,攻击者可利用符号链接、重命名等操作在检查(T1)与实际使用(T2)的时间窗口内改变文件身份,从而绕过安全策略。开发安全过滤驱动时,务必假设文件系统状态是动态变化的,并采用原子化验证手段,避免依赖单次路径查询结果。
#include <fltKernel.h>
PFLT_FILTER g_FilterHandle = NULL;
FLT_PREOP_CALLBACK_STATUS
PreCreateCallback(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_Flt_CompletionContext_Outptr_ PVOID *CompletionContext
)
{
PFLT_FILE_NAME_INFORMATION nameInfo = NULL;
NTSTATUS status;
BOOLEAN deny = FALSE;
status = FltGetFileNameInformation(Data,
FLT_FILE_NAME_NORMALIZED |
FLT_FILE_NAME_QUERY_DEFAULT,
&nameInfo);
if (NT_SUCCESS(status))
{
status = FltParseFileNameInformation(nameInfo);
if (NT_SUCCESS(status))
{
if (wcsstr(nameInfo->Name.Buffer, L"C:\\Protected\\target.txt") != NULL)
{
deny = TRUE;
}
}
FltReleaseFileNameInformation(nameInfo);
}
if (deny)
{
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
Data->IoStatus.Information = 0;
return FLT_PREOP_COMPLETE;
}
return FLT_PREOP_SUCCESS_NO_CALLBACK;
}
const FLT_OPERATION_REGISTRATION Callbacks[] = {
{ IRP_MJ_CREATE,
0,
PreCreateCallback,
NULL },
{ IRP_MJ_OPERATION_END }
};
赞赏
- [原创]第三方MiniFilter中常见的一种TOCTOU漏洞 1985
- [原创]利用导入表劫持实现DLL注入以干掉杀毒软件 6253
- [原创]一个漏洞驱动利用以结束杀软进程 2760
- [原创]一个恶意驱动的逆向分析 17060
- [翻译]Beyond BIOS 第二章翻译 UEFI基础架构 1254