简介:这份资源围绕Windows平台下的串口驱动过滤技术展开,面向具备一定驱动开发基础、希望深入理解串口通信拦截与定制的中高级开发者。内容涵盖串口驱动过滤的基本原理,以及上滤驱动与下滤驱动协同工作的实现思路,帮助读者在不改动原始驱动和应用程序代码的前提下,对串口I/O请求进行监控、修改或增强安全性。压缩包共5个文件,约121KB,包含sln解决方案、vcxproj工程文件、cpp源码、dll动态库与exe可执行程序,分别对应驱动工程组织、过滤逻辑实现、编译产物及串口测试工具,便于直接研究源码或安装验证。资源已有383人学习下载,读者可从中获取基于WDM或UWD框架的驱动工程结构、过滤驱动核心代码,以及配合串口工具进行收发测试与Windbg调试的实践参考,适合作为串口过滤驱动入门与二次开发的样例素材。
1. 串口过滤驱动到底拦的是什么:从一次数据丢包排查说起
去年帮一个做数据采集的团队排查问题,他们的上位机每隔几小时就会丢一帧报文,应用层日志干干净净,串口助手单独测试又完全正常。最后用 Bus Hound 抓 IRP 才发现,问题出在一个第三方虚拟串口工具和系统串口驱动之间的交互上——数据在驱动栈里被改写了,应用层根本看不到。这件事之后,我开始认真研究串口驱动过滤这个方向,也拆了不少现成的过滤驱动源码包。
串口驱动过滤,说白了就是在 Windows 驱动栈里插一层自己的驱动,夹在用户态应用程序和底层硬件驱动之间,对串口的 I/O 请求做拦截、记录、修改或者转发。它能解决的核心问题是:你不想改应用代码,也不想动系统自带驱动,但需要对串口数据流做点事情——比如加日志、做协议转换、过滤非法指令、统计流量。适合谁?做工业数据采集、串口设备安全审计、协议逆向、以及需要给老旧串口软件加一层"中间件"的驱动开发者。这个资源包给的就是一套能编译、能装、能跑的 KMDF 过滤驱动骨架,外加测试用的串口工具和依赖库,拿来就能改。
2. KMDF 过滤驱动的骨架:从 Driver1.sln 到第一个能拦 IRP 的版本
2.1 为什么选 KMDF 而不是 WDM 来写过滤驱动
资源包里给的是 KMDF 工程(Driver1.sln / Driver1.vcxproj),不是老式的 WDM。这个选择本身就有讲究。WDM 写过滤驱动要自己处理 IRP 的层层传递、手动挂载设备对象、管理 PnP 和电源状态机,代码量大且容易在卸载时蓝屏。KMDF 把这些模板化了,框架帮你处理 IRP 转发、队列管理、即插即用和电源回调,你只需要在关键回调里写业务逻辑。
对于串口过滤这种场景,KMDF 的优势更明显:串口设备经常被热插拔(USB 转串口),KMDF 的 PnP 回调能让你在设备到达和移除时自动挂载/卸载过滤逻辑,不用自己写一堆状态判断。常见做法是用WdfFdoInitSetFilter把驱动声明为过滤驱动,然后通过WdfDeviceCreateDeviceInterface注册一个设备接口,让上层应用能找到你。
不过 KMDF 也有边界:它封装了太多细节,如果你需要精确控制 IRP 的完成顺序,或者要处理一些框架不暴露的底层操作,还是得回到 WDM 或者用WdfDeviceInitAssignWdmIrpPreprocessCallback做预处理。资源包里的 main.cpp 就是 KMDF 的入口,结构清晰,适合先跑通再深挖。
2.2 用 Visual Studio 2017 编译 Driver1 工程的完整步骤
资源包里的工程是 VS2017 格式,但用 VS2019 或 VS2022 打开也能自动升级。编译前需要装 WDK(Windows Driver Kit),版本要和 SDK 匹配。我一般会先确认三件事:WDK 已安装、工程属性里的目标平台版本正确、驱动签名模式设为测试签名。
# 以管理员身份打开 VS 开发者命令提示符,进入工程目录 cd "串口驱动\KMDF Driver1" # 用 msbuild 编译,Debug 配置,x64 平台 msbuild "KMDF Driver1.vcxproj" /p:Configuration=Debug /p:Platform=x64 # 编译成功后,在 x64\Debug 下会生成 .sys 和 .inf 文件 # 查看输出 dir x64\Debug\*.sys dir x64\Debug\*.inf编译逻辑说明:msbuild是 VS 自带的构建工具,/p:Configuration指定 Debug 或 Release,/p:Platform指定 x64 或 Win32。Debug 版本会带调试符号,方便用 WinDbg 跟。如果编译报错 "Cannot find WDK",说明环境变量没配好,检查 WDK 安装路径是否在%WindowsSdkDir%下。
参数上要注意:目标平台版本(Target Platform Version)在工程属性 → 驱动程序设置 → 常规里,一般选你系统对应的 SDK 版本。如果选错,会出现 "Inf2Cat error" 或者签名失败。我习惯在编译前把Inf2Cat的/os参数改成10_X64,10_X86,避免只生成单一平台。
2.3 过滤驱动挂载到串口设备栈的两种方式
编译出 .sys 之后,怎么让它挂到目标串口上?资源包里没有自动安装脚本,需要手动操作。常见做法有两种:一种是用devcon工具安装,另一种是改注册表让驱动作为 UpperFilter 加载。
# 方式一:用 devcon 安装驱动到指定设备 devcon install "KMDF Driver1.inf" "root\SerialFilter" # 方式二:手动添加 UpperFilter(需要知道目标串口的硬件 ID) # 在注册表 HKLM\SYSTEM\CurrentControlSet\Enum\<设备路径>\Device Parameters 下 # 新建 MultiString 值 UpperFilters,内容写你的驱动服务名 reg add "HKLM\SYSTEM\CurrentControlSet\Enum\ACPI\PNP0501\1\Device Parameters" /v UpperFilters /t REG_MULTI_SZ /d SerialFilter /f逻辑说明:devcon install会创建根枚举设备并加载驱动,适合测试。改注册表的方式是让系统在加载串口驱动时自动把你的过滤驱动插进去,顺序在系统串口驱动之上。注意UpperFilters的值是驱动服务名,不是文件名,服务名在 .inf 的[DefaultInstall.Services]段里定义。
参数上,PNP0501是标准串口的硬件 ID,但 USB 转串口通常是USB\VID_xxxx&PID_xxxx,需要先用设备管理器查看硬件 ID。改完注册表要重启或者禁用再启用设备才生效。这里有个血泪经验:如果过滤驱动加载失败,设备会直接变成黄色感叹号,串口功能全丢,所以一定要先准备好卸载脚本。
3. 在过滤驱动里读写串口数据:IRP 拦截与缓冲区处理
3.1 拦截 IRP_MJ_READ 和 IRP_MJ_WRITE 的关键回调
KMDF 过滤驱动拦截读写请求,靠的是注册EvtIoRead和EvtIoWrite回调。资源包里的 main.cpp 已经搭好了框架,但默认是直接转发,没有做数据处理。要加自己的逻辑,就在这两个回调里动手。
// 在 EvtIoRead 回调里拦截读请求 VOID SerialFilterEvtIoRead( _In_ WDFQUEUE Queue, _In_ WDFREQUEST Request, _In_ size_t Length ) { // 先获取请求对应的内存对象 WDFMEMORY memory; NTSTATUS status = WdfRequestRetrieveOutputMemory(Request, &memory); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } // 在这里可以记录日志、修改缓冲区内容 // 比如打印本次读请求的长度 KdPrint(("SerialFilter: Read request, length = %zu\n", Length)); // 转发给下层驱动 WdfRequestFormatRequestUsingCurrentType(Request); WdfRequestSend(Request, WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)), WDF_NO_SEND_OPTIONS); }逻辑说明:WdfRequestRetrieveOutputMemory拿到的是用户态传下来的输出缓冲区,读操作完成后数据会填到这里。你可以在转发之前修改缓冲区内容,或者在转发之后用完成例程再处理。WdfRequestFormatRequestUsingCurrentType保持原请求类型不变,WdfRequestSend发往默认 I/O 目标(也就是下层驱动)。
参数上,Length是请求的数据长度,但实际传输可能小于这个值,完成例程里要用WdfRequestGetInformation获取实际字节数。注意不要在回调里做耗时操作,否则会阻塞整个队列。如果需要异步处理,用WdfRequestForwardToIoQueue转到另一个队列。
3.2 缓冲区拷贝与数据修改的边界条件
在过滤驱动里改数据,最容易翻车的地方是缓冲区长度和内存对齐。串口数据是字节流,但 IRP 的缓冲区可能是METHOD_BUFFERED、METHOD_IN_DIRECT或METHOD_NEITHER,处理方式完全不同。
// 处理 METHOD_BUFFERED 类型的写请求 VOID SerialFilterEvtIoWrite( _In_ WDFQUEUE Queue, _In_ WDFREQUEST Request, _In_ size_t Length ) { WDFMEMORY memory; NTSTATUS status = WdfRequestRetrieveInputMemory(Request, &memory); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } // 获取缓冲区指针 PVOID buffer = WdfMemoryGetBuffer(memory, NULL); if (buffer != NULL && Length > 0) { // 示例:把第一个字节改成 0xAA(仅用于演示,实际按协议改) PUCHAR pData = (PUCHAR)buffer; pData[0] = 0xAA; KdPrint(("SerialFilter: Write buffer modified, first byte = 0x%02X\n", pData[0])); } // 转发 WdfRequestFormatRequestUsingCurrentType(Request); WdfRequestSend(Request, WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)), WDF_NO_SEND_OPTIONS); }逻辑说明:WdfRequestRetrieveInputMemory拿到输入缓冲区,WdfMemoryGetBuffer返回内核态可访问的指针。对于METHOD_BUFFERED,系统已经把用户数据拷到内核缓冲区,直接改就行。但如果是METHOD_NEITHER,拿到的是用户态地址,必须用ProbeForRead/ProbeForWrite校验,否则会蓝屏。
参数上,Length是请求长度,但缓冲区实际大小可能更大,不要越界写。我一般会在改数据前先判断Length >= 1,并且只改协议允许的字段。另外,串口驱动可能对数据有对齐要求,改完数据后要确保长度不变,否则下层驱动可能拒绝。
3.3 用 KdPrint 和 WinDbg 验证过滤逻辑是否生效
驱动编译好、装好之后,怎么确认它真的在拦截数据?最直接的办法是用KdPrint输出日志,然后用 DebugView 或 WinDbg 看。资源包里没有带调试工具,但 Windows SDK 里有 WinDbg。
# 用 WinDbg 附加到内核,查看 KdPrint 输出 # 先设置符号路径 .sympath srv*C:\Symbols*https://msdl.microsoft.com/download/symbols .reload # 查看过滤驱动的调试输出 !dbgprint # 或者用 DebugView(需管理员运行,勾选 Capture Kernel)逻辑说明:KdPrint在 Debug 版本里会输出到内核调试器,Release 版本会被编译掉。!dbgprint是 WinDbg 的扩展命令,能直接看内核调试缓冲区。如果看不到输出,检查驱动是否真的加载了——用!drvobj SerialFilter查看驱动对象。
参数上,WinDbg 需要开启内核调试模式,可以用bcdedit /debug on和bcdedit /dbgsettings配置。如果是本地调试,用bcdedit /dbgsettings local也行,但会稍微影响性能。注意KdPrint的输出速率有限,高频读写场景下会丢日志,这时候要用 ETW 或者自定义的环形缓冲区。
4. 避坑与排查:串口过滤驱动最容易翻车的五个地方
4.1 现象:装完驱动串口直接消失,设备管理器黄色感叹号
原因:过滤驱动加载失败,系统无法完成设备栈的构建。常见原因是 .inf 文件里的服务名和注册表里的UpperFilters值不一致,或者驱动签名验证没通过。
解决:先卸载驱动,用devcon remove或者手动删注册表项。然后检查 .inf 里的ServiceName和AddService段,确保和UpperFilters里写的完全一致。签名问题用bcdedit /set testsigning on开启测试签名模式,重启后再装。
4.2 现象:驱动能装,但收不到任何 IRP,KdPrint 没输出
原因:过滤驱动没有正确挂到目标设备栈上。可能是UpperFilters加错了设备路径,或者 KMDF 的EvtDeviceAdd回调里没有创建 I/O 队列。
解决:用!devstack查看设备栈,确认你的驱动对象在栈里。如果没有,检查注册表路径是否对应正确的硬件 ID。KMDF 里要在EvtDeviceAdd里调用WdfIoQueueCreate创建默认队列,否则请求不会进来。
4.3 现象:读写数据时蓝屏,错误码 0x000000D1(DRIVER_IRQL_NOT_LESS_OR_EQUAL)
原因:在过高的 IRQL 级别访问了分页内存,或者直接操作用户态地址没有校验。串口驱动的读写回调可能在 DISPATCH_LEVEL 被调用,这时候不能访问分页内存。
解决:用WdfRequestRetrieveInputMemory/WdfRequestRetrieveOutputMemory拿内存对象,不要直接解引用用户态指针。如果必须访问用户缓冲区,用WdfRequestRetrieveUnsafeUserInputBuffer并配合ProbeForRead。确保所有代码在PASSIVE_LEVEL或DISPATCH_LEVEL下都能安全执行。
4.4 现象:过滤驱动导致串口吞吐量下降,延迟明显增加
原因:在回调里做了同步的耗时操作,比如写文件日志、等待事件、调用KeStallExecutionProcessor。这些会阻塞 I/O 队列,导致后续请求堆积。
解决:把日志写到环形缓冲区,用单独的线程异步刷盘。不要在EvtIoRead/EvtIoWrite里做任何可能阻塞的操作。如果必须做协议解析,用WdfRequestForwardToIoQueue转到自定义队列,在EvtIoCanceledOnQueue里处理取消。
4.5 现象:卸载驱动时蓝屏,错误码 0x000000CE(DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATIONS)
原因:驱动卸载时还有未完成的 IRP 或未释放的资源。KMDF 框架虽然帮你管理了大部分对象,但如果你手动创建了 WDFMEMORY 或 WDFREQUEST 没有释放,就会出问题。
解决:在EvtDeviceContextCleanup或EvtDriverUnload里确保所有队列已清空、所有请求已完成。用WdfObjectDelete释放手动创建的对象。我一般会在卸载前先禁用设备,等所有 I/O 停掉再卸载驱动。
5. 进阶:用串口工具验证过滤效果与动态开关设计
资源包里带了串口工具和Hyper Terminal.exe,还有serialport.dll,这些是验证过滤驱动的好帮手。我一般会先用串口工具发固定模式的数据,然后在过滤驱动里打印出来,对比是否一致。如果要做协议转换,就在驱动里改完数据后,用另一个串口工具接收,看输出是否符合预期。
// 在过滤驱动里加一个动态开关,通过 IOCTL 控制是否启用过滤 #define IOCTL_SERIALFILTER_ENABLE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) VOID SerialFilterEvtIoDeviceControl( _In_ WDFQUEUE Queue, _In_ WDFREQUEST Request, _In_ size_t OutputBufferLength, _In_ size_t InputBufferLength, _In_ ULONG IoControlCode ) { if (IoControlCode == IOCTL_SERIALFILTER_ENABLE) { // 从输入缓冲区读取开关状态 PVOID buffer = NULL; NTSTATUS status = WdfRequestRetrieveInputBuffer(Request, sizeof(BOOLEAN), &buffer, NULL); if (NT_SUCCESS(status)) { gFilterEnabled = *(PBOOLEAN)buffer; KdPrint(("SerialFilter: Filter enabled = %d\n", gFilterEnabled)); } WdfRequestComplete(Request, STATUS_SUCCESS); return; } // 其他 IOCTL 直接转发 WdfRequestFormatRequestUsingCurrentType(Request); WdfRequestSend(Request, WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)), WDF_NO_SEND_OPTIONS); }逻辑说明:IOCTL_SERIALFILTER_ENABLE是自定义的控制码,应用层用DeviceIoControl调用。WdfRequestRetrieveInputBuffer拿到输入数据,这里是一个 BOOLEAN 值。全局变量gFilterEnabled控制读写回调里是否执行过滤逻辑。这样可以在不卸载驱动的情况下动态开关,方便调试和对比。
参数上,CTL_CODE的FILE_DEVICE_UNKNOWN是自定义设备类型,0x800是功能号,METHOD_BUFFERED表示用缓冲方式传输。应用层调用时,DeviceIoControl的dwIoControlCode要传这个值。注意gFilterEnabled要用volatile或者加锁,因为可能在多核上并发访问。
验证方法:用串口工具发数据,先关闭过滤,看接收端是否正常;再打开过滤,看数据是否被修改。如果修改生效,说明 IOCTL 和过滤逻辑都通了。我习惯在驱动里加一个计数器,统计拦截了多少个请求,用!dbgprint看计数变化,比单看日志更直观。
从那以后我每次写过滤驱动,都会先加一个动态开关和请求计数器,确认基础框架没问题再往上堆业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取