驱动开发基础回顾:WDF 与 NT 驱动模型、IRP 与派遣函数、驱动入口与卸载例程

在深入驱动安全测试之前,必须牢固掌握 Windows 驱动开发的两大核心模型——NT 式驱动与 WDF(Windows Driver Frameworks)。本章将系统梳理驱动的基本骨架:入口/卸载例程、IRP 请求分发机制以及派遣函数的注册与实现。这些概念是后续静态分析与动态测试的基石。

2.1 NT 驱动模型 vs. WDF 驱动模型

Windows 驱动开发经历了从 NT 式驱动(传统 WDM)到 WDF(KMDF/UMDF)的演进。NT 模型直接暴露 I/O 管理器接口,开发者需手动处理大量底层细节;WDF 则封装了通用逻辑,提供更安全、更简洁的编程框架。

对比维度 NT 式驱动(传统) WDF(KMDF/UMDF)
框架层次 直接与 I/O 管理器交互 通过框架对象封装 I/O 管理器
即插即用/电源管理 手动处理 IRP_MJ_PNP / IRP_MJ_POWER 框架自动处理,仅需注册回调
同步与并发 开发者自旋锁/互斥体 框架提供自动锁定与队列管理
错误处理 手动检查每个 IRP 状态 框架提供默认完成例程与错误路径
代码量 较大,模板代码多 减少 30%~50% 样板代码
安全审计重点 IRP 派遣函数、缓冲区处理 EvtIo* 回调、请求对象生命周期
安全提示: NT 式驱动中,开发者直接操作 IRP 堆栈和缓冲区指针,极易产生缓冲区溢出、空指针解引用等漏洞。WDF 通过框架对象管理内存,但仍需注意 EvtIoRead/EvtIoWrite 回调中的请求验证。

2.2 IRP 与派遣函数机制

IRP(I/O Request Packet) 是 Windows I/O 子系统的核心数据结构,用于在驱动层之间传递 I/O 请求。每个 IRP 包含主功能码(MajorFunction)和次功能码(MinorFunction),驱动通过注册 派遣函数(Dispatch Function) 来处理不同类型的请求。

关键 IRP 主功能码:
  • IRP_MJ_CREATE —— 打开设备(CreateFile)
  • IRP_MJ_CLOSE —— 关闭设备句柄
  • IRP_MJ_READ / IRP_MJ_WRITE —— 读写操作
  • IRP_MJ_DEVICE_CONTROL —— 设备控制(DeviceIoControl)
  • IRP_MJ_PNP —— 即插即用事件
  • IRP_MJ_POWER —— 电源管理
  • IRP_MJ_INTERNAL_DEVICE_CONTROL —— 内部设备控制

NT 式驱动通过 DriverObject->MajorFunction[] 数组注册派遣函数;WDF 则通过 WdfDeviceIoQueueCreate 和 EvtIo* 回调注册。

// NT 式驱动:注册派遣函数示例
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DriverObject->MajorFunction[IRP_MJ_CREATE]          = DispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]           = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  = DispatchIoControl;
    DriverObject->MajorFunction[IRP_MJ_READ]            = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE]           = DispatchWrite;
    DriverObject->DriverUnload                          = DriverUnload;
    // ... 创建设备对象、符号链接等
    return STATUS_SUCCESS;
}

// 派遣函数通用签名
NTSTATUS DispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    // 处理打开请求
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}
// WDF (KMDF) 驱动:注册 EvtIo* 回调示例
EVT_WDF_DRIVER_DEVICE_ADD KmdfEvtDeviceAdd;
EVT_WDF_IO_QUEUE_IO_READ KmdfEvtIoRead;
EVT_WDF_IO_QUEUE_IO_WRITE KmdfEvtIoWrite;
EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL KmdfEvtIoDeviceControl;

NTSTATUS KmdfEvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
    WDFDEVICE device;
    WDF_IO_QUEUE_CONFIG queueConfig;
    // ... 初始化设备
    WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchSequential);
    queueConfig.EvtIoRead = KmdfEvtIoRead;
    queueConfig.EvtIoWrite = KmdfEvtIoWrite;
    queueConfig.EvtIoDeviceControl = KmdfEvtIoDeviceControl;
    WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, WDF_NO_HANDLE);
    return STATUS_SUCCESS;
}
安全陷阱: 在 IRP_MJ_DEVICE_CONTROL 派遣函数中,如果未正确验证 IoStackLocation->Parameters.DeviceIoControl.InputBufferLength 和 OutputBufferLength,极易导致内核内存泄漏或越界写入。WDF 中同样需要检查 WdfRequestRetrieveInputBuffer 返回的缓冲区大小。

2.3 驱动入口与卸载例程

每个 Windows 驱动必须包含 DriverEntry 入口函数和 DriverUnload 卸载例程(NT 式)或 EvtDriverUnload(WDF)。入口函数负责初始化驱动对象、注册派遣函数、创建设备对象;卸载例程负责清理资源、删除设备对象和符号链接。

