VS2019下minifilter驱动开发实战:从工程搭建到回调注册详解

发布时间:2026/9/13 12:18:45
VS2019下minifilter驱动开发实战:从工程搭建到回调注册详解
简介面向Windows内核驱动开发者的VS2019 Minifilter文件系统过滤驱动初始化示例包完整演示从DriverEntry入口函数出发依次完成FltRegisterFilter过滤注册、卷实例创建、PreCreate/PostWrite等回调挂接再到FltStartFiltering启动过滤与卸载清理的整个生命周期适合刚接触Filter Manager机制、希望对照真实工程搭建驱动骨架的开发者。压缩包共82个文件以c/cpp源码、inf安装配置、vcxproj工程文件为主体另附tlog编译日志、res资源与调试pdb整体仅161KB轻量小巧便于快速导入Visual Studio研读项目采用x64 Release配置主干与测试代码分离目录结构清晰。已有447人学习浏览。借助示例可梳理初始化顺序注册信息如何填写、卷绑定与回调怎样生效、安装脚本与inf如何配合日志输出如何帮助定位编译或加载异常此外还附带SDV静态驱动验证脚本与配置可辅助检查代码缺陷是一份实用的VS2019过滤驱动入门参考。1. 在 VS2019 里搭 minifilter 工程先搞清楚它和普通驱动差在哪文件监控、防勒索、透明加密这类需求放在 Windows 上绕不开文件系统微过滤驱动minifilter。它和旧式文件系统过滤驱动的最大区别是不再直接挂到卷设备栈上而是把回调注册给 FltMgr由 FltMgr 统一调度。VS2019 之所以是这个工程环境里的高频词是因为从 VS2019 的某次更新开始WDK 扩展把 minifilter 模板直接挂进了新建项目向导Altitude、INF、项目属性这几个最容易出错的地方都留出了可视化入口不再像老 SDK 时代那样靠手动改 .vcxproj 和 sources 文件。适合正在做终端安全、文件审计、备份恢复这类功能的人也适合准备从传统过滤驱动迁移过来的内核开发。这篇文章就按“注册—回调—部署—验证”的顺序把一套能跑的 minifilter 工程骨架讲清楚。2. 理论minifilter 的加载模型与 VS2019 工程里的 Altitude 和 INF2.1 过滤管理器与 minifilter 的层级为什么 VS2019 项目里总有一个 .inf普通内核驱动用 IoCreateDevice 创建设备对象再附加到某个设备栈上处理自己关心的 IRP。minifilter 完全不是这个套路。它先调用 FltRegisterFilter 把自己注册给 FltMgr再调用 FltStartFiltering 告诉 FltMgr“你可以开始把操作分发给我了”。真正挂到卷设备栈上的是 FltMgrminifilter 只是 FltMgr 下面的一个回调提供者。这个层级关系如果不建立起来后面在 VS2019 里写代码特别容易走偏你会不自觉地去处理没注册的操作或者在你的 DriverEntry 里试图访问设备对象。VS2019 的 WDK minifilter 模板默认会生成一个 .inf 文件。很多第一次接触内核过滤的人以为 .inf 只是普通的安装脚本装完驱动就完成任务其实这个 .inf 里的 AddService 段决定了系统如何创建服务也决定了 fltmc 能否识别你这个过滤驱动。如果把 .inf 里的 ServiceType 或 StartType 写错即使 .sys 编译通过加载时也会挂在服务控制管理器上。处理这种问题我一般会先把 .inf 里DefaultInstall段中与 Altitude、StartType 相关的字段列出来逐一确认后再编译。2.2 VS2019 的 minifilter 工程属性里Altitude、StartType 与 CopyFiles 怎么设在 VS2019 中新建项目时从“Windows 驱动程序”分类下选择模板生成的工程默认已经包含一个最小可编译的 minifilter。不过 WDK 给出的模板只是“能编”离“能按预期工作”还差三处配置。Altitude 是过滤器在 FltMgr 排序表里的唯一标识。Windows 启动后FltMgr 会对所有 minifilter 按 Altitude 从高到低排列同一个卷上的过滤操作会按这个顺序串起来。这个值必须是没有被占用的数字字符串官方建议在发布前申请正式区间自用测试可以选 300000 到 370000 之间的低风险段。设成重复值不会导致编译失败但 fltmc load 时会返回 STATUS_FLT_ALTITUDE_ALREADY_EXISTS这个错误码排错时很容易漏。StartType 决定 minifilter 什么时候被 FltMgr 拉起。绝大多数文件系统过滤场景用SERVICE_AUTO_START2或SERVICE_DEMAND_START3。如果你要在系统启动早期拦截卷挂载会需要 0但 BOOT_START 类驱动对签名和依赖要求极严普通应用场景不建议碰。把 StartType 设置成 3 时要注意sc.exe start与fltmc load是有差别的前者走 SCM 接口后者直接找 FltMgr测试环境里两者响应并不总是一致的。CopyFiles 段的作用是把 .sys 复制到%SystemRoot%\system32\drivers。VS2019 模板默认会用文件宏把输出文件带进来手动精简 INF 时最容易犯的错是只保留驱动服务注册、删掉了文件复制段结果安装报“系统找不到指定的文件”。检查时优先看 INF 里的CopyFiles和DestinationDirs是否成对出现。下面给出一个简化 INF 的核心片段实际使用时应以 VS2019 模板生成的 GUID 与路径为准[Version] Signature $WINDOWS NT$ Class ActivityMonitor ClassGuid {替换为VS2019模板值} CatalogFile MyMini.cat [DefaultInstall.NTamd64] CopyFiles MyMini.sys AddService MyMini, 0x00000002, MyMini_Service_Inst [MyMini_Service_Inst] DisplayName MyMini Filter ServiceType 2 StartType 2AddService的第二个参数 0x00000002 表示“创建服务”不是启动驱动。ServiceType 2表示内核驱动StartType 2表示系统启动时自动加载。需要手动控制时把StartType改成 3并配合fltmc load或sc start使用。2.3 从 VS2019 到 WDK 扩展模板缺失先查这两个组件安装 WDK 后新建项目时看不到 Filter Driver 模板是 VS2019 搭建环境最常见的问题。这里有一个容易混淆的点WDK 本身安装成功并不代表 VS2019 就能识别驱动项目。还需要在 Visual Studio Installer 的“单个组件”里勾选对应版本的“WDK VS 扩展”或“Windows Driver Kit (WDK) 扩展”。一个是工具链一个是 IDE 集成缺后者时VS2019 菜单里根本不会出现驱动的项目类型。模板出现后FltMgr 相关的开发库怎么链上又是另一个问题。VS2019 的 WDK 构建环境默认会处理 FltMgr.lib但如果你的项目是从旧工程改的没有经过模板初始化就会出现链接错误LNK2019 unresolved external symbol FltRegisterFilter。此时要么在项目属性“链接器—输入—附加依赖项”里加上 FltMgr.lib要么在代码里加#pragma comment(lib, FltMgr.lib)。我不建议用 pragma 写死在代码里一个工程如果将来要切换不同 WDK 版本库引用放在项目配置里更干净。3. 实现VS2019 下最小 minifilter 的回调注册与 IRP 拦截3.1 DriverEntry 里先初始化 UNICODE_STRING 再注册 FLT_REGISTRATION最小可运行的 minifilter 不需要创建设备对象不需要分发函数只需要 DriverEntry 和 FilterUnload。第一步是把 Altitude 写进 UNICODE_STRING再用 FltRegisterFilter 完成注册。下面这段代码是 VS2019 环境下一个可以直接放到空模板里的骨架#include fltKernel.h #define MINIFILTER_ALTITUDE L360000 NTSTATUS MyFilterUnload(_In_ FLT_FILTER_UNLOAD_FLAGS Flags); FLT_PREOP_CALLBACK_STATUS MyCreatePreCallback( _Inout_ PFLT_CALLBACK_DATA Data, _In_ PCFLT_RELATED_OBJECTS FltObjects, _Flt_CompletionContext_Outptr_ PVOID *CompletionContext); FLT_OPERATION_REGISTRATION MyOps[] { { IRP_MJ_CREATE, 0, MyCreatePreCallback, NULL }, { IRP_MJ_OPERATION_END } }; PFLT_FILTER gFilterHandle NULL; NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { FLT_REGISTRATION reg; UNICODE_STRING altitude; NTSTATUS status; UNREFERENCED_PARAMETER(RegistryPath); RtlInitUnicodeString(altitude, MINIFILTER_ALTITUDE); RtlZeroMemory(reg, sizeof(reg)); reg.Size sizeof(FLT_REGISTRATION); reg.Altitude altitude; reg.FilterUnloadCallback MyFilterUnload; reg.OperationRegistration MyOps; status FltRegisterFilter(DriverObject, reg, gFilterHandle); if (!NT_SUCCESS(status)) { return status; } status FltStartFiltering(gFilterHandle); if (!NT_SUCCESS(status)) { FltUnregisterFilter(gFilterHandle); return status; } return STATUS_SUCCESS; }FltRegisterFilter的第三个参数返回一个PFLT_FILTER句柄这个句柄要在FltStartFiltering时再传一次表示“我注册好了现在开始接收回调”。很多新手在 FltRegisterFilter 成功后就认为驱动已经工作其实还需要 FltStartFiltering。如果它失败必须立刻 FltUnregisterFilter不然驱动会处于已注册但未启动的中间状态可加载但永远没有回调。MyOps 数组末尾的IRP_MJ_OPERATION_END是数组结束标记必须有否则 FltMgr 读越界。3.2 在 PreCreate 回调里取文件名用 FltGetFileNameInformation 替代直接访问minifilter 最常见的需求是看文件被打开时怎么处理的。IRP_MJ_CREATE 的 Pre 回调会在创建操作发生前被调用。取文件名时不要直接读TargetFileObject-FileName那个字段在创建路径上可能是不完整的要用 FltGetFileNameInformation 申请一份经过 FltMgr 解析的文件名信息。代码FLT_PREOP_CALLBACK_STATUS MyCreatePreCallback( _Inout_ PFLT_CALLBACK_DATA Data, _In_ PCFLT_RELATED_OBJECTS FltObjects, _Flt_CompletionContext_Outptr_ PVOID *CompletionContext) { NTSTATUS status; PFLT_FILE_NAME_INFORMATION nameInfo NULL; UNREFERENCED_PARAMETER(FltObjects); *CompletionContext NULL; status FltGetFileNameInformation( Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, nameInfo); if (!NT_SUCCESS(status)) { return FLT_PREOP_SUCCESS_NO_CALLBACK; } if (nameInfo-Name.Length 0) { DbgPrint(MyMini create: %wZ\n, nameInfo-Name); } FltReleaseFileNameInformation(nameInfo); return FLT_PREOP_SUCCESS_NO_CALLBACK; }FLT_FILE_NAME_NORMALIZED表示让 FltMgr 把路径转成规范化形式例如展开盘符和目录符号链接。FLT_FILE_NAME_QUERY_DEFAULT告诉 FltMgr 在三个缓存选项中选择默认行为适合大多数场景。这里注意该调用在创建路径上是一次额外开销如果文件创建频率很高可能成为性能瓶颈。更好的做法是把文件名校验放到 Post 回调或者交给 application 层但最小工程里用 Pre 回调最直观。FLT_PREOP_SUCCESS_NO_CALLBACK表示“这个操作我不需要完成例程”FltMgr 收到它后就不再准备 Post 回调路径。3.3 编译 minifilter 工程时报错时先检查这三处项目设置VS2019 里点了“生成解决方案”输出日志里出现C1189 #error: NTDDI_VERSION must be defined...通常是项目没有继承 WDK 的默认宏。老工程迁移过来的最常见问题是项目属性里“Windows SDK 版本”选到了桌面版而不是 WDK 自己的工具集。把项目属性中的“驱动程序设置—常规—目标平台”和“配置类型”检查一遍minifilter 应该是“驱动程序内核”而不是“应用程序”。第二个常见问题是签名设置。测试环境里没开 test signing生成的驱动在 Windows 10/11 上加载时会报 577 错误。可以在管理员命令行执行bcdedit /set testsigning on后重启也可以使用 WDK 的 MakeCert 自签名测试证书并在项目属性里启用“测试签名”。正式环境需要 EV 签名证书这是发布环节的事。第三个问题是编译警告被当作错误。WDK 默认把不少警告视为错误。如果只是做原型验证可以在项目属性 C/C 的“警告视为错误”里改成“否”但我建议保留。真正要改的是W4或W5级别下对未引用的参数告警像上面的UNREFERENCED_PARAMETER就是为这个而写的保留警告能帮你提前发现回调里忘记使用的变量。4. 实战VS2019 里 minifilter 的编译、部署与验证4.1 生成 .sys 和 .inf 后先用配置管理器确认 x64 输出目录在 VS2019 里开发 minifilter目标机器如果是现代 Windows建议直接配 x64。生成成功后项目目录下通常会有x64\Release或x64\Debug其中能看到 .sys、.inf、.cat 三个文件。Debug 版本的 .sys 里带调试符号信息体积更大运行时也依赖调试器输出Release 版本更适合做加载验证。第一次调试的工程师经常把 .sys 从工程目录直接复制到 System32\drivers结果加载后还是老版本原因是对应 .inf 里的文件中没有更新复制目标。VS2019 项目属性里可以设置输出目录但驱动部署最好是使用可再发行安装包方式而不是手动拷文件。手动拷贝后SCM 里记录的是旧驱动sc stop后用新文件覆盖通常能解决但驱动运行时被占用时文件删除会失败。4.2 用 sc create 和 fltmc load 拉起 minifilter 的服务命令如果 INF 安装不便管理员 PowerShell 或 CMD 里用 sc 也能把 minifilter 服务创建出来。命令如下sc.exe create MyMini type kernel start demand error normal binPath C:\drivers\MyMini.sys DisplayName MyMini Filter sc.exe start MyMini fltmcsc create的参数等号后面必须带空格这是 sc.exe 的老规矩写成typekernel会直接报参数错误。start demand表示不随开机自动启动方便测试完卸载重载。binPath指向 .sys 的绝对路径。启动后运行fltmc能看到系统当前已加载的所有文件系统过滤驱动列表。自己的服务名在列说明 FltMgr 已经接纳了它不在列则说明注册失败需要看系统事件日志。也可以用 INF 安装命令是RUNDLL32.EXE SETUPAPI.DLL,InstallHinfSection DefaultInstall 132 你的文件.inf。这里132表示安装路径标志常用在无人值守安装场景。它比 sc 方式多做了 CatalogFile 校验和文件复制更接近真实发布流程。4.3 用 fltmc、WinDbg 和 DbgView 验证回调是否被触发加载成功后需要验证回调真的被调用了。内核调试器是首选在 VS2019 的“调试—附加到进程”里不会看到内核驱动需要打开 WinDbg内核模式并设置目标机连接。常用命令如下命令作用!fltkd显示 FltMgr 调试扩展是否加载!fltkd.filters列出所有 minifilter 的 Altitude、名称、状态!fltkd.filter MyMini查看指定过滤器详细信息!fltkd.pending查看是否有未完成的回调请求fltmc instances在目标机查看过滤器实例绑定到哪些卷如果不开内核调试器可以用 DbgPrint 输出。默认情况下 DbgPrint 在未连接调试器时不会出现在系统调试器窗口需要使用输出调试字符串工具如 DebugView 并开启捕获内核模式输出。注意 Windows 10 之后普通用户态工具直接读内核调试输出可能受限更多时候要靠 ETW 或!fltkd的字段来确认。4.4 安装失败时先看事件日志、签名与 Altitude 冲突加载失败时首先打开“事件查看器—Windows 日志—系统”查找来源为“Service Control Manager”的 Error 事件里面会有 NtCreateFile 的 NTSTATUS 十六进制状态码。比如 0xC0000428 指向签名问题0xC00002B0 或 0xC02A0002 这类 FltMgr 状态码多数与 Altitude 冲突或重复加载有关。这里有个实用技巧直接运行fltmc看现有过滤驱动列表如果里面已有一个名字相同但路径不同的驱动说明上一次卸载没有完成。先fltmc unload MyMini如果返回失败再看是否有句柄被持有或者进程里有文件句柄没关闭。测试环境中反复修改 Altitude 后有时 fltmc 列表里的旧值还没有更新。这通常是因为驱动文件被旧实例占用sc start实际加载的还是内存里的老镜像。停掉服务、删除 System32\drivers 下的旧 .sys、重新复制新文件再sc start一次比在 VS2019 里反复重编译更能解决问题。这也是我调试 minifilter 时踩过最多的坑不是代码改错而是加载的是旧文件。5. 进阶把 minifilter 工程写稳的三个小技巧5.1 用 Context 代替静态数组保存回调状态minifilter 的回调会并发执行多个 core 可能同时进入 PreCreate。若只是打印日志无所谓但要做过滤策略就不能用全局变量保存单个文件的状态否则所有线程都在竞争同一块内存。FltMgr 为此提供了 Context 机制可以在FLT_CONTEXT_REGISTRATION中声明对应文件对象的上下文注册回调后通过FltGetFileContext获取。记一条经验凡是需要跨 Pre 和 Post 共享的信息都放 Context凡是只站在同一次调用里用的信息放 CompletionContext 参数。5.2 用字符串比较做最简过滤规则拦截文件操作时不要用RtlEqualUnicodeString直接比较Data-Iopb-TargetFileObject-FileName因为它不一定包含完整路径。先用FltGetFileNameInformation拿到规范化路径再对Name字段做比较比较前把大小写统一成RtlUpcaseUnicodeString。只做前缀匹配时用RtlPrefixUnicodeString比 strstr 更可靠因为 Unicode 大小写规则在 NTFS 上并不总是一对一。5.3 在 Post 回调里统计性能落差把文件名校验全部放在 PreCreate 会导致频繁执行FltGetFileNameInformation。遇到性能问题可以只注册IRP_MJ_CREATE的 Post 回调在FLT_POSTOP_CALLBACK_STATUS里延迟到完成之后再做分析和记录。这种方法不阻塞打开路径但拿不到“如果我在 Pre 阶段拦截会怎样”的现场。我一般会用两个工程版本对比Pre 版本用于功能验证Post 版本用于性能回归确认回调本身不是瓶颈后才把策略下变频。5.4 用内核计数器验证回调路径是否真的执行很多排错最终会落在一个问题上代码没问题但目标进程访问文件时回调没进来。这就需要用 Performance Counters 给每个回调打点而不是只依赖 DbgPrint。在 DriverEntry 里调用KeQuerySystemTime然后按进程更新计数器再通过 user mode 程序读取能直观看到不同 PID 对被管控文件的访问频率。这样做比看调试输出稳定不会因为未附加调试器而错过信息。本文还有配套的精品资源点击获取