windows 驱动基础

sky123

Windows 基础

内核对象

内核对象(Kernel Object),是 Windows 内核里对各种内核资源进行统一管理、统一命名、统一访问控制、统一生命周期控制的一种抽象机制。

设计思想

在没有内核对象之前,Windows 内核里存在着大量不同类型的资源(如设备,文件,进程线程等),这些资源每种结构体都不一样,但是每种都要支持用户态访问(有名字,有权限控制,有引用计数,有安全策略)。

因此 Windows 采用了一种类似 Linux 的 “万物皆文件” 的设计思想,即将每个需要统一管理的内核资源都被包装成一个 “内核对象”,交由 Object Manager(对象管理器) 组件管理。这样可以统一的解决下面几个问题:

典型问题 Object Manager 提供的统一解法
内核里有驱动、设备、进程、互斥量、事件、共享内存等几十种资源,各自要命名、要 ACL、要引用计数、要调试符号。 定义 OBJECT_HEADER + OBJECT_BODY 模型;创建/打开/引用/关闭/删除流程全部交给 Object Manager(Ob)。
资源的生命周期复杂:谁来保证用完才释放? PointerCount(内核指针引用) + HandleCount(用户/内核句柄数量)双计数模型,当二者皆为 0 时由 Ob 自动回收。
用户进程需要安全地访问部分内核资源 SECURITY_DESCRIPTOR 嵌进对象;用户通过系统调用走 SeAccessCheck
调试/监控工具需要统一查看 所有命名对象都挂进 对象目录树(Directory Object);Windbg / ETW / AV 可以枚举。

内核对象结构

每个内核对象在被 Object Manager 在分配时,自动套上了一个统一的“对象头部”+(可能存在的附加信息头)+ 对象体数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
|---------------------------|
| POOL_HEADER | 内存池头部
|---------------------------|
| OBJECT_HEADER_NAME_INFO | 名字信息 (可选)
|---------------------------|
| OBJECT_HEADER_HANDLE_INFO | 句柄信息 (可选)
|---------------------------|
| OBJECT_HEADER_QUOTA_INFO | 配额信息 (可选)
|---------------------------|
| OBJECT_HEADER | 内核对象统一头部 (必定存在)
|---------------------------|
| OBJECT_BODY | 对象体结构 (如 DRIVER_OBJECT)
|---------------------------|

其中 OBJECT_HEADER 在内核对象中一定存在,该结构体在不同版本的 Windows 中会发生变化,下面是一些常见字段:

字段 作用
PointerCount (LONG) 内核所有“裸指针”引用计数
HandleCount (LONG) 所有进程句柄数量
Type (POBJECT_TYPE) 指向 DRIVER_OBJECT_TYPE / DEVICE_OBJECT_TYPE
Flags OB_FLAG_PERMANENT / EXCLUSIVE / KERNEL_MODE
InfoMask 标记是否有 Name/Handle/Quota 这三种可选头

注意

Win11 以后 PatchGuard 会随机调整可选头偏移,驱动代码必须使用官方宏(OBJECT_HEADER_NAME_INFO_OFFSET 等)而非写死偏移。

Object Manager 命名空间

Object Manager 命名空间 是 Windows 内核中统一管理一切内核对象的“对象目录树”。

  • 它的本质是一个内核内存中的目录树结构
  • 每个可以被命名的内核对象都被挂载在这棵树上;
  • Object Manager 负责解析路径、查找对象、引用计数、权限控制等一切逻辑。

整个 Object Manager 命名空间以 \ 为根目录,形成一棵类似文件系统的目录树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
\                                (根目录,DirectoryObject)
├── Driver 所有驱动对象 (DriverObject)
│ └── MyDriver

├── Device 所有设备对象 (DeviceObject)
│ └── Harddisk0
│ └── Serial0

├── ?? ★ 符号链接桥梁目录 (SymbolicLinkObject)
│ ├── C: → \Device\HarddiskVolume1
│ ├── COM1 → \Device\Serial0
│ └── MyLink → \Device\MyDevice

├── BaseNamedObjects ★ 用户会话 0 命名空间(全局同步对象)
│ └── MyEvent
│ └── MyMutex

├── Sessions ★ 多用户会话隔离目录
│ └── 1
│ └── BaseNamedObjects (Session 1 的私有命名空间)

├── ObjectTypes ★ 已注册的内核对象类型列表
├── Windows (部分版本存在)
└── 其它系统内部目录

其中每一类目录的具体作用如下:

目录名 作用 常见对象类型
\Driver 存放所有已注册的内核驱动对象 DriverObject
\Device 存放所有设备对象,供 I/O 管理器使用 DeviceObject
\?? 符号链接目录:Win32 路径与内核对象桥接 SymbolicLinkObject
\BaseNamedObjects 全局同步对象命名区(Session 0 共享) Event / Mutex / Semaphore
\Sessions\N\BaseNamedObjects 多用户会话隔离命名空间 各自的同步对象
\ObjectTypes 存放系统内置的对象类型定义表 ObjectTypeObject

其中 \?? 目录(全名为 DosDevices Directory)是 Object Manager 里专门用来桥接 Win32 路径系统 ↔ 内核命名空间 的目录。它里面挂载的都是 符号链接对象(SymbolicLinkObject),用于:

  • 盘符映射 (C:\Device\HarddiskVolumeX)
  • 传统设备名 (COM1\Device\Serial0)
  • 自定义设备别名(通过 IoCreateSymbolicLink() 创建)

提示

所以你看到的 \\.\COM1,Win32 实际内部转为 \??\COM1,由 Object Manager 查找对应符号链接完成跳转。

只要某路径存在于 \??\ 下,并且链接指向有效内核对象,用户态理论上就能访问。(不考虑权限问题)

如果路径不经过 \??\(例如裸的 \Device\xxx\Driver\xxx\BaseNamedObjects\xxx),则 3环无法直接访问。

常用 API

  • ObReferenceObjectByName 函数可以用路径字符串找到任何已存在内核对象,并返回其内核对象指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    NTSTATUS ObReferenceObjectByName(
    IN PUNICODE_STRING ObjectName, // [输入] 要查找的对象完整路径名 (例如: "\\Driver\\MyDriver")
    IN ULONG Attributes, // [输入] 属性标志,常用 OBJ_CASE_INSENSITIVE (忽略大小写匹配)
    IN PACCESS_STATE AccessState OPTIONAL, // [输入] 安全访问状态,普通内核使用时传 NULL
    IN ACCESS_MASK DesiredAccess, // [输入] 请求的访问权限,一般填 0 表示默认即可
    IN POBJECT_TYPE ObjectType, // [输入] 对象类型指针,例如 *IoDriverObjectType、*IoDeviceObjectType 等
    IN KPROCESSOR_MODE AccessMode, // [输入] 访问模式,一般传 KernelMode
    IN PVOID ParseContext OPTIONAL, // [输入] 解析上下文 (高阶场景使用),通常传 NULL
    OUT PVOID *Object // [输出] 成功时返回获取到的对象指针 (注意:需 ObDereferenceObject 释放引用计数)
    );

路径

四类常见路径

Windows 系统有四种路径:

  • Win32 路径(DOS 路径) :用户程序使用的路径,例如:

    1
    C:\Windows\System32\drivers\Test.sys
    • 仅存在于 Win32 API 层;内核本身不识别盘符。

    • 首次进入内核时经 RtlDosPathNameToNtPathName 转成 \??\C:\...

      提示

      \?? 是 Object Manager 命名空间下的一个目录,里面存放大量符号链接(充当快捷方式),这些符号链接用于将盘符、传统设备名等映射到内核对象的真实路径。当 Win32 子系统将用户空间的 DOS 路径传入内核时,会先将其转换为以 \?? 为前缀的 NT 路径,由内核在 \?? 目录中解析出对应的真实内核对象路径。

  • NT 文件路径 :I/O 管理器与文件系统驱动的直接输入,例如:

    1
    \Device\HarddiskVolume1\Windows\System32\drivers\Test.sys
    • 盘符被解析为真正卷设备对象。

    • ZwCreateFile/ZwOpenFile 等内核 API 需传此类路径

  • Win32 设备路径 \\.\

    用户态的路径会被Win32默认认为要访问的是”文件系统”里的文件。Win32 会按照文件系统路径解析的逻辑进行处理:

    1. 解析 C: 盘符;(由于设备路径缺少盘符,一般会在这一步报错❌)

    2. 找到其对应物理卷;

    3. 然后交给文件系统驱动处理。

    然而,像 COM1 串口、物理硬盘 (PhysicalDrive0)、USB 端口、命名管道、内核设备对象这些根本不属于文件系统。文件系统找不到这些对象,它们被挂在 \Device\xxx 下(内核对象管理器里)。因此如果用户程序也希望用标准的 CreateFile() 访问设备对象则 Win32 会试图当做文件路径来走,肯定会失败。于是微软在 Win32 设计了一个特殊标记机制:只要路径以 \\.\ 开头,Win32 不参与文件系统逻辑,而是把后面的内容原样放进 \?? 命名空间,留给内核对象管理器自己去解析。例如:

    1
    2
    \\.\COM1        →  \??\COM1          →  \Device\Serial0
    \\.\PhysicalDrive0 → \??\PhysicalDrive0 → \Device\Harddisk0\DR0
  • Object Manager 路径 :所有内核对象的正式地址。样的路径仅对对象管理函数有效,如 ObReferenceObjectByNameIoCreateDeviceIoCreateSymbolicLink 等 API 可以直接使用;对文件 API 无意义。

典型解析链路

文件示例:

1
2
3
4
5
6
7
8
9
10
11
CreateFile("C:\\Windows\\System32\\drivers\\Test.sys")

└─► Win32 → NT 转换
\??\C:\Windows\System32\drivers\Test.sys

└─► \??\C: (符号链接)

\Device\HarddiskVolume1

└─► 完整 NT 路径
\Device\HarddiskVolume1\Windows\System32\drivers\Test.sys

设备示例:

1
2
3
4
5
6
CreateFile("\\\\.\\COM1")

└─► \??\COM1 (Win32 设备路径)

└─► 符号链接解析
\Device\Serial0 ← 真实设备对象

驱动基本概念

驱动程序(Driver)是运行在操作系统内核或用户模式中的软件组件,负责在操作系统与硬件设备之间“翻译”命令与数据。

驱动框架

微软为简化驱动开发,提供了三种主要框架:

框架名 全称 运行模式 推荐用途
WDM Windows Driver Model 内核模式 底层控制、兼容性极强,但复杂
KMDF Kernel-Mode Driver Framework 内核模式 封装了 WDM 的常见任务(如 PnP、电源管理、同步、I/O 队列等),推荐用于大多数设备驱动开发
UMDF User-Mode Driver Framework 用户模式 WDF 框架的另一部分,适用于开发运行在用户模式的驱动,推荐用于外围、低风险设备驱动

考虑到兼容性,我们通常采用 WDM 框架开发驱动。

驱动服务名

驱动的服务名(Service Name)是系统用来识别和管理驱动程序的逻辑标识符,它是注册表 HKLM\SYSTEM\CurrentControlSet\Services 下的子项名称,也是驱动服务控制、注册、加载、配置等操作的核心索引键。

例如如果我们加载一个名称为 Services.sys 的驱动,则会在注册表中对应创建一个 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services 子项。

  • CurrentControlSet → 实际指向 ControlSet001ControlSet002,系统启动时动态决定
  • Services → 包含所有服务与驱动程序的定义项

在该子项中通常有如下键值对:

键名 类型 示例值 说明
ImagePath REG_EXPAND_SZ \??\C:\Path\to\MyDriver.sys 驱动文件路径,通常位于 %SystemRoot%\System32\drivers\
Type REG_DWORD 1, 2 指定服务/驱动类型(详见下文)
Start REG_DWORD 0, 1, 3 启动类型(详见下文)
Group REG_SZ Base, Boot Bus Extender 指定驱动分组,影响加载顺序
ErrorControl REG_DWORD 1 启动失败时的处理方式
DisplayName REG_SZ My Sample Driver 控制面板中显示的服务名称(可选)
Description REG_SZ Test WDM Driver 人类可读描述信息(可选)
Tag REG_DWORD 分组内排序标识(较少使用)
Parameters REG_KEY 子键 自定义参数保存区,驱动可读取用于配置

\??\Windows 内核对象管理器(Object Manager)中的一个符号链接目录,代表当前会话的 DosDevices(用户态设备路径)目录

\??\ 通常映射到 \GLOBAL??,用于解析用户模式中的路径名,如:

  • \??\C:\Windows\System32 → 实际解析为 \Device\HarddiskVolumeX\Windows\System32
  • \??\COM1 → 实际是 \Device\Serial0

其中 Start 类型表示驱动何时启动,不同的值有如下含义:

含义 示例用途
0 BOOT_START :引导时加载(Boot Loader 加载) 如磁盘控制器驱动
1 SYSTEM_START :内核初始化阶段加载 大多数普通内核驱动
2 AUTO_START :Service Control Manager 启动时加载 系统服务,非 PnP 驱动
3 DEMAND_START :按需手动启动 测试驱动、虚拟设备
4 DISABLED :禁用服务 禁用驱动或服务启动

Type 类型表示服务/驱动的类别,不同的值有如下含义:

