ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Win10 NDIS 6.0 Filter驱动源码:主动收发数据包与MAC查询实战

Win10 NDIS 6.0 Filter驱动源码:主动收发数据包与MAC查询实战

简介:这是一份面向Windows内核网络驱动开发者的NDIS 6.0 Filter驱动案例源码,基于Win10 x64平台,配合VS 17.11.4与WDK 10.0.26100.1工具链构建。资源在原有Filter框架上新增了发送OID请求、发送自定义ICMP数据包以及接收数据包三项功能,并补充了查询网卡MAC地址的实现,适合已具备一定C++基础、希望深入理解内核网络编程的中高级开发者参考。压缩包共16个文件,约37KB,以c与h源文件为主体,涵盖Filter主体逻辑、设备管理、调试输出与功能实现等模块,另含vcxproj、sln、inf、rc及filters等工程与安装配置文件,结构清晰便于按模块研读。目前已有141人学习。通过这份代码,读者可掌握内核OID请求的发送与接收流程、内核资源的分配与回收机制,以及数据包收发与网卡信息查询的具体写法,是理解NDIS Filter驱动开发与网络安全监控原理的实用范例。

1. 从抓包到改包:一份 Win10 下 NDIS 6.0 Filter 驱动源码能省掉多少弯路

如果你在 Windows 上做过网络过滤,大概率经历过这个循环:装好 WDK,翻出微软官方 NDIS 示例,编译通过,装上去,然后发现它只能看不能改,想加个收发数据包的功能得自己从头啃文档。这份基于 Win10 的 NDIS 6.0 Filter 驱动源码,解决的正是这个断层——它在基础过滤框架上补全了两块最常被需要、又最容易卡住的功能:主动收发数据包,以及查询网卡 MAC 地址。适合已经能编译驱动、但对 NDIS 数据路径还不熟的 C++ 开发者,也适合想找一个可运行基线、在其上做协议过滤或流量统计的从业者。下面按「框架怎么搭 → 收发怎么实现 → MAC 怎么查 → 坑在哪」的顺序拆开讲,代码可以直接抄进你的工程里改。

2. NDIS 6.0 Filter 驱动框架:从 DriverEntry 到 FilterAttach 的注册链路

2.1 为什么选 Filter 而不是 Protocol 或 Miniport

NDIS 驱动分三类:Miniport 管物理网卡,Protocol 管协议栈,Filter 夹在两者之间做旁路处理。选 Filter 的理由很直接——它不需要你实现完整的网卡抽象,也不用接管协议绑定,只挂载到已有适配器上就能拿到收发路径上的数据。对于「过滤 + 偶尔主动发包」这种需求,Filter 是侵入性最小的方案。

NDIS 6.0 相比 5.x 最大的变化是引入了NET_BUFFER_LIST和NET_BUFFER结构替代旧的NDIS_PACKET,以及强制要求驱动实现FilterXXX回调集。这意味着你不能拿老代码直接改,必须按新回调模型重写注册逻辑。源码里DriverEntry的核心就是填NDIS_FILTER_DRIVER_CHARACTERISTICS,然后调NdisFRegisterFilterDriver。

// DriverEntry:注册 Filter 驱动,绑定回调集 NDIS_FILTER_DRIVER_CHARACTERISTICS chars = {0}; chars.Header.Type = NDIS_OBJECT_TYPE_FILTER_DRIVER_CHARACTERISTICS; chars.Header.Size = sizeof(NDIS_FILTER_DRIVER_CHARACTERISTICS); chars.Header.Revision = NDIS_FILTER_DRIVER_CHARACTERISTICS_REVISION_2; chars.MajorNdisVersion = 6; chars.MinorNdisVersion = 0; chars.FriendlyName = L"SampleFilter"; chars.UniqueName = L"{...}"; // 唯一 GUID 字符串 chars.ServiceName = L"SampleFilter"; // 必填回调 chars.AttachHandler = FilterAttach; chars.DetachHandler = FilterDetach; chars.RestartHandler = FilterRestart; chars.PauseHandler = FilterPause; // 收发路径回调——这是能改包的关键 chars.SendNetBufferListsHandler = FilterSendNetBufferLists; chars.ReturnNetBufferListsHandler = FilterReturnNetBufferLists; chars.ReceiveNetBufferListsHandler = FilterReceiveNetBufferLists; NdisFRegisterFilterDriver(DriverObject, RegistryPath, &chars, &g_FilterHandle);

