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] 注册处理函数。
IO_STACK_LOCATION 结构,其中携带了控制码(IoControlCode)、输入/输出缓冲区指针及长度。驱动必须正确解析这些参数,否则将导致缓冲区溢出或信息泄露。
3.2 IOCTL 分发例程与安全校验
分发例程是驱动安全的第一道防线。一个典型的安全分发例程应包含以下步骤:
- 验证 IoControlCode:确保控制码在预期范围内,避免处理未定义的控制码。
- 验证输入/输出缓冲区长度:防止缓冲区溢出或越界读取。
- 验证缓冲区指针:对于 METHOD_NEITHER,必须使用
ProbeForRead/ProbeForWrite进行探测。 - 处理 IRP:完成实际功能后,设置
Irp->IoStatus并调用IoCompleteRequest。
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;
}
3.4 缓冲区访问方式选择指南
选择正确的缓冲区访问方式可以显著降低安全风险。以下是一些指导原则:
- 优先使用 METHOD_BUFFERED:对于小数据量(通常 < 4KB),BUFFERED 方式最安全,因为 I/O 管理器负责拷贝和隔离。
- 大数据量使用 DIRECT 方式:当传输大量数据(如视频帧、大文件)时,DIRECT 方式避免额外拷贝,性能更好。
- 避免使用 METHOD_NEITHER:除非有特殊性能要求且完全理解安全风险,否则不要使用 NEITHER 方式。如果必须使用,务必进行完整的指针探测和异常处理。