ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

NDIS 6.0 Filter Driver实战:数据包过滤与MAC地址查询

NDIS 6.0 Filter Driver实战:数据包过滤与MAC地址查询 简介基于Windows 10 x64平台的NDIS 6.0 Filter驱动示例专为C/Windows内核驱动开发者设计重点演示如何在Filter驱动中新增收发数据包、发送OID请求及查询网卡MAC地址的实现可作NDIS网络驱动开发案例学习。资源包共16个文件以C/C源代码为主5个.c、5个.h另含Visual Studio项目文件.sln/.vcxproj、INF安装配置、RC资源及演示程序压缩包仅37KB结构清晰便于直接阅读与二次开发。项目代码覆盖filter.c设备逻辑、device.c管理逻辑、flt_dbg.c调试输出等功能模块集中展现内核OID请求发送接收、内核内存资源分配与回收、数据包构造与收发等关键流程帮助开发者掌握NDIS Filter驱动开发的核心技术路径。目前已有137人学习适合正在研究Windows网络驱动或需要参考过滤驱动拦截/注入数据包的中高级开发者。1. 项目概述Filter 驱动在什么场景下才是正确选择做 Windows 驱动开发的同学应该对这个场景不陌生业务方突然甩过来一个需求说要在网卡收发路径上做点事情——可能是统计流量、过滤特定报文、修改数据包内容或者像我这个项目一样单纯要把网卡的真实 MAC 地址从内核态捞出来透传给上层应用。这时候摆在面前的有三条路TDI/Winsock Kernel已经淘汰了、NDIS 协议驱动Protocol Driver、NDIS 过滤驱动Filter Driver。我最终选了 NDIS 6.0 Filter Driver核心原因有三个一是 Filter Driver 挂在协议驱动和网卡驱动之间它以过滤的方式介入数据路径所有从应用层下来的发送请求和从网卡上来的接收中断都会经过它天然适合做截获和修改。二是它相对于协议驱动简单太多不需要自己维护绑定关系不需要处理复杂的即插即用事件NDIS 帮你管理了大部分生命周期。三是从 Win10 开始系统的网络栈原生优先支持 NDIS 6.0Filter Driver 是微软官方推荐的数据路径扩展方式。这个项目具体做了三件事挂载到系统所有网卡上、在发送和接收路径上枚举 NET_BUFFER_LIST、通过 OID 请求查询网卡 MAC 地址。适用人群很明确正在学习 NDIS 驱动开发的入门者、需要在业务系统中插入网络过滤逻辑的工程师、以及被怎么拿到真实 MAC这种小问题卡住的老哥。提示NDIS 6.0 是 Vista 时代定下的规范版本Win10 的系统 netio.sys 依然完全兼容所以项目目标写成 NDIS 6.0 Filter 在新老系统上都能跑这个选型没有历史包袱。2. 工程搭建与驱动骨架VS WDK 的组合是效率最优解2.1 环境准备与 INF 文件的关键配置我用的环境是 Win10 22H2 Visual Studio 2019 WDK 10.0.19041。这套组合的好处是 VS 自带驱动项目模板新建项目时直接搜 Filter Driver 就能生成一个带完整 INF 的骨架省掉手写一堆配置的麻烦。如果你还用着老掉牙的 WDK 7建议尽早迁移新版 WDK 对 NDIS 6.0 的支持只多不少。INF 文件里有一处必须注意FilterClass 值决定驱动在过滤栈中的位置。常见取值包括 compression、encryption、scheduler 等如果只是做流量监控写成这样即可[DefaultInstall.NT] CopyFiles ... AddReg ... [DefaultInstall.NT.Services] AddService MyNdisFilter,%SPSVCINST_ASSOCSERVICE%,MyNdisFilter_Service_Inst [MyNdisFilter_Service_Inst] DisplayName My NDIS Filter Driver ServiceType 1 StartType 2 ErrorControl 1 ServiceBinary %12%\myNdisFilter.sys [MyNdisFilter_Device_Inst.NT] Characteristics 0x100这里Characteristics 0x100表示 NCF_LW_FILTER告诉 NDIS 这是一个 lightweight filter。如果忘了这个值驱动加载时直接给你报参数错误非常典型。2.2 驱动入口与特性注册的结构体初始化WDK 生成的模板里DriverEntry 里需要调用NdisFRegisterFilterDriver并把一个NDIS_FILTER_DRIVER_CHARACTERISTICS结构体传进去。这里面最关键的几个函数指针必须实现FilterAttach、FilterDetach、FilterSendNetBufferLists、FilterReceiveNetBufferLists、FilterOidRequest。我的初始化代码大致长这样NDIS_FILTER_DRIVER_CHARACTERISTICS fChars; NdisZeroMemory(fChars, sizeof(fChars)); fChars.Header.Type NDIS_OBJECT_TYPE_FILTER_DRIVER; fChars.Header.Size sizeof(NDIS_FILTER_DRIVER_CHARACTERISTICS); fChars.Header.Revision NDIS_FILTER_DRIVER_CHARACTERISTICS_REVISION_2; fChars.MajorNdisVersion 6; fChars.MinorNdisVersion 0; fChars.FilterAttachHandler FilterAttach; fChars.FilterDetachHandler FilterDetach; fChars.FilterSendNetBufferListsHandler FilterSendNetBufferLists; fChars.FilterReceiveNetBufferListsHandler FilterReceiveNetBufferLists; fChars.FilterOidRequestHandler FilterOidRequest; fChars.FilterOidRequestCompleteHandler FilterOidRequestComplete; fChars.FilterSendNetBufferListsCompleteHandler FilterSendNetBufferListsComplete; fChars.FilterReturnNetBufferListsHandler FilterReturnNetBufferLists; Status NdisFRegisterFilterDriver(DriverObject, fChars, FilterHandle);这里要提醒一个小坑如果只做监控不改包不实现 FilterSendNetBufferListsComplete 和 FilterReturnNetBufferLists 也能运行但如果要在发送/接收路径上 Clone 或挂起 NBL比如延迟处理这两个完成回调必须补上否则 NDIS 在等待返还时会直接 Bugcheck。3. 数据包收发路径的实现细节NBL 的所有权是灵魂3.1 发送路径FilterSendNetBufferLists 的转发与拦截数据包在 NDIS 6.0 里统一用NET_BUFFER_LIST简称 NBL表示。一个 NBL 里面挂着至少一个NET_BUFFER每个 NET_BUFFER 又指向一个 MDL 描述的内存块真正存数据的就是这个 MDL 指向的缓冲区。应用层调用 ws2_32 的 send 之后数据一路到协议驱动比如 TCPIP.sys协议驱动封装好 IP/TCP 头然后以 NBL 形式调用NdisSendNetBufferLists往下发。Filter 驱动的 FilterSendNetBufferLists 正是在这个时机被 NDIS 调用的。最简单的实现是原样转发VOID FilterSendNetBufferLists( NDIS_HANDLE FilterModuleContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG SendFlags) { PIO_WORKITEM pWorkItem NULL; // 这里可以做你想做的事计数、日志、改包、丢包 // 比如记录包数量: InterlockedIncrement(pFilter-SendPacketCount); // 原样转发到下一层 NdisFSendNetBufferLists(FilterModuleContext, NetBufferLists, PortNumber, SendFlags); }那丢包怎么实现呢只拿第一个 NBL剩下的和其他 NBL 直接转发或者返还给协议层。如果什么都不做直接返回不转发上层协议栈会挂起等待所以明确我要丢弃时必须调用NdisFSendNetBufferListsComplete把它返回给发送方。这块涉及两个计数需要特别小心引用计数ReferenceCount如果你把 NBL 缓存起来了但还没调用NdisFSendNetBufferLists转发此时需要一个额外的引用。不要天真地以为指针在你手里就能随便持有NDIS 的规则是所有权跟着引用计数走。返还计数SendComplete 和 Return如果你 Clone 了一个 NBL原 NBL 可以继续走正常路径Clone 的处理完之后必须NdisFSendNetBufferListsComplete或者NdisFReturnNetBufferLists回去二选一但绝不能两个都做。3.2 接收路径FilterReceiveNetBufferLists 的两种处理风格接收方向比发送方向多了一个概念——NDIS_RECEIVE_FLAGS_RESOURCES。网卡中断里跑的是 DISPATCH_LEVEL数据上来时如果 NDIS 资源紧张会把ReceiveFlags里的 RESOURCES 位置 1意思是我给你这批包你最好快点处理不行就照原样往下传别在我这里做耗时操作。VOID FilterReceiveNetBufferLists( NDIS_HANDLE FilterModuleContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, NDIS_SERIAL_NUMBER NumberOfNetBuffers, ULONG ReceiveFlags) { // 先从链表上往下传再遍历统计 // 注意顺序先传再遍历防止 NBL 被释放后再访问 NdisFReturnNetBufferLists(FilterModuleContext, NetBufferLists, ReceiveFlags); }筛选/拦截接收包的常见做法直接透传上面这份代码同时在 FilterAttach 里分配一个计数器在遍历 NBL 时计数如果要对某个协议号做过滤需要解析 NET_BUFFER 里的数据内容这里注意Ethernet 帧头 IP 头 传输层头的解析必须正确很多协议字段在平铺内存里是连续的但是有 VLAN Tag 时偏移会变不要写死偏移如果要把包截获下来给应用层不要直接在 FilterReceiveNetBufferLists 里 Copy 内存正确姿势是 Clone NBL 挂 WorkItem 到 PASSIVE_LEVEL 再处理避免在 DISPATCH_LEVEL 上做文件写入或锁竞争。3.3 改包场景的经验补充分享我这个项目只是新增加了收发数据包的代码严格讲只是过一遍和拿数据还没做改包。但我之前踩过一个坑必须分享出来想改包时直接改 NET_BUFFER 的数据不总是安全的。因为 NBL 里的 MDL 可能指向只读的共享内存比如 TCP/IP 协议栈为了性能做了 Zero Copy直接写会触发蓝屏或者改了不生效。正确做法是调用NdisAllocateCloneNetBufferList克隆一份改克隆体然后把克隆体发出去原 NBL 正常返还。这样一不影响上游二避免共享内存竞争。等后面测试完性能开销再考虑直接原地改包的优化方案这里先求稳。4. 查询网卡 MAC 地址OID 请求的同步与异步实现4.1 OID 是什么以及为什么有些网卡查不到MAC 地址这个信息在 NDIS 体系里不是直接注册表读取而是通过OIDObject Identifier这种请求/应答机制向底层网卡驱动查询。常用的两个 OIDOID_802_3_PERMANENT_ADDRESS出厂永久 MAC烧录地址OID_802_3_CURRENT_ADDRESS当前网卡实际使用的 MAC可能是软改过的问题在于Filter 驱动本身不回答 OID必须把请求下发给下层 miniport 驱动。如果你的 Filter 挂载在多块网卡上而某块网卡比如某些虚拟网卡不支持某个 OID查回来的状态会是NDIS_STATUS_NOT_SUPPORTED代码里一定要做这个状态判断否则容易拿错数据。4.2 发起同步 OID 查询的正确姿势在 FilterAttach 成功、拿到 FilterModuleContext 之后就可以发起 OID 请求了。NDIS 6.0 里NdisFOidRequest是异步接口它返回的只是请求是否被接受真正的结果在FilterOidRequestComplete回调里拿。但为了不搞出一堆回调状态机我用了事件 异步封装的方式typedef struct _MAC_QUERY_CONTEXT { NDIS_EVENT_EVENT Event; NDIS_STATUS Status; PVOID Data; ULONG DataSize; } MAC_QUERY_CONTEXT; VOID FilterOidRequestComplete( NDIS_HANDLE FilterModuleContext, PNDIS_OID_REQUEST OidRequest, NDIS_STATUS Status) { PMAC_QUERY_CONTEXT ctx (PMAC_QUERY_CONTEXT)OidRequest-RequestId; ctx-Status Status; if (Status NDIS_STATUS_SUCCESS) { // 拷贝结果到自己的缓冲区 ctx-Data OidRequest-DATA.QUERY_INFORMATION.InformationBuffer; ctx-DataSize OidRequest-DATA.QUERY_INFORMATION.BytesWritten; } NdisSetEvent(ctx-Event); } NDIS_STATUS QueryMacAddress(PFILTER_MODULE pFilter, PVOID pMacOut) { NDIS_OID_REQUEST OidRequest; MAC_QUERY_CONTEXT ctx; NDIS_STATUS status; NdisInitializeEvent(ctx.Event); NdisZeroMemory(OidRequest, sizeof(OidRequest)); OidRequest.Header.Type NDIS_OBJECT_TYPE_OID_REQUEST; OidRequest.Header.Revision NDIS_OID_REQUEST_REVISION_1; OidRequest.Header.Size sizeof(NDIS_OID_REQUEST); OidRequest.RequestType NdisRequestQueryInformation; OidRequest.RequestId (PVOID)ctx; // 用于 Complete 里识别上下文 OidRequest.DATA.QUERY_INFORMATION.Oid OID_802_3_CURRENT_ADDRESS; OidRequest.DATA.QUERY_INFORMATION.InformationBuffer pMacOut; OidRequest.DATA.QUERY_INFORMATION.InformationBufferLength 6; status NdisFOidRequest(pFilter-FilterModuleHandle, OidRequest); if (status NDIS_STATUS_PENDING) { NdisWaitEvent(ctx.Event, 5 * 1000); // 5秒超时别死等 status ctx.Status; } return status; }注意RequestId字段我塞了上下文指针这是 NDIS 异步回调的常见做法——Complete 里拿不到发起时的局部变量只能通过 NBL 或 OID_REQUEST 自带的字段找回自己的上下文。但千万别直接传栈上变量的指针给异步请求函数已经返回了咋办要么把 ctx 分配成堆内存要么像我上面一样传结构体指针确保生命周期跨函数存在。4.3 如果要在应用层查看 MAC 地址其实还有更简单的路说句实话如果你最终目的只是为了在应用层拿到网卡 MAC根本不用大动干戈到内核里查 OID。用户态直接GetAdaptersInfo或 Windows PowerShell 里Get-NetAdapter | Select MacAddress就到手了。那内核态查 MAC 的合理场景是什么我总结有三类驱动需要在发送/接收路径上做MAC 层过滤比如只放行特定源 MAC 的帧驱动做网卡绑定校验比如 License 和 MAC 绑定驱动本身需要核验底层有多个虚拟网卡用户态拿到的名字和内核里 FilterModule 可能对不上必须在内核侧确认当前处理的是哪块物理网卡。我这次记录在内核态查 MAC为的是后续在底层做 MAC-IP 绑定策略时直接拿到真实硬件地址不依赖用户态传参从根本上防止应用层伪造。5. 调试、安装与常见问题排坑实录5.1 驱动签名与测试模式不要再被安装失败卡住Win10 强制驱动签名调试阶段的驱动直接右键 INF 安装大概率报错 第三方 INF 不包含数字签名信息。我常用做法是把目标机切到测试模式bcdedit /set testsigning on重启后驱动可以加载未签名驱动。生产环境部署时再上 WHQL/Attestation 签名。另外提醒一个老坑32 位系统和 64 位系统的驱动签名策略不同x64 下连管理员权限都不能绕过只能测试模式或者正式签名。5.2 WinDbg 双机调试的配置建议NDIS 过滤器出问题最恶心的就是蓝屏或者死锁这时候 WinDbg 是唯一的救命稻草。如果你用 VMware 做虚拟机调试串口配置里选命名管道然后 WinDbg 端这样启动windbg -k com:port\\.\pipe\com1,baud115200,pipe -y调试时常用的几个命令!ndiskd.filter列出系统所有 Filter 驱动及其 Attached 状态!ndiskd.netbuffer查看 NBL 的详细内存结构!ndiskd.miniport确认网卡 miniport 是否正常。有一次我写 Filter 驱动收包路径上直接 Print 了每个包的前 64 字节跑了几分钟系统直接死机。!analyze -v看崩溃栈才发现问题NdisFReturnNetBufferLists调用时机太晚导致网卡驱动把这块内存回收了Filter 层还在访问。接收路径上不要做耗时的逐包打印代码里预留的开关默认必须关掉上线前把日志等级调成 Error 级这个教训值三个晚上。5.3 常见问题速查表现象根因解决办法驱动安装时有黄色感叹号代码 52签名未通过或 INF 配置不对测试模式开启检查 INF 中 FilterClass 和 Characteristics驱动可以安装但网卡掉线FilterAttach 返回失败或内存分配失败在 FilterAttach 里检查NdisAllocateNetBufferListPool的返回值不要持有锁进入等待收包时蓝屏 CRITICAL_STRUCTURE_CORRUPTIONNBL 被释放后仍被访问检查返还路径NdisFReturnNetBufferLists 只能调用一次Clone 的 NBL 必须单独返还查询 MAC 返回 NDIS_STATUS_NOT_SUPPORTED底层网卡不支持该 OID 或 Filter 拦截了 OID 请求改用OID_802_3_PERMANENT_ADDRESS检查 FilterOidRequest 里是否误拦截了下发请求应用层调用 DeviceIoControl 阻塞内核里用了NdisWaitEvent且超时时间过长给所有等待加超时别死等 5 秒以上OID 请求的回调尽量用异步模式发送路径丢包率高在 FilterSendNetBufferLists 里做了阻塞等待发送和接收路径尽量无锁无等待需要缓存数据时用 Lookaside 列表不要自旋5.4 关于驱动卸载和重装的一点补充开发阶段改代码最烦的就是新版本驱动编译好了但旧驱动还被系统占着安装时提示正在使用。我在 Win10 上惯用的卸载清理方式是用pnputilpnputil /enum-drivers pnputil /delete-driver oem##.inf /uninstall /force然后重启一次再装新版。因为你根本不知道还有哪个应用层句柄打开了过滤设备如果有的话强删有时候会蓝屏。千万别在生产环境这么做开发机随便造。6. 项目复盘Filter 驱动后续还能做的几件事开发到这一步这个 Filter 驱动已经具备了挂载、收发统计、OID 查询的能力结构能当模板复用。我个人建议如果你要拿这个框架继续扩展优先考虑三个方向第一把收发路径上的包按五元组做哈希统计这样能直接做一个哪些 IP 在占用流量的观察层。第二把 MAC 地址和 OID 查询结果封装成 IOCTL 暴露给应用层配一个简单的管理工具你就能在用户态实时看到每个网卡的真实物理地址这个在设备管理、漏洞溯源场景下非常实用。第三如果后面真的需要改数据包内容记得上我第 3.3 节提到的 Clone NBL 方案先把稳定性和性能测明白再落地。回到最初的需求这次改造最核心的价值其实不是代码量的增加而是打通了一个内核态数据可见的通道过去应用层拿 MAC、看流量都得靠轮询和 WIN32 API 去猜现在驱动层有了自主感知和干预能力后续不管做安全产品还是网络工具扩展空间都大了很多。如果你也正在做 NDIS 相关的项目需要交流 FilterAttach 里资源分配的具体姿势或者调试时碰到什么诡异蓝屏欢迎评论区把现状抛出来我这边能帮上忙的一定知无不言。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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