含义 示例
1 内核驱动(SERVICE_KERNEL_DRIVER .sys 驱动,运行在 Ring 0
2 文件系统驱动(SERVICE_FILE_SYSTEM_DRIVER NTFS、FAT 等
10 Win32 服务(用户模式,SERVICE_WIN32_OWN_PROCESS 普通服务程序

驱动加载

Windows 支持两种主要的内核驱动加载方式:

  • 高层推荐方式:通过 SCM(服务控制管理器)
  • 底层直接方式:通过 ZwLoadDriver(系统调用)

这两种方式都依赖于 驱动服务名对应的注册表项

  • SCM 方式加载不是由本进程完成的(实际由系统进程,如 services.exe),因此在 0 环不容易通过行为定位到进程。
  • ZwLoadDriver 方式加载过程可控,不容易被系统策略拦截。实际情况下 ZwLoadDriver 方式加载签名异常驱动的成功率高一些。

SCM 加载(服务控制管理器)

SCM 加载是 Windows 推荐的标准驱动加载方式。驱动作为一种特殊的“服务”被注册(类型为 SERVICE_KERNEL_DRIVER),然后由 服务控制管理器(SCM) 调用底层内核服务 NtLoadDriver 加载 .sys 驱动文件。

原理流程

Windows 把驱动程序视为一种特殊的服务,类型为 SERVICE_KERNEL_DRIVER。通过一套标准 API,开发者可以注册、启动、停止和卸载驱动。每个 API 都与注册表和内核交互紧密关联。

  • OpenSCManager :连接到本地或远程计算机上的 SCM(服务控制管理器),并获取一个 SCM 句柄,用于后续服务管理操作。

    1
    2
    3
    4
    5
    SC_HANDLE OpenSCManager(
    LPCSTR lpMachineName, // 计算机名,NULL 表示本地
    LPCSTR lpDatabaseName, // 数据库名,通常为 NULL 或 "ServicesActive"
    DWORD dwDesiredAccess // 访问权限(如 SC_MANAGER_ALL_ACCESS)
    );
    • lpMachineName :目标计算机名称。为 NULL 时表示本地计算机。
    • lpDatabaseName :服务数据库名称,通常为 NULL 或默认值 "ServicesActive"
    • dwDesiredAccess :请求的访问权限。建议使用 SC_MANAGER_ALL_ACCESS 以便执行创建、删除等所有操作。
  • CreateService :在 SCM 中注册一个新服务(或驱动),生成注册表项并配置驱动加载参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    SC_HANDLE CreateService(
    SC_HANDLE hSCManager, // 打开的服务控制管理器句柄
    LPCSTR lpServiceName, // 服务逻辑名称(注册表键名)
    LPCSTR lpDisplayName, // 显示名称(服务管理器界面显示)
    DWORD dwDesiredAccess, // 返回句柄的访问权限
    DWORD dwServiceType, // 服务类型(如内核驱动)
    DWORD dwStartType, // 启动类型(如手动、系统、引导)
    DWORD dwErrorControl, // 启动失败时的系统响应方式
    LPCSTR lpBinaryPathName, // 驱动/服务可执行文件路径
    LPCSTR lpLoadOrderGroup, // 所属分组,决定加载顺序
    LPDWORD lpdwTagId, // 输出的标签值(排序用)
    LPCSTR lpDependencies, // 所依赖的服务列表(以双 \0 结尾)
    LPCSTR lpServiceStartName, // 服务启动账户(驱动设为 NULL)
    LPCSTR lpPassword // 启动账户的密码(驱动设为 NULL)
    );
    • hSCManager :由 OpenSCManager 返回的句柄。
    • lpServiceName :服务的逻辑名称,对应注册表子项名,必须唯一。
    • lpDisplayName :显示名称,出现在服务管理器界面中。
    • dwDesiredAccess :服务句柄的访问权限,推荐 SERVICE_ALL_ACCESS
    • dwServiceType :服务类型。驱动应设为 SERVICE_KERNEL_DRIVER(值 0x1)。
    • dwStartType :启动方式:
      • SERVICE_BOOT_START(0)→ 引导加载
      • SERVICE_SYSTEM_START(1)→ 内核加载
      • SERVICE_DEMAND_START(3)→ 手动加载
    • dwErrorControl :启动失败时系统行为:
      • SERVICE_ERROR_IGNORE(0)→ 忽略错误
      • SERVICE_ERROR_NORMAL(1)→ 记录日志
      • SERVICE_ERROR_SEVERE(2)→ 启动安全模式
    • lpBinaryPathName :驱动路径(如 "C:\\Drivers\\MyDriver.sys")。
    • lpLoadOrderGroup :加载分组(如 Base,影响加载顺序,可为 NULL)。
    • lpdwTagId :输出值,指定分组内的排序标识(可为 NULL)。
    • lpDependencies :依赖服务名称,多个用 \0 分隔,以 \0\0 结尾。
    • lpServiceStartName :服务启动账户,驱动设为 NULL 表示 LocalSystem。
    • lpPassword :账户密码,驱动设为 NULL

    对于我们测试的驱动,CreateService 示例传参如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    CreateService(
    hSCManager, // 服务控制管理器句柄(来自 OpenSCManager)
    "MyDriver", // 服务名称(注册表键名,必须唯一)
    "My Kernel Driver", // 服务显示名称(可在服务管理器中显示)
    SERVICE_ALL_ACCESS, // 访问权限(允许所有操作:启动、停止、删除等)
    SERVICE_KERNEL_DRIVER, // 服务类型:内核模式驱动(对应 .sys 文件)
    SERVICE_DEMAND_START, // 启动类型:按需启动(需手动调用 StartService)
    SERVICE_ERROR_NORMAL, // 错误控制:加载失败时记录日志,继续启动系统
    "C:\\Drivers\\MyDriver.sys", // 驱动程序路径(必须为绝对路径)
    NULL, // 加载顺序组(不指定)
    NULL, // Tag ID 输出参数(排序标识,不需要时设 NULL)
    NULL, // 依赖服务列表(无依赖)
    NULL, // 启动账户(驱动使用 LocalSystem,设为 NULL)
    NULL // 启动账户密码(同上,设为 NULL)
    );
  • OpenService :打开已存在的服务(或驱动),获取用于后续控制(启动、停止、删除)的句柄。

    1
    2
    3
    4
    5
    SC_HANDLE OpenService(
    SC_HANDLE hSCManager, // 来自 OpenSCManager 的 SCM 句柄
    LPCSTR lpServiceName, // 要打开的服务名
    DWORD dwDesiredAccess // 所需权限(如 SERVICE_START | STOP)
    );
    • hSCManager :由 OpenSCManager 获取的 SCM 句柄。
    • lpServiceName :服务名称,必须精确匹配已注册服务名。
    • dwDesiredAccess :访问权限(如 SERVICE_START | SERVICE_STOP | DELETE)。
  • StartService :启动指定服务或驱动。对于驱动,会由 SCM 调用 NtLoadDriver,将 .sys 文件加载到内核。

    1
    2
    3
    4
    5
    BOOL StartService(
    SC_HANDLE hService, // 目标服务的句柄
    DWORD dwNumServiceArgs, // 参数个数(驱动设为 0)
    LPCSTR *lpServiceArgVectors // 参数数组(驱动设为 NULL)
    );
    • hService :来自 CreateServiceOpenService 的服务句柄。
    • dwNumServiceArgs :参数个数,驱动无参数则设为 0
    • lpServiceArgVectors :参数数组,驱动无参数则设为 NULL
  • ControlService :向运行中的服务发送控制命令。用于停止驱动(需驱动实现 Unload 函数)。

    1
    2
    3
    4
    5
    BOOL ControlService(
    SC_HANDLE hService, // 服务句柄
    DWORD dwControl, // 控制命令(如 SERVICE_CONTROL_STOP)
    LPSERVICE_STATUS lpServiceStatus // 输出当前服务状态
    );
    • hService :目标服务句柄。
    • dwControl :控制命令,停止服务时设为 SERVICE_CONTROL_STOP(0x1)。
    • lpServiceStatus :接收服务状态的结构体指针。
  • DeleteService :删除指定服务或驱动注册信息(从注册表清除),不会立即卸载已加载驱动。

    1
    2
    3
    BOOL DeleteService(
    SC_HANDLE hService // 目标服务句柄
    );
    • hService :目标服务句柄,需具有 DELETE 权限。
  • CloseServiceHandle :关闭服务或 SCM 句柄,释放资源。

    1
    2
    3
    BOOL CloseServiceHandle(
    SC_HANDLE hSCObject // 可为服务句柄或 SCM 句柄
    );
    • hSCObject :服务或控制管理器的句柄。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <windows.h>
#include <stdio.h>

int main() {
// 打开服务控制管理器
SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!hSCManager) {
printf("[-] OpenSCManager failed: %lu\n", GetLastError());
return 1;
}

// 创建一个内核驱动类型的服务
SC_HANDLE hService = CreateService(
hSCManager,
"MyDriver", // 驱动服务名(逻辑名)
"MyDriver", // 显示名称
SERVICE_ALL_ACCESS, // 权限
SERVICE_KERNEL_DRIVER, // 类型:内核驱动
SERVICE_DEMAND_START, // 启动方式:手动启动
SERVICE_ERROR_NORMAL,
"C:\\Drivers\\MyDriver.sys", // 驱动文件路径
NULL, NULL, NULL, NULL, NULL
);

if (!hService) {
if (GetLastError() == ERROR_SERVICE_EXISTS) {
printf("[*] 服务已存在,尝试打开...\n");
hService = OpenService(hSCManager, "MyDriver", SERVICE_ALL_ACCESS);
} else {
printf("[-] CreateService failed: %lu\n", GetLastError());
CloseServiceHandle(hSCManager);
return 1;
}
}

// 启动驱动
if (!StartService(hService, 0, NULL)) {
DWORD err = GetLastError();
if (err == ERROR_SERVICE_ALREADY_RUNNING) {
printf("[*] 驱动已在运行中。\n");
} else {
printf("[-] StartService failed: %lu\n", err);
}
} else {
printf("[+] 驱动已成功加载。\n");
}

// 可选:停止驱动并删除服务
SERVICE_STATUS status;
ControlService(hService, SERVICE_CONTROL_STOP, &status);
DeleteService(hService);

// 关闭句柄
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
return 0;
}

相关命令

SCM(Service Control Manager)方式加载驱动,除了直接用 WinAPI 外,Windows 提供了标准命令行工具。

sc 是 Windows 提供的服务控制命令行工具,全名为 Service Control。它支持创建、启动、停止、删除内核驱动服务。

  • 创建服务(注册驱动)

    1
    sc create MyDriver type= kernel binPath= "C:\Path\To\MyDriver.sys"
    • MyDriver:驱动服务名(服务项名称)
    • type= kernel:表示是内核驱动(不可省略)
    • binPath= ...:驱动文件路径(推荐绝对路径)

    注意

    type=, binPath= 后必须留空格,语法严格。

  • 启动驱动服务(实际加载)

    会触发 Service Control Manager 调用 NtLoadDriver 加载 .sys 文件,驱动的 DriverEntry 将被执行。

    1
    sc start MyDriver
  • 停止驱动服务(触发卸载)

    要求驱动实现了 DriverUnload 函数,否则会失败。

    1
    sc stop MyDriver
  • 删除服务(清除注册表项)

    1
    sc delete MyDriver

ZwLoadDriver(系统调用 + 注册表)

这是更“底层”的方式,绕过 SCM,直接调用内核的 ZwLoadDriver 系统服务加载驱动。常用于调试工具、PoC 框架、测试加载器或绕过方式。

原理流程

  1. 首先用户态程序提前创建注册表项(路径一般为):HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyDriver

    其中必须设置至少两个关键键值:

    • ImagePathREG_EXPAND_SZ):驱动文件路径,如:\??\C:\Path\to\MyDriver.sys

    • TypeDWORD):必须为 1,表示该服务为内核驱动。

  2. 构造 NT 路径并调用 ZwLoadDriver 加载驱动,该函数原型如下:

    1
    2
    3
    NTSYSAPI NTSTATUS NTAPI ZwLoadDriver(
    IN PUNICODE_STRING DriverServiceName
    );
    • 参数:注册表路径,格式为:
      \Registry\Machine\System\CurrentControlSet\Services\MyDriver
    • 返回值:NTSTATUS 错误码,常见:
      • STATUS_SUCCESS:成功
      • STATUS_OBJECT_NAME_NOT_FOUND:注册表路径错误
      • STATUS_IMAGE_ALREADY_LOADED:已加载

    提示

    ZwLoadDriverNtLoadDriver 实际上是同一个函数的两个符号。

    Windows 内核设计中,NtXxxZwXxx 实际上代表的是同一个系统服务接口(Syscall)函数,但它们存在 调用上下文(user mode vs kernel mode)下的行为差异API 访问路径差异,而 在用户态时,它们几乎是完全等价的入口符号

  3. 与驱动加载类似,卸载驱动的时候需要调用 ZwUnloadDriver,并传入同样格式的注册表路径。

    注意

    驱动必须自己实现 DriverUnload 回调,系统才会调用卸载。

    ZwUnloadDriver 函数原型如下:

    1
    2
    3
    NTSYSAPI NTSTATUS NTAPI ZwUnloadDriver(
    IN PUNICODE_STRING DriverServiceName
    );
    • 参数同上,指定已加载驱动的注册表路径;
    • 若驱动未实现 DriverUnload,调用将失败(一般是 STATUS_INVALID_DEVICE_REQUEST)。
  4. 清理注册表项。 ZwUnloadDriver 不会自动删除注册表项 ,即驱动从内核卸载后,注册表中的服务项仍然存在,必须你手动清理。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <Windows.h>
#include <winternl.h>
#include <iostream>
#include <string>

#pragma comment(lib, "ntdll.lib")

extern "C" {
NTSYSAPI NTSTATUS NTAPI ZwLoadDriver(IN PUNICODE_STRING DriverServiceName);
NTSYSAPI NTSTATUS NTAPI ZwUnloadDriver(IN PUNICODE_STRING DriverServiceName);
NTSYSAPI VOID NTAPI RtlInitUnicodeString(PUNICODE_STRING DestinationString, PCWSTR SourceString);
}

// 自动提取驱动服务名(不含扩展名)
std::wstring ExtractDriverName(const std::wstring& fullPath) {
size_t slash = fullPath.find_last_of(L"\\/");
size_t dot = fullPath.find_last_of(L'.');

if (slash == std::wstring::npos) slash = -1;
if (dot == std::wstring::npos || dot <= slash) dot = fullPath.size();

return fullPath.substr(slash + 1, dot - slash - 1);
}

// 构造 \Registry\Machine\System\CurrentControlSet\Services\DriverName
std::wstring BuildRegPath(const std::wstring& driverName) {
return L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\" + driverName;
}

// 注册表项创建
bool CreateDriverServiceRegistry(const std::wstring& driverName, const std::wstring& driverPath) {
std::wstring keyPath = L"SYSTEM\\CurrentControlSet\\Services\\" + driverName;
HKEY hKey;
if (RegCreateKeyW(HKEY_LOCAL_MACHINE, keyPath.c_str(), &hKey) != ERROR_SUCCESS) {
std::wcerr << L"[!] Failed to create registry key.\n";
return false;
}

DWORD type = 1;
std::wstring imagePath = L"\\??\\" + driverPath;
if (RegSetValueExW(hKey, L"Type", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&type), sizeof(type)) != ERROR_SUCCESS ||
RegSetValueExW(hKey, L"ImagePath", 0, REG_EXPAND_SZ,
reinterpret_cast<const BYTE*>(imagePath.c_str()),
static_cast<DWORD>((imagePath.size() + 1) * sizeof(wchar_t))) != ERROR_SUCCESS) {
std::wcerr << L"[!] Failed to set registry values.\n";
RegCloseKey(hKey);
return false;
}

RegCloseKey(hKey);
std::wcout << L"[+] Registry entry created at: " << keyPath << std::endl;
return true;
}

// 删除服务对应注册表项
bool DeleteDriverServiceRegistry(const std::wstring& driverName) {
std::wstring keyPath = L"SYSTEM\\CurrentControlSet\\Services\\" + driverName;
LONG result = RegDeleteKeyW(HKEY_LOCAL_MACHINE, keyPath.c_str());
if (result == ERROR_SUCCESS) {
std::wcout << L"[+] Registry key deleted: " << keyPath << std::endl;
return true;
} else {
std::wcerr << L"[!] Failed to delete registry key. Error: " << result << std::endl;
return false;
}
}

bool LoadDriver(const std::wstring& driverName) {
std::wstring regPath = BuildRegPath(driverName);
UNICODE_STRING ustr;
RtlInitUnicodeString(&ustr, regPath.c_str());

NTSTATUS status = ZwLoadDriver(&ustr);
std::wcout << L"[+] ZwLoadDriver status: 0x" << std::hex << status << std::endl;

return status == STATUS_SUCCESS || status == STATUS_IMAGE_ALREADY_LOADED;
}

bool UnloadDriver(const std::wstring& driverName) {
std::wstring regPath = BuildRegPath(driverName);
UNICODE_STRING ustr;
RtlInitUnicodeString(&ustr, regPath.c_str());

NTSTATUS status = ZwUnloadDriver(&ustr);
std::wcout << L"[+] ZwUnloadDriver status: 0x" << std::hex << status << std::endl;

return status == STATUS_SUCCESS;
}

int wmain(int argc, wchar_t* argv[]) {
if (argc != 2) {
std::wcerr << L"Usage: DriverLoader.exe <PathToDriver.sys>\n";
return 1;
}

std::wstring driverPath = argv[1];
std::wstring driverName = ExtractDriverName(driverPath);

std::wcout << L"[+] Driver path: " << driverPath << std::endl;
std::wcout << L"[+] Driver name: " << driverName << std::endl;

if (!CreateDriverServiceRegistry(driverName, driverPath)) {
return 1;
}

if (!LoadDriver(driverName)) {
std::wcerr << L"[!] Driver load failed.\n";
DeleteDriverServiceRegistry(driverName); // 清理失败也清注册表
return 1;
}

std::wcout << L"[+] Driver loaded successfully.\nPress Enter to unload...\n";
std::wcin.get();

if (!UnloadDriver(driverName)) {
std::wcerr << L"[!] Driver unload failed. Ensure DriverUnload is implemented.\n";
return 1;
}

DeleteDriverServiceRegistry(driverName); // 卸载成功后清理注册表

std::wcout << L"[+] Driver unloaded and registry cleaned up.\n";
return 0;
}

驱动开发基础

基本代码

通常一个最基本的 WDM 驱动代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <ntddk.h>  // 内核开发必要头文件

// 驱动卸载回调函数
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
DbgPrint("Driver unloaded.\n");
}

// 驱动入口函数(系统加载驱动时调用)
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint("Driver loaded.\n");

// 注册卸载回调函数
DriverObject->DriverUnload = DriverUnload;

return STATUS_SUCCESS;
}

其中 DriverEntry 是 Windows 驱动程序的主入口函数(Entry Point),等同于用户程序中的 main() 函数。

1
2
3
4
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject, // [输入] 驱动对象,由系统分配
IN PUNICODE_STRING RegistryPath // [输入] 驱动注册表路径
);

驱动加载时,系统会调用此函数来完成驱动的初始化过程。

  • PDRIVER_OBJECT DriverObject :内核为每个加载的驱动创建一个 DRIVER_OBJECT 结构,此参数就是它的指针。你需要通过它来注册 IRP 分发表、卸载函数、创建设备等。

    DRIVER_OBJECT 是 Windows 内核用来描述一个驱动程序核心信息的数据结构,驱动开发时我们通过它设置入口函数、分发表、卸载逻辑,是驱动生命周期管理的中心。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //0xA8 bytes
    struct _DRIVER_OBJECT {
    CSHORT Type; // 内核对象类型 (固定 DRIVER_OBJECT(0x04))
    CSHORT Size; // 结构体大小 (0xA8 字节)
    struct _DEVICE_OBJECT* DeviceObject; // 📌设备对象链表头
    ULONG Flags; // 驱动状态标志
    PVOID DriverStart; // 📌驱动映像起始地址
    ULONG DriverSize; // 📌驱动映像总大小
    PVOID DriverSection; // 📌加载模块节点,KLDR_DATA_TABLE_ENTRY结构 (挂载到 PsLoadedModuleList)
    struct _DRIVER_EXTENSION* DriverExtension; // 扩展区域 (含 AddDevice)
    UNICODE_STRING DriverName; // 📌驱动名 (\Driver\XXX)
    UNICODE_STRING* HardwareDatabase; // 硬件数据库路径 (历史用途)
    PFAST_IO_DISPATCH FastIoDispatch; // 快速 I/O 分发表 (文件系统驱动用)
    PDRIVER_INITIALIZE DriverInit; // 📌初始化入口 (内部使用)
    PDRIVER_STARTIO DriverStartIo; // 串行化 I/O 支持 (极少用)
    PDRIVER_UNLOAD DriverUnload; // 📌驱动卸载函数指针
    PDRIVER_DISPATCH MajorFunction[28]; // 📌IRP 主功能分发表
    };
  • PUNICODE_STRING RegistryPath :指向一个 Unicode 字符串,表示驱动在注册表中的键路径,如 \Registry\Machine\System\CurrentControlSet\Services\MyDriver

DriverEntry 中,我们主要做一些初始化的操作,比如创建设备对象,初始化全局变量,注册 IRP 分发表等等。

注意

  • DriverEntry 中要的是设置驱动卸载函数 DriverObject->DriverUnload如果这一步没有做则驱动无法卸载

  • 只有全部成功后才返回 STATUS_SUCCESS,否则系统自动撤销加载。因此我们不需要担心在 DriverEntry 中由于出错提前返回没有设置设置驱动卸载函数而导致驱动无法卸载,因为驱动根本就没有加载成功。

功能技巧

返回值

几乎所有内核 API 和驱动入口函数都使用 NTSTATUS 类型作为返回值。

1
typedef LONG NTSTATUS;

同时 WDK 中提供了几个宏用根据返回值判断 api 调用结果。

宏函数 作用
NT_SUCCESS(Status) 判断是否成功(高位为 0)
NT_ERROR(Status) 判断是否是错误(高位为 1)
NT_WARNING(Status) 判断是否是警告

因此一个标准的 API 调用的返回值检测应该是下面这种写法:

1
2
3
4
5
6
7
8
9
10
NTSTATUS MyFunction()
{
NTSTATUS status = DoSomething();
if (!NT_SUCCESS(status)) {
return status; // 向上传播错误
}

// 继续处理...
return STATUS_SUCCESS;
}

日志输出

内核调试输出需要使用专门的 api,并且输出内容走的是 DbgPrint Buffer,通常只有连接 WinDbg / KD 等调试器时可实时显示;无内核调试连接时,有些版本仍可借助 DebugView(SysInternals 工具)捕获部分内核日志。

  • DbgPrint:最基本的内核调试输出函数,用法与 printf 类似,默认输出优先级较低,相当于 DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, ...)

    1
    2
    3
    4
    ULONG DbgPrint(
    PCSTR Format, // 格式化字符串,类似 printf()
    ... // 可变参数
    );
  • DbgPrintExDbgPrint 的增强版,允许指定组件类别和日志等级,便于在复杂项目中分类控制输出。

    1
    2
    3
    4
    5
    6
    ULONG DbgPrintEx(
    ULONG ComponentId, // 模块分类 (WDF 框架建议填写 DPFLTR_DRIVER_FRAMEWORK_ID)
    ULONG Level, // 日志级别 (DPFLTR_XXX_LEVEL)
    PCSTR Format, // 格式化字符串
    ... // 可变参数
    );

    这里常见 ComponentId 值有:

    • DPFLTR_DEFAULT_ID:默认组件
    • DPFLTR_IO_ID:I/O 子系统
    • DPFLTR_PNP_ID:PnP 子系统
    • DPFLTR_DRIVER_FRAMEWORK_ID:WDF 框架日志

    常见的 Level 值有:

    • DPFLTR_INFO_LEVEL:普通信息
    • DPFLTR_WARNING_LEVEL:警告
    • DPFLTR_ERROR_LEVEL:错误
    • DPFLTR_MASK :所有级别
  • KdPrintEx:实际上是对 DbgPrintEx 的宏封装,编写格式需使用两层括号,优势在于统一兼容内核版本控制,WDK 推荐使用,另外可以在 Release 版本自动去除日志输出。

    1
    KdPrintEx((DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "MyDriver running.\n"));

断点

DbgBreakPoint()Windows 内核提供的标准调试断点函数,专门用于驱动或内核模块中设置断点。它的作用是:当内核代码执行到 DbgBreakPoint() 处时,如果系统当前处于调试状态(例如 WinDbg 已附加),将触发调试器断点中断。

DbgBreakPoint() 是由内核导出的函数,声明如下:

1
VOID NTAPI DbgBreakPoint(VOID);

kdBreakPoint

返回地址

数据结构

字符串

字符串类型

在 Windows 开发中有多种字符串类型,但是在内核驱动开发中为了安全起见,有额外引入了 UNICODE_STRING 这一新的字符串类型。

类型 说明 使用场景
UNICODE_STRING UTF-16 编码,结构体包装 内核中最常见的字符串类型,用于路径、设备名、对象名等
WCHAR[] C 风格宽字符串(null结尾) 常用于初始化 UNICODE_STRING
CHAR[] C 风格窄字符串(null结尾) 常用于初始化 ANSI_STRING
PWSTR / PCHAR 指向上述数组的指针 宽/窄字符数组地址,传参常用

UNICODE_STRING

UNICODE_STRING 字符串类型本质上就是将宽字符串利用一个结构体进行了一次封装。

1
2
3
4
5
typedef struct _UNICODE_STRING {
USHORT Length; // 单位:字节,不包括 NULL
USHORT MaximumLength; // 最大长度(字节)
PWSTR Buffer; // 指向宽字符串(WCHAR[])
} UNICODE_STRING;

注意

  • Length 单位是字节,不是字符数;

  • Buffer 不强制 null 结尾;

