资讯详情 Win10 NDIS 6.0 Filter驱动源码:主动收发数据包与MAC查询实战
📅 2026/10/21 9:55:23
简介这是一份面向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 或 MiniportNDIS 驱动分三类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 LSampleFilter; chars.UniqueName L{...}; // 唯一 GUID 字符串 chars.ServiceName LSampleFilter; // 必填回调 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);逻辑说明MajorNdisVersion6和MinorNdisVersion0锁定了 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_LIST3.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把用户缓冲区映射成 MDLNdisAllocateNetBufferAndNetBufferList从池里分配 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] 是目的 MACdata[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下输出大量日志会拖慢整个网络栈。源码里预留了 WPPWindows 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在收发路径上的坑踩过一次就不想再踩第二次。希望帮到你。本文还有配套的精品资源点击获取