逻辑说明:MajorNdisVersion=6和MinorNdisVersion=0锁定了 NDIS 6.0 语义,Win10 向下兼容这个版本。SendNetBufferListsHandler拦截发送方向,ReceiveNetBufferListsHandler拦截接收方向,这两个是后续做收发和改包的入口。UniqueName必须是 GUID 格式字符串,随便填会导致注册失败。

参数说明:FriendlyName是设备管理器里显示的名字,ServiceName必须和 INF 文件里的服务名一致,否则安装后驱动加载不起来。Revision用 2 是为了拿到完整的回调字段,用 1 会缺几个 handler。

2.2 FilterAttach 里要做的三件事

FilterAttach在驱动挂载到某个网卡时被调用,源码里做了三件事:分配上下文、设置过滤属性、注册 MAC 查询用的 OID 请求。

NDIS_STATUS FilterAttach( NDIS_HANDLE NdisFilterHandle, NDIS_HANDLE FilterDriverContext, PNDIS_FILTER_ATTACH_PARAMETERS AttachParameters) { PMY_FILTER_CONTEXT ctx = ExAllocatePoolWithTag(NonPagedPool, sizeof(MY_FILTER_CONTEXT), 'fltC'); if (!ctx) return NDIS_STATUS_RESOURCES; NdisZeroMemory(ctx, sizeof(MY_FILTER_CONTEXT)); ctx->FilterHandle = NdisFilterHandle; // 设置过滤能力:数据 + 可选 NDIS_FILTER_ATTRIBUTES attrs = {0}; attrs.Header.Type = NDIS_OBJECT_TYPE_FILTER_ATTRIBUTES; attrs.Header.Size = sizeof(NDIS_FILTER_ATTRIBUTES); attrs.Header.Revision = NDIS_FILTER_ATTRIBUTES_REVISION_1; attrs.Flags = NDIS_FILTER_ATTRIBUTE_DATA_OPTIONAL; NdisFSetAttributes(NdisFilterHandle, ctx, &attrs); // 查询 MAC 地址——在 Attach 阶段就发 OID QueryMacAddress(ctx); return NDIS_STATUS_SUCCESS; }

逻辑说明:NDIS_FILTER_ATTRIBUTE_DATA_OPTIONAL表示这个过滤器对数据不是必须的,系统可以在资源紧张时跳过它,避免蓝屏。QueryMacAddress在 Attach 阶段调用是因为此时适配器已经就绪,OID 请求能拿到有效返回。

参数说明:ExAllocatePoolWithTag的 tag 用'fltC'方便在池监控工具里定位泄漏。上下文结构体里至少要存FilterHandle和 MAC 地址,后续收发回调都要用。

提示:Attach 阶段不要做耗时操作,OID 查询是同步的,如果网卡没就绪会阻塞,建议加超时判断。

3. 收发数据包:在 Filter 里主动构造和注入 NET_BUFFER_LIST

3.1 发送方向:从 FilterSendNetBufferLists 到 NdisFSendNetBufferLists

发送路径的拦截点很清晰——系统要发包时先调你的FilterSendNetBufferLists,你处理完再调NdisFSendNetBufferLists往下传。源码里在这个回调里加了一个统计逻辑,同时演示了如何构造一个新包注入发送队列。

VOID FilterSendNetBufferLists( NDIS_HANDLE FilterModuleContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG SendFlags) { PMY_FILTER_CONTEXT ctx = (PMY_FILTER_CONTEXT)FilterModuleContext; // 遍历 NBL 链,统计包数量 for (PNET_BUFFER_LIST nbl = NetBufferLists; nbl; nbl = NET_BUFFER_LIST_NEXT_NBL(nbl)) { ctx->SendCount++; // 这里可以改包:遍历 NET_BUFFER 修改内容 } // 原样下发 NdisFSendNetBufferLists(ctx->FilterHandle, NetBufferLists, PortNumber, SendFlags); }

逻辑说明:NET_BUFFER_LIST是一个链表,每个节点代表一批数据包。遍历时用NET_BUFFER_LIST_NEXT_NBL而不是直接访问Next字段,这是 NDIS 6.0 的封装要求。如果要改包内容,需要先调NdisGetFirstNetBuffer拿到NET_BUFFER,再用NdisGetDataBuffer映射数据区。

参数说明:SendFlags常见值有NDIS_SEND_FLAGS_DISPATCH_LEVEL,表示当前在 DISPATCH_LEVEL 中断级别,此时不能调分页内存相关函数。PortNumber在多端口网卡上区分端口,单端口传 0 即可。

主动发包的构造流程更复杂一些,源码里封装了一个SendTestPacket函数:

NDIS_STATUS SendTestPacket(PMY_FILTER_CONTEXT ctx, PUCHAR payload, ULONG len) { PMDL mdl = NdisAllocateMdl(ctx->FilterHandle, payload, len); if (!mdl) return NDIS_STATUS_RESOURCES; PNET_BUFFER_LIST nbl = NdisAllocateNetBufferAndNetBufferList( ctx->NblPool, 0, 0, mdl, 0, len); if (!nbl) { NdisFreeMdl(mdl); return NDIS_STATUS_RESOURCES; } // 设置源 MAC 和目的 MAC(以太网帧头) // ... 填充帧头逻辑 ... NdisFSendNetBufferLists(ctx->FilterHandle, nbl, 0, 0); return NDIS_STATUS_SUCCESS; }

逻辑说明:NdisAllocateMdl把用户缓冲区映射成 MDL,NdisAllocateNetBufferAndNetBufferList从池里分配 NBL 并关联 MDL。发送后 NBL 的所有权转移给 NDIS,不能立即释放,要在FilterReturnNetBufferLists回调里回收。

参数说明:NblPool需要在 Attach 时用NdisAllocateNetBufferListPool创建,指定NET_BUFFER_LIST_POOL_PARAMETERS里的DataSize为 0 表示不预分配数据区。NdisFSendNetBufferLists的第三个参数是端口号,第四个SendFlags传 0 表示在 PASSIVE_LEVEL 调用。

3.2 接收方向:FilterReceiveNetBufferLists 里的数据提取

接收路径和发送对称,系统收到包后调FilterReceiveNetBufferLists,你处理完调NdisFIndicateReceiveNetBufferLists往上递。源码里在这个回调里做了 MAC 地址过滤的演示。

VOID FilterReceiveNetBufferLists( NDIS_HANDLE FilterModuleContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG NumberOfNetBufferLists, ULONG ReceiveFlags) { PMY_FILTER_CONTEXT ctx = (PMY_FILTER_CONTEXT)FilterModuleContext; for (PNET_BUFFER_LIST nbl = NetBufferLists; nbl; nbl = NET_BUFFER_LIST_NEXT_NBL(nbl)) { PNET_BUFFER nb = NET_BUFFER_LIST_FIRST_NB(nbl); if (nb) { // 提取以太网帧头,检查目的 MAC PUCHAR data = (PUCHAR)NdisGetDataBuffer(nb, 14, NULL, 1, 0); if (data) { // data[0..5] 是目的 MAC,data[6..11] 是源 MAC ctx->RecvCount++; } } } NdisFIndicateReceiveNetBufferLists(ctx->FilterHandle, NetBufferLists, PortNumber, NumberOfNetBufferLists, ReceiveFlags); }

逻辑说明:NdisGetDataBuffer的第二个参数 14 表示要读取的字节数(以太网帧头长度),第三个参数传 NULL 表示不需要对齐缓冲区,第四个参数 1 表示如果数据在多个 MDL 里要拷贝到连续内存。返回的指针只在当前回调内有效,不能存起来后续使用。

参数说明:ReceiveFlags里NDIS_RECEIVE_FLAGS_RESOURCES表示 NDIS 需要立即回收 NBL,此时不能调NdisFIndicateReceiveNetBufferLists延迟处理,必须当场返回。这个标志是新手最容易翻车的地方,后面避坑章节会细说。

注意:接收回调可能在 DISPATCH_LEVEL 执行,所有操作必须是非分页的,不能调ExAllocatePoolWithTag带PagedPool参数。

4. 查询网卡 MAC 地址:OID 请求的同步与异步两条路

4.1 用 NdisFOidRequest 同步查询

MAC 地址查询走 OID 请求,最直接的方式是在 Attach 后调NdisFOidRequest同步获取。源码里QueryMacAddress的实现如下:

NDIS_STATUS QueryMacAddress(PMY_FILTER_CONTEXT ctx) { NDIS_OID oid = OID_802_3_CURRENT_ADDRESS; UCHAR macBuf[6] = {0}; ULONG bytesNeeded = 0; NDIS_STATUS status = NdisFOidRequest(ctx->FilterHandle, oid, macBuf, sizeof(macBuf), &bytesNeeded); if (status == NDIS_STATUS_SUCCESS && bytesNeeded == 6) { NdisCopyMemory(ctx->MacAddress, macBuf, 6); return NDIS_STATUS_SUCCESS; } return status; }

逻辑说明:OID_802_3_CURRENT_ADDRESS是标准 OID,返回 6 字节 MAC。NdisFOidRequest是同步调用,会阻塞当前线程直到网卡返回。在 Attach 阶段调用是安全的,因为此时系统还没开始大量收发。

参数说明:bytesNeeded是输出参数,返回实际需要的字节数。如果传入的缓冲区太小,会返回NDIS_STATUS_BUFFER_TOO_SHORT并填充bytesNeeded。MAC 地址固定 6 字节,所以缓冲区给 6 就够。

4.2 异步查询与 OID 完成回调

同步查询在 Attach 阶段没问题,但如果在运行中查询,可能会因为网卡忙而阻塞。源码里也提供了异步版本,用NdisFOidRequestComplete回调接收结果。

// 异步发起 NDIS_STATUS QueryMacAsync(PMY_FILTER_CONTEXT ctx) { PNDIS_OID_REQUEST req = ExAllocatePoolWithTag(NonPagedPool, sizeof(NDIS_OID_REQUEST), 'oidR'); if (!req) return NDIS_STATUS_RESOURCES; NdisZeroMemory(req, sizeof(NDIS_OID_REQUEST)); req->Header.Type = NDIS_OBJECT_TYPE_OID_REQUEST; req->Header.Revision = NDIS_OID_REQUEST_REVISION_1; req->Header.Size = sizeof(NDIS_OID_REQUEST); req->RequestType = NdisRequestQueryInformation; req->DATA.QUERY_INFORMATION.Oid = OID_802_3_CURRENT_ADDRESS; req->DATA.QUERY_INFORMATION.InformationBuffer = ctx->MacAddress; req->DATA.QUERY_INFORMATION.InformationBufferLength = 6; return NdisFOidRequest(ctx->FilterHandle, req); } // 完成回调 VOID FilterOidRequestComplete( NDIS_HANDLE FilterModuleContext, PNDIS_OID_REQUEST OidRequest, NDIS_STATUS Status) { PMY_FILTER_CONTEXT ctx = (PMY_FILTER_CONTEXT)FilterModuleContext; if (Status == NDIS_STATUS_SUCCESS) { // ctx->MacAddress 已被填充 } ExFreePoolWithTag(OidRequest, 'oidR'); }

逻辑说明:异步版本需要自己分配NDIS_OID_REQUEST结构,填好 OID 和缓冲区指针,然后调NdisFOidRequest。完成后 NDIS 调FilterOidRequestComplete,在这个回调里释放请求结构。注意InformationBuffer指向的内存必须在请求完成前保持有效,所以源码里直接指向上下文里的MacAddress字段。

参数说明:RequestType用NdisRequestQueryInformation表示查询。FilterOidRequestComplete需要在NDIS_FILTER_DRIVER_CHARACTERISTICS里注册,字段名是OidRequestCompleteHandler。

提示:异步查询适合在运行中周期性刷新 MAC,但要注意并发——如果上一次请求还没完成就发起新的,会返回NDIS_STATUS_BUSY。

5. 避坑与排查:Filter 驱动最容易翻车的五个点

5.1 装完驱动网卡直接消失

现象:INF 安装成功,设备管理器里网卡带黄色感叹号,或者网络适配器列表里少了一块网卡。

原因:FilterAttach返回了失败状态,NDIS 会把整个过滤器栈卸载,连带影响底层网卡。常见触发点是NdisFSetAttributes没调,或者FilterRestart里没正确处理NDIS_STATUS_SUCCESS。

解决:在FilterAttach每个可能失败的分支加DbgPrint,用 DebugView 抓日志。重点检查NdisFSetAttributes的返回值,以及QueryMacAddress是否因为 OID 不支持而返回错误。如果 OID 查询失败,不要直接返回失败,改成记录日志后继续,避免影响网卡正常挂载。

5.2 收发回调里访问分页内存导致蓝屏

现象:驱动运行一段时间后随机蓝屏,错误码IRQL_NOT_LESS_OR_EQUAL。

原因:FilterSendNetBufferLists和FilterReceiveNetBufferLists可能在DISPATCH_LEVEL执行,此时访问分页内存(如PagedPool分配的结构、用户态缓冲区)会触发页错误。

解决:所有在收发回调里访问的数据结构必须用NonPagedPool分配。如果需要在回调里做复杂处理,用NdisQueueIoWorkItem把工作项丢到PASSIVE_LEVEL的 worker 线程里执行。源码里上下文结构体就是用NonPagedPool分配的,这一点不能省。

5.3 NBL 回收时机搞错导致内存泄漏

现象:驱动跑几小时后系统内存持续增长,池监控显示NblPool标签的分配数只增不减。

原因:NdisFSendNetBufferLists发送后 NBL 所有权转移给 NDIS,但很多人在FilterSendNetBufferLists里直接NdisFreeNetBufferList,导致 NDIS 后续访问已释放内存。或者相反,在FilterReturnNetBufferLists里忘了释放自己构造的 NBL。

解决:记住规则——谁分配谁释放,但发送后释放权转移。自己构造的 NBL 在FilterReturnNetBufferLists回调里释放,系统传入的 NBL 不要动,原样传给NdisFSendNetBufferLists或NdisFIndicateReceiveNetBufferLists即可。源码里SendTestPacket构造的 NBL 就是在返回回调里统一回收的。

5.4 MAC 地址查询返回全零

现象:QueryMacAddress返回成功,但MacAddress数组全是 0。

原因:OID_802_3_CURRENT_ADDRESS在某些虚拟网卡或未就绪的适配器上返回全零。另外,如果bytesNeeded不是 6,说明返回的不是标准 MAC 格式。

解决:加判断bytesNeeded == 6且 MAC 不全零才认为有效。对于虚拟网卡,可以尝试OID_802_3_PERMANENT_ADDRESS获取永久地址。如果两个都失败,在FilterRestart里重试,因为 Attach 阶段网卡可能还没完全初始化。

5.5 安装后网络时通时断

现象:驱动装上后,网络能通但延迟抖动大,或者偶尔断几秒。

原因:FilterReceiveNetBufferLists里处理太慢,导致 NDIS 接收队列积压。或者NDIS_RECEIVE_FLAGS_RESOURCES标志处理错误——这个标志表示 NDIS 需要立即回收 NBL,如果你还调NdisFIndicateReceiveNetBufferLists延迟处理,会导致 NBL 被提前释放。

解决:检查接收回调里是否有耗时操作,比如DbgPrint大量日志、复杂计算。如果有NDIS_RECEIVE_FLAGS_RESOURCES标志,必须当场处理完并返回,不能调NdisFIndicateReceiveNetBufferLists。源码里在接收回调开头就判断了这个标志,直接原样上递。

6. 进阶技巧:用 WPP 跟踪替代 DbgPrint 做收发路径调试

DbgPrint在收发回调里是灾难——它本身可能触发分页操作,而且在DISPATCH_LEVEL下输出大量日志会拖慢整个网络栈。源码里预留了 WPP(Windows Software Trace Preprocessor)的埋点,这是调试 Filter 驱动的正确姿势。

WPP 的用法分三步。第一步,在工程里加入 WPP 预处理器,在源文件头部加控制块:

// 在文件开头定义 WPP 控制块 #include "wpptrace.h" #define WPP_CONTROL_GUIDS \ WPP_DEFINE_CONTROL_GUID(MyFilterTraceGuid, \ (12345678,1234,1234,1234,123456789012), \ WPP_DEFINE_BIT(TRACE_SEND) \ WPP_DEFINE_BIT(TRACE_RECV) \ WPP_DEFINE_BIT(TRACE_OID))

第二步,在收发回调里用DoTraceMessage替代DbgPrint:

// 在 FilterSendNetBufferLists 里 DoTraceMessage(TRACE_SEND, "Send NBL count=%lu flags=%lu", NumberOfNetBufferLists, SendFlags);

第三步,编译后用traceview或logman启动跟踪会话,指定 GUID 和 TMF 文件。TMF 文件是编译时生成的,包含日志格式信息。

:: 启动跟踪会话 logman create trace MyFilterSession -p {12345678-1234-1234-1234-123456789012} -o filter.etl -ets :: 停止并转换 logman stop MyFilterSession -ets tracefmt filter.etl -o filter.txt -tmf MyFilter.tmf

逻辑说明:WPP 的日志写入是异步的,不会阻塞收发路径。-ets参数表示立即启动,不用先创建再启动两步走。tracefmt把二进制 ETL 转成可读文本,-tmf指定格式文件。

参数说明:GUID 必须和代码里WPP_DEFINE_CONTROL_GUID的一致。WPP_DEFINE_BIT定义的位用于分类过滤,比如只跟踪发送方向就加-flag 1。TMF 文件在编译输出目录里,文件名通常是工程名加.tmf。

从那以后我每次调 Filter 驱动,都强制走一遍 WPP 跟踪,哪怕只是加一行日志——DbgPrint在收发路径上的坑,踩过一次就不想再踩第二次。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进