UNICODE_STRING 可以通过 RtlInitUnicodeString 函数和 RTL_CONSTANT_STRING 宏两种方式进行初始化。

注意

这两种初始化方法都不会拷贝字符串内容,而只设置结构体,指针仍指向原始常量字符串。

  • RtlInitUnicodeString 函数原型如下:

    1
    2
    3
    4
    VOID RtlInitUnicodeString(
    PUNICODE_STRING DestinationString,
    PCWSTR SourceString
    );
    • 该函数会设置结构体的 Length, MaximumLength, Buffer 字段。
    • SourceString 必须是 null 结尾的常量或合法缓冲区

    示例代码:

    1
    2
    UNICODE_STRING uStr;
    RtlInitUnicodeString(&uStr, L"\\Device\\MyDriver");
  • RTL_CONSTANT_STRING

    RTL_CONSTANT_STRING 用于编译期静态构造一个 UNICODE_STRING,该宏的定义如下:

    1
    #define RTL_CONSTANT_STRING(s) { sizeof(s) - sizeof((s)[0]), sizeof(s), s }

    示例代码:

    1
    UNICODE_STRING path = RTL_CONSTANT_STRING(L"\\Device\\MyDriver");

    注意

    RTL_CONSTANT_STRING 不能用于变量字符串,只能用于编译期可见的字符串常量(即 L"..." 形式的字面量)。如果你错误地用它去初始化一个运行时变量,会导致:

    • 结构体字段内容不正确(长度计算可能错误)

    • 潜在的内存越界访问

    • 编译器不报错,但运行时行为未定义

字符串转换

在实际开发中,经常会遇到将用户传入的 ANSI 字符串转换为内核 API 可用格式这种需求,这就需要我们将 char * 字符串转换为 UNICODE_STRING 类型,具体步骤如下:

  1. 首先我们需要利用 RtlInitAnsiString 函数将 char * 字符串转换为 ANSI_STRING 类型:

    1
    2
    3
    4
    char* ansi = "MyDevice\\Test";
    ANSI_STRING ansiStr;

    RtlInitAnsiString(&ansiStr, ansi);

    提示

    在有些教程中这一步会使用 RtlInitString 函数将将 char * 字符串转换为 STRING 类型,实际上这里的 STRING 类型实际上就是 ANSI_STRING 的旧别名,结构相同。

  2. 使用 RtlAnsiStringToUnicodeString 函数将 ANSI_STRING 字符串转换为 UNICODE_STRING 字符串。这里 RtlAnsiStringToUnicodeString 函数原型如下:

    1
    2
    3
    4
    5
    NTSTATUS RtlAnsiStringToUnicodeString(
    PUNICODE_STRING DestinationString,
    PCANSI_STRING SourceString,
    BOOLEAN AllocateDestinationString
    );
    • DestinationString:输出的 Unicode 结构体
    • SourceString:输入的 ANSI 结构体
    • AllocateDestinationString:如果为 TRUE,系统会分配 DestinationString->Buffer;否则你必须事先分配好 ANSI_STRING.Buffer 并设置 MaximumLength,否则可能崩溃或数据丢失。

    这里为了方便封装,我们采用 AllocateDestinationStringTRUE 的写法。对于这样产生的 UNICODE_STRING 字符串,使用完毕时候我们需要调用 RtlFreeUnicodeString 函数将其释放,这里释放的是 ANSI_STRING.Buffer

在实际开发中,我们一般习惯将上述步骤封装成一个函数:

1
2
3
4
5
NTSTATUS ConvertAnsiToUnicode(_In_ const char* input, _Out_ PUNICODE_STRING uStr) {
ANSI_STRING aStr;
RtlInitAnsiString(&aStr, input);
return RtlAnsiStringToUnicodeString(uStr, &aStr, TRUE);
}

常用函数

  • RtlStringCbPrintfA/W:内核安全字符串格式化函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    NTSTATUS RtlStringCbPrintfA(
    _Out_ CHAR *pszDest,
    _In_ size_t cbDest,
    _In_ const CHAR *pszFormat,
    ...
    );

    NTSTATUS RtlStringCbPrintfW(
    _Out_ WCHAR *pszDest,
    _In_ size_t cbDest,
    _In_ const WCHAR *pszFormat,
    ...
    );
    • pszDest:输出目标缓冲区
    • cbDest:缓冲区总字节数(注意单位:字节,不是字符数)
    • pszFormat:格式化字符串,类似 printf 格式
  • RtlCompareUnicodeStringUNICODE_STRING 安全比较

    1
    2
    3
    4
    5
    LONG RtlCompareUnicodeString(
    _In_ const UNICODE_STRING *String1,
    _In_ const UNICODE_STRING *String2,
    _In_ BOOLEAN CaseInSensitive
    );
    • CaseInSensitive:是否大小写无关(TRUE 表示忽略大小写)
    • 返回值:返回逻辑类似 C 标准库 strcmp
      • 0:相等
      • <0String1 小于 String2
      • >0String1 大于 String2

双向链表(LIST_ENTRY)

在 Windows 中有一个专门描述链表节点的结构 LIST_ENTRY,该结构定义如下:

1
2
3
4
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; // Forward Link (前向指针)
struct _LIST_ENTRY *Blink; // Backward Link (后向指针)
} LIST_ENTRY, *PLIST_ENTRY;

在 Windows 的设计思想中,双向链表有两种成员组成:

  • ListHead:即链表头,通常类型为 LIST_ENTRY 结构体,有时会作为一个成员放到另一个结构体中,但是作为“链表头”本身仍是 LIST_ENTRY 类型。链表头自己不存储任何数据,只是链表控制块,但会被串到双向链表中

    1
    2
    LIST_ENTRY MyList;
    InitializeListHead(&MyList);

    注意

    Windows 的 LIST_ENTRY 初始状态必须是:

    1
    2
    ListHead->Flink = ListHead;
    ListHead->Blink = ListHead;

    任何链表必须先初始化,否则后续操作容易蓝屏。Windows有一个专门用于初始化 ListEntry 的函数 InitializeListHead

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * @brief 初始化链表头部,设置链表为空。
    *
    * 该函数将链表头部的 Flink 和 Blink 指针都指向链表头本身,
    * 以表示该链表为空。空链表的头部不指向任何有效节点,确保
    * 链表操作的一致性。
    *
    * @param ListHead 指向链表头部的指针。
    *
    * @return 无返回值。
    */
    FORCEINLINE
    VOID
    InitializeListHead(
    _Out_ PLIST_ENTRY ListHead ///< 指向链表头部的指针
    )
    {
    // 将链表头部的 Flink 和 Blink 都指向链表头本身,表示该链表为空
    ListHead->Flink = ListHead->Blink = ListHead;

    return; // 函数执行完成,不需要返回值
    }
  • Entry:用来将节点链入双向链表中的一个结构体成员,类型同样为 LIST_ENTRY。例如下面这个结构体中的 List 就是一个 Entry,我们可以通过从链表头遍历双向链表找到所有链表中的 MY_NODE 结构体。

    1
    2
    3
    4
    typedef struct _MY_NODE {
    ULONG ID;
    LIST_ENTRY List;
    } MY_NODE;

    注意

    我们通过双向链表遍历找到的结构体地址实际上是 List 成员的地址,要想获取到结构体地址还需要借助 CONTAINING_RECORD 宏。

    1
    2
    #define CONTAINING_RECORD(address, type, field) \
    ((type *)((PCHAR)(address) - (ULONG_PTR)(&((type *)0)->field)))

    例如:

    1
    2
    PLIST_ENTRY pEntry = RemoveHeadList(&MyList);
    PMY_NODE pNode = CONTAINING_RECORD(pEntry, MY_NODE, List);

针对双向链表,Windows 提供了众多 API 用于操作双向链表中的成员:

  • InsertHeadList():将 Entry 插入到链表头部(头节点后,ListHead->Flink 方向)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    /**
    * @brief 将一个节点插入到链表头部。
    *
    * 该函数将指定的节点插入到链表的头部,将节点放置到链表头部之后,
    * 使其成为链表的第一个元素。插入时,更新相关节点的 Flink 和 Blink 指针。
    *
    * @param ListHead 指向链表头部的指针。
    * @param Entry 要插入的节点指针。该节点将成为链表头部的下一个节点。
    *
    * @return 无返回值。
    *
    * @note 此函数会检查链表的完整性,并且在调试模式下进行链表一致性检查。
    */
    FORCEINLINE
    VOID
    InsertHeadList(
    _Inout_ PLIST_ENTRY ListHead, ///< 链表头部指针,输入输出参数
    _Out_ __drv_aliasesMem PLIST_ENTRY Entry ///< 要插入的节点,输出参数
    )
    {
    PLIST_ENTRY NextEntry;

    #if DBG
    // 调试模式下,检查链表的完整性
    RtlpCheckListEntry(ListHead);
    #endif

    // 获取链表头部后继节点
    NextEntry = ListHead->Flink;

    // 检查链表一致性,确保后继节点的 Blink 指针指向链表头部
    if (NextEntry->Blink != ListHead) {
    FatalListEntryError((PVOID)ListHead,
    (PVOID)NextEntry,
    (PVOID)NextEntry->Blink);
    }

    // 将新节点插入到链表头部
    Entry->Flink = NextEntry; // 新节点的 Flink 指向原头部的下一个节点
    Entry->Blink = ListHead; // 新节点的 Blink 指向链表头部
    NextEntry->Blink = Entry; // 原头部的下一个节点的 Blink 指向新节点
    ListHead->Flink = Entry; // 链表头部的 Flink 指向新节点

    return; // 函数执行完成,无需返回值
    }
  • InsertTailList():插入到链表尾部(头节点前,ListHead->Blink 方向)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    /**
    * @brief 将一个节点插入到链表尾部。
    *
    * 该函数将指定的节点插入到链表的尾部。插入时,会更新链表的前驱和后继节点的 Flink 和 Blink 指针,
    * 使得新节点成为链表的最后一个节点。
    *
    * @param ListHead 指向链表头部的指针。该参数在函数中被修改,表示链表的头部。
    * @param Entry 要插入的节点指针。该节点将被插入到链表的尾部。
    *
    * @return 无返回值。
    *
    * @note 链表一致性检查:在插入节点之前,函数会检查链表的完整性,确保链表结构没有损坏。
    */
    FORCEINLINE
    VOID
    InsertTailList(
    _Inout_ PLIST_ENTRY ListHead, ///< 链表头部指针,输入输出参数
    _Out_ __drv_aliasesMem PLIST_ENTRY Entry ///< 要插入的节点,输出参数
    )
    {
    PLIST_ENTRY PrevEntry; // 当前链表尾部的前驱节点

    #if DBG
    // 调试模式下,检查链表一致性
    RtlpCheckListEntry(ListHead);
    #endif

    // 获取链表尾部的前驱节点
    PrevEntry = ListHead->Blink;

    // 检查链表一致性,确保链表尾部的前驱节点的 Flink 正确指向链表头部
    if (PrevEntry->Flink != ListHead) {
    // 如果链表结构不一致,调用 FatalListEntryError 处理错误
    FatalListEntryError((PVOID)PrevEntry,
    (PVOID)ListHead,
    (PVOID)PrevEntry->Flink);
    }

    // 将新节点插入到链表尾部
    Entry->Flink = ListHead; // 新节点的 Flink 指向链表头部
    Entry->Blink = PrevEntry; // 新节点的 Blink 指向链表尾部的前驱节点
    PrevEntry->Flink = Entry; // 原尾部的前驱节点的 Flink 指向新节点
    ListHead->Blink = Entry; // 链表头部的 Blink 指向新节点

    return; // 函数执行完成,不需要返回值
    }

  • RemoveEntryList():从链表中删除指定节点。

    注意

    从链表删除的节点的 FlinkBlink 没有进行任何修改,因此如果使用这个函数实现断链隐藏操作,需要额外调用 InitializeListHead 进行初始化,否则容易导致后续通不过链表相关检查导致蓝屏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    /**
    * @brief 从链表中移除一个节点。
    *
    * 该函数将指定的节点从链表中移除,更新链表中前驱和后继节点的指针。
    * 在移除节点之前,函数会检查链表的一致性,确保链表结构正确。
    *
    * @param Entry 要从链表中移除的节点指针。
    *
    * @return 如果链表只剩下一个节点(即前驱和后继节点相同),返回 TRUE;否则返回 FALSE。
    *
    * @note 如果链表出现不一致,函数会调用 `FatalListEntryError` 来处理错误。
    */
    FORCEINLINE
    BOOLEAN
    RemoveEntryList(
    _In_ PLIST_ENTRY Entry // 要从链表中移除的节点
    )
    {
    PLIST_ENTRY PrevEntry; // 当前节点的前驱节点
    PLIST_ENTRY NextEntry; // 当前节点的后继节点

    // 获取当前节点的前驱和后继节点
    NextEntry = Entry->Flink;
    PrevEntry = Entry->Blink;

    // 检查链表一致性:前驱节点的 Flink 和后继节点的 Blink 是否正确指向当前节点
    if ((NextEntry->Blink != Entry) || (PrevEntry->Flink != Entry)) {
    // 如果链表存在不一致,调用错误处理函数
    FatalListEntryError((PVOID)PrevEntry,
    (PVOID)Entry,
    (PVOID)NextEntry);
    }

    // 从链表中移除当前节点
    PrevEntry->Flink = NextEntry; // 将前驱节点的 Flink 指向后继节点
    NextEntry->Blink = PrevEntry; // 将后继节点的 Blink 指向前驱节点

    // 如果前驱节点和后继节点指向的是同一个节点,说明链表只剩下当前一个节点,返回 TRUE
    return (BOOLEAN)(PrevEntry == NextEntry);
    }
  • RemoveHeadList()/RemoveTailList():移除链表头部第一个(最后一个)元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    /**
    * @brief 移除链表头部的节点。
    *
    * 该函数将移除链表的第一个节点(头部节点的下一个节点),更新链表的头部指针。
    * 移除节点后,链表的第一个节点将变为原头部的下一个节点。
    *
    * @param ListHead 指向链表头部的指针。
    *
    * @return 返回被移除的节点。
    *
    * @note 链表一致性检查:函数在移除节点之前会检查链表的一致性,确保链表结构没有损坏。
    */
    FORCEINLINE
    PLIST_ENTRY
    RemoveHeadList(
    _Inout_ PLIST_ENTRY ListHead ///< 链表头部指针,输入输出参数
    )
    {
    PLIST_ENTRY Entry; // 要移除的节点
    PLIST_ENTRY NextEntry; // 当前节点的下一个节点

    Entry = ListHead->Flink; // 获取链表头部的下一个节点

    #if DBG
    // 调试模式下,检查链表一致性
    RtlpCheckListEntry(ListHead);
    #endif

    NextEntry = Entry->Flink; // 获取下一个节点
    // 检查链表一致性:当前节点的 Blink 和下一个节点的 Blink 是否正确
    if ((Entry->Blink != ListHead) || (NextEntry->Blink != Entry)) {
    // 如果链表有错误,调用 FatalListEntryError 处理
    FatalListEntryError((PVOID)ListHead, (PVOID)Entry, (PVOID)NextEntry);
    }

    // 更新链表头部指针,移除当前节点
    ListHead->Flink = NextEntry;
    NextEntry->Blink = ListHead;

    return Entry; // 返回被移除的节点
    }

    /**
    * @brief 移除链表尾部的节点。
    *
    * 该函数将移除链表的最后一个节点(尾部节点的前一个节点),更新链表的尾部指针。
    * 移除节点后,链表的最后一个节点将变为原尾部的前一个节点。
    *
    * @param ListHead 指向链表头部的指针。
    *
    * @return 返回被移除的节点。
    *
    * @note 链表一致性检查:函数在移除节点之前会检查链表的一致性,确保链表结构没有损坏。
    */
    FORCEINLINE
    PLIST_ENTRY
    RemoveTailList(
    _Inout_ PLIST_ENTRY ListHead ///< 链表头部指针,输入输出参数
    )
    {
    PLIST_ENTRY Entry; // 要移除的节点
    PLIST_ENTRY PrevEntry; // 当前节点的前一个节点

    Entry = ListHead->Blink; // 获取链表尾部的前一个节点

    #if DBG
    // 调试模式下,检查链表一致性
    RtlpCheckListEntry(ListHead);
    #endif

    PrevEntry = Entry->Blink; // 获取前一个节点
    // 检查链表一致性:当前节点的 Flink 和前一个节点的 Flink 是否正确
    if ((Entry->Flink != ListHead) || (PrevEntry->Flink != Entry)) {
    // 如果链表有错误,调用 FatalListEntryError 处理
    FatalListEntryError((PVOID)PrevEntry, (PVOID)Entry, (PVOID)ListHead);
    }

    // 更新链表尾部指针,移除当前节点
    ListHead->Blink = PrevEntry;
    PrevEntry->Flink = ListHead;

    return Entry; // 返回被移除的节点
    }
  • IsListEmpty():检查链表是否为空。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * @brief 检查链表是否为空。
    *
    * 该函数检查给定的链表是否为空。链表为空时,链表头部的 Flink 指针会指向链表头部本身。
    *
    * @param ListHead 指向链表头部的指针。
    *
    * @return 如果链表为空,返回 TRUE;否则返回 FALSE。
    *
    * @note 链表为空时,头部的 Flink 指针会指向链表头部本身,而不是其他节点。
    */
    _Must_inspect_result_
    BOOLEAN
    CFORCEINLINE
    IsListEmpty(
    _In_ const LIST_ENTRY * ListHead ///< 指向链表头部的指针
    )
    {
    // 如果链表头部的 Flink 指向链表头部本身,说明链表为空
    return (BOOLEAN)(ListHead->Flink == ListHead);
    }

通用平衡树框架(RTL_GENERIC_TABLE)

RTL_GENERIC_TABLE 提供通用平衡树框架(Windows 早期是 Splay;Win7+ 全部切换为 AVL),支持 按键快速查找/插入/删除

RTL_GENERIC_TABLE 的初始化函数 RtlInitializeGenericTable 定义如下:

1
2
3
4
5
6
7
NTSTATUS RtlInitializeGenericTable(
PRTL_GENERIC_TABLE Table, // [OUT] 指向 RTL_GENERIC_TABLE 结构体,初始化后返回表控制块
PRTL_GENERIC_COMPARE_ROUTINE CompareRoutine, // [IN] 比较函数指针:用于比较 Key 大小(必填)
PRTL_GENERIC_ALLOCATE_ROUTINE AllocateRoutine, // [IN] 分配函数指针:用于为新节点分配内存(必填)
PRTL_GENERIC_FREE_ROUTINE FreeRoutine, // [IN] 释放函数指针:用于释放节点内存(必填)
PVOID TableContext // [IN] 上下文参数(可选):用户自定义传入,在回调函数中使用
);
  • Table:用于保存初始化后的通用表结构体。你需要先定义好 RTL_GENERIC_TABLE 结构体,把地址传进来,内核将填充其中的指针和配置字段。

  • CompareRoutine比较函数指针,内核在插入/查找/删除时调用此函数以判定 Key 大小顺序。你需要实现此函数来定义排序规则,返回值为 GenericLessThan / GenericGreaterThan / GenericEqual。回调函数声明如下:

    1
    2
    3
    4
    5
    typedef RTL_GENERIC_COMPARE_RESULTS (*PRTL_GENERIC_COMPARE_ROUTINE)(
    PRTL_GENERIC_TABLE Table,
    PVOID FirstStruct,
    PVOID SecondStruct
    );
  • AllocateRoutine分配函数指针,在插入新节点时,内核通过此函数为节点分配内存。通常传入封装好的 ExAllocatePoolWithTag()

    1
    2
    3
    4
    typedef PVOID (*PRTL_GENERIC_ALLOCATE_ROUTINE)(
    PRTL_GENERIC_TABLE Table,
    CLONG ByteSize
    );
  • FreeRoutine释放函数指针,当删除节点时内核通过此函数释放节点内存。通常封装调用 ExFreePool()

    1
    2
    3
    4
    typedef VOID (*PRTL_GENERIC_FREE_ROUTINE)(
    PRTL_GENERIC_TABLE Table,
    PVOID Buffer
    );
  • TableContext上下文指针,供你在比较函数 / 分配函数内部做额外业务逻辑用(可选,可以传 NULL)。例如存放配置信息、同步锁、日志上下文等。

