资讯详情

资讯详情

Windows NDIS 6.0 Filter 驱动实战:拦截收发数据包与查询 MAC 地址

简介本资源是一份面向Windows内核网络驱动开发者的NDIS 6.0 Filter驱动案例代码基于Win10 x64平台使用Visual Studio 17.11.4与WDK 10.0.26100.1构建适合具备一定C与驱动基础、希望深入理解NDIS过滤驱动机制的中高级开发者学习参考。压缩包共16个文件约37KB以c与h源码为主体辅以vcxproj、sln工程文件、inf安装信息、rc资源脚本及cpp演示程序覆盖驱动主体逻辑、设备管理、调试输出与功能实现等模块。资源在原有Filter驱动基础上新增了发送OID请求、以ICMP为例发送自定义数据包以及接收数据包三项功能并补充了查询网卡MAC地址的代码。读者可从中学习内核OID请求的发送与接收流程、内核资源的分配与回收机制以及数据包的构造、发送与捕获方法对网络协议分析、安全监控与故障诊断类开发具有实际参考价值。目前已有141人学习下载。1. 从抓包到改包一个 NDIS 6.0 Filter 驱动能帮你做到什么如果你在 Windows 上做过网络相关的底层开发大概率遇到过这样的场景想拦截某个进程发出的所有 HTTP 请求做审计或者想给特定网卡加一层自定义的收发逻辑又或者只是单纯想拿到本机所有网卡的 MAC 地址做设备指纹。应用层 API 能做的事太有限了WFP 虽然强大但学习曲线陡峭这时候 NDIS Filter 驱动就是那个绕不开的选择。这份资源给的是一个基于 Win10 的 NDIS 6.0 Filter 驱动工程用 C 编写跑在内核态核心能力是三件事拦截并处理网卡收发包、主动构造并发送数据包、查询本机网卡的 MAC 地址。它适合两类人一是正在学 Windows 内核驱动开发、想找一个能跑起来的完整 Filter 示例的开发者二是需要在网络层做数据面定制、又不想从零搭框架的工程人员。整个工程用 Visual Studio 加 WDK 就能编译加载后通过netcfg或设备管理器绑定到目标网卡即可生效。2. 工程结构与核心模块从入口到数据路径的完整拆解2.1 目录布局与关键文件职责拿到源码包后先别急着打开.sln花两分钟把目录结构看清楚后面改代码时能省很多时间。这个工程的目录组织是典型的 WDK 驱动项目风格核心文件不多但每个都有明确分工。NDISFilterDemo/ ├── NDISFilterDemo.sln # VS 解决方案文件 ├── NDISFilterDemo.vcxproj # 项目文件配置了 WDK 依赖 ├── driver/ │ ├── DriverEntry.cpp # 驱动入口注册 Filter 回调 │ ├── Filter.cpp # Filter 核心逻辑附着、收发、查询 │ ├── Filter.h # 结构体与函数声明 │ └── Filter.rc # 版本资源 ├── shared/ │ └── FilterCommon.h # 与用户态共享的 IOCTL 码定义 └── userapp/ └── QueryMac.cpp # 用户态测试程序发 IOCTL 查 MACDriverEntry.cpp是整个驱动的起点它做的事情就是填充NDIS_FILTER_DRIVER_CHARACTERISTICS结构体把各个回调函数指针挂上去然后调用NdisFRegisterFilterDriver完成注册。Filter.cpp是真正干活的地方附着、分离、收发、OID 查询都在这里。FilterCommon.h定义了用户态和内核态共享的 IOCTL 控制码这个文件被两边同时 include保证控制码一致。userapp目录下是一个简单的控制台程序用来验证驱动是否正常工作。注意如果你用的是较新版本的 WDK.vcxproj里的WindowsTargetPlatformVersion可能需要改成你本机安装的 SDK 版本否则编译会报找不到头文件。2.2 Filter 注册与附着驱动怎么挂到网卡上驱动加载后并不会自动生效它需要附着到具体的网卡上。这个过程分两步先在DriverEntry里注册 Filter 驱动然后在AttachHandler回调里完成对具体网卡的附着。先看注册部分的代码。// DriverEntry.cpp - 驱动入口注册 Filter 驱动 NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NDIS_FILTER_DRIVER_CHARACTERISTICS fChars { 0 }; NDIS_STRING filterName, serviceName, uniqueName; // 填充 NDIS 要求的版本信息 fChars.Header.Type NDIS_OBJECT_TYPE_FILTER_DRIVER_CHARACTERISTICS; fChars.Header.Revision NDIS_FILTER_DRIVER_CHARACTERISTICS_REVISION_2; fChars.Header.Size sizeof(NDIS_FILTER_DRIVER_CHARACTERISTICS); fChars.MajorNdisVersion 6; // NDIS 6.0 fChars.MinorNdisVersion 0; // 挂载各个回调函数 fChars.AttachHandler FilterAttach; fChars.DetachHandler FilterDetach; fChars.RestartHandler FilterRestart; fChars.PauseHandler FilterPause; fChars.SendNetBufferListsHandler FilterSendNetBufferLists; fChars.ReturnNetBufferListsHandler FilterReturnNetBufferLists; fChars.ReceiveNetBufferListsHandler FilterReceiveNetBufferLists; fChars.OidRequestHandler FilterOidRequest; fChars.CancelOidRequestHandler FilterCancelOidRequest; // 驱动名称必须唯一 NdisInitUnicodeString(filterName, LNDISFilterDemo); NdisInitUnicodeString(serviceName, LNDISFilterDemo); NdisInitUnicodeString(uniqueName, L{1B2C3D4E-5F6A-7B8C-9D0E-1F2A3B4C5D6E}); fChars.FriendlyName filterName; fChars.UniqueName uniqueName; fChars.ServiceName serviceName; // 注册 Filter 驱动 return NdisFRegisterFilterDriver(DriverObject, RegistryPath, fChars, g_FilterDriverHandle); }这段代码的关键在于NDIS_FILTER_DRIVER_CHARACTERISTICS结构体的填充。MajorNdisVersion和MinorNdisVersion决定了驱动使用的 NDIS 版本这里设为 6.0 对应 Win10 的兼容范围。各个 Handler 是 NDIS 在特定事件发生时回调的入口其中AttachHandler和DetachHandler管理生命周期SendNetBufferListsHandler和ReceiveNetBufferListsHandler是数据路径的核心。UniqueName必须是一个 GUID 字符串NDIS 用它来区分不同的 Filter 驱动如果你复制这个工程做二次开发第一件事就是换掉这个 GUID。注册成功后当系统中有网卡可用时NDIS 会调用FilterAttach。在这个回调里你需要分配一个自定义的上下文结构体来保存每个网卡实例的状态然后调用NdisFSetAttributes把上下文绑定上去。// Filter.cpp - 附着到网卡时分配上下文 NDIS_STATUS FilterAttach( NDIS_HANDLE NdisFilterHandle, NDIS_HANDLE FilterDriverContext, PNDIS_FILTER_ATTACH_PARAMETERS AttachParameters) { PMY_FILTER_CONTEXT ctx NULL; NDIS_FILTER_ATTRIBUTES attributes { 0 }; NDIS_STATUS status; // 分配每个网卡实例的上下文 ctx (PMY_FILTER_CONTEXT)NdisAllocateMemoryWithTagPriority( NdisFilterHandle, sizeof(MY_FILTER_CONTEXT), FILTER_TAG, LowPoolPriority); if (!ctx) return NDIS_STATUS_RESOURCES; NdisZeroMemory(ctx, sizeof(MY_FILTER_CONTEXT)); ctx-NdisFilterHandle NdisFilterHandle; ctx-MacAddressLength AttachParameters-CurrentMacAddress.Length; // 保存网卡 MAC 地址后续查询直接返回 if (ctx-MacAddressLength 0 ctx-MacAddressLength 6) { NdisMoveMemory(ctx-MacAddress, AttachParameters-CurrentMacAddress.Address, ctx-MacAddressLength); } // 设置 Filter 属性绑定上下文 attributes.Header.Type NDIS_OBJECT_TYPE_FILTER_ATTRIBUTES; attributes.Header.Revision NDIS_FILTER_ATTRIBUTES_REVISION_1; attributes.Header.Size sizeof(NDIS_FILTER_ATTRIBUTES); attributes.Flags 0; status NdisFSetAttributes(NdisFilterHandle, ctx, attributes); if (status ! NDIS_STATUS_SUCCESS) { NdisFreeMemory(ctx, sizeof(MY_FILTER_CONTEXT), 0); return status; } ctx-State FilterStateRunning; return NDIS_STATUS_SUCCESS; }AttachParameters-CurrentMacAddress是 NDIS 在附着时直接传进来的当前网卡 MAC 地址长度存在Length字段里地址本身在Address指针指向的缓冲区。这里直接把它拷贝到自定义上下文里后面用户态查询时就不用再发 OID 请求了省一次内核态往返。NdisFSetAttributes把上下文和 Filter 实例绑定之后所有回调里通过NdisFilterHandle都能取回这个上下文。2.3 收发数据包NBL 的处理与转发数据路径是 Filter 驱动最核心的部分。NDIS 6.0 用NET_BUFFER_LIST简称 NBL来描述数据包每个 NBL 可以包含一个或多个NET_BUFFER每个 NET_BUFFER 又通过MDL链指向实际的数据缓冲区。先看发送路径。// Filter.cpp - 拦截发送方向的数据包 VOID FilterSendNetBufferLists( NDIS_HANDLE FilterModuleContext, PNET_BUFFER_LIST NetBufferLists, NDIS_PORT_NUMBER PortNumber, ULONG SendFlags) { PMY_FILTER_CONTEXT ctx (PMY_FILTER_CONTEXT)FilterModuleContext; PNET_BUFFER_LIST nbl NetBufferLists; // 遍历 NBL 链这里可以做包过滤或修改 while (nbl) { PNET_BUFFER nb NET_BUFFER_LIST_FIRST_NB(nbl); while (nb) { ULONG dataLength NET_BUFFER_DATA_LENGTH(nb); // 示例只打印包长度实际可在此解析协议头 DbgPrint(Send packet, length%lu\n, dataLength); nb NET_BUFFER_NEXT_NB(nb); } nbl NET_BUFFER_LIST_NEXT_NBL(nbl); } // 原样转发给下层驱动 NdisFSendNetBufferLists(ctx-NdisFilterHandle, NetBufferLists, PortNumber, SendFlags); }发送路径的FilterSendNetBufferLists收到的是上层协议驱动比如 TCPIP.SYS传下来的 NBL 链。你可以在这里遍历每个 NBL 和 NB读取数据长度、解析协议头、甚至修改数据内容。处理完后必须调用NdisFSendNetBufferLists把包转发给下层否则网络就断了。SendFlags里可能包含NDIS_SEND_FLAGS_DISPATCH_LEVEL表示当前在 DISPATCH_LEVEL 中断级别这时候不能做分页内存访问。接收路径的逻辑类似但方向相反。// Filter.cpp - 拦截接收方向的数据包 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; // 遍历接收到的包可在此做入站过滤 PNET_BUFFER_LIST nbl NetBufferLists; while (nbl) { PNET_BUFFER nb NET_BUFFER_LIST_FIRST_NB(nbl); if (nb) { DbgPrint(Recv packet, length%lu\n, NET_BUFFER_DATA_LENGTH(nb)); } nbl NET_BUFFER_LIST_NEXT_NBL(nbl); } // 转发给上层协议驱动 NdisFIndicateReceiveNetBufferLists( ctx-NdisFilterHandle, NetBufferLists, PortNumber, NumberOfNetBufferLists, ReceiveFlags); }接收路径的FilterReceiveNetBufferLists拿到的是网卡上报的 NBL。这里有个关键点如果你在函数返回前就调用了NdisFIndicateReceiveNetBufferLists并且ReceiveFlags里带了NDIS_RECEIVE_FLAGS_RESOURCES那说明 NDIS 要求你同步处理完不能把 NBL 挂起来异步处理。如果你需要异步处理就得先拷贝数据或者调用NdisFIndicateReceiveNetBufferLists时把NDIS_RECEIVE_FLAGS_RESOURCES清掉然后自己负责在合适的时候归还 NBL。2.4 查询 MAC 地址OID 请求与 IOCTL 通道查询 MAC 地址有两条路一条是驱动内部直接读上下文里保存的地址另一条是用户态通过 IOCTL 发请求下来。先看用户态怎么发请求。// userapp/QueryMac.cpp - 用户态发起 IOCTL 查询 #include windows.h #include stdio.h #include FilterCommon.h int main() { HANDLE hDevice CreateFileW( L\\\\.\\NDISFilterDemo, // 设备名与驱动里创建的一致 GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDevice INVALID_HANDLE_VALUE) { printf(Open device failed, error%lu\n, GetLastError()); return 1; } BYTE macBuffer[6] { 0 }; DWORD bytesReturned 0; BOOL ok DeviceIoControl( hDevice, IOCTL_FILTER_QUERY_MAC, // 控制码定义在 FilterCommon.h NULL, 0, macBuffer, sizeof(macBuffer), bytesReturned, NULL); if (ok bytesReturned 6) { printf(MAC: %02X-%02X-%02X-%02X-%02X-%02X\n, macBuffer[0], macBuffer[1], macBuffer[2], macBuffer[3], macBuffer[4], macBuffer[5]); } else { printf(IOCTL failed, error%lu\n, GetLastError()); } CloseHandle(hDevice); return 0; }用户态通过CreateFileW打开驱动创建的设备对象然后发IOCTL_FILTER_QUERY_MAC控制码。这个控制码在FilterCommon.h里用CTL_CODE宏定义两边必须一致。驱动侧收到 IOCTL 后从上下文里取出之前保存的 MAC 地址拷贝回用户缓冲区。// Filter.cpp - 处理 IOCTL 请求 NDIS_STATUS FilterOidRequest( NDIS_HANDLE FilterModuleContext, PNDIS_OID_REQUEST OidRequest) { PMY_FILTER_CONTEXT ctx (PMY_FILTER_CONTEXT)FilterModuleContext; // 如果是查询当前 MAC 地址的 OID直接返回缓存值 if (OidRequest-RequestType NdisRequestQueryInformation OidRequest-DATA.QUERY_INFORMATION.Oid OID_802_3_CURRENT_ADDRESS) { PUCHAR dest (PUCHAR)OidRequest-DATA.QUERY_INFORMATION.InformationBuffer; ULONG len OidRequest-DATA.QUERY_INFORMATION.InformationBufferLength; if (len ctx-MacAddressLength) { NdisMoveMemory(dest, ctx-MacAddress, ctx-MacAddressLength); OidRequest-DATA.QUERY_INFORMATION.BytesWritten ctx-MacAddressLength; NdisFOidRequestComplete(ctx-NdisFilterHandle, OidRequest, NDIS_STATUS_SUCCESS); return NDIS_STATUS_SUCCESS; } } // 其他 OID 请求透传给下层 return NdisFOidRequest(ctx-NdisFilterHandle, OidRequest); }FilterOidRequest拦截了OID_802_3_CURRENT_ADDRESS查询直接从上下文返回缓存的 MAC 地址避免了下层网卡可能不支持该 OID 的情况。其他 OID 请求通过NdisFOidRequest透传给下层驱动。这里要注意NdisFOidRequestComplete和NdisFOidRequest的调用时机不同前者是同步完成后者是异步转发转发后 NDIS 会在下层完成时回调你的FilterOidRequestComplete。3. 编译、安装与调试让驱动在 Win10 上跑起来3.1 环境准备与编译配置编译这个工程需要三样东西Visual Studio 2019 或 2022、对应版本的 WDK、以及 Windows 10 SDK。安装 WDK 时注意版本要和 VS 匹配比如 VS 2022 对应 WDK 10.0.22621 或更高。装完后打开.sln检查项目属性里的Driver Settings是否正确指向了 WDK。编译时选择Debug配置和x64平台直接 Build 即可。如果报错Cannot find file ndis.h说明 WDK 的 include 路径没配好去项目属性的VC Directories里确认$(WDKContentRoot)\Include\$(WindowsTargetPlatformVersion)\km在包含路径里。编译成功后会在x64\Debug\下生成NDISFilterDemo.sys和NDISFilterDemo.inf。提示Win10 默认开启了驱动签名强制测试阶段可以在开机时按 F8 选择「禁用驱动程序强制签名」或者用bcdedit /set testsigning on开启测试签名模式后重启。3.2 安装与绑定网卡驱动安装有两种方式一是右键.inf文件选择「安装」二是用netcfg命令行。推荐用netcfg因为 Filter 驱动需要绑定到具体网卡才能生效。# 安装 Filter 驱动 netcfg -v -l NDISFilterDemo.inf -c f -i NDISFilterDemo # 查看已安装的 Filter netcfg -v -s n # 绑定到指定网卡需要知道网卡的 ComponentId netcfg -v -c n -i NDISFilterDemo -b 网卡ComponentId安装完成后在「网络连接」里右键任意网卡 → 属性应该能看到NDISFilterDemo这个 Filter 出现在列表中勾选它即可绑定。绑定后驱动开始工作DbgPrint输出的日志可以用 DebugView 工具查看。3.3 用 WinDbg 做内核调试内核驱动调试离不开 WinDbg。配置双机调试的常见做法是目标机开启内核调试模式主机用 WinDbg 通过串口或网络连接。Win10 下可以用bcdedit配置网络调试。# 目标机执行配置网络内核调试 bcdedit /debug on bcdedit /dbgsettings net hostip:192.168.1.100 port:50000 key:1.2.3.4 # 重启后生效 shutdown /r /t 0主机端 WinDbg 选择Kernel Debug→Net填入相同的端口和 key 即可连接。连接成功后可以用lm查看已加载模块用bp NDISFilterDemo!FilterSendNetBufferLists下断点用dt查看上下文结构体内容。调试时注意内核断点会挂起整个系统别在断点处停留太久。4. 避坑与常见问题那些让我熬夜的坑4.1 驱动加载失败错误码 577 或 127现象是netcfg安装时报错或者设备管理器里显示黄色感叹号。原因通常是驱动签名问题或 INF 文件格式不对。Win10 64 位强制要求驱动有有效签名测试阶段必须开启测试签名模式。另外 INF 文件里的[Manufacturer]和[Models]段必须和.sys文件名匹配ServiceBinary路径要指向%12%\NDISFilterDemo.sys。解决方法是先用bcdedit /set testsigning on开启测试模式重启然后检查 INF 里的硬件 ID 和 Service 名称是否一致。4.2 绑定网卡后网络直接断了这是最吓人的情况通常发生在FilterSendNetBufferLists或FilterReceiveNetBufferLists里没有正确转发 NBL。如果你在发送路径里直接return而没有调用NdisFSendNetBufferLists包就被吞了网络自然断。接收路径同理必须调用NdisFIndicateReceiveNetBufferLists。另一个可能是你在FilterAttach里返回了失败状态但没有释放已分配的内存导致 NDIS 认为 Filter 不可用而拒绝绑定。排查时先用 WinDbg 在收发回调里下断点确认函数被调用且转发语句执行到了。4.3 DbgPrint 看不到输出DbgPrint的输出需要 DebugView 工具捕获而且要在 DebugView 里勾选Capture Kernel和Enable Verbose Kernel Output。如果还是看不到检查注册表HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter下的DEFAULT值是否设为了0xFFFFFFFF。Win10 默认会过滤掉很多内核打印不设这个值的话DbgPrint基本是静默的。另外DbgPrint在 DISPATCH_LEVEL 下也能用但性能很差生产环境建议用 ETW 或 WPP。4.4 查询 MAC 地址返回全零用户态发 IOCTL 后拿到 6 个字节全是 0说明驱动侧没有正确保存 MAC 地址。检查FilterAttach里AttachParameters-CurrentMacAddress.Length是否大于 0有些虚拟网卡或回环网卡这个字段可能是空的。另外NdisMoveMemory的源地址是AttachParameters-CurrentMacAddress.Address这是一个指针不是数组本身别直接传AttachParameters-CurrentMacAddress.Address。如果确实拿不到可以在FilterOidRequest里主动发OID_802_3_CURRENT_ADDRESS查询下层网卡。4.5 卸载驱动时蓝屏蓝屏代码通常是DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATIONS或IRQL_NOT_LESS_OR_EQUAL。原因一般是FilterDetach里没有正确释放上下文内存或者还有未完成的 OID 请求。在FilterDetach里必须调用NdisFreeMemory释放FilterAttach里分配的上下文并且确保所有异步操作都已经完成。如果用了NdisFOidRequest转发请求要在FilterOidRequestComplete里处理完成回调不能在 Detach 后还有未完成的请求挂着。5. 二次开发从示例到生产级 Filter 的改造思路拿到这个工程后大多数人不会满足于只打印包长度。真正要落地第一步是加包过滤规则。我一般会在FilterSendNetBufferLists和FilterReceiveNetBufferLists里先解析以太网头和 IP 头拿到源/目的 IP、端口、协议号然后和一个规则表做匹配。规则表可以通过 IOCTL 从用户态下发用ExAllocatePoolWithTag分配非分页内存存储匹配时注意加自旋锁保护因为收发回调可能在 DISPATCH_LEVEL 并发执行。第二步是处理 NBL 的拷贝与修改。如果你想修改包内容不能直接改原始 NBL 的 MDL 数据因为那可能是只读的或者被其他组件共享。正确做法是调用NdisCopySendNetBufferListInfo和NdisAllocateNetBufferList构造一个新的 NBL把数据拷贝过去修改完再转发。接收方向同理用NdisAllocateNetBufferAndNetBufferList分配新的 NBL。这里有个血泪经验拷贝时一定要处理NET_BUFFER_LIST_CONTEXT和NET_BUFFER_LIST_INFO否则上层协议栈可能因为缺少元数据而丢弃包。第三步是加统计和日志。生产环境不能用DbgPrint性能扛不住。常见做法是用 ETW 注册一个 Provider在收发路径里调用EventWrite写事件用户态用traceview或自定义消费者收集。如果只是简单计数可以用InterlockedIncrement更新全局计数器用户态通过 IOCTL 定期读取。第四步是处理多网卡场景。这个示例只保存了一个 MAC 地址实际环境中一台机器可能有多块网卡每块网卡都会触发一次FilterAttach每次都会分配独立的上下文。用户态查询时需要指定网卡索引可以在 IOCTL 输入缓冲区里传一个网卡序号驱动侧遍历上下文链表找到对应的实例。上下文链表用LIST_ENTRY维护插入和删除时用ExAcquirePushLockExclusive保护。最后一步是验证。改完代码后我习惯先用ping和curl做基本连通性测试确认网络没断。然后用 Wireshark 抓包对比看 Filter 驱动有没有丢包或改包。如果要做性能测试用iperf3跑吞吐对比绑定 Filter 前后的差异。内核驱动的调试没有后悔药每次改完收发路径我都会在虚拟机里先跑一遍确认稳定了再上真机。从那以后我每次改 NBL 处理逻辑都强制走一遍「编译 → 测试签名 → 虚拟机验证 → 真机灰度」的流程希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →