Windows内核驱动完整实现:创建设备、注册派遣函数与完成IRP请求

零基础学逆向 | 2026-07-08 17:58 | 四川

封面图

整理了一下当初学习Windows内核驱动开发的笔记
理解CPU权限级别和完整的驱动程序实现


一、CPU权限级别:为什么驱动是"内核级"的?

在理解Windows驱动之前,必须先搞懂CPU的运行模式。

CPU的四种运行模式

  • 实模式(Real Mode):CPU通电后进入的模式,兼容16位操作系统和应用。人人平等,没有权限之分。
  • 保护模式(Protected Mode):32位操作系统运行的模式,引入了权限等级的概念。
  • 虚拟8086模式(Virtual-8086 Mode):在保护模式下模拟实模式环境,以运行16位程序。

保护模式到底保护什么?

保护模式的核心就是保护硬件。它把权限划分为4个级别(ring0~ring3):

级别用途特点
ring0操作系统内核代码最高权限,可直接访问硬件
ring3应用程序代码最低权限,不能直接操作硬件

CPU权限级别图

应用程序访问硬件的路径:

应用程序 → 系统调用(API) → 操作系统 → 硬件

驱动程序就在 ring0 运行,是操作系统与硬件之间的桥梁。

保护模式提供的关键能力

  1. 权限管理 — 防止应用程序胡作非为
  2. 内存隔离 — 一个进程崩溃不影响其他进程
  3. 任务管理(Task)
  4. 系统调用
  5. 中断管理

如何进入ring0?

  1. 系统级别漏洞(不推荐)
  2. 操作系统为驱动厂商提供的接口 — 即驱动开发框架

💡 操作系统提供驱动接口,厂家编写 .sys 文件,操作系统加载它(映射到高2G虚拟地址空间),然后调用它。

驱动的分类

类型说明
内核驱动不操作硬件的驱动(如虚拟光驱)
硬件驱动直接操作硬件的驱动(键盘、鼠标、显卡)

驱动分类图

⚠️ 驱动代码与操作系统同等级,崩溃可能导致系统蓝屏。务必使用虚拟机测试!


二、驱动开发环境搭建

WDK 的选择

WDK(Windows Driver Kit)是驱动开发的必备工具。

  • WDK新版:与 Visual Studio 绑定
  • WDK老版:提供离线文档
  • EWDK:独立开发包,不绑定VS

下载地址:https://docs.microsoft.com/zh-cn/windows-hardware/drivers/other-wdk-downloads

安装顺序(高版本):

安装 VS → 安装 SDK → 安装 WDK → 自动安装 VS 插件

⚠️ 安装过程不能中断,否则只能重装系统才能再安装插件!

驱动框架体系

框架说明
WDMWindows Driver Model,即插即用,硬件驱动标准
KMDFKernel-Mode Driver Framework,内核模式驱动(NT驱动)
WDFWindows Driver Framework,C++ 驱动框架
UMDFUser-Mode Driver Framework,3环驱动

驱动框架体系图

第一个驱动程序

  1. 准备 sources 文件(指定目标文件名、类型、源文件)
  2. 切换路径到编译目录
  3. build 编译
    • free → release 版
    • checked → debug 版

驱动安装与启动

SC 方式:

CreateService  →  安装服务
StartService   →  启动驱动

关于签名:

  • XP 系统无签名要求,可关掉签名验证
  • Win11 已无32位版本,仅支持64位驱动

三、完整的驱动程序 —— 创建设备、注册派遣函数、完成请求

现在进入最核心的部分:手写一个完整的驱动程序。

驱动程序的入口:DriverEntry

NTSTATUS DriverEntry(
    __in struct _DRIVER_OBJECT* DriverObject,
    __in PUNICODE_STRING RegistryPath
)

四个核心任务

  1. 创建设备(提供设备名称、扩展数据)
  2. 创建符号链接(与设备名称绑定,供3环应用访问)
  3. 注册派遣函数(处理 IRP 请求)
  4. 注册卸载函数(驱动停止时清理资源)

驱动对象与设备对象

┌─────────────────┐          ┌─────────────────┐
│  DRIVER_OBJECT   │ ──────→ │  DEVICE_OBJECT   │
│  (驱动对象)    │  绑定    │  (设备对象)    │
│  含派遣函数数组  │          │  含扩展指针      │
│  含设备对象链表  │          │                  │
└─────────────────┘          └─────────────────┘
  • DRIVER_OBJECT:系统创建的描述驱动信息的对象,绑定了派遣函数指针数组
  • DEVICE_OBJECT:给3环操作的对象(一个驱动可对应多个设备)

创建设备:IoCreateDevice

NTSTATUS IoCreateDevice(
    PDRIVER_OBJECT,              // 驱动对象
    ULONG DeviceExtensionSize,   // 扩展数据大小
    PUNICODE_STRING DeviceName,  // 设备名称
    DEVICE_TYPE DeviceType,      // 驱动类型
    ULONG DeviceCharacteristics, // 设备特性
    BOOLEAN Exclusive,           // 是否独占
    PDEVICE_OBJECT* DeviceObject // 返回的设备对象指针
);

IoCreateDevice参数图

设备扩展(DeviceExtension)
  • 存放设备的额外数据
  • 操作时向该指针指向的区域读写数据
  • 推荐使用扩展指针而不是全局变量(多个设备共用时才用全局变量)

创建设备名称与符号链接

// 创建设备名称
UNICODE_STRING devName;
RtlInitUnicodeString(&devName, L"\\Device\\MyDriverDevice");

// 创建符号链接
UNICODE_STRING symLink;
RtlInitUnicodeString(&symLink, L"\\??\\MyDriver");

NTSTATUS status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status)) {
    IoDeleteDevice(DeviceObject);
    return status;
}

0环设备名格式:\Device\XXX
3环通过符号链接访问:\??\XXX\DosDevices\XXX

注册派遣函数

DriverObject->MajorFunction[IRP_MJ_CREATE]         = MyCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE]          = MyClose;
DriverObject->MajorFunction[IRP_MJ_READ]           = MyRead;
DriverObject->MajorFunction[IRP_MJ_WRITE]          = MyWrite;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControl;

NTSTATUS MyFunction(PDEVICE_OBJECT DeviceObject, PIRP Irp)

完成请求:IoCompleteRequest

NTSTATUS MyCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    DbgPrint("[MyDriver] %s", __FUNCTION__);
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

读写实现要点

NTSTATUS MyWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    ULONG length = stack->Parameters.Write.Length;
    PVOID buffer = Irp->AssociatedIrp.SystemBuffer;

    // ⚠️ 安全检查:缓冲区大小由用户指定,需要验证
    if (length > MAX_BUFFER_SIZE) {
        Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
        return STATUS_INVALID_PARAMETER;
    }

    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = length;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

卸载函数

VOID Unload(PDRIVER_OBJECT DriverObject)
{
    // 1. 删除符号链接
    UNICODE_STRING symLink;
    RtlInitUnicodeString(&symLink, L"\\??\\MyDriver");
    IoDeleteSymbolicLink(&symLink);

    // 2. 删除设备
    if (DriverObject->DeviceObject) {
        IoDeleteDevice(DriverObject->DeviceObject);
    }

    DbgPrint("[MyDriver] Driver unloaded successfully");
}

四、控制函数与缓冲区通讯方式

控制码(IOCTL)

#define IOCTL_MY_READ CTL_CODE(
    FILE_DEVICE_UNKNOWN, 0x800,
    METHOD_BUFFERED, FILE_ANY_ACCESS
)

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG ctlCode = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG inLen   = stack->Parameters.DeviceIoControl.InputBufferLength;
ULONG outLen  = stack->Parameters.DeviceIoControl.OutputBufferLength;

三种缓冲区通讯方式

通讯方式机制安全性能推荐场景
缓冲区方式系统申请内核缓冲区,自动拷贝✅ 最安全❌ 较慢小数据量、安全优先
直接方式锁定用户内存,内核重新映射✅ 安全✅ 较快大数据量传输
其它方式直接访问用户地址❌ 不安全✅ 最快⚠️ 不推荐

缓冲区通讯方式图

1️⃣ 缓冲区方式(推荐)
PVOID buffer = Irp->AssociatedIrp.SystemBuffer;
2️⃣ 直接方式
PMDL mdl = Irp->MdlAddress;
PVOID buffer = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority);
if (buffer == NULL) {
    // 处理错误...
}
3️⃣ 其它方式(NEITHER)

⚠️ 三环地址不可信任!用户程序可能在驱动处理期间改变地址内容,导致安全漏洞。


五、内核内存管理——分页与非分页内存

驱动使用的内存类型

内存类型特点用途
非分页内存池常驻内存,不会被换出重要的代码、DPC例程、中断处理
分页内存池可用时在内存,需要时换出一般的驱动代码
堆空间(ExAllocatePool)动态分配运行时需要的内存

申请与释放

// 申请非分页内存
PVOID ptr = ExAllocatePool(NonPagedPool, size);

// 申请分页内存
PVOID ptr = ExAllocatePool(PagedPool, size);

// 释放
ExFreePool(ptr);

注意事项

⚠️ 重要的代码使用非分页内存,不重要的使用分页内存

  • 3环程序没有资格申请非分页内存,只有分页内存
  • 0环函数默认在非分页内存,可以用 #pragma alloc_text() 指定在不同节
  • DriverEntry 函数执行完后内存会被释放(属于 INIT 节),所以 Unload 函数不能放在 INIT 节
// 将 DriverEntry 放到 INIT 节,执行完释放
#pragma alloc_text(INIT, DriverEntry)

💡 非分页内存越用越少,申请完后系统无法回收——非分页内存耗尽可能导致系统崩溃!


结语:写给驱动开发新手的几点建议

  1. 虚拟机是第一生产力
    驱动的bug几乎都意味着蓝屏,反复实机测试 = 反复重启

  2. 安全是第一优先级
    内核代码的缓冲区溢出不只是崩溃,还可能被恶意利用

  3. 从 WDM/KMDF 开始
    不要一上来就搞 WDF,先把基础框架搞清楚

  4. 用好 DbgPrint 和 WinDbg
    内核调试和用户态调试完全不一样,早点掌握双机调试

  5. 阅读操作系统行为
    用 WinObj 查看设备对象,理解系统的内部视图

驱动开发是一扇门,推开它,你就真正进入了操作系统的底层世界。
希望这篇文章能帮你迈出第一步。


📌 如果你觉得这篇文章有帮助,欢迎点赞、在看、转发!
📌 关注本公众号,持续输出内核驱动、Windows底层技术干货。

#Windows驱动 #内核开发 #驱动开发 #C语言 #系统编程
2. 安全是第一优先级
内核代码的缓冲区溢出不只是崩溃,还可能被恶意利用

  1. 从 WDM/KMDF 开始
    不要一上来就搞 WDF,先把基础框架搞清楚

  2. 用好 DbgPrint 和 WinDbg
    内核调试和用户态调试完全不一样,早点掌握双机调试

  3. 阅读操作系统行为
    用 WinObj 查看设备对象,理解系统的内部视图

驱动开发是一扇门,推开它,你就真正进入了操作系统的底层世界。
希望这篇文章能帮你迈出第一步。


📌 如果你觉得这篇文章有帮助,欢迎点赞、在看、转发!
📌 关注本公众号,持续输出内核驱动、Windows底层技术干货。

#Windows驱动 #内核开发 #驱动开发 #C语言 #系统编程

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