3. IOCTL 基础:IRP 与 MajorFunction、IOCTL 分发例程、缓冲区访问方式

IOCTL(I/O Control Code)是用户态应用程序与内核驱动进行通信的核心机制。理解 IOCTL 的基础设施——IRP、MajorFunction 分发例程以及缓冲区访问方式——是编写安全驱动的前提。本章将深入剖析这些概念,并指出常见的安全缺陷。

3.1 IRP 与 MajorFunction 分发机制

当用户态程序调用 DeviceIoControl 时,I/O 管理器会构建一个 IRP(I/O Request Packet),并将其发送到驱动注册的 MajorFunction 分发例程。驱动通过 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] 注册处理函数。

关键点: 每个 IRP 都包含一个 IO_STACK_LOCATION 结构,其中携带了控制码(IoControlCode)、输入/输出缓冲区指针及长度。驱动必须正确解析这些参数,否则将导致缓冲区溢出或信息泄露。
IRP 从用户态到驱动分发例程的流程 用户态应用 内核态 I/O 管理器 构建 IRP MajorFunction 数组 [IRP_MJ_DEVICE_CONTROL] DriverDispatch 例程 IO_STACK_LOCATION 包含 IoControlCode 等参数

3.2 IOCTL 分发例程与安全校验

分发例程是驱动安全的第一道防线。一个典型的安全分发例程应包含以下步骤:

  1. 验证 IoControlCode:确保控制码在预期范围内,避免处理未定义的控制码。
  2. 验证输入/输出缓冲区长度:防止缓冲区溢出或越界读取。
  3. 验证缓冲区指针:对于 METHOD_NEITHER,必须使用 ProbeForRead/ProbeForWrite 进行探测。
  4. 处理 IRP:完成实际功能后,设置 Irp->IoStatus 并调用 IoCompleteRequest。
常见缺陷: 许多驱动在分发例程中直接信任用户传入的缓冲区长度,未做校验就进行内存拷贝,导致经典的“缓冲区溢出”漏洞。另一个常见问题是未对 METHOD_NEITHER 的指针进行探测,导致任意地址读写。

3.3 缓冲区访问方式详解

Windows 内核提供了四种缓冲区访问方式,由 IOCTL 控制码中的 TransferType 位决定。选择错误的访问方式或错误地处理缓冲区,是驱动漏洞的主要来源。

访问方式 TransferType 值 缓冲区位置 安全风险
METHOD_BUFFERED 0 系统分配的非分页池 长度校验不当导致溢出
METHOD_IN_DIRECT 1 MDL 映射的系统内存 MDL 未正确锁定/解锁
METHOD_OUT_DIRECT 2 MDL 映射的系统内存 输出长度未校验
METHOD_NEITHER 3 用户态虚拟地址 未探测指针导致任意地址访问

3.3.1 METHOD_BUFFERED

I/O 管理器在非分页池中分配一个缓冲区,大小等于 InputBufferLength 和 OutputBufferLength 中的较大值。驱动通过 Irp->AssociatedIrp.SystemBuffer 访问该缓冲区。输入数据由 I/O 管理器从用户态拷贝到系统缓冲区,输出数据在 IRP 完成后由 I/O 管理器拷贝回用户态。

// METHOD_BUFFERED 示例:安全拷贝
NTSTATUS DispatchBuffered(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
    ULONG inLen = irpSp->Parameters.DeviceIoControl.InputBufferLength;
    ULONG outLen = irpSp->Parameters.DeviceIoControl.OutputBufferLength;
    PVOID buffer = Irp->AssociatedIrp.SystemBuffer;

    // 安全缺陷:未校验 inLen 是否足够大
    if (inLen < sizeof(MY_INPUT_STRUCT)) {
        Irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
        return STATUS_BUFFER_TOO_SMALL;
    }

    // 安全处理...
    RtlCopyMemory(buffer, inputData, sizeof(MY_INPUT_STRUCT));
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = sizeof(MY_OUTPUT_STRUCT);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

3.3.2 METHOD_IN_DIRECT / METHOD_OUT_DIRECT

这两种方式使用 MDL(Memory Descriptor List) 来描述用户缓冲区。I/O 管理器会锁定用户内存页并构建 MDL,驱动通过 Irp->MdlAddress 访问。对于 METHOD_IN_DIRECT,输入数据在 MDL 中;对于 METHOD_OUT_DIRECT,输出数据在 MDL 中。

安全要点: 驱动必须调用 MmGetSystemAddressForMdlSafe 获取 MDL 映射的系统地址,并在完成后调用 MmUnlockPages 解锁。未解锁 MDL 会导致内存泄漏,进而引发系统崩溃。
// METHOD_IN_DIRECT 示例:安全映射
PVOID mappedBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
if (!mappedBuffer) {
    Irp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_INSUFFICIENT_RESOURCES;
}
// 使用 mappedBuffer 进行读写...
// 注意:MDL 的解锁由 I/O 管理器在 IRP 完成后自动处理,驱动无需手动解锁

3.3.3 METHOD_NEITHER

这是最危险的方式。I/O 管理器不进行任何缓冲区拷贝或锁定,直接将用户态虚拟地址传递给驱动。驱动通过 irpSp->Parameters.DeviceIoControl.Type3InputBuffer 获取输入缓冲区地址,输出缓冲区地址则通过 Irp->UserBuffer 获取。

高危缺陷: 驱动必须使用 ProbeForRead 和 ProbeForWrite 在 try/except 块中探测用户地址。许多漏洞驱动直接解引用用户指针,导致攻击者可以传入任意内核地址实现提权。
// METHOD_NEITHER 示例:安全探测
NTSTATUS DispatchNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
    PVOID inputBuffer = irpSp->Parameters.DeviceIoControl.Type3InputBuffer;
    PVOID outputBuffer = Irp->UserBuffer;
    ULONG inLen = irpSp->Parameters.DeviceIoControl.InputBufferLength;
    ULONG outLen = irpSp->Parameters.DeviceIoControl.OutputBufferLength;

    __try {
        // 探测输入缓冲区可读
        ProbeForRead(inputBuffer, inLen, sizeof(UCHAR));
        // 探测输出缓冲区可写
        ProbeForWrite(outputBuffer, outLen, sizeof(UCHAR));
    } __except (EXCEPTION_EXECUTE_HANDLER) {
        Irp->IoStatus.Status = GetExceptionCode();
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
        return Irp->IoStatus.Status;
    }

    // 安全处理...
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = outLen;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}
四种缓冲区访问方式对比 METHOD_BUFFERED I/O 管理器分配 系统缓冲区 自动拷贝数据 METHOD_IN_DIRECT I/O 管理器锁定 用户内存页 构建 MDL METHOD_OUT_DIRECT I/O 管理器锁定 用户内存页 构建 MDL METHOD_NEITHER 直接传递 用户态虚拟地址 无任何处理 安全等级:BUFFERED > IN_DIRECT ≈ OUT_DIRECT >> NEITHER 驱动开发者的安全责任 校验缓冲区长度 | 探测用户指针 (NEITHER) | 正确管理 MDL | 避免越界访问 使用 try/except 保护异常处理 | 设置正确的 IoStatus.Information

3.4 缓冲区访问方式选择指南

选择正确的缓冲区访问方式可以显著降低安全风险。以下是一些指导原则:

  • 优先使用 METHOD_BUFFERED:对于小数据量(通常 < 4KB),BUFFERED 方式最安全,因为 I/O 管理器负责拷贝和隔离。
  • 大数据量使用 DIRECT 方式:当传输大量数据(如视频帧、大文件)时,DIRECT 方式避免额外拷贝,性能更好。
  • 避免使用 METHOD_NEITHER:除非有特殊性能要求且完全理解安全风险,否则不要使用 NEITHER 方式。如果必须使用,务必进行完整的指针探测和异常处理。
安全实践: 在驱动入口处统一校验所有 IOCTL 的缓冲区长度,建立“白名单”机制。对于 METHOD_NEITHER,建议封装一个安全访问宏,自动包含 ProbeForRead/ProbeForWrite 和 try/except 块,避免遗漏。