WDM 是什么?
Windows 驱动模型(WDM)是编写驱动程序的"老派"方式。许多较新的驱动程序使用内核模式驱动程序框架(KMDF),该框架为开发者处理了大量样板代码。尽管如此,你在实际中遇到的许多驱动程序仍然基于 WDM。
驱动程序本质上只是一个普通的 PE 文件,通过创建服务,以内核权限加载和执行。
一个基本的 WDM 驱动程序骨架如下:
1. 驱动程序入口(DriverEntry)
在这里,通常会创建一个设备对象(Device Object)和一个指向它的符号链接(Symbolic Link)。用户模式进程可以使用这个符号链接(例如,通过 CreateFile 打开 \\??\\BasicWdmLink)来获取驱动程序的句柄,并向其发送消息(IOCTLs)进行通信。
#include <ntddk.h>
PDEVICE_OBJECT g_DeviceObject = NULL;
UNICODE_STRING g_DeviceName = RTL_CONSTANT_STRING(L"\\Device\\BasicWdmDevice");
UNICODE_STRING g_SymbolicLink = RTL_CONSTANT_STRING(L"\\??\\BasicWdmLink");
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
// 创建设备
status = IoCreateDevice(DriverObject, 0, &g_DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &g_DeviceObject);
// 创建符号链接
status = IoCreateSymbolicLink(&g_SymbolicLink, &g_DeviceName);
}
2. 注册分发例程
在 DriverEntry 中,驱动程序会注册不同的分发例程(Dispatch Routines),这些例程描述了驱动程序在被交互时的行为。
DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;
DriverObject->DriverUnload = DriverUnload;
3. IOCTL 分发例程
这是最有趣的部分,因为它处理来自用户模式程序的调用。CTL_CODE 宏用于构建一个唯一的 32 位值来标识一个 IOCTL。
#define IOCTL_ECHO_DATA CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
NTSTATUS DispatchIoctl(_In_ PDEVICE_OBJECT DeviceObject, _Inout_ PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
switch (code) {
case IOCTL_ECHO_DATA:
// 处理来自用户模式的数据
break;
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
}
用户模式程序可以通过 DeviceIoControl 发送相同的 IOCTL_ECHO_DATA。通信有多种方法,文中以 METHOD_BUFFERED 为例,这意味着输入和输出共享一个缓冲区。
静态逆向工程
文章以 afd.sys(一个用于套接字通信的驱动程序)为例,在 IDA Pro 中进行分析。
1. 定位驱动入口
打开 afd.sys,进入 DriverEntry 函数。如果看到的是一个简单的包装函数,它会调用真正的入口函数。
2. 寻找设备名称和创建调用
在入口函数中,可以找到创建 Unicode 字符串(如 \\Device\\Afd)和调用 IoCreateDevice 的代码,这个设备名是用户态打开句柄的目标。
3. 定位 IOCTL 处理函数
寻找设置 MajorFunction 的代码块。IRP_MJ_DEVICE_CONTROL 的值是 14(0x0E),因此被赋值为 0x0E 的那个函数就是 IOCTL 处理函数。
4. 分析 IOCTL 处理函数
进入该函数后,关键是对 IRP 中的 IO_STACK_LOCATION 结构选择正确的联合体(Union)字段。右键点击 CurrentStackLocation 相关的代码,选择正确的联合体字段,通常是 DeviceIoControl.IoControlCode,这样 IDA 的伪代码就会变得清晰。
函数通常会提取 IOCTL 代码,进行验证,然后通过函数表或 switch 语句调用相应的处理函数。
5. 分析具体处理函数
在处理函数内部,再次需要为访问 IRP 选择正确的联合体。例如,对于 METHOD_BUFFERED,通常选择 Irp->AssociatedIrp.SystemBuffer 来查看用户态传入和传出的缓冲区。
可以根据 IOCTL 代码中的 Method 字段(可用 OSR Ioctl Decoder 等工具解析)来确定应使用哪个 IRP 联合体成员。
动态分析
作者指出,新手逆向工程师容易陷入只看 IDA 伪代码的误区,但动态分析往往更为重要。建议设置双虚拟机环境进行远程内核调试:
- 设置两台虚拟机(一台调试器,一台被调试机)
- 在被调试机上启用内核调试
- 配置其进行远程内核调试(最好通过网络)并连接到调试器虚拟机的 IP
- 在调试器虚拟机上运行 WinDbg
总结
这篇文章为逆向 Windows 驱动程序的 IOCTL 通信提供了一个入门指南,帮助读者在 IDA 中快速上手静态分析。