// NT 式驱动:DriverEntry 与 DriverUnload 完整骨架
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    NTSTATUS status;
    PDEVICE_OBJECT deviceObject = NULL;
    UNICODE_STRING deviceName, symLinkName;

    // 1. 注册派遣函数
    DriverObject->MajorFunction[IRP_MJ_CREATE]          = DispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]           = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  = DispatchIoControl;
    DriverObject->MajorFunction[IRP_MJ_READ]            = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE]           = DispatchWrite;
    DriverObject->DriverUnload                          = DriverUnload;

    // 2. 创建设备对象
    RtlInitUnicodeString(&deviceName, L"\\Device\\MyDriverDevice");
    status = IoCreateDevice(DriverObject, 0, &deviceName,
                            FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject);
    if (!NT_SUCCESS(status)) return status;

    // 3. 创建符号链接
    RtlInitUnicodeString(&symLinkName, L"\\DosDevices\\MyDriver");
    status = IoCreateSymbolicLink(&symLinkName, &deviceName);
    if (!NT_SUCCESS(status)) {
        IoDeleteDevice(deviceObject);
        return status;
    }
    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING symLinkName;
    RtlInitUnicodeString(&symLinkName, L"\\DosDevices\\MyDriver");
    IoDeleteSymbolicLink(&symLinkName);
    IoDeleteDevice(DriverObject->DeviceObject);
}
// WDF (KMDF) 驱动:DriverEntry 与 EvtDriverUnload 骨架
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    WDF_DRIVER_CONFIG config;
    WDF_DRIVER_CONFIG_INIT(&config, KmdfEvtDeviceAdd);
    config.EvtDriverUnload = KmdfEvtDriverUnload;  // 注册卸载回调
    return WdfDriverCreate(DriverObject, RegistryPath,
                           WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
}

VOID KmdfEvtDriverUnload(WDFDRIVER Driver)
{
    // 框架自动清理设备对象和队列,通常只需记录日志或释放全局资源
    DbgPrint("MyKMDFDriver: Unloading...\n");
}
安全最佳实践: 卸载例程中必须确保所有未完成的 IRP 已被取消或完成,否则会导致系统崩溃。WDF 框架自动管理请求队列生命周期,但 NT 式驱动需要手动遍历并取消挂起的 IRP。建议在卸载前调用 IoCancelIrp 或设置取消安全标志。

2.4 驱动对象与设备对象关系图

下图展示了 NT 式驱动中 驱动对象(DRIVER_OBJECT)、设备对象(DEVICE_OBJECT) 与 IRP 派遣表 之间的核心关系。理解此结构是进行驱动逆向与安全分析的前提。

驱动对象、设备对象与 IRP 派遣表关系图 DRIVER_OBJECT - DriverUnload - DeviceObject (链表头) - MajorFunction[0..27] - DriverStartIo - DriverExtension MajorFunction[] 派遣表 [0] IRP_MJ_CREATE [1] IRP_MJ_CREATE_NAMED_PIPE [2] IRP_MJ_CLOSE [3] IRP_MJ_READ [4] IRP_MJ_WRITE ... 共 28 个槽位 DEVICE_OBJECT - DriverObject (回指) - NextDevice (链表) - CurrentIrp - Flags / Characteristics IRP (I/O Request Packet) - IoStatus - AssociatedIrp - Tail.Overlay.CurrentStackLocation - MdlAddress / UserBuffer 指向 DeviceObject 链表 CurrentIrp 安全分析要点 • 检查 MajorFunction 是否覆盖 • 验证派遣函数指针合法性 • 审计 IRP 完成路径 常见漏洞模式 • 派遣函数未注册导致蓝屏 • 卸载后设备对象未删除 • IRP 完成例程双重释放

2.5 派遣函数注册与 IRP 处理流程

当用户态调用 CreateFile、ReadFile、DeviceIoControl 等 API 时,I/O 管理器构造 IRP 并调用驱动注册的派遣函数。下图展示了从用户态请求到派遣函数执行的完整路径。

IRP 从用户态到派遣函数的处理流程 用户态应用 CreateFile / DeviceIoControl I/O 管理器 构造 IRP,查找设备对象 派遣函数 MajorFunction[IRP_MJ_*] 系统调用 调用派遣 完成 IRP IoCompleteRequest 挂起 IRP MarkIrpPending / 队列 传递 IRP IoCallDriver 下一层 安全审计关键点 ① 检查派遣函数是否对 IRP 主功能码进行正确分发 ② 验证 IRP 完成路径是否唯一,避免双重完成 ③ 挂起 IRP 后是否在卸载时正确取消 ④ 传递 IRP 时是否修改了堆栈位置

2.6 驱动开发安全基线清单

在进入静态分析之前,请确保掌握以下安全基线。这些条目将作为后续测试用例设计的依据。

检查项 NT 式驱动 WDF 驱动 安全风险等级
入口函数返回值 必须返回 STATUS_SUCCESS 或错误码 WdfDriverCreate 返回 NTSTATUS 高
派遣函数覆盖 至少注册 CREATE/CLOSE/DEVICE_CONTROL EvtIoDeviceControl 必须实现 高
缓冲区验证 检查 InputBufferLength / OutputBufferLength 检查 WdfRequestRetrieveInputBuffer 大小 高
IRP 完成 每个 IRP 必须且仅完成一次 框架自动完成,但 EvtIo* 中不可重复完成 高
卸载清理 删除设备对象、符号链接、取消挂起 IRP 框架自动清理,但需释放全局资源 中
同步保护 自旋锁/互斥体保护共享数据 框架队列自动序列化 中
符号链接权限 检查设备对象 ACL WdfDeviceInitAssignSDDLString 中
实战提醒: 在后续章节的静态分析中,我们将使用 IDA Pro 和 Ghidra 逆向驱动二进制,重点定位 MajorFunction 数组的初始化位置、IoCreateDevice 调用点以及 DriverUnload 中的资源释放逻辑。WDF 驱动则需关注 WdfDriverCreate 参数和 EvtDriverUnload 回调地址。