NDIS中间层驱动开发实战:拦截转发网络包的核心原理与调试
2026/9/10 2:57:07 网站建设 项目流程

简介:本资源是一份面向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.copenclos.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.x(Windows 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 替代
关键参数说明:
  • MajorNdisVersionMinorNdisVersion:决定可用 API 集合。设为6.30表示支持 NDIS 6.30 功能(如 RSS、VMQ),但本示例未启用高级特性,故保持6.0兼容性更广;
  • InitializeHandler:在此函数中必须调用NdisMRegisterAdapter注册适配器,并通过NdisAllocateSpinLock初始化自旋锁——这是后续多线程安全的基础;
  • OidRequestHandler:必须处理OID_GEN_CURRENT_PACKET_FILTER等基础 OID,否则系统无法正确配置网卡过滤模式(如混杂模式);
  • SendPacketsHandlerReceivePacketHandler:这两个函数构成数据流主干,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_BUFFEREDMETHOD_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_SIZENDIS_PACKET结构体大小,FIELD_OFFSET计算MiniportReserved偏移量——三者相加才是实际帧数据起始地址,直接NdisQueryPacket获取 Buffer 可能返回 NULL
  • NdisMIndicateReceivePacket的第二个参数是PNDIS_PACKET*类型,即包指针的地址,NDIS 会修改该指针(如拆分大包),因此必须传入&Packet而非Packet
  • InterlockedIncrement是原子操作,因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 ); } }
关键约束:
  • NdisQueryBufferSafeHighPagePriority参数确保在 IRQL >= DISPATCH_LEVEL 时能安全获取虚拟地址,若用LowPagePriority可能导致蓝屏;
  • 修改帧数据前必须确认length >= 14,否则越界写入会破坏内核内存;
  • NdisMSendPacketsPacketArray参数必须是PNDIS_PACKET*类型,与NdisMIndicateReceivePacket一致,不能直接传packet

3.3 驱动生命周期管理:Open/Close 与资源释放

openclos.c实现了用户态应用对驱动的访问控制。DriverObject->MajorFunction[IRP_MJ_CREATE]指向MyDispatchCreateClose

NTSTATUS 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: quiet

4.2 WinDbg 调试 NDIS 驱动的实战步骤

驱动加载后蓝屏(BSOD)是常态,WinDbg 是唯一可靠工具。典型调试流程:

  1. 启动内核调试:目标机启用bcdedit /debug on,主机 WinDbg 连接串口/USB/网络;
  2. 加载符号.sympath srv*c:\symbols*https://msdl.microsoft.com/download/symbols
  3. 定位崩溃点!analyze -v自动分析 dump 文件,重点关注STACK_TEXT中的MyNDISDriver!MiniportSendPackets调用栈;
  4. 检查 IRQL!irql查看当前中断级别,若MiniportReceivePacket中调用ExAllocatePool(要求 PASSIVE_LEVEL)会触发IRQL_NOT_LESS_OR_EQUAL
  5. 验证自旋锁!ndiskd.miniport 0xfffff800查看适配器状态,!ndiskd.locks检查锁持有情况。
常见蓝屏代码与修复:
BugCheck Code常见原因修复方法
0x000000D1 (DRIVER_IRQL_NOT_LESS_OR_EQUAL)在 DISPATCH_LEVEL 调用分页内存函数(如ExAllocatePoolWithTag改用ExAllocatePoolWithTagPriority并指定NonPagedPoolNx
0x000000EA (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字符串;
  • 若日志中出现大量IRQL=2(DISPATCH_LEVEL)下的DEBUG_PRINT,说明日志本身成为性能瓶颈——应改用KdPrintEx并设置DPFLTR_IHVNETWORK_IDDBG_VERBOSE级别;
  • 性能瓶颈常出现在MiniportSendPackets循环中,NdisQueryBufferSafe调用开销大,可预先缓存PNDIS_BUFFER指针于NDIS_PACKETMiniportReserved区域。

5. NDIS驱动与现代网络栈的兼容性实践:从 Windows 10 到 Windows 11 的平滑迁移

5.1 NDIS 6.8x 新特性适配:RSS 与 VMQ 的条件启用

Windows 10 20H1 引入 NDIS 6.80,新增 RSS(Receive Side Scaling)和 VMQ(Virtual 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_PARAMETERSFlags字段必须包含NDIS_RSS_PARAM_FLAG_HASH_INFO_UNCHANGED,否则 NDIS 会拒绝设置(Windows 10+ 强制校验);
  • 若目标系统为 Windows 7(NDIS 6.1),此代码块会被跳过,不影响原有功能。

5.2 用户态通信优化:从 DeviceIoControl 到 WFP 的演进路径

read.c中的IRP_MJ_DEVICE_CONTROL处理函数仅支持基础控制码(如IOCTL_MYNDIS_GET_STATS)。但在 Windows 10+ 环境中,更推荐与 Windows Filtering Platform(WFP)协同工作:

方案优势局限
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”,包括吊销状态、证书链完整性、EKU(Extended Key Usage)是否包含Code Signing

注意:若使用自签名证书测试,必须在测试机执行bcdedit /set testsigning on并重启,否则sc create会返回Access Denied

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询