另外 RTL_GENERIC_TABLE 提供了一系列的成员操作函数:

API 功能 复杂度
RtlInsertElementGenericTable 若无重复,则插入新节点并返回指针 O(log N)
RtlLookupElementGenericTable 按键查找 O(log N)
RtlDeleteElementGenericTable 删除节点 O(log N)
RtlEnumerateGenericTable 按字典序迭代 O(1) 步进
RtlEnumerateGenericTableWithoutSplaying 枚举但不再平衡 Win7+ AVL 下同样平衡;保留兼容
RtlGetElementGenericTable 按序号 (0..N-1) 获取 O(N)
RtlNumberGenericTableElements 统计元素个数 O(1)
RtlIsGenericTableEmpty 是否为空 O(1)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <ntifs.h>

// =======================================
// 定义数据节点结构体
// 注意:平衡树节点头必须在首字段 (必须包含 RTL_BALANCED_LINKS)
// =======================================
typedef struct _MY_DATA_ENTRY
{
RTL_BALANCED_LINKS Links; // AVL树内部链接信息
ULONG Id; // 主键字段,按此排序
ULONG X; // 业务字段1
ULONG Y; // 业务字段2
} MY_DATA_ENTRY, *PMY_DATA_ENTRY;

// 全局通用表对象
RTL_GENERIC_TABLE gTable;

// =======================================
// 比较函数:用于排序/查找/去重
// 返回值决定二叉树排序方向
// =======================================
RTL_GENERIC_COMPARE_RESULTS NTAPI MyCompare(
_In_ struct _RTL_GENERIC_TABLE *Table,
_In_ PVOID FirstStruct,
_In_ PVOID SecondStruct
)
{
PMY_DATA_ENTRY a = (PMY_DATA_ENTRY)FirstStruct;
PMY_DATA_ENTRY b = (PMY_DATA_ENTRY)SecondStruct;

if (a->Id < b->Id) return GenericLessThan;
if (a->Id > b->Id) return GenericGreaterThan;
return GenericEqual;
}

// =======================================
// 分配函数:在插入新节点时被调用
// 注意:必须使用池分配内存
// =======================================
PVOID NTAPI MyAllocate(
_In_ struct _RTL_GENERIC_TABLE *Table,
_In_ CLONG ByteSize
)
{
return ExAllocatePoolWithTag(NonPagedPoolNx, ByteSize, 'TgDT');
}

// =======================================
// 释放函数:在删除节点时被调用
// =======================================
VOID NTAPI MyFree(
_In_ struct _RTL_GENERIC_TABLE *Table,
_In_ __drv_freesMem(Mem) _Post_invalid_ PVOID Buffer
)
{
ExFreePool(Buffer);
}

// =======================================
// 驱动卸载函数:清理通用表资源
// =======================================
VOID DriverUnload(_In_ PDRIVER_OBJECT DriverObject)
{
PVOID RestartKey = NULL;
PMY_DATA_ENTRY pEntry;

// 枚举删除表内所有元素,避免内存泄漏
while ((pEntry = (PMY_DATA_ENTRY)RtlEnumerateGenericTable(&gTable, &RestartKey)) != NULL)
{
BOOLEAN deleted = RtlDeleteElementGenericTable(&gTable, pEntry);
UNREFERENCED_PARAMETER(deleted);
}

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Driver unloaded.\n");
}

// =======================================
// 驱动入口函数
// =======================================
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);

// 初始化通用表
RtlInitializeGenericTable(&gTable, MyCompare, MyAllocate, MyFree, NULL);

// 准备测试数据
MY_DATA_ENTRY data[] = {
{ 0, 1, 10, 20 },
{ 0, 2, 30, 40 },
{ 0, 3, 50, 60 },
{ 0, 4, 70, 80 },
};

// 插入测试数据
for (int i = 0; i < ARRAYSIZE(data); i++)
{
BOOLEAN newElement;
RtlInsertElementGenericTable(&gTable, &data[i], sizeof(MY_DATA_ENTRY), &newElement);
}

// 测试查找
MY_DATA_ENTRY search = { 0 };
search.Id = 3;

PMY_DATA_ENTRY found = (PMY_DATA_ENTRY)RtlLookupElementGenericTable(&gTable, &search);
if (found)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Found: Id=%lu X=%lu Y=%lu\n", found->Id, found->X, found->Y);
}

// 遍历所有元素
PVOID RestartKey = NULL;
PMY_DATA_ENTRY pEntry;

while ((pEntry = (PMY_DATA_ENTRY)RtlEnumerateGenericTable(&gTable, &RestartKey)) != NULL)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "Enumerate: Id=%lu X=%lu Y=%lu\n", pEntry->Id, pEntry->X, pEntry->Y);
}

DriverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

驱动常用代码

创建线程

在内核中通常使用 PsCreateSystemThread 函数创建内核线程,该函数原型如下:

1
2
3
4
5
6
7
8
9
NTSTATUS PsCreateSystemThread(
OUT PHANDLE ThreadHandle, // [输出] 线程句柄,成功返回后需 ZwClose 关闭
IN ACCESS_MASK DesiredAccess, // [输入] 访问权限,通常填写 THREAD_ALL_ACCESS
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, // [输入] 对象属性,内核线程一般填 NULL
IN HANDLE ProcessHandle OPTIONAL, // [输入] 进程句柄,内核线程填 NULL (表示系统进程)
OUT PCLIENT_ID ClientId OPTIONAL, // [输出] 线程 Client ID(包含 PID/TID),一般填 NULL
IN PKSTART_ROUTINE StartRoutine, // [输入] 线程入口函数 (函数指针)
IN PVOID StartContext // [输入] 入口函数参数 (传入自定义上下文)
);
  • ThreadHandle:函数成功返回时,输出新创建线程的句柄。

    • 驱动一般创建完线程立即关闭句柄,因为不需要持续持有。
    • 必须在成功创建后调用 ZwClose() 关闭句柄,否则可能会泄漏句柄表项。
    • 即使关闭句柄,线程本身仍在运行,句柄只是内核对象的一个引用。
  • DesiredAccess:指定希望线程句柄具有的访问权限。因内核线程通常不操作自身句柄,直接用 THREAD_ALL_ACCESS 或 0 都可。

  • ObjectAttributes:定义线程对象的名称、属性等。仅极少情况才会用,例如为线程创建命名对象供调试器附加。绝大部分内核驱动开发直接传 NULL

  • ProcessHandle:指定新线程在哪个进程空间中运行。

    • NULL 表示创建的是内核线程(属于系统进程 System,PID=4)。
    • 若传入用户进程句柄,则创建用户进程中的远程线程。
  • ClientId:可选输出参数,返回新创建线程的唯一标识(进程 ID + 线程 ID)。如果不需要则传 NULL 即可。

  • StartRoutine:线程的入口函数指针(回调函数),函数原型为:

    1
    VOID StartRoutine(PVOID StartContext);
    • 必须确保该函数永远不返回,最后用 PsTerminateSystemThread() 主动结束线程。
    • StartContext 参数由第七个参数传入,方便传递上下文数据。
  • StartContext:传入给入口函数的自定义参数,通常为结构体指针或简单数据,用于向新线程传递启动上下文信息(如配置、句柄、共享内存等)。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <ntddk.h>

// 线程控制结构体
typedef struct _MY_THREAD_CONTEXT
{
HANDLE ThreadHandle;
PETHREAD ThreadObject;
volatile BOOLEAN ShouldStop; // 控制退出标志
ULONG Parameter; // 模拟业务参数
} MY_THREAD_CONTEXT, *PMY_THREAD_CONTEXT;

// 线程上下文全局变量 (可替换为动态分配)
MY_THREAD_CONTEXT g_ThreadContext = { 0 };

// 线程入口函数
VOID MyKernelThread(IN PVOID Context)
{
PMY_THREAD_CONTEXT ThreadCtx = (PMY_THREAD_CONTEXT)Context;
LARGE_INTEGER Interval;
Interval.QuadPart = -10 * 1000 * 1000LL; // 1秒

DbgPrint("内核线程启动,参数: %lu\n", ThreadCtx->Parameter);

while (!ThreadCtx->ShouldStop)
{
DbgPrint("线程循环执行中...\n");
KeDelayExecutionThread(KernelMode, FALSE, &Interval);
}

DbgPrint("内核线程检测到退出请求\n");
PsTerminateSystemThread(STATUS_SUCCESS);
}

// 创建内核线程
NTSTATUS StartKernelThread()
{
NTSTATUS status;

g_ThreadContext.ShouldStop = FALSE;
g_ThreadContext.Parameter = 1234; // 模拟业务数据

status = PsCreateSystemThread(
&g_ThreadContext.ThreadHandle,
THREAD_ALL_ACCESS,
NULL,
NULL,
NULL,
MyKernelThread,
&g_ThreadContext
);

if (!NT_SUCCESS(status))
{
DbgPrint("创建内核线程失败: 0x%08X\n", status);
return status;
}

// 引用线程对象,方便后续等待退出
status = ObReferenceObjectByHandle(
g_ThreadContext.ThreadHandle,
THREAD_ALL_ACCESS,
*PsThreadType,
KernelMode,
(PVOID*)&g_ThreadContext.ThreadObject,
NULL
);

ZwClose(g_ThreadContext.ThreadHandle); // 关闭句柄本身

return status;
}

// 关闭线程
VOID StopKernelThread()
{
if (g_ThreadContext.ThreadObject)
{
g_ThreadContext.ShouldStop = TRUE;

// 等待线程退出
KeWaitForSingleObject(
g_ThreadContext.ThreadObject,
Executive,
KernelMode,
FALSE,
NULL
);

ObDereferenceObject(g_ThreadContext.ThreadObject);
g_ThreadContext.ThreadObject = NULL;

DbgPrint("内核线程已成功退出并释放资源\n");
}
}

// 驱动入口
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint("驱动加载\n");

DriverObject->DriverUnload = DriverUnload;

if (!NT_SUCCESS(StartKernelThread()))
{
DbgPrint("内核线程创建失败,驱动加载终止\n");
return STATUS_UNSUCCESSFUL;
}

return STATUS_SUCCESS;
}

// 卸载例程
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
DbgPrint("驱动卸载,准备停止线程\n");

StopKernelThread();
}

内存强写

基于 CR0

CR0 寄存器的 WP(bit 16)控制写保护机制

  • WP = 1:开启写保护,无法写入只读页;
  • WP = 0:允许内核修改只读页(比如 .text 段、系统表等)。

注意

关闭 CR0.WP 确实会破坏 COW(Copy-On-Write)机制。在 CR0.WP 写保护关闭期间(即允许修改只读内存页)必须使用 _disable() 禁用中断、_enable() 恢复中断,否则可能会在中断处理过程中访问未受保护的页,导致系统崩溃(蓝屏)或不一致行为

由于需要关闭中断,该行为 只应用于极短的 Patch 窗口,如 SSDT hook、NOP patch、Inline Hook 等,不建议长时间保持 WP 关闭。否则系统在这个 CPU 核心上会失去调度能力(时钟中断被屏蔽),导致系统卡死(中断不处理)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ULONG_PTR DisableWP()
{
ULONG_PTR cr0 = __readcr0();
_disable(); // 禁止中断
__writecr0(cr0 & ~0x10000); // 清除 CR0.WP 位(bit 16)
return cr0;
}

void RestoreWP(ULONG_PTR cr0)
{
__writecr0(cr0); // 恢复原始 CR0
_enable(); // 恢复中断
}

// 使用方式
ULONG_PTR oldCR0 = DisableWP();
// 修改内存...
RestoreWP(oldCR0);

基于 MDL

MDL(Memory Descriptor List) 是驱动中用于描述一段物理内存的结构体,结合 MmMapLockedPagesSpecifyCache 可将目标物理页映射为内核可写的非缓存内存

具体过程为:

  1. 将目标地址构造成 MDL;
  2. 锁页(或声明其为 NonPagedPool);
  3. 通过 MmMapLockedPagesSpecifyCache(..., MmNonCached) 获取可写映射;
  4. 修改后解除映射并释放资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* @brief 将指定虚拟地址范围映射为系统空间可访问的非缓存物理页映射。
*
* @param virtualAddress 虚拟地址起始位置(必须是页对齐的内存区域)
* @param length 映射的字节长度
* @param accessMode 访问模式(UserMode 或 KernelMode)
*
* @return 成功:映射后的系统虚拟地址(PVOID)
* 失败:NULL,调用者无需释放资源
*
* @note 本函数会分配 MDL 并锁定物理页,异常或失败时会自动释放资源;
* 映射成功后不需要立即释放 MDL,但调用者如不再使用,应使用:
* - MmUnmapLockedPages(mappedVa, mdl);
* - MmUnlockPages(mdl);
* - IoFreeMdl(mdl);
*
* @warning 不要对不合法或非驻留内存区域调用此函数,否则会触发异常;
* 仅用于内核模式下操作用户空间或非分页内存。
*/
PVOID MdlMapVirtual(PVOID virtualAddress, ULONG_PTR length, MODE accessMode)
{
PMDL mdl = NULL; // 描述物理页的内核 MDL 结构
PVOID mappedVa = NULL; // 映射后的虚拟地址
BOOLEAN pagesLocked = FALSE; // 是否已成功锁定页

__try
{
// 分配 MDL 以描述虚拟地址区域
mdl = IoAllocateMdl(virtualAddress, length, FALSE, FALSE, NULL);
if (!mdl)
{
__leave;
}

// 锁定页面,防止换出
MmProbeAndLockPages(mdl, accessMode, IoReadAccess);
pagesLocked = TRUE;

// 映射物理页到系统空间(非缓存)
mappedVa = MmMapLockedPagesSpecifyCache(
mdl,
accessMode,
MmNonCached,
NULL,
FALSE,
NormalPagePriority
);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// 异常期间跳出,下面统一释放资源
}

// 映射失败则释放资源
if (!mappedVa)
{
if (pagesLocked)
{
MmUnlockPages(mdl);
}

if (mdl)
{
IoFreeMdl(mdl);
}

return NULL;
}

return mappedVa;
}

/**
* @brief 释放通过 MdlMapVirtual 映射的内存,包括解除映射、解锁页、释放 MDL。
*
* @param mappedVa 通过 MdlMapVirtual 返回的映射虚拟地址
* @param mdl MdlMapVirtual 内部使用的 MDL 指针
*
* @note 本函数应在不再使用映射内存时调用,用于清理资源;
* 若 mappedVa 或 mdl 为 NULL,则不执行操作。
*/
VOID MdlUnmapVirtual(PVOID mappedVa, PMDL mdl)
{
if (mappedVa && mdl)
{
MmUnmapLockedPages(mappedVa, mdl); // 解除映射
MmUnlockPages(mdl); // 解锁页
IoFreeMdl(mdl); // 释放 MDL 结构
}
}

对于内核的非分页内存,我们不需要使用 MmProbeAndLockPages 去验证并锁定页面,而是直接用 MmBuildMdlForNonPagedPool 快速构建 MDL,无需验证或锁页。这样可以提升效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* @brief 将非分页内存(NonPagedPool)构造成可映射 MDL。
*
* @param virtualAddress 非分页池中分配的缓冲区地址
* @param length 映射长度(必须小于等于分配长度)
*
* @return 成功:映射后的虚拟地址
* 失败:NULL(同时会释放资源)
*/
PVOID MdlMapVirtual_NonPagedPool(PVOID virtualAddress, ULONG_PTR length, PMDL *mdlOut)
{
PMDL mdl = IoAllocateMdl(virtualAddress, length, FALSE, FALSE, NULL);
if (!mdl)
return NULL;

// 对于 NonPagedPool 内存,直接构建 MDL,无需 Probe & Lock
MmBuildMdlForNonPagedPool(mdl);

PVOID mappedVa = MmMapLockedPagesSpecifyCache(
mdl,
KernelMode,
MmNonCached,
NULL,
FALSE,
NormalPagePriority
);

if (!mappedVa)
{
IoFreeMdl(mdl);
return NULL;
}

if (mdlOut)
*mdlOut = mdl;

return mappedVa;
}

/**
* @brief 解除 NonPagedPool 的 MDL 映射并释放资源。
*
* @param mappedVa 映射的虚拟地址
* @param mdl 映射时构造的 MDL
*/
VOID MdlUnmapVirtual(PVOID mappedVa, PMDL mdl)
{
if (mappedVa && mdl)
{
MmUnmapLockedPages(mappedVa, mdl); // 解除映射
IoFreeMdl(mdl); // 非分页内存无需 Unlock,只需释放 MDL
}
}

地址范围获取

基于 RtlPcToFileHeader

RtlPcToFileHeader 是 Windows 内核中一个非常有用的函数,用于根据某个地址(如代码或数据地址)确定其所属模块(image)的基地址。该函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//
// RtlPcToFileHeader
//
// 根据指定的地址(PcValue),判断该地址是否属于某个已加载模块(PE 映像)。
// 如果属于,将返回该模块的基地址,并通过 BaseOfImage 输出该地址。
//
// 通常用于定位当前执行地址或任意地址所属的映像模块,
// 适用于用户态和内核态模块定位、调试和反汇编等场景。
//
// 参数:
// PcValue [in] - 任意地址(代码地址、数据地址、返回地址等)
// 用于判定其是否落在某个已加载映像模块中
//
// BaseOfImage [out] - 如果 PcValue 所在地址属于某个模块,
// 则此参数将被设置为该模块的基地址(通常是 DOS Header)
// 否则,该值为 NULL
//
// 返回值:
// 成功: 返回所属模块的基地址(与 *BaseOfImage 相同)
// 失败: 返回 NULL,表示该地址不属于任何模块区域
//
// 注意事项:
// - 该函数不会判断地址是否有效(例如未映射内存也可能匹配)
// - 返回的基地址可以直接用于解析 IMAGE_DOS_HEADER / IMAGE_NT_HEADERS
// - 常用于结合 PE 分析获取节表、导出表等信息
//

NTSYSAPI
PVOID
NTAPI
RtlPcToFileHeader(
_In_ PVOID PcValue,
_Out_ PVOID *BaseOfImage
);

基于 RtlPcToFileHeader 我们可以获取模块地址,然后再根据模块地址解析 PE 结构获取模块的范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//
// 通用内存区域结构:用于描述模块或节的内存布局(起始地址 + 大小)
//
typedef struct _MEMORY_REGION
{
PVOID BaseAddress; // 区域起始地址
SIZE_T Size; // 区域长度(单位:字节)
} MEMORY_REGION, *PMEMORY_REGION;

/**
* GetModuleRegion - 获取模块基址与大小
*
* 使用 RtlPcToFileHeader 获取任意地址所属模块的基址,并解析 PE 头获取 SizeOfImage。
*
* 参数:
* @PcAddress: 任意模块内地址
* @Region: 输出内存区域(模块基址与大小)
*
* 返回值:
* TRUE - 成功获取模块信息
* FALSE - 地址无效或模块解析失败
*/
BOOLEAN
GetModuleRegion(
_In_ PVOID PcAddress,
_Out_ PMEMORY_REGION Region
)
{
if (!PcAddress || !Region)
return FALSE;

PVOID base = NULL;
if (!RtlPcToFileHeader(PcAddress, &base) || !base)
return FALSE;

__try
{
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
if (dos->e_magic != IMAGE_DOS_SIGNATURE)
return FALSE;

PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((PUCHAR)base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE)
return FALSE;

Region->BaseAddress = base;
Region->Size = nt->OptionalHeader.SizeOfImage;
return TRUE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return FALSE;
}
}

/**
* GetModuleSectionRegion - 获取模块中指定节(如 ".text")的虚拟地址与大小
*
* 参数:
* @PcAddress: 模块中任意地址(用于定位模块)
* @SectionName: 要查找的节名称(如 ".text",大小写不敏感)
* @Region: 输出节的起始地址与大小(单位:字节)
*
* 返回值:
* TRUE - 找到指定节,Region 填充有效数据
* FALSE - 模块或节查找失败
*/
BOOLEAN
GetModuleSectionRegion(
_In_ PVOID PcAddress,
_In_ PCCHAR SectionName,
_Out_ PMEMORY_REGION Region
)
{
if (!PcAddress || !SectionName || !Region)
return FALSE;

MEMORY_REGION module = { 0 };
if (!GetModuleRegion(PcAddress, &module))
return FALSE;

__try
{
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)module.BaseAddress;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((PUCHAR)module.BaseAddress + dos->e_lfanew);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(nt);

for (USHORT i = 0; i < nt->FileHeader.NumberOfSections; i++, section++)
{
CHAR name[9] = { 0 };
RtlCopyMemory(name, section->Name, min(sizeof(section->Name), 8));

if (_stricmp(name, SectionName) == 0)
{
Region->BaseAddress = (PUCHAR)module.BaseAddress + section->VirtualAddress;
Region->Size = section->Misc.VirtualSize;
return TRUE;
}
}

return FALSE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return FALSE;
}
}

提示

这里查询模块地址范围需要我们提供一个模块中的地址,我们可以利用 MmGetSystemRoutineAddress 函数获取一个模块中导出的函数的地址用来查询模块地址范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* MmGetSystemRoutineAddress - 获取指定系统函数的运行时地址
*
* 参数:
* @SystemRoutineName:
* 指向一个 UNICODE_STRING 结构,表示要获取的函数名称
* 示例值如:L"ZwQuerySystemInformation"、L"MmIsAddressValid"
*
* 返回值:
* 成功:返回目标函数的函数指针(PVOID),可强制转换为实际函数类型使用;
* 失败:如果指定函数不存在,返回 NULL。
*
* 使用场景:
* - 某些 API(如 Zw 系列、Rtl 系列)未在 WDK 的 import lib 中显式导出
* - 某些函数在不同系统版本中存在差异或未实现
* - 为提高兼容性、避免链接失败,使用该函数动态获取符号地址
*
* 典型用法:
* UNICODE_STRING routineName;
* RtlInitUnicodeString(&routineName, L"ZwQuerySystemInformation");
* PVOID func = MmGetSystemRoutineAddress(&routineName);
*/
PVOID
MmGetSystemRoutineAddress(
_In_ PUNICODE_STRING SystemRoutineName
);

基于 ZwQuerySystemInformation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include <ntifs.h>
#include <ntstrsafe.h>

//
// 通用内存区域描述结构(可表示模块/段/区域)
//
typedef struct _MEMORY_REGION {
PVOID BaseAddress; // 区域起始地址
ULONG RegionSize; // 区域大小(单位:字节)
} MEMORY_REGION, *PMEMORY_REGION;

//
// ZwQuerySystemInformation - Native API,用于查询系统级信息
//
NTSYSAPI
NTSTATUS
NTAPI
ZwQuerySystemInformation(
_In_ ULONG SystemInformationClass, // 信息类型
_Inout_ PVOID SystemInformation, // 输出缓冲区
_In_ ULONG SystemInformationLength, // 缓冲区大小
_Out_opt_ PULONG ReturnLength // 实际返回大小
);

//
// 查询模块信息用的类型 ID
//
#define SystemModuleInformation 11

//
// 单个系统模块信息结构(非公开结构体)
// 通过 SystemModuleInformation 获取
//
typedef struct _SYSTEM_MODULE_ENTRY {
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase; // 模块加载基址
ULONG ImageSize; // 模块大小(单位:字节)
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT ModuleNameOffset; // ImageName 中模块名的偏移
CHAR ImageName[256]; // 完整路径,如 \SystemRoot\system32\ntoskrnl.exe
} SYSTEM_MODULE_ENTRY, *PSYSTEM_MODULE_ENTRY;

//
// 所有模块信息总结构(包含模块数量和模块数组)
//
typedef struct _SYSTEM_MODULE_INFORMATION {
ULONG ModulesCount;
SYSTEM_MODULE_ENTRY Modules[1]; // 实际为不定长数组
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

/**
* GetKernelModuleRegion - 获取指定内核模块的基址和大小信息
*
* 参数:
* @ModuleName: 模块名称(如 "ntoskrnl.exe",不含路径,大小写不敏感)
* @ModuleRegion: 输出结构,返回模块的内存起始地址与大小
*
* 返回值:
* 成功返回 TRUE,失败返回 FALSE
*/
BOOLEAN
GetKernelModuleRegion(
_In_ PCSTR ModuleName,
_Out_ PMEMORY_REGION ModuleRegion
)
{
NTSTATUS status;
ULONG bufferSize = 0;
PSYSTEM_MODULE_INFORMATION pInfo = NULL;
ANSI_STRING targetName;
BOOLEAN found = FALSE;

// 参数校验
if (!ModuleName || !ModuleRegion)
return FALSE;

// 初始化输出结构
RtlZeroMemory(ModuleRegion, sizeof(MEMORY_REGION));
RtlInitAnsiString(&targetName, ModuleName);

// 首次调用 ZwQuerySystemInformation 获取所需缓冲区大小
status = ZwQuerySystemInformation(SystemModuleInformation, NULL, 0, &bufferSize);
if (status != STATUS_INFO_LENGTH_MISMATCH)
return FALSE;

// 分配内存保存模块信息
pInfo = (PSYSTEM_MODULE_INFORMATION)ExAllocatePool(NonPagedPool, bufferSize);
if (!pInfo)
return FALSE;

RtlZeroMemory(pInfo, bufferSize);

// 第二次调用获取真实模块信息
status = ZwQuerySystemInformation(SystemModuleInformation, pInfo, bufferSize, &bufferSize);
if (!NT_SUCCESS(status)) {
ExFreePool(pInfo);
return FALSE;
}

// 遍历所有模块,查找目标模块名
for (ULONG i = 0; i < pInfo->ModulesCount; i++) {
PSYSTEM_MODULE_ENTRY entry = &pInfo->Modules[i];

// 提取文件名部分(去掉路径)
const CHAR* namePtr = strrchr(entry->ImageName, '\\');
namePtr = namePtr ? namePtr + 1 : entry->ImageName;

ANSI_STRING currentName;
RtlInitAnsiString(&currentName, namePtr);

// 比较模块名称(忽略大小写)
if (RtlCompareString(&currentName, &targetName, TRUE) == 0) {
ModuleRegion->BaseAddress = entry->ImageBase;
ModuleRegion->RegionSize = entry->ImageSize;
found = TRUE;
break;
}
}

// 释放内存
ExFreePool(pInfo);
return found;
}

特征码

特征码提取

特征码提取规则如下:

操作数结构类型 举例汇编 提取结果示意 策略说明
o_reg, o_reg / o_reg, o_void / o_reg, o_phrase mov eax, ecxjmp eax 8BC1 全保留(每个字节都精确匹配)
o_reg, o_displ mov eax, [ecx+4] 8B41* 保留前两字节,后面是偏移,用 *
o_displ, o_reg / o_displ, o_imm mov [ebp+8], ecxmov [ebp+4], 0x1 894D* / C745****** 保留前两字节,其余通配
o_phrase, o_reg push dword ptr [eax] FF30 全保留(操作地址固定)
其他复杂结构(含立即数、地址、偏移) call dword ptr [esi+0Ch] FF15****** 保留前缀字节,其余全部通配
指令以 FF/66/67 前缀开始 FF 15 xx xx xx xx FF15****** 保留前两个字节,其余通配
无法识别操作数结构 N/A XX**** 保留一个或两个前缀字节,其余通配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# coding=utf-8
"""
IDA 特征码提取工具

功能:
- 从选中指令中提取结构特征码(HEX + 通配符 *)
- 若未选中,自动 fallback 到当前指令
- 支持 Alt-Z 热键提取
- 兼容 IDA Pro 7.5+ ~ 9.x
"""

import idc
import ida_bytes
import ida_ua
import ida_kernwin


def print_banner():
print("=" * 60)
print(" 特征码提取工具 | Alt-Z 快捷提取结构化签名")
print("=" * 60)


def format_byte(ea: int) -> str:
"""读取并格式化地址字节为 'XX' 十六进制字符串"""
return f"{ida_bytes.get_byte(ea):02X}"


def get_opcode_pattern(ea: int, size: int, keep: int = 1) -> str:
"""返回指令前 keep 字节 + 通配符构成的签名片段"""
pattern = ''.join(format_byte(ea + i) for i in range(min(size, keep)))
pattern += '*' * max(size - keep, 0)
return pattern


def extract_signature() -> str:
"""
特征码提取逻辑:
- 如果用户选中一段指令,则遍历整段;
- 如果未选中任何内容,则默认提取光标所在指令;
- 根据操作数结构决定通配规则。
"""
print_banner()

start, end = idc.read_selection_start(), idc.read_selection_end()

if start == idc.BADADDR or end == idc.BADADDR or end <= start:
# fallback 到光标所在指令
start = idc.here()
insn_len = idc.get_item_size(start)
end = start + insn_len
print(f"[*] 未检测到选区,默认提取当前指令 @ {hex(start)}")

result = ""
ea = start

while ea < end:
insn = ida_ua.insn_t()
if ida_ua.decode_insn(insn, ea) == 0:
print(f"[-] 无法解码指令 @ {ea:#x}")
break

size = insn.size
op1, op2 = idc.get_operand_type(ea, 0), idc.get_operand_type(ea, 1)

# 根据操作数类型决定保留策略
if op1 == idc.o_reg and op2 in (idc.o_reg, idc.o_void, idc.o_phrase):
result += get_opcode_pattern(ea, size, keep=size)
elif (op1 == idc.o_reg and op2 == idc.o_displ) or \
(op1 == idc.o_displ and op2 in (idc.o_reg, idc.o_imm)):
result += get_opcode_pattern(ea, size, keep=2)
elif op1 == idc.o_phrase and op2 == idc.o_reg:
result += get_opcode_pattern(ea, size, keep=size)
else:
# 默认保留前缀判断(如 FF/66/67)
prefix = format_byte(ea).upper()
keep = 2 if prefix in ("FF", "66", "67") and size >= 2 else 1
result += get_opcode_pattern(ea, size, keep=keep)

ea += size

# 输出信息
func_start = idc.get_func_attr(start, idc.FUNCATTR_START)
func_name = idc.get_func_name(start)
offset = start - func_start if func_start != idc.BADADDR else 0

print(f"[+] 函数: {func_name}")
print(f"[+] 起始偏移: {hex(offset)}")
print(f"[+] 特征码: {result}")
return result


def register_hotkey(shortcut="Alt-Z"):
"""注册热键并绑定到提取函数"""
def callback():
extract_signature()
return 1

if ida_kernwin.add_hotkey(shortcut, callback):
print(f"[+] 热键已注册:{shortcut}")
else:
print(f"[-] 热键注册失败:{shortcut}")


# === 初始化执行 ===
print_banner()
register_hotkey("Alt-Z")

特征码搜索

定义 SIGNATURE_PATTERN 结构用来描述搜索的特征。

1
2
3
4
5
6
7
8
9
10
// 通配符判断宏:'*'、'?'、'.' 都表示任意单字节
#define IS_WILDCARD(c) ((c) == '*' || (c) == '?' || (c) == '.')

typedef struct _SIGNATURE_PATTERN
{
PUCHAR Pattern; // 特征码字节序列(动态分配)
PCHAR Mask; // 掩码字符串:'x' = 精确,'*' / '?' / '.' = 通配
SIZE_T Length; // 模式长度(字节)
LONG Offset; // 匹配地址相对于返回地址的偏移(返回地址 = 匹配地址 - Offset)
} SIGNATURE_PATTERN, *PSIGNATURE_PATTERN;

SIGNATURE_PATTERN 通过 InitSignaturePattern 函数初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* InitSignaturePattern - 初始化一个 SIGNATURE_PATTERN 结构
*
* @Sig: 输出的结构指针,函数成功后填入 Pattern、Mask、Length、Offset
* @PatternString: 特征码字符串,格式如 "488B*90",支持通配符 * ? .
* @Offset: 匹配地址 - 返回地址 的偏移量(匹配成功后最终返回地址 = 匹配位置 - Offset)
*
* 说明:
* - 支持紧凑格式:无空格、连续 HEX 字符(大小写均可)与通配符混合;
* - 动态申请 Pattern / Mask 缓冲区;
* - 初始化成功后需调用 FreeSignaturePattern 释放;
*
* 返回值:
* TRUE 表示初始化成功;
* FALSE 表示格式非法或内存申请失败。
*/
BOOLEAN
InitSignaturePattern(
_Out_ PSIGNATURE_PATTERN Sig,
_In_ PCSTR PatternString,
_In_ LONG Offset
)
{
SIZE_T PatternMaxLength = strlen(PatternString); // 缓冲最大长度等于输入字符串长度
SIZE_T i = 0;

// 分配 Pattern 缓冲和掩码缓冲
PUCHAR patternBuf = (PUCHAR)ExAllocatePool(NonPagedPool, PatternMaxLength);
PCHAR maskBuf = (PCHAR) ExAllocatePool(NonPagedPool, PatternMaxLength);

if (!patternBuf || !maskBuf)
{
DbgPrint("[-] InitSignaturePattern: 内存分配失败 (PatternMaxLength = %Iu)\n", PatternMaxLength);
if (patternBuf) ExFreePool(patternBuf);
if (maskBuf) ExFreePool(maskBuf);
return FALSE;
}

// 按位解析特征码字符串
while (*PatternString && i < PatternMaxLength)
{
if (IS_WILDCARD(*PatternString))
{
// 通配符:写入 0x00,掩码为 '*'
patternBuf[i] = 0x00;
maskBuf[i] = '*';
PatternString++;
}
else if (isxdigit((UCHAR)PatternString[0]) && isxdigit((UCHAR)PatternString[1]))
{
// 两位十六进制字符:转为字节
CHAR temp[3] = { PatternString[0], PatternString[1], '\0' };
patternBuf[i] = (UCHAR)strtoul(temp, NULL, 16);
maskBuf[i] = 'x';
PatternString += 2;
}
else
{
// 非法字符:终止并清理资源
DbgPrint("[-] InitSignaturePattern: 非法字符解析失败 at offset %Iu (char: 0x%02X)\n", i, (UCHAR)*PatternString);
ExFreePool(patternBuf);
ExFreePool(maskBuf);
return FALSE;
}

i++;
}

if (i == 0)
{
DbgPrint("[-] InitSignaturePattern: 空特征码\n");
ExFreePool(patternBuf);
ExFreePool(maskBuf);
return FALSE;
}

// 填写结构体字段
Sig->Pattern = patternBuf;
Sig->Mask = maskBuf;
Sig->Length = i;
Sig->Offset = Offset;

DbgPrint("[+] Signature 初始化成功: 长度 = %Iu,Offset = %d\n", i, Offset);
return TRUE;
}


/**
* FreeSignaturePattern - 释放 InitSignaturePattern 动态分配的缓冲
*
* @Sig: 要释放的 SIGNATURE_PATTERN 结构体指针
*/
VOID
FreeSignaturePattern(
_Inout_ PSIGNATURE_PATTERN Sig
)
{
if (Sig->Pattern)
{
ExFreePool(Sig->Pattern);
Sig->Pattern = NULL;
}

if (Sig->Mask)
{
ExFreePool(Sig->Mask);
Sig->Mask = NULL;
}

Sig->Length = 0;
Sig->Offset = 0;
}

搜索时需要一个 SIGNATURE_PATTERN 数组,当所有特征匹配时返回匹配到的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* FindMultiplePatternsInRange - 在指定内存范围内查找同时满足多个特征码的位置
*
* 所有特征码都以当前地址为基准,各自加上 Offset 后进行匹配,只有全部匹配成功才视为有效。
*
* 参数:
* @StartAddress: 搜索的起始虚拟地址
* @RangeSize: 搜索的字节范围(从 StartAddress 开始)
* @Patterns: SIGNATURE_PATTERN 数组指针(已初始化)
* @PatternCount: 数组中的特征码个数
*
* 返回值:
* 成功:返回满足所有特征码的位置(当前地址,即 p)
* 失败:返回 NULL,表示未找到满足条件的位置
*/
PVOID
FindMultiplePatternsInRange(
_In_ PVOID StartAddress,
_In_ SIZE_T RangeSize,
_In_ PSIGNATURE_PATTERN Patterns,
_In_ SIZE_T PatternCount
)
{
PUCHAR base = (PUCHAR)StartAddress; // 起始地址(转换为字节指针)
PUCHAR end = base + RangeSize; // 结束地址

if (PatternCount == 0)
{
DbgPrint("[-] Pattern 数组为空,无法匹配\n");
return NULL;
}

// 从 base 开始逐字节尝试作为匹配基准地址
for (PUCHAR p = base; p < end; p++)
{
BOOLEAN allMatched = TRUE; // 标记当前地址是否能通过所有特征码匹配

// 遍历每一个特征码进行匹配验证
for (SIZE_T k = 0; k < PatternCount; k++)
{
PSIGNATURE_PATTERN sig = &Patterns[k];
PUCHAR target = p + sig->Offset; // 计算本特征码的实际匹配起始位置

// 检查匹配地址是否合法,是否会越界
if (target + sig->Length > end || !MmIsAddressValid(target))
{
allMatched = FALSE;
break;
}

// 对每个字节进行匹配或通配判断
for (SIZE_T i = 0; i < sig->Length; i++)
{
if (!IS_WILDCARD(sig->Mask[i]) && target[i] != sig->Pattern[i])
{
allMatched = FALSE;
break;
}
}

if (!allMatched)
break; // 有任意一个特征码不匹配,立即跳过该地址
}

// 所有特征码均匹配成功,返回当前地址 p 作为结果地址
if (allMatched)
{
PUCHAR final = p;
DbgPrint("[+] 所有特征码匹配成功,返回地址 = %p\n", final);
return final;
}
}

// 整个搜索范围内无任何符合条件的地址
DbgPrint("[-] 未找到满足所有特征码的地址\n");
return NULL;
}

驱动隐藏

隐藏思路

在 Windows 内核中,驱动模块在系统内部存在两个最关键的暴露点:

  • PsLoadedModuleList:系统全局模块双向链表(记录所有已加载驱动模块)
  • DriverObject 结构体:系统所有已注册的驱动对象(包含驱动模块信息)

因此隐藏的本质是:

  • PsLoadedModuleList 断链 → 让系统模块枚举 API 查不到
  • 抹除 DriverObject 内关键字段 → 让安全软件和调试器无法逆推出模块信息

隐藏流程

  1. 延迟隐藏逻辑 :如果在 DriverEntry() 阶段立即隐藏,可能会因为内核后续调用尚未完成而引发异常。这是因为驱动加载的后续流程可能会用到 DriverObject 结构体中一些对象。因此我们可以启动一个内核线程,延迟约 100ms 后再执行隐藏逻辑
  2. 断链模块表 :遍历 PsLoadedModuleList,逐个对比 BaseDllName 与目标模块名。找到后执行 RemoveEntryList() 完成断链。
  3. 筛选合法伪造模块 :在遍历链表时顺便记录第一个合法存在的其它模块节点。该节点用作伪造用 DriverSection
  4. 定位 DriverObject :使用内核 API ObReferenceObjectByName() 定位目标驱动的 DriverObject
  5. 抹除与伪造 :将 DriverInitDriverSectionType 字段抹除或伪造。其中 DriverSection 需要执行前面筛选的合法 DriverSection,防止安全软件在扫描 DriverObject->DriverSection 时蓝屏。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include <ntddk.h>

// =======================
// 精简版 KLDR_DATA_TABLE_ENTRY (省略部分字段)
//
// 注意:这里用简化版结构体,
// 实际项目中可用 Windbg dt命令导出完整结构体以提升兼容性
// =======================
typedef struct _KLDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
ULONG __Undefined1;
ULONG __Undefined2;
ULONG __Undefined3;
ULONG NonPagedDebugInfo;
ULONG DllBase;
ULONG EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT __Undefined5;
ULONG __Undefined6;
ULONG CheckSum;
ULONG TimeDateStamp;
// ...(后续字段省略)
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;

// =======================
// 目标模块名称与DriverObject名称 (请根据目标驱动修改)
// =======================
#define HIDE_MODULE_NAME L"TestDriver.sys"
#define HIDE_DRIVER_OBJECT_NAME L"\\Driver\\TestDriver"

// 隐藏线程句柄
HANDLE g_HideThreadHandle = NULL;

// =======================
// 模块断链 + 伪造 DriverSection + DriverObject隐藏 统一逻辑
// =======================
VOID UnlinkAndHide(void)
{
// 获取 PsLoadedModuleList 链表头
PKLDR_DATA_TABLE_ENTRY pLdr = (PKLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->Flink;
PLIST_ENTRY PsLoadedModuleListHead = pLdr->InLoadOrderLinks.Blink->Flink;

// 生成目标模块名字符串用于匹配
UNICODE_STRING targetName;
RtlInitUnicodeString(&targetName, HIDE_MODULE_NAME);

PKLDR_DATA_TABLE_ENTRY RemovedEntry = NULL; // 记录被断链的模块节点
PKLDR_DATA_TABLE_ENTRY FakeEntry = NULL; // 记录合法伪造用模块

// 遍历模块链表
PLIST_ENTRY pList = PsLoadedModuleListHead->Flink;
while (pList != PsLoadedModuleListHead)
{
PKLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pList, KLDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

// 只判断 BaseDllName.Length 非 0 简化合法性判定
if (pEntry->BaseDllName.Length == 0)
{
pList = pList->Flink;
continue;
}

// 是否为要隐藏的目标模块
if (RtlCompareUnicodeString(&pEntry->BaseDllName, &targetName, TRUE) == 0)
{
DbgPrint("找到目标模块: %wZ -> 执行断链隐藏\n", &pEntry->BaseDllName);
RemoveEntryList(&pEntry->InLoadOrderLinks);
RemovedEntry = pEntry;
}
else if (FakeEntry == NULL)
{
// 记录第一个合法可伪造模块作为 DriverSection 替代项
FakeEntry = pEntry;
}

pList = pList->Flink;
}

// ========================
// DriverObject抹除及 DriverSection伪造逻辑
// ========================
UNICODE_STRING drvName;
RtlInitUnicodeString(&drvName, HIDE_DRIVER_OBJECT_NAME);

PDRIVER_OBJECT pTargetDriver = NULL;
NTSTATUS status = ObReferenceObjectByName(
&drvName,
OBJ_CASE_INSENSITIVE,
NULL,
0,
*IoDriverObjectType,
KernelMode,
NULL,
(PVOID*)&pTargetDriver
);

if (NT_SUCCESS(status))
{
DbgPrint("成功定位 DriverObject,执行隐藏逻辑\n");

// 抹除关键字段 (防止逆向工具利用)
pTargetDriver->DriverInit = NULL;
pTargetDriver->Type = 0;

// 伪造合法 DriverSection 避免蓝屏
if (FakeEntry != NULL)
pTargetDriver->DriverSection = FakeEntry;

// 减少 ObReferenceObjectByName 增加的引用次数
ObDereferenceObject(pTargetDriver);
}
else
{
DbgPrint("ObReferenceObjectByName 获取 DriverObject 失败: 0x%08X\n", status);
}
}

// =======================
// 延迟隐藏线程逻辑 (核心隐藏动作在此执行)
// =======================
VOID HideThreadProc(PVOID StartContext)
{
UNREFERENCED_PARAMETER(StartContext);

// 延迟 100ms 保证系统加载流程稳定
LARGE_INTEGER interval;
interval.QuadPart = -10 * 1000 * 100LL; // 100毫秒延迟

DbgPrint("隐藏线程启动,延迟 100ms 后执行隐藏流程\n");
KeDelayExecutionThread(KernelMode, FALSE, &interval);

UnlinkAndHide();

DbgPrint("隐藏逻辑完成,退出隐藏线程\n");
PsTerminateSystemThread(STATUS_SUCCESS);
}

// =======================
// 驱动卸载逻辑
// =======================
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
DbgPrint("驱动卸载完成\n");
}

// =======================
// 驱动入口逻辑
// =======================
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;

DbgPrint("驱动加载,准备启动隐藏线程\n");

NTSTATUS status = PsCreateSystemThread(
&g_HideThreadHandle,
THREAD_ALL_ACCESS,
NULL, NULL, NULL,
HideThreadProc,
NULL
);

if (!NT_SUCCESS(status))
{
DbgPrint("创建隐藏线程失败: 0x%08X\n", status);
return status;
}

ZwClose(g_HideThreadHandle);
return STATUS_SUCCESS;
}

驱动通信

设备对象

绝大多数情况下,一个内核驱动如果没有创建任何设备对象 (DeviceObject),那么用户态(Ring3)无法直接与该驱动通信。这是因为设备对象是内核通信入口,CreateFile / DeviceIoControl / ReadFile / WriteFile 等通信 API 只能打开设备对象。

设备对象主要有三类:

  • PDO(Physical Device Object) :总线驱动创建,表示物理设备本身的存在性。例如 USB、PCI、SATA 控制器、蓝牙模块等。PDO 只描述:“有这么个硬件挂上来了”,不控制它如何工作。
  • FDO(Functional Device Object) :功能驱动创建,负责控制硬件功能、提供核心业务逻辑。FDO 负责解释 IRP 请求、控制硬件寄存器、管理协议栈、提供用户空间接口,真正把硬件功能带给系统。
  • Filter Device Object :过滤驱动创建,可插在 FDO 上下两侧,负责监控、修改、拦截 I/O 请求,属于透明扩展层。它不控制硬件,而是做中间层逻辑处理。

这些设备对象彼此层叠形成的一条逻辑设备处理链。用户与设备交互的数据在 I/O 层叠栈中的设备对象中层层转发,每一层都可以可以拦截、监控、修改、阻断用户请求。这种结构被称为 I/O 层叠栈(Stacked Device Stack)

1
2
3
4
5
6
7
8
9
10
11
用户 I/O 请求

[Filter Device (Upper Filter)]

[Functional Device (FDO)]

[Filter Device (Lower Filter)]

[Physical Device (PDO)]

硬件

不过大多数普通第三方内核驱动开发者实际上写的都是类似 FDO 或 FilterPDO 只能由 Bus Driver 创建(通常系统自带)。

设备类型 PDO 创建者 FDO 创建者 Filter 创建者
USB 存储 USB Hub 驱动 UAS 驱动(如 usbstor.sys) 杀毒软件过滤层
网卡 PCI Bus 驱动 NIC 功能驱动 防火墙、抓包驱动
虚拟设备 Root Enumerator 虚拟驱动 监控、调试工具

设备对象驱动对象的关系:

  • 一个驱动可以创建IoCreateDevice)或附加IoAttachDevice)多个设备对象,用于负责处理和过滤多个设备的消息。
  • 一个物理设备可以绑定多个设备对象,这些设备对象形成了一个设备对象堆栈,可以层层过滤用户程序向设备发送的消息。

设备对象定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct _DEVICE_OBJECT
{
SHORT Type; // 内核对象类型标识,固定为0x03表示DEVICE_OBJECT类型
USHORT Size; // 结构体大小(字节),不同版本Windows大小略有不同,典型为0xB8
LONG ReferenceCount; // 内核内部引用计数,自动维护,表示被多少模块或线程引用

struct _DRIVER_OBJECT* DriverObject; // 📌当前设备对象所属的驱动对象指针
struct _DEVICE_OBJECT* NextDevice; // 📌同一个驱动下多个设备对象通过NextDevice组成链表
struct _DEVICE_OBJECT* AttachedDevice; // 📌指向附加在本设备对象上的过滤设备对象(设备堆叠时使用)

struct _IRP* CurrentIrp; // 当前处理的IRP,仅老式串行StartIo驱动模型下使用,绝大多数驱动不用管

struct _IO_TIMER* Timer; // 设备专用I/O定时器指针,通过IoInitializeTimer注册,支持周期性回调

ULONG Flags; // 📌设备标志控制设备的I/O模型和电源行为,常见有:DO_BUFFERED_IO(缓冲IO)、DO_DIRECT_IO(直接IO)、DO_POWER_PAGABLE(分页支持)
ULONG Characteristics; // 设备特性标志,控制设备特性行为,例如FILE_REMOVABLE_MEDIA(可移动介质)、FILE_READ_ONLY_DEVICE(只读设备),普通控制驱动一般填0

struct _VPB* Vpb; // 卷参数块,仅文件系统与存储类驱动使用,普通控制型驱动恒为NULL

VOID* DeviceExtension; // 📌设备扩展区,驱动自定义业务数据区,创建设备时指定大小并在此区域挂载私有结构体

ULONG DeviceType; // 📌设备类型,定义设备类别,如FILE_DEVICE_UNKNOWN(控制型驱动通用)、FILE_DEVICE_DISK、FILE_DEVICE_NETWORK等
CHAR StackSize; // 📌设备栈深度,表示此设备在过滤栈中的层数,每附加一层过滤器栈自动+1

union { struct _LIST_ENTRY ListEntry; struct _WAIT_CONTEXT_BLOCK Wcb; } Queue; // 内核内部用队列或DMA上下文,极少数底层硬件驱动使用

ULONG AlignmentRequirement; // 设备I/O缓冲区内存对齐要求,DMA设备特别关注对齐要求,普通驱动为默认对齐
struct _KDEVICE_QUEUE DeviceQueue; // 串行I/O请求队列,主要用于串口、磁带等硬件控制型串行设备

struct _KDPC Dpc; // 延迟过程调用对象,用于中断下半部处理逻辑,配合ISR分离快速中断和实际数据处理
ULONG ActiveThreadCount; // 正在处理本设备对象IRP的线程数量,内核自动管理,用于内部同步统计

VOID* SecurityDescriptor; // 设备对象安全描述符,定义DACL权限控制,控制型驱动通常填NULL表示默认安全性
struct _KEVENT DeviceLock; // 内部同步锁,配合串行I/O等同步场景控制并发访问

USHORT SectorSize; // 扇区大小,存储设备使用,非存储型控制驱动通常为0
USHORT Spare1; // 保留字段,未来扩展用

struct _DEVOBJ_EXTENSION* DeviceObjectExtension; // 内核扩展区,供PNP、电源管理等系统模块使用,驱动一般无需关心
VOID* Reserved; // 预留字段
};
  • DriverObject:所属驱动的 DriverObject,用于找到设备对象所属的驱动对象

  • NextDevice:将一个驱动所属的所有设备对象 DriverObject 串联成一个单向链表,链表头为 DriverObject->DeviceObject

    1
    2
    3
    DriverObject
    |
    +--> DeviceObject1 --> DeviceObject2 --> DeviceObject3 --> NULL
  • AttachedDeviceAttachedDevice 构成的是跨驱动I/O 层叠栈(Stacked Device Stack)。它让不同驱动可以挂接在同一设备上层,共同参与 I/O 请求的流转与处理。设备对象的 AttachedDevice 指向下一层的设备对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    IRP 入口

    DeviceObject_Filter3 <-- 过滤层3(最上层)
    ↓ AttachedDevice
    DeviceObject_Filter2 <-- 过滤层2
    ↓ AttachedDevice
    DeviceObject_Filter1 <-- 过滤层1
    ↓ AttachedDevice
    DeviceObject_Functional (FDO) <-- 功能设备对象 (目标核心驱动)
    ↓ AttachedDevice
    DeviceObject_PDO <-- 物理设备对象 (底层物理设备)
    ↓ AttachedDevice
    NULL
  • StackSize:从当前设备对象所在的位置,往下直到整个设备栈的最底层(PDO)为止,所需要的 IRP 栈帧数量总和。换句话说:当前设备对象在整个 IRP 传递链中,自己算在内,往下有多少设备对象要参与。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    用户层 I/O 请求

    [DeviceObject_Filter3] StackSize=5

    [DeviceObject_Filter2] StackSize=4

    [DeviceObject_Filter1] StackSize=3

    [DeviceObject_FDO] StackSize=2

    [DeviceObject_PDO] StackSize=1

设备对象创建

在内核中,设备对象通常通过 IoCreateDevice 函数进行创建,该函数由 I/O 管理器提供,用于在 Object Manager 中注册新的设备对象,并分配相应的内存与扩展数据区。以下是函数原型:

1
2
3
4
5
6
7
8
9
NTSTATUS IoCreateDevice(
IN PDRIVER_OBJECT DriverObject, // [输入] 驱动对象指针
IN ULONG DeviceExtensionSize, // [输入] 设备扩展区大小(字节数)
IN PUNICODE_STRING DeviceName OPTIONAL, // [输入] 设备名称(Object Manager 路径)
IN DEVICE_TYPE DeviceType, // [输入] 设备类型标识
IN ULONG DeviceCharacteristics, // [输入] 设备特性标志
IN BOOLEAN Exclusive, // [输入] 是否独占设备
OUT PDEVICE_OBJECT *DeviceObject // [输出] 返回新建的设备对象指针
);
  • DriverObject:指定所属驱动对象,通常传入 DriverEntry 函数中的 PDRIVER_OBJECT,用于将新创建的设备对象挂接到驱动对象下,由驱动统一管理。
  • DeviceExtensionSize:指定设备扩展区的大小(以字节为单位)。内核会在分配 DEVICE_OBJECT 结构时附加这一段额外空间,驱动可通过 DeviceObject->DeviceExtension 访问此区域,用于存储与设备实例相关的自定义上下文信息。如果不需要扩展区则传 0
  • DeviceName:指定设备对象的命名路径(完整的 Object Manager 路径),如 \Device\MyDevice。如果传入 NULL,则创建匿名设备对象,不注册命名空间,不可通过名称访问;一般控制型驱动需提供命名,供用户态程序通过符号链接访问。
  • DeviceType:指定设备对象类型,用于指明设备类别,内核使用该类型决定某些默认行为。例如:
    • FILE_DEVICE_UNKNOWN默认通用类型,绝大多数控制型驱动使用;
    • FILE_DEVICE_DISK:磁盘设备;
    • FILE_DEVICE_NETWORK:网络设备;
    • FILE_DEVICE_FILE_SYSTEM:文件系统设备;
    • 其他类型视具体功能选用。
  • DeviceCharacteristics:指定设备特性标志,用于控制设备的附加行为。常见取值包括:
    • FILE_DEVICE_SECURE_OPEN:启用安全性访问检查;
    • FILE_REMOVABLE_MEDIA:表示可移动介质;
    • 一般自定义控制型驱动可传 0,表示不声明任何特殊设备行为,内核使用默认通用行为对待该设备对象。
  • Exclusive:指定设备对象是否独占访问。当设为 TRUE 时,系统仅允许一个线程/进程打开该设备对象,后续打开请求会失败(返回 STATUS_DEVICE_BUSY)。通常设为 FALSE,允许并发访问。
  • DeviceObject:输出参数,返回成功创建的设备对象指针。驱动可通过此指针访问扩展区、设置属性,并在卸载时配合 IoDeleteDevice 正确释放内存资源。

然而 IoCreateDevice 只负责把设备对象的内存空间从内核池分配出来,做了最基本初始化,但没有对外暴露接口。因此我们还要注册符号链接建立 Win32 层访问路径,确保用户态能够通过 CreateFile() 调用访问。

例如我们将设备名为 \Device\MyDevice 的设备通过 IoCreateSymbolicLink 创建了一个到 \??\MyDevice 的软连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 设备命名:注册到 Object Manager 命名空间下
UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\MyDevice");

// 符号链接命名:供用户态 CreateFile 使用 (Win32 层访问路径)
UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(L"\\??\\MyDevice");

// 创建符号链接 (供用户态访问)
status = IoCreateSymbolicLink(&symLinkName, &deviceName);
if (!NT_SUCCESS(status)) {
DbgPrint("IoCreateSymbolicLink 创建符号链接失败: 0x%08X\n", status);
IoDeleteDevice(DeviceObject); // 创建失败时需回滚释放设备对象
return status;
}

那么用户程序就可以通过设备路径 \\.\MyDevice 来操作这个设备了。

最后我们需要对 DeviceObject->Flags 清除初始化标志。这一步实际上是为了兼容一些老的操作系统,这一类操作系统不会自动去除初始化标志,导致设备对象创建之后始终处于未初始化状态,导致一些对设备的操作失败。

1
2
// 设备初始化完成,清除初始化标志 (兼容老系统)
DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <ntddk.h>

// 设备扩展结构体示例(自定义业务数据)
typedef struct _MY_DEVICE_EXTENSION {
ULONG ExampleField;
} MY_DEVICE_EXTENSION, *PMY_DEVICE_EXTENSION;

// 卸载例程声明
VOID DriverUnload(PDRIVER_OBJECT DriverObject);

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status;
PDEVICE_OBJECT DeviceObject = NULL;

// 设备命名:注册到 Object Manager 命名空间下
UNICODE_STRING deviceName = RTL_CONSTANT_STRING(L"\\Device\\MyDevice");

// 符号链接命名:供用户态 CreateFile 使用 (Win32 层访问路径)
UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(L"\\??\\MyDevice");

// 创建设备对象
status = IoCreateDevice(
DriverObject, // 绑定到当前驱动
sizeof(MY_DEVICE_EXTENSION), // 设备扩展区大小
&deviceName, // 设备名 (具名注册)
FILE_DEVICE_UNKNOWN, // 设备类型
0, // 设备特性 (默认传 0)
FALSE, // 非独占 (允许并发访问)
&DeviceObject // 返回创建好的设备对象指针
);

if (!NT_SUCCESS(status)) {
DbgPrint("IoCreateDevice 创建设备失败: 0x%08X\n", status);
return status;
}

// 创建符号链接 (供用户态访问)
status = IoCreateSymbolicLink(&symLinkName, &deviceName);
if (!NT_SUCCESS(status)) {
DbgPrint("IoCreateSymbolicLink 创建符号链接失败: 0x%08X\n", status);
IoDeleteDevice(DeviceObject); // 创建失败时需回滚释放设备对象
return status;
}

// 设备初始化完成,清除初始化标志 (兼容老系统)
DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

// 注册卸载例程
DriverObject->DriverUnload = DriverUnload;

DbgPrint("驱动加载成功,设备与符号链接已创建完成\n");

return STATUS_SUCCESS;
}

