资讯动态

NDIS中间层驱动开发实战:拦截转发网络包的核心原理与调试

发布时间:2026/9/10 2:57:09 来源:尧图企业网站定制
简介本资源是一份面向Windows驱动开发初学者的NDIS网络驱动入门示例聚焦协议驱动层开发实践帮助开发者理解NDIS架构下驱动与协议栈、硬件适配器的交互机制。压缩包共7个文件含4个C源文件实现驱动初始化、收发包、打开/关闭等核心逻辑、1个头文件定义NDIS结构体与接口函数、1个资源脚本含版本信息及1个Visual Studio项目文件支持构建与调试整体仅21KB轻量易读。已有477人学习下载适合配合WDK环境动手编译、单步调试深入掌握NdisRegisterProtocolDriver、NdisSend、NdisMIndicateReceivePacket等关键API调用时机与参数含义并实践自旋锁同步、中断响应、数据包缓冲区管理等底层机制。代码结构清晰模块职责分明是理解Windows网络驱动分层模型与真实开发流程的优质起点。1. NDIS驱动不是“写个.sys就能跑”的黑盒而是协议栈与硬件之间的精密协作者很多人第一次接触 Windows 驱动开发时会误以为只要照着 MSDN 示例改几个函数名、编译出 .sys 文件加载后就能收发网络包——结果在NdisOpenAdapter返回NDIS_STATUS_FAILURE时彻底卡住。这个driver-revised.zip里的 NDIS 中间层驱动Intermediate Driver示例恰恰击中了这种认知偏差它不操作物理网卡也不实现 TCP/IP 协议而是在协议驱动如 tcpip.sys和 miniport 驱动如 e1d63x64.sys之间“插队”对每个流经的数据包做透明拦截、标记或转发。这意味着你必须同时理解三层上下文上层协议如何下发NDIS_PACKET结构体、中间层如何用NdisMIndicateReceivePacket向上“冒泡”、下层 miniport 如何通过NdisMTransferData回调交付原始帧。本示例的packet.c和openclos.c并非教学玩具而是真实生产环境中流量镜像、QoS 标记、防火墙预过滤等场景的最小可行原型。适合已掌握 C 语言指针与结构体内存布局、熟悉 Windows 内核模式基本概念如 IRQL、DriverObject、DeviceObject、且正在从用户态开发转向内核态网络模块的工程师——它不教“怎么注册服务”而直击“为什么NdisAcquireSpinLock必须在 DISPATCH_LEVEL 下调用”这一类硬核约束。2. NDIS中间层驱动的架构选型与初始化流程解析2.1 为什么选择中间层驱动而非协议驱动或 miniport 驱动NDIS 定义了三类驱动角色协议驱动Protocol Driver如 TCP/IP、NDISWAN、微型端口驱动Miniport Driver直接控制网卡硬件、中间层驱动Intermediate Driver位于二者之间。本示例采用中间层核心原因有三第一零硬件依赖——无需申请 PCI 设备资源、不处理 DMA 映射、不编写中断服务例程ISR规避了硬件调试的高门槛第二双向拦截能力——既能通过NdisMIndicateReceivePacket拦截上行包从网卡到协议栈又能通过NdisMSendPackets拦截下行包从协议栈到网卡而协议驱动只能接收、miniport 只能发送第三兼容性鲁棒——Windows 10/11 对中间层驱动的签名要求低于 miniport且NdisRegisterProtocolDriver的注册机制比NdisMRegisterMiniport更易调试。提示若目标是抓取所有网卡流量包括虚拟网卡中间层是唯一选择若需修改以太网帧头字段如 VLAN Tag则必须用 miniport 驱动——本示例不涉及此场景。2.2 驱动入口与 NDIS 初始化的关键参数设置packet.c中的DriverEntry函数是整个驱动的起点其核心逻辑围绕NDIS_MINIPORT_DRIVER_CHARACTERISTICS结构体展开。该结构体并非简单填充函数指针而是定义了 NDIS 运行时对驱动行为的契约NDIS_MINIPORT_DRIVER_CHARACTERISTICS MiniportChars; NdisZeroMemory(MiniportChars, sizeof(NDIS_MINIPORT_DRIVER_CHARACTERISTICS)); MiniportChars.MajorNdisVersion 0x06; // NDIS 6.xWindows Vista MiniportChars.MinorNdisVersion 0x30; // 6.0 → 0x60, 6.30 → 0x630注意十六进制换算 MiniportChars.MajorNdisVersion 6; MiniportChars.MinorNdisVersion 30; MiniportChars.InitializeHandler MiniportInitialize; // 必须实现分配资源、注册回调 MiniportChars.HaltHandler MiniportHalt; // 必须实现释放资源、取消注册 MiniportChars.HandleInterruptHandler MiniportHandleInterrupt; // 可选仅当需响应硬件中断 MiniportChars.CheckForHangHandler MiniportCheckForHang; // 可选检测网卡死锁 MiniportChars.ResetHandler MiniportReset; // 可选重置网卡状态 MiniportChars.OidRequestHandler MiniportOidRequest; // 必须实现处理 IOCTL 查询如 MTU、MAC 地址 MiniportChars.SendPacketsHandler MiniportSendPackets; // 必须实现处理上层下发的发送请求 MiniportChars.ReturnPacketHandler MiniportReturnPacket; // 必须实现回收已发送完成的包 MiniportChars.ReceivePacketHandler MiniportReceivePacket; // 必须实现接收网卡上来的包 MiniportChars.TransferDataHandler MiniportTransferData; // 已废弃NDIS 6 用 SendPackets/ReceivePacket 替代关键参数说明MajorNdisVersion和MinorNdisVersion决定可用 API 集合。设为6.30表示支持 NDIS 6.30 功能如 RSS、VMQ但本示例未启用高级特性故保持6.0兼容性更广InitializeHandler在此函数中必须调用NdisMRegisterAdapter注册适配器并通过NdisAllocateSpinLock初始化自旋锁——这是后续多线程安全的基础OidRequestHandler必须处理OID_GEN_CURRENT_PACKET_FILTER等基础 OID否则系统无法正确配置网卡过滤模式如混杂模式SendPacketsHandler与ReceivePacketHandler这两个函数构成数据流主干MiniportSendPackets接收上层协议发来的NDIS_PACKET链表MiniportReceivePacket则被 miniport 驱动调用以传递入站包。2.3 驱动对象注册与设备创建的内存模型openclos.c中的DriverEntry调用NdisMInitializeWrapper后紧接着执行设备对象创建// 创建设备对象非即插即用驱动 status IoCreateDevice( g_DriverObject, // 驱动对象指针 sizeof(DEVICE_EXTENSION), // 设备扩展大小存储私有数据 deviceName, // 设备名 \Device\MyNDISDriver FILE_DEVICE_UNKNOWN, // 设备类型NDIS 驱动通常用 UNKNOWN FILE_DEVICE_SECURE_OPEN, // 访问标志 FALSE, // 是否为独占设备 deviceObject // 输出设备对象指针 ); if (!NT_SUCCESS(status)) { return status; } // 设置设备对象属性 deviceObject-Flags | DO_DIRECT_IO; // 使用直接 I/O避免缓冲区拷贝 deviceObject-Flags ~DO_DEVICE_INITIALIZING; // 标记初始化完成 // 绑定设备对象到驱动对象 g_DriverObject-DeviceObject deviceObject; // 创建符号链接供用户态访问 RtlInitUnicodeString(symbolicLinkName, L\\DosDevices\\MyNDISDriver); status IoCreateSymbolicLink(symbolicLinkName, deviceName);内存模型要点DEVICE_EXTENSION结构体必须包含NDIS_HANDLE MiniportAdapterHandle字段该句柄由NdisMRegisterAdapter返回是后续所有 NDIS API如NdisMSendPackets的上下文标识DO_DIRECT_IO标志意味着用户态应用通过DeviceIoControl发送的 I/O 请求将绕过系统缓冲区直接映射物理内存——这对高性能包处理至关重要但要求METHOD_BUFFERED或METHOD_DIRECT的 I/O 控制码必须严格匹配符号链接\DosDevices\MyNDISDriver是用户态程序打开设备的路径若未创建CreateFile(\\\\.\\MyNDISDriver, ...)将失败。3. 数据包拦截与转发的核心实现逻辑3.1 上行包拦截从 Miniport 到 Protocol 的透明桥接read.c中的MiniportReceivePacket函数是上行数据流的入口。NDIS 规范要求中间层驱动在此处决定是否“吞噬”该包不向上递送或“透传”调用NdisMIndicateReceivePacket向上层协议指示。本示例采用透传策略但插入自定义逻辑VOID MiniportReceivePacket( IN NDIS_HANDLE MiniportAdapterContext, IN PNDIS_PACKET Packet ) { PDEVICE_EXTENSION pDevExt (PDEVICE_EXTENSION)MiniportAdapterContext; PNDIS_PACKET_HEADER pHeader NDIS_PACKET_FIRST_NDIS_BUFFER(Packet); // 1. 获取原始以太网帧首地址跳过 NDIS 内部头 PUCHAR pFrame (PUCHAR)NdisGetPoolFromPacket(Packet) NDIS_PACKET_SIZE FIELD_OFFSET(NDIS_PACKET, MiniportReserved); // 2. 解析以太网头部前14字节 if (pFrame NdisGetPacketLength(Packet) 14) { USHORT ethType ntohs(*(USHORT*)(pFrame 12)); // 字节序转换 if (ethType 0x0800) { // IPv4 // 在此处插入统计逻辑记录 IPv4 包数量 InterlockedIncrement(pDevExt-IPv4Count); } } // 3. 透传给上层协议关键必须调用 NdisMIndicateReceivePacket NdisMIndicateReceivePacket( pDevExt-MiniportAdapterHandle, // 适配器句柄 Packet, // 包指针地址NDIS 会修改链表 1 // 包数量单包 ); }参数与陷阱说明NdisGetPoolFromPacket返回的是 NDIS 分配的内存池基址NDIS_PACKET_SIZE是NDIS_PACKET结构体大小FIELD_OFFSET计算MiniportReserved偏移量——三者相加才是实际帧数据起始地址直接NdisQueryPacket获取 Buffer 可能返回 NULLNdisMIndicateReceivePacket的第二个参数是PNDIS_PACKET*类型即包指针的地址NDIS 会修改该指针如拆分大包因此必须传入Packet而非PacketInterlockedIncrement是原子操作因MiniportReceivePacket可能在任意 CPU 上被并发调用普通会导致计数丢失。3.2 下行包拦截协议栈下发包的修改与重定向write.c中的MiniportSendPackets处理协议栈下发的发送请求。NDIS 6.x 以链表形式传递PNDIS_PACKET需遍历处理VOID MiniportSendPackets( IN NDIS_HANDLE MiniportAdapterContext, IN PPNDIS_PACKET PacketArray, IN UINT NumberOfPackets ) { PDEVICE_EXTENSION pDevExt (PDEVICE_EXTENSION)MiniportAdapterContext; UINT i; for (i 0; i NumberOfPackets; i) { PNDIS_PACKET packet PacketArray[i]; PNDIS_BUFFER buffer; PVOID virtualAddress; UINT length; // 1. 获取第一个缓冲区通常含完整帧 NdisQueryPacket(packet, NULL, NULL, buffer, NULL); if (buffer NULL) continue; NdisQueryBufferSafe(buffer, virtualAddress, length, HighPagePriority); if (virtualAddress NULL || length 14) continue; // 2. 修改以太网源 MAC 地址演示用途 PUCHAR frame (PUCHAR)virtualAddress; RtlCopyMemory(frame, pDevExt-FakeMacAddress, 6); // 覆盖源 MAC // 3. 转发给下层 miniport关键调用 NdisMSendPackets NdisMSendPackets( pDevExt-MiniportAdapterHandle, packet, 1 ); } }关键约束NdisQueryBufferSafe的HighPagePriority参数确保在 IRQL DISPATCH_LEVEL 时能安全获取虚拟地址若用LowPagePriority可能导致蓝屏修改帧数据前必须确认length 14否则越界写入会破坏内核内存NdisMSendPackets的PacketArray参数必须是PNDIS_PACKET*类型与NdisMIndicateReceivePacket一致不能直接传packet。3.3 驱动生命周期管理Open/Close 与资源释放openclos.c实现了用户态应用对驱动的访问控制。DriverObject-MajorFunction[IRP_MJ_CREATE]指向MyDispatchCreateCloseNTSTATUS MyDispatchCreateClose( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PIO_STACK_LOCATION irpStack IoGetCurrentIrpStackLocation(Irp); NTSTATUS status STATUS_SUCCESS; if (irpStack-MajorFunction IRP_MJ_CREATE) { // 允许打开无权限检查 Irp-IoStatus.Status STATUS_SUCCESS; Irp-IoStatus.Information 0; } else if (irpStack-MajorFunction IRP_MJ_CLOSE) { // 关闭时不释放资源驱动卸载时统一释放 Irp-IoStatus.Status STATUS_SUCCESS; Irp-IoStatus.Information 0; } IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }生命周期要点IRP_MJ_CREATE不做鉴权因 NDIS 驱动通常由 SYSTEM 权限服务加载用户态应用只需获得句柄即可IRP_MJ_CLOSE不释放任何资源因为MiniportHalt才是真正的清理入口——若在此处释放NdisFreeSpinLock会导致MiniportHalt再次释放引发双重释放IoCompleteRequest必须调用否则 IRP 永远挂起应用线程阻塞。4. 构建、调试与常见蓝屏故障定位4.1 Visual Studio 项目配置与 WDK 工具链衔接read.dsp是 Visual Studio 6.0 项目文件现代开发需迁移到 VS2019 与 WDK 22H2。关键配置项如下项目属性设置值说明Configuration TypeUtility (.exe)NDIS 驱动必须为.sys但 VS 项目类型需设为 Utility 以禁用默认链接器Target Extension.sys强制输出扩展名General → Windows SDK Version10.0 (WDK 22H2)决定可用 API 版本C/C → General → Additional Include Directories$(DDK_INC_PATH)\src\inc;$(DDK_INC_PATH)\inc\api包含 NDIS 头文件路径Linker → General → Output File$(OutDir)$(ProjectName).sys输出路径Linker → Advanced → Entry PointDriverEntry驱动入口函数名构建命令行等效于# 使用 WDK Build Environment build -cZg -n -q # -c: clean, -Zg: debug info, -n: no rebuild if up-to-date, -q: quiet4.2 WinDbg 调试 NDIS 驱动的实战步骤驱动加载后蓝屏BSOD是常态WinDbg 是唯一可靠工具。典型调试流程启动内核调试目标机启用bcdedit /debug on主机 WinDbg 连接串口/USB/网络加载符号.sympath srv*c:\symbols*https://msdl.microsoft.com/download/symbols定位崩溃点!analyze -v自动分析 dump 文件重点关注STACK_TEXT中的MyNDISDriver!MiniportSendPackets调用栈检查 IRQL!irql查看当前中断级别若MiniportReceivePacket中调用ExAllocatePool要求 PASSIVE_LEVEL会触发IRQL_NOT_LESS_OR_EQUAL验证自旋锁!ndiskd.miniport 0xfffff800查看适配器状态!ndiskd.locks检查锁持有情况。常见蓝屏代码与修复BugCheck Code常见原因修复方法0x000000D1 (DRIVER_IRQL_NOT_LESS_OR_EQUAL)在 DISPATCH_LEVEL 调用分页内存函数如ExAllocatePoolWithTag改用ExAllocatePoolWithTagPriority并指定NonPagedPoolNx0x000000EA (THREAD_STUCK_IN_DEVICE_DRIVER)MiniportCheckForHang未及时返回或MiniportReset死循环在MiniportCheckForHang中添加超时计数MiniportReset必须保证有限步退出0x0000007E (SYSTEM_THREAD_EXCEPTION_NOT_HANDLED)访问无效指针如NdisQueryPacket返回 NULL 后未检查所有NdisXXX调用后必须检查返回值NULL检查不可省略4.3 DebugView 实时日志与性能瓶颈识别packet.h中定义的调试宏#ifdef DBG #define DEBUG_PRINT(x) DbgPrint x #else #define DEBUG_PRINT(x) #endif在MiniportInitialize中插入DEBUG_PRINT((MyNDISDriver: Adapter %p initialized, IRQL%d\n, pDevExt-MiniportAdapterHandle, KeGetCurrentIrql()));日志分析技巧启动 DebugView勾选Capture Global Win32过滤MyNDISDriver字符串若日志中出现大量IRQL2DISPATCH_LEVEL下的DEBUG_PRINT说明日志本身成为性能瓶颈——应改用KdPrintEx并设置DPFLTR_IHVNETWORK_ID与DBG_VERBOSE级别性能瓶颈常出现在MiniportSendPackets循环中NdisQueryBufferSafe调用开销大可预先缓存PNDIS_BUFFER指针于NDIS_PACKET的MiniportReserved区域。5. NDIS驱动与现代网络栈的兼容性实践从 Windows 10 到 Windows 11 的平滑迁移5.1 NDIS 6.8x 新特性适配RSS 与 VMQ 的条件启用Windows 10 20H1 引入 NDIS 6.80新增 RSSReceive Side Scaling和 VMQVirtual Machine Queue支持。若目标系统支持应在MiniportInitialize中显式启用// 查询 RSS 支持 NDIS_STATUS status; NDIS_RSS_CAPABILITIES rssCaps; NdisZeroMemory(rssCaps, sizeof(NDIS_RSS_CAPABILITIES)); rssCaps.Header.Type NDIS_OBJECT_TYPE_DEFAULT; rssCaps.Header.Revision NDIS_RSS_CAPABILITIES_REVISION_1; rssCaps.Header.Size NDIS_SIZEOF_NDIS_RSS_CAPABILITIES_REVISION_1; status NdisMQueryInformation( pDevExt-MiniportAdapterHandle, NdisInformationClassRssCapabilities, rssCaps, sizeof(rssCaps), BytesReturned ); if (NT_SUCCESS(status) rssCaps.RssSupported) { // 启用 RSS设置哈希密钥与处理器映射 NDIS_RSS_PARAMETERS rssParams; NdisZeroMemory(rssParams, sizeof(rssParams)); rssParams.Header.Type NDIS_OBJECT_TYPE_DEFAULT; rssParams.Header.Revision NDIS_RSS_PARAMETERS_REVISION_1; rssParams.Header.Size NDIS_SIZEOF_NDIS_RSS_PARAMETERS_REVISION_1; rssParams.Flags NDIS_RSS_PARAM_FLAG_HASH_INFO_UNCHANGED | NDIS_RSS_PARAM_FLAG_HASH_KEY_UNCHANGED; NdisMSetInformation( pDevExt-MiniportAdapterHandle, NdisInformationClassRssParameters, rssParams, sizeof(rssParams), BytesReturned ); }兼容性要点NdisMQueryInformation查询NdisInformationClassRssCapabilities前必须先调用NdisMRegisterAdapter成功NDIS_RSS_PARAMETERS的Flags字段必须包含NDIS_RSS_PARAM_FLAG_HASH_INFO_UNCHANGED否则 NDIS 会拒绝设置Windows 10 强制校验若目标系统为 Windows 7NDIS 6.1此代码块会被跳过不影响原有功能。5.2 用户态通信优化从 DeviceIoControl 到 WFP 的演进路径read.c中的IRP_MJ_DEVICE_CONTROL处理函数仅支持基础控制码如IOCTL_MYNDIS_GET_STATS。但在 Windows 10 环境中更推荐与 Windows Filtering PlatformWFP协同工作方案优势局限DeviceIoControl完全可控、低延迟、无需 WFP 签名需自行实现 ACL、无法与系统防火墙策略联动WFP Callout Driver与系统策略无缝集成、支持 TLS 解密、自动适配网络位置变化开发复杂度高、需额外 WFP SDK、部分 API 仅限内核模式迁移建议保留DeviceIoControl作为调试通道生产环境叠加 WFP Callout 实现策略控制。例如在FwpmCalloutNotify中注册FWPM_LAYER_STREAM_V4层回调拦截AF_INET流量再通过FwpsStreamInjectAsync0注入自定义元数据——这比在 NDIS 层解析 IP 头更稳定。5.3 驱动签名与 Windows 11 强制要求应对Windows 11 22H2 要求所有内核驱动必须具备EV Code Signing Certificate签名且时间戳服务需符合 RFC 3161。构建后执行# 使用 signtool 签名需 EV 证书 signtool sign /v /ac DigiCertCA.crt /s my /n Your Company Name /tr http://timestamp.digicert.com /td SHA256 /fd SHA256 MyNDISDriver.sys # 验证签名 signtool verify /v /pa MyNDISDriver.sys关键验证点/tr参数必须指向 DigiCert 或 Sectigo 的 RFC 3161 时间戳服务器http://timestamp.digicert.com已弃用/fd SHA256指定哈希算法Windows 11 拒绝 SHA1 签名signtool verify /pa中的/pa表示“perform all checks”包括吊销状态、证书链完整性、EKUExtended Key Usage是否包含Code Signing。注意若使用自签名证书测试必须在测试机执行bcdedit /set testsigning on并重启否则sc create会返回Access Denied。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价