filemon V4.33:Windows内核文件过滤驱动调试与IRP拦截实战
发布时间:2026/10/12 18:40:25 锦皓数字建站

简介filemon V4.33 是一款经典的系统文件监控工具面向系统管理员、开发人员及需要排查软件冲突或性能问题的技术用户。它能实时跟踪进程对文件与注册表的访问记录操作类型、进程名、文件路径及返回值并支持过滤、搜索与日志记录且不修改系统状态属于安全的非侵入式诊断工具。相比部分新版本在特定环境下出现的运行错误V4.33 稳定性更好兼容范围更广适合快速测试或不愿改动现有系统配置的场景。资源包共 2 个文件包含 1 个 html 说明文档与 1 个 zip 压缩包整体约 77KB说明文档提供使用指导与授权信息压缩包内为可执行程序及相关文件。目前已有 246 人学习下载读者可借此掌握文件活动监控方法定位软件冲突、优化系统性能并理解程序运行机制但需注意该工具较老使用前应确认与当前操作系统的兼容性。1. filemon V4.33一个被低估的文件系统过滤驱动调试利器如果你在 Windows 内核开发或安全研究领域待过一段时间大概率听过 filemon 这个名字。它最早是 Sysinternals 出品的文件系统监控工具后来演化为 Process Monitor 的一部分。但这里要聊的 filemon V4.33指的是那个经典的文件系统过滤驱动框架版本——一个可以直接编译、加载、拦截 IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE 等文件操作的驱动级监控方案。很多人第一次接触它是为了搞清楚某个进程到底在偷偷读写哪些文件也有人拿它做文件防勒索的底层拦截原型。不管你是做终端安全、EDR 对抗还是单纯想理解 Windows 文件过滤驱动minifilter的工作机制filemon V4.33 都是一个极佳的起点。它不依赖复杂的用户态服务核心逻辑全在内核里跑调试信息通过 DbgPrint 或文件日志输出结构清晰改起来不玄学。接下来我会从驱动加载、IRP 拦截、日志输出到实际排错把这条链路完整走一遍。2. filemon V4.33 的驱动加载与过滤绑定从 SCM 到 Filter Manager2.1 为什么选 minifilter 而不是传统 sfilterfilemon V4.33 的代码结构里最核心的选择是用了 Filter Manager 模型也就是 minifilter而不是更老的传统过滤驱动sfilter。传统 sfilter 需要自己挂设备栈、处理 IRP 的透传和完成例程代码量大且容易蓝屏。minifilter 把设备栈管理、IRP 转发、上下文管理都封装好了你只需要注册回调函数。对于文件监控这个场景minifilter 的 FltRegisterFilter 和 FltStartFiltering 两个调用就能把驱动挂到文件系统栈上之后所有 IRP_MJ_CREATE 都会先经过你的 PreOperation 回调。这个选择直接决定了后续代码的复杂度——用 sfilter 写同样的功能代码量至少翻三倍而且调试时蓝屏概率高得多。2.2 驱动入口与过滤注册的完整代码下面是一个最小可编译的 filemon V4.33 风格驱动入口基于 WDK 环境目标系统是 Windows 10 x64。代码里保留了 V4.33 版本中常见的注册逻辑和回调设置。#include fltKernel.h PFLT_FILTER gFilterHandle NULL; // 预操作回调拦截 IRP_MJ_CREATE FLT_PREOP_CALLBACK_STATUS FileMonPreCreate( _Inout_ PFLT_CALLBACK_DATA Data, _In_ PCFLT_RELATED_OBJECTS FltObjects, _Flt_CompletionContext_Outptr_ PVOID *CompletionContext ) { PFLT_FILE_NAME_INFORMATION nameInfo NULL; NTSTATUS status; UNREFERENCED_PARAMETER(CompletionContext); // 获取文件路径信息注意 FLT_FILE_NAME_NORMALIZED 会触发重解析 status FltGetFileNameInformation( Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, nameInfo); if (NT_SUCCESS(status)) { FltParseFileNameInformation(nameInfo); // 输出进程ID和文件路径实际项目中可写入环形缓冲区 DbgPrint([filemon] PID%lu File%wZ\n, FltGetRequestorProcessId(Data), nameInfo-Name); FltReleaseFileNameInformation(nameInfo); } return FLT_PREOP_SUCCESS_NO_CALLBACK; } // 注册结构只挂 CREATE其他操作透传 CONST FLT_OPERATION_REGISTRATION Callbacks[] { { IRP_MJ_CREATE, 0, FileMonPreCreate, NULL }, { IRP_MJ_OPERATION_END_OF_QUEUE, 0, NULL, NULL } }; // 过滤注册结构 CONST FLT_REGISTRATION FilterRegistration { sizeof(FLT_REGISTRATION), FLT_REGISTRATION_VERSION, 0, NULL, Callbacks, NULL, // Unload 回调生产环境必须实现 NULL, // InstanceSetup NULL, // InstanceQueryTeardown NULL, // InstanceTeardownStart NULL, // InstanceTeardownComplete NULL, NULL, NULL }; // DriverEntry NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { NTSTATUS status; UNREFERENCED_PARAMETER(RegistryPath); status FltRegisterFilter(DriverObject, FilterRegistration, gFilterHandle); if (!NT_SUCCESS(status)) { DbgPrint([filemon] FltRegisterFilter failed: 0x%X\n, status); return status; } status FltStartFiltering(gFilterHandle); if (!NT_SUCCESS(status)) { FltUnregisterFilter(gFilterHandle); DbgPrint([filemon] FltStartFiltering failed: 0x%X\n, status); return status; } DbgPrint([filemon] V4.33 filter started.\n); return STATUS_SUCCESS; }这段代码的逻辑很直接DriverEntry 里先注册过滤器和回调数组然后启动过滤。FileMonPreCreate 是核心它拿到当前 IRP 的文件名信息解析后打印进程 ID 和路径。参数说明上FLT_FILE_NAME_NORMALIZED 表示获取规范化路径会解析符号链接和短文件名FLT_FILE_NAME_QUERY_DEFAULT 表示如果缓存没有则查询文件系统。FltGetRequestorProcessId 返回发起操作的进程 ID这个值在排查“谁在动我的文件”时非常关键。注意回调返回 FLT_PREOP_SUCCESS_NO_CALLBACK表示不需要后操作回调直接放行。2.3 编译与加载从 inf 到 sc create编译这个驱动需要 WDK 和 Visual Studio项目类型选“Empty WDM Driver”或“Filter Driver”。编译产物是一个 .sys 文件。加载方式有两种一种是写 inf 文件用 pnputil 安装另一种是直接用 sc create 创建内核服务。我一般用后者做快速调试sc create filemon type kernel binPath C:\path\to\filemon.sys sc start filemon启动后立刻用 DbgView 捕获内核输出如果看到[filemon] V4.33 filter started.说明加载成功。停止用sc stop filemon删除用sc delete filemon。注意内核驱动没有签名在测试模式下会被拒绝加载需要开启测试签名模式或者用测试证书签名。这一步翻车最多很多人卡在“驱动无法启动”其实只是签名问题。3. 拦截 IRP_MJ_WRITE 与日志落盘把内核事件变成可分析数据3.1 为什么只挂 CREATE 不够只监控 IRP_MJ_CREATE 只能看到文件被打开但看不到写入内容。勒索软件加密文件时CREATE 之后紧跟大量 WRITE 操作。要判断一个进程是否在批量改写文件必须拦截 IRP_MJ_WRITE。filemon V4.33 的原始代码里对 WRITE 的处理比较粗糙只记录偏移和长度不记录内容。实际落地时我建议至少记录写入偏移、写入长度和进程 ID这三个字段足以画出“哪个进程在哪个时间段密集写哪些文件”的图谱。3.2 WRITE 回调的实现与缓冲区处理FLT_PREOP_CALLBACK_STATUS FileMonPreWrite( _Inout_ PFLT_CALLBACK_DATA Data, _In_ PCFLT_RELATED_OBJECTS FltObjects, _Flt_CompletionContext_Outptr_ PVOID *CompletionContext ) { PFLT_FILE_NAME_INFORMATION nameInfo NULL; NTSTATUS status; ULONG writeLength 0; LARGE_INTEGER byteOffset {0}; UNREFERENCED_PARAMETER(CompletionContext); UNREFERENCED_PARAMETER(FltObjects); // 获取写入长度和偏移 if (Data-Iopb-Parameters.Write.Length) { writeLength Data-Iopb-Parameters.Write.Length; } if (Data-Iopb-Parameters.Write.ByteOffset.QuadPart ! 0) { byteOffset Data-Iopb-Parameters.Write.ByteOffset; } status FltGetFileNameInformation( Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, nameInfo); if (NT_SUCCESS(status)) { FltParseFileNameInformation(nameInfo); DbgPrint([filemon] WRITE PID%lu Offset%I64d Len%lu File%wZ\n, FltGetRequestorProcessId(Data), byteOffset.QuadPart, writeLength, nameInfo-Name); FltReleaseFileNameInformation(nameInfo); } return FLT_PREOP_SUCCESS_NO_CALLBACK; }这里的关键参数是 Data-Iopb-Parameters.Write.Length 和 ByteOffset。Length 是本次写入的字节数ByteOffset 是文件内的偏移。注意 ByteOffset 为 0 不一定表示从文件头写也可能是追加写具体要看文件打开时的模式。FltGetRequestorProcessId 在 WRITE 回调里同样有效。这段代码只做记录不修改数据所以返回 FLT_PREOP_SUCCESS_NO_CALLBACK 让操作继续。3.3 日志落盘的三种方式与选择DbgPrint 只适合调试生产环境需要把日志写到文件或环形缓冲区。常见做法有三种第一种是在内核里直接写文件但要注意不能递归触发自己的过滤回调需要设置一个标志位跳过第二种是写入内核环形缓冲区用户态服务定期读取第三种是通过 FltSendMessage 发给用户态。我一般用第二种因为性能影响最小。环形缓冲区用 ExAllocatePoolWithTag 分配非分页内存写入时用自旋锁保护。用户态读取用 DeviceIoControl 发 IOCTL 获取数据。这个方案在文件操作密集的场景下CPU 占用可以控制在 5% 以内。4. 避坑与排查filemon V4.33 落地时最容易翻车的五个点4.1 蓝屏 0x0000007E回调里调用了分页内存现象驱动加载后一访问文件就蓝屏错误码 0x7E。原因在 PreOperation 回调里调用了 FltGetFileNameInformation 并传入 FLT_FILE_NAME_QUERY_DEFAULT这个调用在某些 IRQL 下会触发分页操作。解决确保回调运行在 PASSIVE_LEVEL 或 APC_LEVEL如果必须在 DISPATCH_LEVEL 下获取文件名改用 FLT_FILE_NAME_QUERY_ALWAYS_ALLOW_CACHE_LOOKUP 并检查返回状态。更稳妥的做法是在 InstanceSetup 里预先注册文件名缓存。4.2 日志丢失DbgPrint 缓冲区溢出现象用 DbgView 看日志发现大量 WRITE 事件丢失只看到零星几条。原因DbgPrint 在高速写入时会被限流内核默认的调试输出缓冲区很小。解决不要依赖 DbgPrint 做生产日志改用环形缓冲区或 ETW。如果只是调试可以在注册表里调大HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter的值但这不是长久之计。4.3 驱动无法卸载Unload 回调缺失现象sc stop filemon卡住驱动状态一直是 STOP_PENDING。原因FLT_REGISTRATION 结构里的 Unload 回调传了 NULLFilter Manager 无法完成清理。解决必须实现 Unload 回调在里面调用 FltUnregisterFilter 并释放所有分配的内存。filemon V4.33 原始代码里这个回调是有的但很多人移植时图省事删掉了结果就是卸载不掉只能重启。4.4 递归拦截自己的日志写入触发自己的回调现象驱动加载后系统变慢日志里出现大量对自己日志文件的 WRITE 记录。原因内核里写日志文件时IRP 再次经过自己的过滤驱动形成递归。解决在写日志前设置一个线程局部标志或者在回调里判断当前进程 ID 是否等于日志写入进程如果是则直接放行。更干净的做法是用 FltSendMessage 把日志发给用户态内核里不碰文件。4.5 文件名获取失败短路径与符号链接现象FltGetFileNameInformation 返回 STATUS_FLT_NAME_CACHE_MISS 或路径显示为\Device\HarddiskVolume1\...。原因文件系统还没完成名称解析或者查询的是短文件名。解决先调用 FltGetFileNameInformation 获取 FLT_FILE_NAME_OPENED 格式再调用 FltParseFileNameInformation 解析。如果还是失败检查是否在 IRP_MJ_CREATE 的 PreOperation 里过早调用可以改到 PostOperation 里获取。5. 进阶技巧用 filemon V4.33 做文件行为基线比对5.1 从原始日志到行为指纹filemon V4.33 输出的日志是逐条事件直接看很累。我一般会写一个用户态解析脚本把日志按进程 ID 和文件路径聚合生成“进程-文件-操作次数”的三元组。然后对同一个进程在正常状态和异常状态下的三元组做差集差集里出现的新文件路径就是可疑点。这个思路在排查“某个服务突然开始写奇怪文件”时特别有效。下面是一个 Python 解析示例import re from collections import defaultdict # 假设日志格式[filemon] WRITE PID1234 Offset0 Len4096 File\Device\HarddiskVolume1\test.dat pattern re.compile( r\[filemon\] (\w) PID(\d) Offset(\d) Len(\d) File(.) ) def parse_log(path): events [] with open(path, r, encodingutf-16) as f: for line in f: m pattern.search(line) if m: events.append({ op: m.group(1), pid: int(m.group(2)), offset: int(m.group(3)), len: int(m.group(4)), file: m.group(5).strip() }) return events def build_fingerprint(events): fp defaultdict(lambda: defaultdict(int)) for e in events: key (e[pid], e[file]) fp[key][e[op]] 1 return fp # 比对两次采集的指纹 def diff_fingerprint(base, target): new_entries [] for key, ops in target.items(): if key not in base: new_entries.append((key, ops)) else: for op, cnt in ops.items(): if cnt base[key].get(op, 0) * 2: # 操作次数翻倍视为异常 new_entries.append((key, {op: cnt})) return new_entries if __name__ __main__: base_events parse_log(baseline.log) target_events parse_log(suspect.log) base_fp build_fingerprint(base_events) target_fp build_fingerprint(target_events) for entry in diff_fingerprint(base_fp, target_fp): print(entry)这段脚本的逻辑是先解析日志行提取操作类型、进程 ID、偏移、长度和文件路径然后按进程和文件聚合操作次数最后比对两次采集的指纹如果某个进程对某个文件的操作次数翻倍就标记为异常。参数上cnt base[key].get(op, 0) * 2里的倍数可以根据场景调整文件服务器场景可以放宽到 5 倍终端场景 2 倍就够了。5.2 性能调优减少回调里的字符串操作filemon V4.33 在高并发文件操作下瓶颈往往在 FltGetFileNameInformation 和 DbgPrint 的字符串格式化上。我一般会把文件名缓存起来用 FltGetFileNameInformation 拿到一次后在同一个 IRP 的后续回调里复用。另外DbgPrint 的%wZ格式化开销很大可以改成只记录文件名哈希用户态再还原。实测在 10 万次文件操作下去掉字符串格式化能把 CPU 占用从 15% 降到 4% 左右。5.3 一个我踩过的坑不要在生产环境用 DbgView 长期抓日志早期我在一个测试机上用 DbgView 抓了三天日志结果 DbgView 自己占了 2GB 内存系统卡到无法操作。后来改成环形缓冲区加用户态轮询内存稳定在 50MB 以内。这个教训让我明白内核日志工具本身也是资源消耗大户调试可以长期跑必须换方案。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。