// 卸载例程 (驱动卸载时自动调用)
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(L"\\??\\MyDevice");

// 删除符号链接 (先删符号链接,再删设备对象)
IoDeleteSymbolicLink(&symLinkName);

// 释放设备对象 (注意可能存在多个设备,需遍历释放)
PDEVICE_OBJECT DeviceObject = DriverObject->DeviceObject;
while (DeviceObject != NULL)
{
PDEVICE_OBJECT NextDevice = DeviceObject->NextDevice;
IoDeleteDevice(DeviceObject);
DeviceObject = NextDevice;
}

DbgPrint("驱动卸载完成,资源已释放\n");
}

设备对象附加

在 Windows 内核 I/O 框架中,驱动可以将自己编写的设备对象附加到现有的设备对象之上,形成设备对象堆栈(Device Stack)。这种附加行为常用于开发过滤驱动、监控驱动、保护驱动、文件过滤驱动等场景。

内核提供 IoAttachDeviceIoAttachDeviceToDeviceStack 函数用于实现附加操作。

IoAttachDevice 函数原型如下:

1
2
3
4
5
PDEVICE_OBJECT IoAttachDevice(
IN PDEVICE_OBJECT SourceDevice, // [输入] 自己新创建的设备对象
IN PUNICODE_STRING TargetDevice, // [输入] 要附加到的目标设备对象路径
OUT PDEVICE_OBJECT *AttachedTo // [输出] 实际附加成功后的目标设备对象指针
);
  • SourceDevice:本驱动中用 IoCreateDevice() 创建好的设备对象,作为过滤层插入堆栈;
  • TargetDevice:目标设备对象的全路径(如 \Device\Harddisk0\DR0),指定要附加到哪个设备;
  • AttachedTo:附加成功后返回目标设备对象指针,即被附加的设备对象。也就是说附加后 SourceDevice->AttachedDevice = AttachedTo

IoAttachDeviceToDeviceStack 则需要我们直接提供要被附加的设备对象,而不是设备路径,该函数定义如下:

1
2
3
4
PDEVICE_OBJECT IoAttachDeviceToDeviceStack(
IN PDEVICE_OBJECT SourceDevice, // [输入] 要附加的过滤设备对象 (本驱动创建)
IN PDEVICE_OBJECT TargetDevice // [输入] 目标设备对象 (被附加对象)
);
  • SourceDevice:指定新创建的过滤层设备对象(一般通过 IoCreateDevice() 创建),将被插入到设备堆栈顶端,成为新的栈顶对象。附加成功后,该对象位于整个设备栈最顶层,优先接收 IRP 请求。

  • TargetDevice:要附加的目标设备对象。系统会根据其 AttachedDevice 自动遍历整个设备栈,找到当前栈顶位置然后附加。

当完成附加后,当用户态通过 CreateFile() 打开设备时,虽然传入的设备路径仍然是被附加的设备对象。

但是由于我们在内核中通过 IoAttachDeviceToDeviceStack() 已经把自己的设备对象挂入了目标设备对象的 I/O 栈顶,因此用户请求命中目标设备对象时,内核始终从栈顶开始派发 IRP。而我们的过滤设备对象就在这条栈上,所有请求自然会经过我们的驱动。

设备对象附加的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NTSTATUS AttachToTargetDevice(PDEVICE_OBJECT MyDevice)
{
NTSTATUS status = STATUS_SUCCESS;
PDEVICE_OBJECT TargetDevice = NULL;
UNICODE_STRING TargetDeviceName = RTL_CONSTANT_STRING(L"\\Device\\Harddisk0\\DR0");

status = IoGetDeviceObjectPointer(
&TargetDeviceName,
FILE_READ_DATA,
NULL,
&TargetDevice
);
if (!NT_SUCCESS(status)) {
DbgPrint("获取目标设备失败: 0x%08X\n", status);
return status;
}

// 附加到目标设备栈
PDEVICE_OBJECT AttachedTo = IoAttachDeviceToDeviceStack(MyDevice, TargetDevice);
if (AttachedTo == NULL) {
DbgPrint("附加设备失败\n");
return STATUS_UNSUCCESSFUL;
}

DbgPrint("成功附加到目标设备\n");
return STATUS_SUCCESS;
}

在驱动卸载的时候,我们需要在删除自己的设备对象之前先从设备栈分离。

1
2
IoDetachDevice(MyDeviceExtension->NextDevice);
IoDeleteDevice(MyDeviceObject);

IRP(I/O Request Packet)

IRP(I/O Request Packet)是 Windows 内核 I/O 子系统内部使用的统一请求数据结构,负责在设备驱动之间传递 I/O 操作请求。所有内核驱动层(文件系统驱动、网络驱动、过滤驱动、控制驱动等等)之间的 I/O 交互,都是通过 IRP 结构体完成。

IRP 结构体

_IRP 是内核 I/O 子系统的核心数据结构,用于描述一次完整的 I/O 请求状态与控制信息。所有 IRP 派发、过滤、传递、完成逻辑,都是围绕该结构体展开的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 0x70 bytes (sizeof)
struct _IRP
{
SHORT Type; // IRP对象标识,固定为0x06(IRP类型)
USHORT Size; // IRP结构体大小 (当前为0x70)

struct _MDL* MdlAddress; // 📌内存描述列表 (用于 Direct I/O 模式时映射缓冲区)

ULONG Flags; // IRP状态标志位,控制I/O管理器内部行为
// 常见标志如:IRP_BUFFERED_IO, IRP_INPUT_OPERATION

union {
struct _IRP* MasterIrp; // (分散聚集I/O使用)
LONG IrpCount; // (多IRP合并时的计数器)
VOID* SystemBuffer; // 📌(Buffered I/O 模式下的系统缓冲区指针)
} AssociatedIrp;

struct _LIST_ENTRY ThreadListEntry; // 挂接到线程 I/O 请求链的双向链表节点

struct _IO_STATUS_BLOCK IoStatus; // 📌I/O 操作状态与返回值

CHAR RequestorMode; // 发起请求方的CPU模式:UserMode / KernelMode
UCHAR PendingReturned; // 内核内部标志,表示IRP是否挂起返回
CHAR StackCount; // 📌IRP栈总深度 (栈帧数量)
CHAR CurrentLocation; // 📌当前 IRP 栈位置 (栈帧索引)

UCHAR Cancel; // 是否被请求取消 (1=正在取消)
UCHAR CancelIrql; // 取消时所处IRQL (中断优先级)
CHAR ApcEnvironment; // APC 环境信息
UCHAR AllocationFlags; // IRP分配标志 (一般由内核内部管理)

struct _IO_STATUS_BLOCK* UserIosb; // 用户空间的 IO_STATUS_BLOCK 指针(异步操作返回)
struct _KEVENT* UserEvent; // 用户空间的同步事件(供异步通知)

union {
struct {
union {
VOID (*UserApcRoutine)(VOID* Context, struct _IO_STATUS_BLOCK* IoStatus, ULONG Reserved);
// 用户APC回调函数指针 (异步完成通知用)
VOID* IssuingProcess; // 发起IRP请求的进程 (内核内部用)
};
VOID* UserApcContext; // APC回调上下文参数
} AsynchronousParameters;

union _LARGE_INTEGER AllocationSize; // (文件系统使用: 分配文件大小)
} Overlay;

VOID (*CancelRoutine)(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
// 取消时调用的回调函数

VOID* UserBuffer; // 📌用户空间缓冲区 (Direct I/O 模式下映射)

union {
struct {
union {
struct _KDEVICE_QUEUE_ENTRY DeviceQueueEntry; // 设备队列链表节点
VOID* DriverContext[4]; // 驱动扩展上下文数据 (驱动自由使用)
};

struct _ETHREAD* Thread; // 所属线程对象指针
CHAR* AuxiliaryBuffer; // 辅助缓冲区 (文件系统扩展使用)
struct _LIST_ENTRY ListEntry; // 通用链表节点 (供I/O管理器组织IRP列表)

union {
struct _IO_STACK_LOCATION* CurrentStackLocation; // 📌当前IRP栈帧 (IO_STACK_LOCATION)
ULONG PacketType; // 内部用标志
};

struct _FILE_OBJECT* OriginalFileObject; // 原始文件对象
} Overlay;

struct _KAPC Apc; // (特殊用法: 内核APC控制结构体)
VOID* CompletionKey; // (完成端口/队列扩展用)
} Tail;
};

I/O 层叠栈

由于 Windows 的设备对象组成了一个 I/O 层叠栈(Stacked Device Stack)的结构,因此 IRP 为了能够在按照 I/O 层叠栈(Stacked Device Stack)的结构回的调对应 IRP 派发函数传参,因此其内部也是一个类似堆栈的结构:

  • StackCount:总共有多少个设备对象参与 IRP 派发(即设备栈深度)。通常等于最上层 DeviceObject 的 StackSize
  • CurrentLocation:当前 IRP 正处于第几层派发阶段。每调用 IoSkipCurrentIrpStackLocation()IoCallDriver() 时自动递减。
  • Tail.Overlay.CurrentStackLocation:指向当前设备对象的 IO_STACK_LOCATION 结构,记录当前派发层级的参数、控制信息、IRP 参数(如 I/O 控制码、读写长度等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
IRP 栈状态:
StackCount = 5 ← 栈帧总数
CurrentLocation = 3 ← 当前派发进度
CurrentStackLocation → IO_STACK_LOCATION 3 (Filter1)

设备对象堆叠对应关系:
──────────────────────────────────────
DeviceObject_Filter3 ←→ IO_STACK_LOCATION 5 (派发已完成)
DeviceObject_Filter2 ←→ IO_STACK_LOCATION 4 (派发已完成)
DeviceObject_Filter1 ←→ IO_STACK_LOCATION 3 ← 当前派发 (CurrentStackLocation 所在位置)
DeviceObject_Functional(FDO) ←→ IO_STACK_LOCATION 2 (等待后续派发)
DeviceObject_PDO ←→ IO_STACK_LOCATION 1 (等待后续派发)
──────────────────────────────────────

很多关于 IRP 结构体的 API 本质上就是在操作这三个字段:

API 函数 作用 本质操作的字段变化
IoGetCurrentIrpStackLocation() 获取当前派发栈帧指针 返回 CurrentStackLocation
IoGetNextIrpStackLocation() 获取下一个栈帧指针(仅指针偏移,不修改位置) 返回 CurrentStackLocation - 1
IoSetNextIrpStackLocation() 手动推进派发位置(很少用) CurrentLocation--CurrentStackLocation--
IoSkipCurrentIrpStackLocation() 抵消 IoCallDriver 函数内部的“推进派发位置”的操作,使得下一层设备对象仍然处理当前栈帧 CurrentLocation++CurrentStackLocation++

参数结构

IO_STACK_LOCATION 中存储了参数信息,由于是所有类型的 IRP 派发函数公用,因此内部有一个联合体记录了每种类型的 IRP 派发函数对应的参数结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 0x24 bytes (sizeof)
struct _IO_STACK_LOCATION
{
UCHAR MajorFunction; // 0x0 📌IRP 主功能码 (IRP_MJ_*),驱动派发逻辑的核心依据
UCHAR MinorFunction; // 0x1 IRP 子功能码 (IRP_MN_*),配合 MajorFunction 做更精细的区分
UCHAR Flags; // 0x2 控制标志,部分操作行为控制(如 SL_OVERRIDE_VERIFY_VOLUME 等)
UCHAR Control; // 0x3 I/O 子系统内部控制标志

// 📌参数联合体:根据不同的 MajorFunction 类型,使用对应的子结构
union
{
struct // IRP_MJ_CREATE 使用
{
struct _IO_SECURITY_CONTEXT* SecurityContext; // 0x4 安全上下文
ULONG Options; // 0x8 创建选项标志 (如 FILE_DIRECTORY_FILE 等)
USHORT FileAttributes; // 0xC 文件属性 (如 FILE_ATTRIBUTE_NORMAL)
USHORT ShareAccess; // 0xE 共享模式 (如 FILE_SHARE_READ)
ULONG EaLength; // 0x10 EA长度 (扩展属性)
} Create;

struct // IRP_MJ_CREATE_NAMED_PIPE 使用
{
struct _IO_SECURITY_CONTEXT* SecurityContext;
ULONG Options;
USHORT Reserved;
USHORT ShareAccess;
struct _NAMED_PIPE_CREATE_PARAMETERS* Parameters; // 0x10 命名管道专用参数
} CreatePipe;

struct // IRP_MJ_CREATE_MAILSLOT 使用
{
struct _IO_SECURITY_CONTEXT* SecurityContext;
ULONG Options;
USHORT Reserved;
USHORT ShareAccess;
struct _MAILSLOT_CREATE_PARAMETERS* Parameters;
} CreateMailslot;

struct // IRP_MJ_READ 使用
{
ULONG Length; // 0x4 读取长度
ULONG Key; // 0x8 用于文件系统校验等用途
union _LARGE_INTEGER ByteOffset; // 0xC 读取偏移
} Read;

struct // IRP_MJ_WRITE 使用
{
ULONG Length;
ULONG Key;
union _LARGE_INTEGER ByteOffset;
} Write;

struct // IRP_MJ_DEVICE_CONTROL 使用
{
ULONG OutputBufferLength; // 0x4 输出缓冲区长度
ULONG InputBufferLength; // 0x8 输入缓冲区长度
ULONG IoControlCode; // 0xC IOCTL 控制码
VOID* Type3InputBuffer; // 0x10 输入缓冲区指针 (IOCTL第三类缓冲模式用)
} DeviceIoControl;

struct // IRP_MJ_FILE_SYSTEM_CONTROL (部分)
{
ULONG OutputBufferLength;
ULONG InputBufferLength;
ULONG FsControlCode;
VOID* Type3InputBuffer;
} FileSystemControl;

// 其它子结构太多,简化列出部分常用:
struct { ULONG Length; } SetEa;
struct { ULONG Length; enum _FILE_INFORMATION_CLASS FileInformationClass; } QueryFile;
struct { enum _DEVICE_RELATION_TYPE Type; } QueryDeviceRelations;
struct { struct _POWER_SEQUENCE* PowerSequence; } PowerSequence;
struct { struct _SCSI_REQUEST_BLOCK* Srb; } Scsi;
struct { VOID* Argument1; VOID* Argument2; VOID* Argument3; VOID* Argument4; } Others;
// 还有很多其它子结构,类似逻辑。
} Parameters; // 0x4 主参数区

struct _DEVICE_OBJECT* DeviceObject; // 0x14 当前派发到的设备对象 (本层目标设备)
struct _FILE_OBJECT* FileObject; // 0x18 当前关联的文件对象 (通常在文件操作中使用)

LONG (*CompletionRoutine)(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp, VOID* Context);
// 0x1C 完成例程回调函数指针 (用于注册完成回调逻辑)

VOID* Context; // 0x20 完成例程上下文参数 (传入 CompletionRoutine)
};

通常我们会使用 IoGetCurrentIrpStackLocationIRP 结构体中拿当前栈帧对应的 IO_STACK_LOCATION 参数。当然也可以通过 IoGetNextIrpStackLocation 获取下一层栈帧对应的 IO_STACK_LOCATION 参数或者手动解析 IRP 结构体拿任意一层的参数。

不过下一层的 IO_STACK_LOCATION 默认是空白的。如果我们作为过滤驱动,需要通过 IoCopyCurrentIrpStackLocationToNext 函数将当前栈帧中的参数拷贝到下一层才能让下一层的设备对象对应的驱动在当前栈帧中拿到参数。

而如果是底层设备栈的第一层(创建 IRP 时)则内核早已填好栈帧了,不会有这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FORCEINLINE
VOID IoCopyCurrentIrpStackLocationToNext(
_Inout_ PIRP Irp // [输入输出] 目标 IRP 指针
)
{
//
// 获取当前栈帧 (当前派发层)
//
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);

//
// 获取下一层栈帧 (即 IoCallDriver 下一次派发使用的栈帧)
//
PIO_STACK_LOCATION nextIrpSp = IoGetNextIrpStackLocation(Irp);

//
// 拷贝当前栈帧内容到下一栈帧 (注意只拷贝到 CompletionRoutine 之前的字段)
// 即:将本层参数 (如 MajorFunction、Parameters、FileObject 等) 直接传递给下一层
//
RtlCopyMemory(
nextIrpSp,
irpSp,
FIELD_OFFSET(IO_STACK_LOCATION, CompletionRoutine) // 仅拷贝到 CompletionRoutine 之前
);

//
// 清空 Control 字段,保证新派发时控制标志干净
//
nextIrpSp->Control = 0;
}

另外通过 IoSkipCurrentIrpStackLocation 跳过当前设备对象的话也是同样的效果。

返回结构

对于参数传递,IRP 针对每层的设备对象都有对应的 IO_STACK_LOCATION,然而对于返回值,所有层最终共用同一个 IoStatus 返回区和缓冲区,只要有一层完成请求,设置好 IoStatus.StatusIoStatus.Information 即可。

IoStatusIRP 结构体中的一个成员,该成员类型为 IO_STATUS_BLOCK,定义如下:

1
2
3
4
5
6
7
8
9
10
// 0x8 bytes (sizeof)
struct _IO_STATUS_BLOCK
{
union
{
LONG Status; // IRP 完成状态码 (NTSTATUS),表示 I/O 请求执行结果,供内核/用户态读取
VOID* Pointer; // 可选指针 (某些异步/特殊I/O场景用,极少用到)
};
ULONG Information; // 通常存放返回的字节数(如 Read/Write 实际传输数据长度),供用户态API返回
};

其中 Status 最终会变成 Win32 API 的返回值,而 Information 会变成 Win32 API 返回的输出字节数。至于输出的数据的存放位置,这个取决于 I/O 缓冲区机制

I/O 缓冲模式 数据缓冲区位置 数据写入哪
Buffered I/O (DO_BUFFERED_IO) Irp->AssociatedIrp.SystemBuffer 驱动填充 SystemBuffer,内核在 IoCompleteRequest() 时拷贝回用户缓冲区
Direct I/O (DO_DIRECT_IO) Irp->MdlAddress(MDL映射的缓冲区) 驱动使用 MmGetSystemAddressForMdlSafe() 获得内核虚拟地址,直接写入用户缓冲区映射
Neither I/O Irp->UserBuffer(直接原始用户地址) 驱动直接操作用户缓冲区(前提是地址合法性自己负责验证)

I/O 缓冲区机制

在驱动开发中,3 环用户程序与 0 环内核驱动需要频繁交换数据。交换数据的过程中需要内核I/O管理器在两者之间做好地址转换、访问隔离、安全控制,为此 Windows 提供了 3 种 I/O 缓冲区机制

  • Buffered I/O(系统缓冲 I/O)
  • Direct I/O(直接 I/O)
  • Neither I/O(无缓冲 I/O)

I/O 缓冲区机制设置

在 Windows 内核中,用户态与内核态的数据传递的应用主要有两种场景:

  • 普通读写:IRP_MJ_READ / IRP_MJ_WRITE
  • 设备控制:IRP_MJ_DEVICE_CONTROL

这两种场景的 I/O 缓冲区机制的设置方法是不同的。

  • IRP_MJ_READ / IRP_MJ_WRITE 这种类型的 IRP 派发函数主要由设备对象 Flags 决定缓冲区模式。

    当用户调用 ReadFile() / WriteFile() 时,内核通过 IRP 派发到 IRP_MJ_READ / IRP_MJ_WRITE。此时内核用 DeviceObject->Flags 中的缓冲模式标志位 决定使用哪种缓冲机制:

    • DO_BUFFERED_IO:Buffered I/O(系统缓冲 I/O)
    • DO_DIRECT_IO:Direct I/O(直接 I/O)

    注意

    • DO_BUFFERED_IODO_DIRECT_IO 互斥,驱动在创建设备时只需二选一。
    • 如果两个标志都未设置,默认当做 Buffered I/O 处理。
    • IRP_MJ_READ / IRP_MJ_WRITE 不存在 Neither I/O(无缓冲 I/O)模式。
  • IRP_MJ_DEVICE_CONTROL 这种类型的 IRP 派发函数主要由 IOCTL 控制码决定缓冲区模式。

    WDK 提供的 CTL_CODE 宏用于在驱动开发中定义 IOCTL(Input/Output Control)和 FSCTL(File System Control)请求的控制码。控制码本质上是一个 32 位整数,四个参数共同编码出一个唯一的请求类型,供内核和驱动识别具体的控制命令。

    1
    2
    3
    #define CTL_CODE(DeviceType, Function, Method, Access) ( \
    ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
    )
    • DeviceType:设备类型代码,占用高 16 位(位 31-16)。如 FILE_DEVICE_UNKNOWNFILE_DEVICE_DISK 等。由微软规范分配。
    • Access:访问权限,占用 2 位(位 15-14),控制调用时用户需具备的访问权限。常见取值:
      • FILE_ANY_ACCESS (0):不做权限限制
      • FILE_READ_ACCESS (1):需要读权限
      • FILE_WRITE_ACCESS (2):需要写权限
      • FILE_READ_ACCESS | FILE_WRITE_ACCESS (3):需同时具备读写权限
    • Function:功能号,占用 12 位(位 13-2),表示具体的功能编号。
      • 取值范围:0 ~ 4095
      • 其中 0 ~ 2047 为微软保留,2048 ~ 4095 供厂商自定义。
      • 通常你自己写驱动时使用 2048 以上的数字定义私有控制码,避免与系统冲突。
    • Method:缓冲区传递方式,占用 2 位(位 1-0),指定 I/O 缓冲机制。对应四种传输模式:
      • METHOD_BUFFERED (0):Buffered I/O(系统缓冲 I/O)
      • METHOD_IN_DIRECT (1)/METHOD_OUT_DIRECT (2):输入走 Buffered I/O(系统缓冲 I/O);输出走 Direct I/O(直接 I/O)。
      • METHOD_NEITHER (3):Neither I/O(无缓冲 I/O)

Buffered I/O(系统缓冲 I/O)

Buffered I/O(系统缓冲 I/O)机制指的是系统在内核空间自动分配一个中间缓冲区SystemBuffer),用于驱动与用户态之间的数据交互。

说明用户缓冲区的缓冲 i/o 的关系图。

  • 输入数据: 用户数据从用户态缓冲区拷贝至 Irp->AssociatedIrp.SystemBuffer
  • 输出数据: 驱动程序在处理完成后将结果放入 SystemBuffer,完成请求时,内核自动从 SystemBuffer 将数据拷贝回用户态缓冲区。

这种方式对驱动开发者来说简单安全,避免了内核直接访问用户空间地址带来的复杂性,但是需要额外申请中间缓冲区和拷贝数据,效率较低,资源消耗较大,适用于小规模数据传输、配置参数交互。

Neither I/O(无缓冲 I/O)

Neither I/O(无缓冲 I/O)机制指的是内核不提供任何缓冲区转换或保护措施,驱动程序直接收到用户空间的原始虚拟地址指针(Irp->UserBuffer),需要自行探测、校验用户空间指针的合法性,风险较高。

在这种机制下,为了避免蓝屏驱动必须:

  • 主动调用 ProbeForRead() 探测读权限;ProbeForWrite() 探测写权限。
  • 使用 __try/__except 机制防止访问非法地址。
1
2
3
4
5
6
7
8
9
10
// Neither I/O 示例(需主动探测)
PUCHAR userBuffer = (PUCHAR)Irp->UserBuffer;
__try {
ProbeForWrite(userBuffer, dataSize, sizeof(UCHAR));
RtlCopyMemory(userBuffer, sourceData, dataSize);
Irp->IoStatus.Information = dataSize; // 返回实际数据长度
} __except(EXCEPTION_EXECUTE_HANDLER) {
status = GetExceptionCode();
Irp->IoStatus.Information = 0;
}

注意

微软官方建议:探测用户地址空间 只能用 ProbeForRead / ProbeForWrite ,不要用 MmIsAddressValid()。因为对于用户缓冲区,MmIsAddressValid() 既不可靠也不安全,在 IRQL >= DISPATCH_LEVEL 时调用还有可能蓝屏。

Direct I/O(直接 I/O)

Buffered I/O(系统缓冲 I/O)方式拷贝数据太慢,尤其是大数据量时(如磁盘、网卡、DMA)。而 Neither I/O(无缓冲 I/O)这种内核态访问用户缓冲区的方式,又必须保证该内存合法不会被换出物理内存,否则一旦页面失效,访问用户空间会蓝屏。

Direct I/O(直接 I/O)在某种程度上是对 Neither I/O(无缓冲 I/O)的改进。该机制确保内核将用户缓冲区锁定为物理内存页面,并创建一个MDL(Memory Descriptor List),驱动通过 Irp->MdlAddress 获取此MDL,再调用内核函数(如MmGetSystemAddressForMdlSafe()将物理内存页映射为驱动可访问的内核虚拟地址

说明使用 dma 的设备的用户缓冲区上的直接 i/o 的示意图。

MmGetSystemAddressForMdlSafe 函数原型如下:

1
2
3
4
PVOID MmGetSystemAddressForMdlSafe(
IN PMDL Mdl, // [输入] 指向内存描述符列表的指针
IN ULONG Priority // [输入] 分配页表映射时的优先级
);
  • Mdl:要映射的 MDL 地址(Memory Descriptor List)。通常是 IRP->MdlAddress,代表被 Direct I/O 锁定的用户缓冲区物理页链表。
  • Priority:映射操作时使用的内存分配优先级,指定内核在资源不足时如何处理:
    • NormalPagePriority — 普通优先级(标准使用,推荐)。
    • HighPagePriority — 高优先级,尽量尝试分配映射资源。
    • LowPagePriority — 低优先级,系统紧张时容易失败。
    • VeryLowPagePriority — 极低优先级,仅适用于诊断或特殊情况。

示例代码如下:

1
2
3
4
5
6
7
8
9
// Direct I/O 示例 (IRP_MJ_READ)
if (Irp->MdlAddress) {
PUCHAR pBuffer = (PUCHAR)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
if (pBuffer) {
// 直接访问 pBuffer,无需额外拷贝
RtlCopyMemory(pBuffer, sourceData, dataSize);
Irp->IoStatus.Information = dataSize; // 返回实际写入的数据长度
}
}

这种机制安全且效率高,适用于大量数据传输(磁盘、网络、DMA设备)和性能要求较高的驱动程序。

IRP 派发函数

IRP 派发函数是驱动程序中专门处理各类 IRP 请求的回调函数。当 I/O 管理器收到用户或内核发起的 I/O 请求时,系统会根据 IRP 的 MajorFunction 字段,自动把 IRP 分发到对应的派发函数。

IRP 派发函数类别

所有 IRP 派发函数的入口存放在 PDRIVER_OBJECT 结构体内的 MajorFunction[] 数组中:

1
2
3
4
5
typedef struct _DRIVER_OBJECT {
...
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
...
} DRIVER_OBJECT, *PDRIVER_OBJECT;

其中 MajorFunction 是一个大小为 28 的函数指针数组,每个元素对应一个 IRP MajorFunction 编号,这些编号的宏定义如下:

IRP MajorFunction (值) 内核派发说明 Native API (Zw/Nt) Win32 API
IRP_MJ_CREATE (0x00) 创建 / 打开设备句柄 ZwCreateFile() / NtCreateFile() CreateFile()
IRP_MJ_CREATE_NAMED_PIPE (0x01) 命名管道专用 ZwCreateNamedPipeFile() CreateNamedPipe()
IRP_MJ_CLOSE (0x02) 关闭设备句柄 ZwClose() CloseHandle()
IRP_MJ_READ (0x03) 读取设备数据 ZwReadFile() ReadFile()
IRP_MJ_WRITE (0x04) 写入设备数据 ZwWriteFile() WriteFile()
IRP_MJ_QUERY_INFORMATION (0x05) 查询文件/设备信息 ZwQueryInformationFile() GetFileInformationByHandle()
IRP_MJ_SET_INFORMATION (0x06) 设置文件/设备信息 ZwSetInformationFile() SetFileInformationByHandle()
IRP_MJ_QUERY_EA (0x07) 查询扩展属性 (EA) ZwQueryEaFile() 无直接 API
IRP_MJ_SET_EA (0x08) 设置扩展属性 (EA) ZwSetEaFile() 无直接 API
IRP_MJ_FLUSH_BUFFERS (0x09) 刷新缓存区 ZwFlushBuffersFile() FlushFileBuffers()
IRP_MJ_QUERY_VOLUME_INFORMATION (0x0A) 查询卷信息 ZwQueryVolumeInformationFile() GetVolumeInformation()
IRP_MJ_SET_VOLUME_INFORMATION (0x0B) 设置卷信息 ZwSetVolumeInformationFile() 无直接 API
IRP_MJ_DIRECTORY_CONTROL (0x0C) 目录操作 ZwQueryDirectoryFile() FindFirstFile() / FindNextFile()
IRP_MJ_FILE_SYSTEM_CONTROL (0x0D) 文件系统控制 ZwFsControlFile() 无直接 API
IRP_MJ_DEVICE_CONTROL (0x0E) 设备控制(IOCTL) ZwDeviceIoControlFile() DeviceIoControl()
IRP_MJ_INTERNAL_DEVICE_CONTROL (0x0F) 内部设备控制 内核内部
IRP_MJ_SHUTDOWN (0x10) 关机通知 ZwShutdownSystem()
IRP_MJ_LOCK_CONTROL (0x11) 锁控制 ZwLockFile() / ZwUnlockFile() LockFile() / UnlockFile()
IRP_MJ_CLEANUP (0x12) 句柄清理 (Close 前触发) 自动派发 CloseHandle()(间接)
IRP_MJ_CREATE_MAILSLOT (0x13) 创建邮件槽 ZwCreateMailslotFile() CreateMailslot()
IRP_MJ_QUERY_SECURITY (0x14) 查询安全信息 ZwQuerySecurityObject() GetSecurityInfo()
IRP_MJ_SET_SECURITY (0x15) 设置安全信息 ZwSetSecurityObject() SetSecurityInfo()
IRP_MJ_POWER (0x16) 电源管理 内核电源管理
IRP_MJ_SYSTEM_CONTROL (0x17) WMI控制 WMI子系统派发 WMI系列API
IRP_MJ_DEVICE_CHANGE (0x18) 设备插拔通知 自动派发 RegisterDeviceNotification() (部分场景)
IRP_MJ_QUERY_QUOTA (0x19) 查询磁盘配额 ZwQueryQuotaInformationFile()
IRP_MJ_SET_QUOTA (0x1A) 设置磁盘配额 ZwSetQuotaInformationFile()
IRP_MJ_PNP (0x1B) 即插即用 PnP子系统派发 设备管理器控制
IRP_MJ_PNP_POWER (0x1B) 历史兼容(已废弃别名) —— ——
IRP_MJ_MAXIMUM_FUNCTION (0x1B) 内核保留 —— ——

IRP 派发函数注册

每个 IRP 派发函数都有统一的标准函数签名:

1
2
3
4
NTSTATUS DispatchFunction(
PDEVICE_OBJECT DeviceObject, // [输入] 当前被调用的设备对象
PIRP Irp // [输入] 当前要处理的 IRP 请求包
);

我们需要在 DriverEntry() 中定义该类型的函数,并将其注册到 MajorFunction[] 数组中。

1
2
3
4
5
DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoControl;

注意

最少也要注册 IRP_MJ_CREATE / IRP_MJ_CLOSE / IRP_MJ_DEVICE_CONTROL,否则基本无法和用户层通信。

  • IRP_MJ_CREATEIRP_MJ_DEVICE_CONTROL 保证驱动能和用户交互,
  • IRP_MJ_CLOSE 保证驱动能安全退出。

IRP 派发函数实现

IRP 派发函数基本都是如下过程:取参数 → 业务逻辑处理 → 设置返回值 → 完成请求

IRP_MJ_DEVICE_CONTROL 为例,常见的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
NTSTATUS MyDispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
ULONG_PTR info = 0;

ULONG controlCode = irpSp->Parameters.DeviceIoControl.IoControlCode;

switch (controlCode)
{
case IOCTL_MY_CUSTOM_COMMAND:
{
// 示例:读取输入缓冲区内容
PUCHAR inputBuffer = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;
ULONG inputLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;

// 业务逻辑处理...

status = STATUS_SUCCESS;
info = 0; // 需要返回的数据长度(若无数据返回则填 0)
break;
}

default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}

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

其中关于最后的完成机制

  • Irp->IoStatus.StatusIrp->IoStatus.InformationIRP 完成结果

    • IoStatus.Status —— 你要告诉 I/O 管理器,这次 IRP 处理的结果:
      • 成功:STATUS_SUCCESS
      • 失败:其他各种 NTSTATUS 错误码
    • IoStatus.Information —— 返回给用户的附加数据长度(如:读取了多少字节、写入了多少字节、DeviceIoControl 输出了多少数据)

    提示

    这两个值并不会自动产生,而是你驱动在派发函数中自己填写进去的。否则内核无法知道你的处理结果。

  • IoCompleteRequest() 的作用 —— 提交完成、唤醒上层

    1
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    这个 API 的职责是通知内核:这个 IRP 完成了,内核会:

    1. 清理 IRP
    2. 唤醒等待该 IRP 完成的用户线程(如果是同步 I/O)
    3. 让用户态的 ReadFile()WriteFile()DeviceIoControl() 等 API 得到返回
    4. 把刚才填的 IoStatus.StatusInformation 传回用户态 API

    注意

    没有调用 IoCompleteRequest(),IRP 永远不会完成,用户线程会一直挂死等待。

  • return status 是内核派发器 IofCallDriver() 内部会接收你的派发函数返回值(也就是 return status)。但注意:最终送到用户态的结果,并不是这个返回值,而是 Irp->IoStatus.Status

IRP 派发过程

IRP 派发(IRP Dispatch)是 Windows 内核向驱动程序发送 I/O 请求的核心机制。每个驱动通过注册的派发函数处理特定类型的 IRP(如 READWRITEDEVICE_CONTROL 等),整个请求沿 I/O 层叠栈(Stacked Device Stack)自顶向下传递。

提示

物理设备附加链(AttachedDevice 链)本身不会直接参与 IRP 派发逻辑,但它确实影响了 IRP 堆栈的分配深度(StackCount)

IRP 派发是通过 IoCallDriver 函数实现的,该函数原型如下:

1
2
3
4
NTSTATUS IoCallDriver(
PDEVICE_OBJECT DeviceObject, // 目标设备对象(通常是下层驱动的 DeviceObject)
PIRP Irp // 要派发的 I/O 请求包(IRP)
);
  • DeviceObject:要将 IRP 派发给的目标设备对象(一般是下层的 Filter 或 Functional Device)。其对应的驱动由 DeviceObject->DriverObject 决定。
  • Irp:要派发的 I/O 请求数据包。调用本函数前,必须设置好 IRP 的栈帧位置(通常使用 IoSkipCurrentIrpStackLocation() 或手动设置 CurrentLocation--)。

IoCallDriver 将一个 IRP 派发到指定的设备对象所对应驱动的派发函数中。这是驱动中用于将 I/O 请求“传递”给下层驱动的标准方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#define IoCallDriver(a,b)   \
IofCallDriver(a,b)

NTSTATUS FASTCALL IofCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
if (pIofCallDriver != NULL) {

//
// 如果开启了 I/O 验证器(Verifier),则这里会跳转到其 Hook 函数 (如 IovCallDriver / IoPerfCallDriver)。
// 这些 Hook 模块用于在开发调试中插入额外的验证逻辑,辅助检测驱动Bug。
//
return pIofCallDriver(DeviceObject, Irp, _ReturnAddress());
}

// 否则直接走默认 I/O 派发实现
return IopfCallDriver(DeviceObject, Irp);
}

NTSTATUS FORCEINLINE IopfCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
PIO_STACK_LOCATION irpSp;
PDRIVER_OBJECT driverObject;
NTSTATUS status;

// 确认传入的确实是合法 IRP 对象
ASSERT( Irp->Type == IO_TYPE_IRP );

//
// 核心逻辑:派发 IRP 前先将当前栈位置往下移动一层 (推进派发深度)
//
Irp->CurrentLocation--;

// 栈溢出检查:若已经到底还继续派发,直接蓝屏(BugCheck)
if (Irp->CurrentLocation <= 0) {
KiBugCheck3(NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR) Irp, 0, 0);
}

//
// 计算新的栈帧指针:CurrentStackLocation 始终指向当前要派发的栈帧
//
irpSp = IoGetNextIrpStackLocation(Irp);
Irp->Tail.Overlay.CurrentStackLocation = irpSp;

//
// 记录本层设备对象指针,供下层驱动获取自身 DeviceObject
// 通常派发 IRP 时下层驱动通过 irpSp->DeviceObject 知道自己是谁
//
irpSp->DeviceObject = DeviceObject;

//
// 获取目标驱动对象
//
driverObject = DeviceObject->DriverObject;

//
// 执行派发:调用目标驱动中对应的 MajorFunction 派发入口
// irpSp->MajorFunction 记录了当前 IRP 的操作类型 (如 IRP_MJ_READ / IRP_MJ_WRITE 等)
//
status = driverObject->MajorFunction[irpSp->MajorFunction](DeviceObject, Irp);

return status;
}

IoCallDriver 函数的实现来看,该函数会:

  1. 推进 IRP 栈帧 CurrentLocation--Tail.Overlay.CurrentStackLocation--
  2. 调用下层驱动的的 IRP 派发函数 DeviceObject->DriverObject->MajorFunction[Irp->Tail.Overlay.CurrentStackLocation->MajorFunction]

而我们知道对于 IRP:

  • 如果是底层设备栈的第一层(创建 IRP 时)则内核早已填好栈帧了。
  • 而下一层的 Tail.Overlay.CurrentStackLocation 默认是空白的。

因此我们需要调用 IoCopyCurrentIrpStackLocationToNext 将当前 IRP 栈帧中的数据拷贝到下一层。这样才能确保过滤驱动不会影响到下层驱动的传参。

然而调用 IoCallDriver 函数会导致 IRP 堆栈减少一层,而如果我们没有事先做设备附加操作的话,可能会导致 IRP 堆栈层数不够造成蓝屏:

1
2
3
4
5
6
7
8
9
//
// 核心逻辑:派发 IRP 前先将当前栈位置往下移动一层 (推进派发深度)
//
Irp->CurrentLocation--;

// 栈溢出检查:若已经到底还继续派发,直接蓝屏(BugCheck)
if (Irp->CurrentLocation <= 0) {
KiBugCheck3(NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR) Irp, 0, 0);
}

我们可以通过 IoSetNextIrpStackLocationCurrentLocation++Tail.Overlay.CurrentStackLocation++ 来抵消 IoCallDriver 函数的影响。因此更通用的写法为:

1
2
3
4
5
6
7
8
9
NTSTATUS MyDispatchIoctl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
// 1. 可选处理参数
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);

// 2. 转发给下层
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(LowerDeviceObject, Irp);
}
  • Title: windows 驱动基础
  • Author: sky123
  • Created at : 2022-09-28 11:45:14
  • Updated at : 2025-07-05 01:11:49
  • Link: https://skyi23.github.io/2022/09/28/windows 驱动基础/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments