windows内核驱动开发

在现代操作系统开发领域,Windows内核开发 是一项涉及系统底层机制的重要技术。本篇文章将围绕 Windows内核架构 和 驱动开发 展开讨论,旨在为读者提供一个全面而深入的学习路径,尤其适合对内核编程和设备驱动开发感兴趣的技术人员。

IO系统

I/O(输入/输出)系统通过抽象化简化了逻辑和物理设备的交互,许多 I/O 操作是在操作系统的执行层中进行的。该系统具备统一命名和安全机制,支持异步通信、即插即用功能、驱动程序的动态加载和卸载以及电源管理等多种重要功能

image.png
上图为 I/O 系统组件的层次结构,应用程序和服务负责与用户交互。中间层是用户模式和内核模式的接口,包括 WMI 服务、PnP 管理器以及安装组件等。WMI 例程、I/O 管理器、PnP 管理器和电源管理器处于内核模式,负责处理 I/O 操作、即插即用设备的管理和电源管理。最底层是驱动程序和硬件抽象层 (HAL),实现硬件与软件之间的通信

硬件抽象层 (HAL): 硬件抽象层(HAL,Hardware Abstraction Layer)是操作系统中的一个关键组件,负责将操作系统与底层硬件设备隔离开来。它通过提供一种标准化的接口,使得操作系统能够与各种硬件平台交互,而不必针对特定硬件进行修改和优化

用户模式与内核模式

image.png
当cpu在执行时,会记录当前程序的权限级别,上图是基于x86架构的,Ring3是最小权限环,在这一环里有很多限制,比如说不能设置cr3寄存器,不能和硬件外设交互,不能执行HLT之类的,当软件在这一环上运行,需要和系统进行交互时,就会转换到Ring0,这里还有Ring2和Ring1,最初它们是为设备驱动准备的,区分了不同的访问级别,但是很少会用到,Ring0是主管模式,在这一环里是没有限制的,可以做任何事,这也是内核运行的地方

Ring 3:用户模式,权限最低,限制较多,无法访问CR3等内核模式寄存器,无法执行HLT指令等。
Ring 0:内核模式,权限最高,可以执行任何指令和访问所有寄存器。
Ring -1:管理模式(主要用于虚拟化),可以拦截敏感操作,确保虚拟机中的用户内核无法无限制地访问主机硬件。

Ring -1

还有一个Ring -1环,但是内核是在Ring 0环的,随着虚拟机的兴起,管理模式的特权开始引发问题。虚拟机的“用户”内核不应该能够无限制地访问主机的物理硬件,Ring -1,管理程序模式。能够拦截用户执行的敏感 Ring 0 操作并在主机操作系统中处理它们

设备驱动程序

设备驱动程序是操作系统和硬件设备之间的桥梁,主要分为用户模式和内核模式。用户模式驱动程序通常应用于较简单的设备(如打印机),并通过 UMDF 框架运行。内核模式驱动程序则负责更为核心的功能,如文件系统、即插即用设备和其他非即插即用设备的管理。设备驱动程序作为可加载模块,通过与内核集成,确保第三方硬件可以与操作系统无缝协作

UMDF:UMDF(User-Mode Driver Framework,用户模式驱动程序框架)是微软为 Windows 操作系统提供的一种框架,用于编写在用户模式下运行的设备驱动程序。与传统的内核模式驱动程序不同,UMDF 驱动程序在用户模式下运行,主要用于一些低风险、低复杂度的设备

image.png
上图展示了从用户模式到内核模式调用设备驱动程序的过程。当应用程序请求读取文件时,它通过用户模式的 API(如 ReadFile)调用,最终通过系统调用机制(如 sysentersyscall)切换到内核模式。在内核模式下,将执行进一步的处理,调用对应的驱动程序函数(如 NtReadFile),并通过驱动程序 driver.sys 执行底层硬件的 I/O 操作

设备访问

为了访问设备,应用程序必须通过 CreateFile 或类似函数打开一个设备句柄。这些函数可以用于用户模式和内核模式下的设备访问,但名称格式和路径指向的是设备的符号链接,而非传统的文件路径。在计算机中,必须使用特定格式(如 \\.\name)来访问设备

这里用winobj展示,WinObj 是 Windows 内部工具之一,它是一个 GUI 工具,用于显示和管理 Windows 操作系统中的对象管理器命名空间。对象管理器是 Windows 内核的一部分,负责管理系统中的各种对象(如设备、文件、注册表项、符号链接、命名管道等)

winobj下载地址:

https://learn.microsoft.com/en-us/sysinternals/downloads/winobj

image.png
GLOBAL?? 是一个特殊目录,通常用于定义设备符号链接,方便用户模式程序访问内核对象。在 Windows 中,设备符号链接是用于提供设备的用户友好名称(例如 COM1),使得用户模式应用程序可以通过符号链接而不是设备名称直接访问设备,例如 C: 盘符被链接到 \Device\HarddiskVolume3

image.png
在Device目录下的这些长名称实际上是硬件设备的符号链接,用于将系统中的逻辑路径映射到底层的物理设备,这里用代码简单演示调用设备的过程

Device 目录直接包含设备对象,每个设备在创建时会在 Device 目录中注册。内核模式驱动程序通常通过设备对象与系统和用户模式程序通信。Device 目录下的对象表示设备在内核空间中的具体表示,通常不直接对用户模式程序可见。

image.png
假如我要创建访问这个Psched设备

#include <Windows.h>
#include <stdio.h>

int main() {
    HANDLE hDevice = CreateFile(L"\\\\.\\Pcsched", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);

    CloseHandle(hDevice);
}

这行代码 HANDLE hDevice = CreateFile(L"\\\\.\\Psched", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); 试图打开一个设备或文件句柄。路径 \\\\.\\Psched 是设备或文件的符号链接,\\\\. 前缀表示访问设备对象,之后使用 CloseHandle(hDevice) 关闭句柄,释放句柄

CreateFile 是 Windows API 中用于创建或打开文件或设备对象的函数。该函数不仅用于处理文件系统中的文件,还可用于打开与设备(如磁盘驱动器、串口设备等)相关的句柄。在使用 CreateFile 操作设备时,通常需要指定 OPEN_EXISTING,表示仅打开已经存在的设备或文件,而不会创建新的设备

HANDLE CreateFile(
    LPCTSTR lpszName,            // 文件或设备的名称
    DWORD fdwAccess,             // 访问模式 (读取、写入等)
    DWORD fdwShareMode,          // 共享模式
    LPSECURITY_ATTRIBUTES lpsa,  // 安全性和继承属性
    DWORD fdwCreate,             // 创建标志
    DWORD fdwAttributes,         // 文件属性和标志
    HANDLE hTemplateFile         // 要复制的文件属性的句柄
);

返回值是文件或设备的句柄,若操作失败,则返回 INVALID_HANDLE_VALUE,可以通过 GetLastError 获取具体的错误信息

IO操作

调用设备常用的四个函数:

ReadFile 用于从文件或设备句柄读取数据。参数包括文件句柄、数据存储的缓冲区、缓冲区的大小,以及指向实际读取字节数的指针

BOOL ReadFile(
    HANDLE hFile,          // 文件句柄
    LPVOID lpBuffer,       // 接收数据的缓冲区
    DWORD nBufferSize,     // 缓冲区大小
    LPDWORD lpBytesRead,   // 实际读取的字节数
    LPOVERLAPPED lpOverlapped // 用于异步 I/O
);

WriteFile 用于将数据写入文件或设备。参数包括文件句柄、指向要写入数据的缓冲区、缓冲区的大小,以及指向实际写入字节数的指针

BOOL WriteFile(
    HANDLE hFile,          // 文件句柄
    LPCVOID lpBuffer,      // 要写入设备的缓冲区
    DWORD nBufferSize,     // 缓冲区大小
    LPDWORD lpBytesWritten,// 实际写入的字节数
    LPOVERLAPPED lpOverlapped // 用于异步 I/O
);

DeviceIoControl 是 Windows 中用于与设备驱动程序进行交互的重要 API。通过这个函数,应用程序可以向设备发送 I/O 控制请求(IOCTL),从而执行特定的设备操作。这些操作超出了标准的读写功能,通常包括一些硬件特定的控制功能

例如,调用 DeviceIoControl 可以用来获取设备状态信息、执行设备特定的操作,或将控制命令发送到硬件设备。参数中最关键的是 dwIoControlCode,它决定了请求的类型,而输入和输出缓冲区提供了操作所需的额外数据或接收设备的响应

BOOL DeviceIoControl(
    HANDLE hDevice,              // 设备或文件的句柄
    DWORD dwIoControlCode,       // IOCTL 控制码
    LPVOID lpInBuffer,           // 输入缓冲区
    DWORD nInBufferSize,         // 输入缓冲区大小
    LPVOID lpOutBuffer,          // 输出缓冲区
    DWORD nOutBufferSize,        // 输出缓冲区大小
    LPDWORD lpBytesReturned,     // 实际返回的字节数
    LPOVERLAPPED lpOverlapped    // 用于异步操作
);

CloseHandle 是一个 Windows API,用于关闭文件、设备或其他内核对象的句柄。句柄是指向特定系统资源的引用,当不再需要该资源时,必须调用 CloseHandle 来释放

BOOL CloseHandle(HANDLE hFile); // 关闭内核对象句柄

设备驱动程序

内核设备驱动程序是 Windows 操作系统中运行在内核模式下的程序,它们与硬件设备直接交互。与用户模式程序不同,内核驱动程序有更高的权限,能够访问系统的核心资源。因此,它们如果出现未处理的异常,会导致整个系统崩溃(即蓝屏)。这些驱动通常以 .SYS 作为文件扩展名,并通过系统 API 函数(如 ReadFileWriteFileDeviceIoControl)与用户模式程序交互。驱动程序还会导出多个功能入口点,供操作系统调用以完成设备管理和控制

这里用process explorer演示,下载地址:

https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer

选择system,然后查看dll

image.png
在上图可以看到加载到系统空间中的所有设备驱动程序

即插即用设备程序

即插即用驱动程序的目的是与即插即用(PnP)管理器和电源管理器进行通信,从而在硬件设备连接到系统时自动识别、配置并加载相应的驱动程序。PnP 驱动程序分为三类:功能驱动程序、总线驱动程序和过滤驱动程序

功能驱动程序 是与硬件设备直接交互的主要驱动程序,负责设备的管理。
总线驱动程序 管理连接多个设备的总线,例如 PCI 或 USB。
过滤驱动程序 则可以在驱动堆栈中拦截和修改 I/O 请求,以执行额外的处理。

Windows 驱动模型 (WDM)

Windows 驱动模型 (WDM) 是为解决不同 Windows 版本之间的驱动程序兼容性问题而设计的驱动程序开发框架。它使得开发者可以为多种设备(如 PCI 设备、USB 设备等)编写通用的驱动程序,且在 Windows 98/ME 和 Windows 2000/XP 之间共享代码,减少开发和维护的复杂性。

WDM 驱动程序支持的设备类型非常广泛,并且具有向后兼容性和可扩展性,支持未来的总线和设备标准,文件系统驱动和视频驱动并不包含在 WDM 模型中,需要使用其他专门的驱动模型来开发

Windows 驱动框架 (WDF)

Windows 驱动框架 (WDF) 是为简化和统一 Windows 驱动程序开发而引入的一种框架。它分为 KMDF 和 UMDF,分别用于内核模式和用户模式驱动程序开发。KMDF 是 WDM(Windows 驱动模型)的进化和替代品,提供了更简洁、更模块化的驱动开发方式

WDF 的特点包括基于对象的编程模型,开发者只需关注事件处理而无需处理底层细节。同时,它还提供了即插即用 (PnP) 和电源管理的自动实现,使驱动开发更加高效。KMDF 具有向后兼容性,支持在旧版 Windows 上运行,同时也支持版本控制,使驱动可以在多个 Windows 版本之间并行运行

安装windows内核开发环境

visual studio 2022下载地址:

https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?sku=Community&channel=Release&version=VS2022&source=VSLandingPage&cid=2030&passive=false

安装图中勾选的内容

image.png
wdk下载地址:

https://learn.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk

image.png
或者直接在安装界面安装

image.png
然后安装 Spectre 缓解库

image.png
安装完成后,可在“新建项目”窗口中搜索“Windows Driver”。如果能够看到诸如“内核模式驱动程序(KMDF)”、“用户模式驱动程序(UMDF)”等相关项目模板,说明 WDK 已成功安装到 Visual Studio

image.png

windows 内核API

Windows 内核的大部分代码集中在 NtOsKrnl.exe 文件中,负责核心的系统操作和管理,Hal.dll 则包含硬件抽象层相关的部分实现,帮助系统与底层硬件交互。内核函数通常带有前缀,例如,前缀“Nt”代表 Windows 的基本服务,前缀“Ke”代表内核模式线程管理

常见API前缀:

Ex - 通用执行函数
Ke - 通用内核函数
Cc - 缓存控制(管理器)
Mm - 内存管理
Rtl - 通用运行库
FsRtl - 文件系统运行库
Flt - 文件系统迷你过滤器
Ob - 对象管理器
Io - 输入/输出管理器
Se - 安全
Ps - 进程结构
Po - 电源管理
Wmi - Windows 管理规范(WMI)
Zw - 本地 API 包装器
Hal - 硬件抽象层

在 Windows 内核中,Nt* 和 Zw* 函数提供了类似的功能,但在调用方式和安全性方面存在差异。通常情况下,Zw* 函数被推荐用于内核模式调用,因为它省去了某些安全检查,适合内核内部的调用需求

编写驱动程序

驱动程序的主要功能是提供接口,以便操作系统的 I/O 系统能够调用不同的例程来执行相应的任务。例如,DriverEntry 是驱动程序加载时的入口,ISR 处理硬件中断,而 Dispatch Routines 负责处理各种 I/O 请求

image.png
需要用到的内核头文件:

<ntddk.h> 是最全面的头文件,包含内核开发的各类定义和功能

<wdm.h> 则是一个精简的选项,仅包含 WDM(Windows 驱动模型)相关内容。对于特定类型的开发(如文件系统、网络驱动等),可以使用相应的专用头文件,如 <ntifs.h>(文件系统)和 <ndis.h>(网络驱动)

打开visual studio,新建项目,选择WDM Drive

image.png
点击下一步,名称和位置可以自己设置,最后点击创建

image.png
将Drive Files文件夹下的默认生成的文件删除,这个.inf文件的作用是用于描述驱动程序的安装过程。这个文件包含驱动的配置信息,包括版本、驱动类型、设备类、文件位置和其他必要信息,指导 Windows 如何将驱动程序安装到系统中

image.png
然后右击Source Files,选择添加,新建项

image.png
选择就可以开始编写驱动程序了,上面说过,每个驱动程序都有一个加载的入口,先编写一个驱动程序入口

image.png

DriveEntry

DriverEntry 是每个 Windows 驱动的入口函数,用于初始化驱动程序并配置其运行环境。通过 DriverEntry 函数,驱动程序可以访问系统传递的参数(如注册表路径),并设置自定义的初始化逻辑。该函数返回 STATUS_SUCCESS 表示初始化成功

#include <ntddk.h>  // 包含 Windows 内核开发头文件

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING /* RegistryPath */) {
    UNREFERENCED_PARAMETER(DriverObject);  // 标记 DriverObject 参数为未使用,避免编译器警告

    return STATUS_SUCCESS;  // 返回成功状态,表示驱动初始化成功
}

extern "C":此关键字用于告诉编译器使用 C 链接方式导出 DriverEntry 函数名,以确保函数名在编译后不会被修改

UNREFERENCED_PARAMETER(DriverObject);:这是一个宏,用于标记 DriverObject 参数未被使用。这样可以避免编译器发出未使用参数的警告

return STATUS_SUCCESS;:函数返回 STATUS_SUCCESS,表示驱动初始化成功。如果返回其他状态,驱动会被自动卸载

使用快捷键F7编译文件,可以看到程序的返回结果为成功编译

image.png

卸载程序

卸载程序是在驱动程序被卸载前调用的函数,用于清理资源并释放在 DriverEntry 中分配的任何内存或对象。该例程的目的是确保驱动程序在卸载时不会留下任何未释放的资源

void MyDriverUnload(PDRIVER_OBJECT DriverObject);

现在扩充之前写的代码

#include <ntddk.h>

void ProcessPowerUnload(PDRIVER_OBJECT);

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    KdPrint(("ProcessPower: DriverEntry\n"));
    KdPrint(("Registry path: %wZ\n", RegistryPath));

    DriverObject->DriverUnload = ProcessPowerUnload;

    RTL_OSVERSIONINFOW vi = { sizeof(vi) };
    NTSTATUS status = RtlGetVersion(&vi);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed in RtlGetVersion (0x%x)\n", status));
        return status;
    }

    return STATUS_SUCCESS;
}

void ProcessPowerUnload(PDRIVER_OBJECT) {
    KdPrint(("ProcessPower: Unload\n"));
}

代码解释:

void ProcessPowerUnload(PDRIVER_OBJECT) {
    KdPrint(("ProcessPower: Unload\n"));
}

这个函数是驱动程序的卸载程序。当驱动程序被卸载时调用该函数,用于执行资源清理和其他卸载操作。在这里,KdPrint 函数被用于向调试器输出日志信息 "ProcessPower: Unload\n"

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    KdPrint(("ProcessPower: DriverEntry\n"));
    KdPrint(("Registry path: %wZ\n", RegistryPath));

    DriverObject->DriverUnload = ProcessPowerUnload;

这是驱动程序的入口函数,DriverEntry 是每个 Windows 驱动程序的主初始化函数,在驱动加载时被系统调用。这个函数的主要作用是初始化驱动程序。具体执行的操作如下:

日志输出:使用 KdPrint 输出 "ProcessPower: DriverEntry\n",记录驱动加载的日志信息;然后输出注册表路径 RegistryPath 的内容

设置卸载例程:将 DriverObject->DriverUnload 设置为 ProcessPowerUnload,以便在驱动卸载时调用 ProcessPowerUnload 函数,进行清理操作

    RTL_OSVERSIONINFOW vi = { sizeof(vi) };
    NTSTATUS status = RtlGetVersion(&vi);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed in RtlGetVersion (0x%x)\n", status));
        return status;
    }

此段代码用于获取操作系统的版本信息:

定义 RTL_OSVERSIONINFOW 结构:vi 用于存储系统的版本信息,并将其大小设置为 sizeof(vi) 以确保结构体传递给 RtlGetVersion 时的正确性

调用 RtlGetVersion:获取系统的版本信息并将其存储在 vi 结构体中。如果调用失败,NT_SUCCESS 将返回 false

错误处理:如果 RtlGetVersion 失败,则通过 KdPrint 输出错误信息 "Failed in RtlGetVersion (0x%x)\n",并返回 status,停止驱动加载

return STATUS_SUCCESS;

如果所有操作都成功完成,DriverEntry 将返回 STATUS_SUCCESS,指示驱动程序初始化成功

使用F7快捷键编译,编译成功

image.png

安装与测试

生成的驱动文件在指定的目录下可以看到

image.png
将系统设置为测试签名模式以允许加载未签名的驱动程序,执行命令后需重启电脑

bcdedit /set testsigning on

image.png
安装驱动程序时,可以通过 sc create 命令或使用 CreateService API 注册驱动,而不必使用 INF 文件

sc create baimaopower type= kernel binPath= c:\Dir\Driver.sys

image.png
打开注册表可以看到系统注册的服务与驱动

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services

可以看到,编写的驱动已经成功安装了

image.png
分辨是用户空间的服务还是驱动程序可以看type字段,如果是数字1,2,3之类的数字代表的是驱动程序,如果是十六进制值那就是用户空间的服务

image.png

image.png
这个工具用于捕获和查看来自应用程序和驱动程序的调试输出

https://docs.microsoft.com/en-us/sysinternals/downloads/debugview

选择捕获内核的日志信息

image.png
现在执行驱动程序

sc start baimaopower

驱动成功运行

image.png
输出了指定的内容

image.png
使用process explorer查看,也可以看到驱动程序已经载入内核了

image.png
卸载驱动:

sc stop baimaopower

调度例程

在 Windows 驱动开发中,驱动对象(DRIVER_OBJECT)的初始化是加载驱动程序的重要步骤。除了 DriverUnload 例程外,驱动程序还需要通过设置 MajorFunction 指针数组来定义处理各种 I/O 操作的主调度函数(如读、写、设备控制等)

NTSTATUS DispatchRoutine(
    _Inout_ PDEVICE_OBJECT DeviceObject,
    _Inout_ PIRP Irp
);

不同的 I/O 请求会生成不同的主要功能代码(IRP_MJ_*)。驱动程序可以根据这些代码定义不同的处理函数,每一个代码对应一种操作类型,常见的如打开、关闭、读写、控制操作等。驱动程序通过设置 MajorFunction 函数指针数组来处理这些操作。以下是常见的主要功能代码:

IRP_MJ_CREATE

当应用程序或其他程序调用 CreateFile(或 ZwCreateFile)来打开驱动设备时,会生成此请求

IRP_MJ_CLOSE

当设备句柄被关闭(如调用 CloseHandleZwClose)时,会生成此请求

IRP_MJ_READ, IRP_MJ_WRITE

IRP_MJ_READ 用于读取数据,IRP_MJ_WRITE 用于写入数据,驱动程序需要根据这些请求访问设备数据。

IRP_MJ_DEVICE_CONTROL

处理 DeviceIoControl 发起的控制操作请求。应用程序使用 DeviceIoControl 与驱动程序进行特定的通信,发送自定义控制码。

这里还是用之前写的驱动程序来演示

#include <ntddk.h>

void ProcessPowerUnload(PDRIVER_OBJECT);

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS ProcessPowerDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp);

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    KdPrint(("ProcessPower: DriverEntry\n"));
    KdPrint(("Registry path: %wZ\n", RegistryPath));

    DriverObject->DriverUnload = ProcessPowerUnload;

    RTL_OSVERSIONINFOW vi = { sizeof(vi) };
    NTSTATUS status = RtlGetVersion(&vi);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed in RtlGetVersion (0x%x)\n", status));
        return status;
    }

    KdPrint(("Windows version: %u.%u.%u\n", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber));
    DriverObject->MajorFunction[IRP_MJ_CREATE] = ProcessPowerCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = ProcessPowerCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ProcessPowerDeviceControl;

    return STATUS_SUCCESS;
}

void ProcessPowerUnload(PDRIVER_OBJECT) {
    KdPrint(("ProcessPower: Unload\n"));
}

新增代码解释:

KdPrint(("Windows version: %u.%u.%u\n", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber));

viRTL_OSVERSIONINFOW 结构的一个实例,其中 dwMajorVersion 表示主版本号(例如 Windows 10 的主版本号为 10),dwMinorVersion 表示次版本号,dwBuildNumber 表示操作系统的构建版本。此行代码用于在内核调试器中显示系统的具体版本信息,便于调试驱动时了解当前运行环境

DriverObject->MajorFunction[IRP_MJ_CREATE] = ProcessPowerCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = ProcessPowerCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ProcessPowerDeviceControl;

IRP_MJ_CREATEIRP_MJ_CLOSE 的请求都指向 ProcessPowerCreateClose 函数。这意味着,当设备被创建或关闭时(如通过 CreateFileCloseHandle),驱动将调用 ProcessPowerCreateClose 函数来处理这些请求

IRP_MJ_DEVICE_CONTROL 请求指向 ProcessPowerDeviceControl 函数,这个函数用于处理设备控制请求(通常由 DeviceIoControl 触发)

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp);

声明了一个处理 IRP_MJ_CREATEIRP_MJ_CLOSE 请求的函数 ProcessPowerCreateClose, 该函数接受两个参数:

DeviceObject:指向请求的设备对象。

Irp:指向当前的 I/O 请求包(IRP)

该函数通常用于初始化或清理设备资源。例如,在创建设备时分配资源,在关闭时释放资源。它将决定请求是否成功完成,并返回相应的 NTSTATUS 状态

NTSTATUS ProcessPowerDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp);

声明了一个处理 IRP_MJ_DEVICE_CONTROL 请求的函数 ProcessPowerDeviceControl,此函数用于处理来自用户模式的控制请求,这些请求通常通过 DeviceIoControl 函数发送

DeviceObject:指向请求的设备对象。

Irp:指向当前的 I/O 请求包。

驱动程序可以在此函数中根据 DeviceIoControl 的控制码来执行不同的操作,例如获取设备状态或设置配置。返回的 NTSTATUS 值表示操作是否成功

驱动程序和设备对象

在 Windows 驱动开发中,DRIVER_OBJECTDEVICE_OBJECT 是两个核心的结构体,用于描述驱动程序和设备之间的关系:

image.png

DRIVER_OBJECT

这是一个内核数据结构,表示一个驱动程序的实例。它由系统在驱动加载时创建。DRIVER_OBJECT 包含了驱动程序的入口函数 DriverEntry、卸载函数 DriverUnload 以及主要的 I/O 操作函数(如打开、关闭、读写等)的指针。通过这个对象,系统能够管理驱动程序的生命周期并调用其导出函数。

DEVICE_OBJECT

DEVICE_OBJECT 是另一个内核数据结构,表示驱动程序创建的每一个设备实例。驱动程序可以为每个设备创建一个 DEVICE_OBJECT,并将其链接到 DRIVER_OBJECT 中,使得驱动程序能够区分不同的设备并为每个设备提供独立的操作。在链表结构中,多个 DEVICE_OBJECT 可以链接在一起,指向下一个设备对象,直到链表的最后一个设备对象指向 NULL

继续新增上面驱动程序内容,让驱动程序创建一个名为 \Device\BaimaoPower 的设备对象

#include <ntddk.h>

void ProcessPowerUnload(PDRIVER_OBJECT);

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS ProcessPowerDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp);

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    KdPrint(("ProcessPower: DriverEntry\n"));
    KdPrint(("Registry path: %wZ\n", RegistryPath));

    DriverObject->DriverUnload = ProcessPowerUnload;

    RTL_OSVERSIONINFOW vi = { sizeof(vi) };
    NTSTATUS status = RtlGetVersion(&vi);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed in RtlGetVersion (0x%x)\n", status));
        return status;
    }

    KdPrint(("Windows version: %u.%u.%u\n", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber));
    //DriverObject->MajorFunction[IRP_MJ_CREATE] = ProcessPowerCreateClose;
    //DriverObject->MajorFunction[IRP_MJ_CLOSE] = ProcessPowerCreateClose;
    //DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ProcessPowerDeviceControl;

    UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\BaimaoPower");
    PDEVICE_OBJECT DeviceObject;
    status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
    if (!NT_SUCCESS(status)) {
    KdPrint(("Failed in IoCreateDevice (0x%X)\n", status));
    return status;
    }

    return STATUS_SUCCESS;
}

void ProcessPowerUnload(PDRIVER_OBJECT DriverObject) {
    KdPrint(("ProcessPower: Unload\n"));
    IoDeleteDevice(DriverObject->DeviceObject);
}

新增内容代码解释:

UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\BaimaoPower");

TL_CONSTANT_STRING 宏用于直接初始化 UNICODE_STRING 类型变量 devNamedevName 用于指定设备对象的名称,在内核中通过这个路径可以访问此设备

PDEVICE_OBJECT DeviceObject;

声明一个指向 DEVICE_OBJECT 的指针 DeviceObject

status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);

IoCreateDevice 是用于创建设备对象的核心函数,成功后,DeviceObject 会指向新创建的设备,且此设备可以通过 \Device\BaimaoPower 名称被访问

DriverObject:驱动对象的指针,表示当前驱动程序。

0:设备扩展空间的大小(以字节为单位)。这里为 0 表示不需要额外的扩展空间。

&devName:设备名称,使用 devName 变量中的路径。

FILE_DEVICE_UNKNOWN:设备类型,这里表示未知设备类型。

0:设备特性标志,设置为 0 表示无特定特性。

FALSE:设备是否为独占模式,设置为 FALSE 表示非独占设备。

&DeviceObject:输出参数,指向新创建的设备对象

if (!NT_SUCCESS(status)) {
    KdPrint(("Failed in IoCreateDevice (0x%X)\n", status));
    return status;
}

检查 IoCreateDevice 的返回状态 status 是否成功

void ProcessPowerUnload(PDRIVER_OBJECT DriverObject) {
    KdPrint(("ProcessPower: Unload\n"));
    IoDeleteDevice(DriverObject->DeviceObject);
}

void ProcessPowerUnload(PDRIVER_OBJECT DriverObject) 定义了一个名为 ProcessPowerUnload 的函数,用于处理驱动程序的卸载操作,参数 PDRIVER_OBJECT DriverObject 是一个指向 DRIVER_OBJECT 结构的指针,用于访问驱动对象及其关联的数据

调用 IoDeleteDevice 函数删除设备对象,DriverObject->DeviceObjectDRIVER_OBJECT 结构中的指针,指向该驱动创建的 DEVICE_OBJECT 设备对象

编译前需要卸载当前驱动程序

image.png
使用F7编译程序

image.png
安装驱动

image.png
成功安装,现在查看设备对象

image.png
成功创建了设备对象

image.png
上图为驱动对象 (DRIVER_OBJECT) 和设备对象 (DEVICE_OBJECT) 的关系

DRIVER_OBJECT 表示整个驱动程序,而 DEVICE_OBJECT 则表示驱动程序所控制的每个具体设备。IoCreateDevice 函数用于为驱动创建一个新的 DEVICE_OBJECT,并将其与 DRIVER_OBJECT 关联。

  1. 驱动对象(DRIVER_OBJECT):

图中是驱动程序在系统中的入口点,包含指向设备对象链表的指针 (DeviceObject)。

image.png
通过该指针,驱动程序可以管理多个设备,每个设备都有各自的 DEVICE_OBJECT 结构。

  1. 设备对象(DEVICE_OBJECT):

每个设备对象包含指向驱动对象的指针 (DriverObject),从而可以访问驱动的全局信息。

NextDevice 字段用于连接链表中的下一个设备对象,形成一个设备链表。

DeviceExtension 是设备特定的扩展结构,用于存储设备状态和控制信息,使驱动程序可以方便地管理设备。

这种结构设计允许一个驱动程序同时管理多个设备,通过链表链接的设备对象,可以对不同设备的请求进行处理和跟踪

设备符号链接

在 Windows 驱动程序中,符号链接是一种用户模式和内核模式之间通信的桥梁。设备的符号链接允许用户应用程序通过路径访问内核设备。例如,一个驱动程序创建的设备对象可能具有复杂的内核路径,而符号链接可以提供一个简单、易访问的路径, 使用 IoCreateSymbolicLink 函数可以创建符号链接

通过创建符号链接,驱动程序可以将内核中的设备对象与一个用户模式可访问的路径关联起来。这样,用户模式应用程序就可以通过易于理解的路径名称(如 \\??\\MyDev1)访问驱动设备,而不必直接引用设备对象的复杂路径

继续上次的驱动程序,为内核设备对象创建一个符号链接,以便用户模式应用程序可以访问该设备

#include <ntddk.h>

void ProcessPowerUnload(PDRIVER_OBJECT);

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS ProcessPowerDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp);

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    KdPrint(("ProcessPower: DriverEntry\n"));
    KdPrint(("Registry path: %wZ\n", RegistryPath));

    DriverObject->DriverUnload = ProcessPowerUnload;

    RTL_OSVERSIONINFOW vi = { sizeof(vi) };
    NTSTATUS status = RtlGetVersion(&vi);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed in RtlGetVersion (0x%x)\n", status));
        return status;
    }

    KdPrint(("Windows version: %u.%u.%u\n", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber));
    //DriverObject->MajorFunction[IRP_MJ_CREATE] = ProcessPowerCreateClose;
    //DriverObject->MajorFunction[IRP_MJ_CLOSE] = ProcessPowerCreateClose;
    //DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ProcessPowerDeviceControl;

    UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\BaimaoPower");
    PDEVICE_OBJECT DeviceObject;
    status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
    if (!NT_SUCCESS(status)) {
    KdPrint(("Failed in IoCreateDevice (0x%X)\n", status));
    return status;
    }

    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");
    status = IoCreateSymbolicLink(&symLink, &devName);
    if (!NT_SUCCESS(status)) {
    IoDeleteDevice(DeviceObject);
    KdPrint(("Failed in IoCreateSymbolLink (0x%x)\n", status));
    return status;
    }

    return STATUS_SUCCESS;
}

void ProcessPowerUnload(PDRIVER_OBJECT DriverObject) {
    KdPrint(("ProcessPower: Unload\n"));
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");
    IoDeleteSymbolicLink(&symLink);
    IoDeleteDevice(DriverObject->DeviceObject);
}

新增代码解释:

UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");

这行代码定义了一个 Unicode 字符串 symLink,用于表示符号链接的路径。\\??\\BaimaoPower 是符号链接的路径,可以在用户模式访问,相当于为内核中的设备提供了一个易访问的接口

status = IoCreateSymbolicLink(&symLink, &devName);

调用 IoCreateSymbolicLink 函数来创建符号链接。&symLink 是符号链接的路径,&devName 是内核中的设备对象路径。这样,通过符号链接路径 \\??\\BaimaoPower 就可以访问设备对象

if (!NT_SUCCESS(status)) {
    IoDeleteDevice(DeviceObject);
    KdPrint(("Failed in IoCreateSymbolLink (0x%x)\n", status));
    return status;
}

这部分代码是错误处理。如果 IoCreateSymbolicLink 失败,返回的 status 将不是成功状态。此时,删除之前创建的设备对象 DeviceObject,打印错误信息,然后返回失败的状态码

void ProcessPowerUnload(PDRIVER_OBJECT DriverObject) {
    KdPrint(("ProcessPower: Unload\n"));
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");
    IoDeleteSymbolicLink(&symLink);
    IoDeleteDevice(DriverObject->DeviceObject);
}

该代码的作用是定义 ProcessPowerUnload 函数,用于卸载驱动程序。在执行过程中,首先打印一条日志信息,然后删除符号链接 \\??\\BaimaoPower,最后删除驱动程序关联的设备对象

编译前需要卸载当前驱动程序

image.png
F7编译驱动程序

image.png
安装驱动,现在可以在GLOBAL??目录下找到设备的符号链接

image.png
这个目录中的符号链接可以映射到不同的设备对象路径,例如 \Device\BaimaoPower。这样用户模式程序可以使用更简便的路径 \\.\GLOBAL??\BaimaoPower 访问内核中的设备

IO请求包(IRP)

I/O 请求包 (IRP, Input/Output Request Packet) 是 Windows 操作系统内核用于驱动程序通信和数据传输的核心数据结构。IRP 的主要作用是将用户模式或内核模式的 I/O 请求传递给驱动程序,并在驱动程序和操作系统之间协调数据处理。IRP 是一种结构体,其中包含了执行请求所需的所有信息,如操作代码、目标设备、缓冲区和状态信息等

image.png
上图为IRP(I/O 请求包)结构体的图示,详细展示了 IRP 的组成部分。IRP 是操作系统用于在驱动程序之间传递 I/O 请求的核心数据结构

MDL (Memory Descriptor List) - 内存描述符列表,指向用户模式或内核模式缓冲区,用于数据传输。
MdlAddress - 指向 MDL 的地址,用于访问数据缓冲区。
AssociatedIrp - 关联 IRP 指针,允许将多个 IRP 链接在一起,用于分散/聚集 I/O 操作。
Current I/O Stack Location - 当前 I/O 栈位置,指向当前驱动程序处理的栈位置。
I/O Stack Locations Count - I/O 栈位置计数,表示请求涉及的驱动程序数量。
IoStatus - 存储请求的状态信息,例如成功、失败、或正在处理中。
User Event - 用户事件指针,用于在请求完成时通知用户模式应用程序。
User Buffer - 用户缓冲区,存储需要传输的数据地址。
Cancel Routine - 取消例程指针,用于在请求被取消时处理必要操作。
MasterIrp - 主 IRP 指针,用于多请求操作,例如读写操作。
IrpCount - IRP 计数器,跟踪引用计数,确保 IRP 在处理完成前不被释放。
SystemBuffer - 系统缓冲区,用于在内核模式中传递数据。
Status - 操作状态码,指示请求的完成状态。
Information - 信息字段,用于存储与请求有关的额外数据,例如传输的字节数。

这里着重演示IoStatus块内容,下图为IoStatus的栈内容

image.png

Major Function(主功能)和Minor Function(次功能):指定此 I/O 请求的主要和次要操作,诸如创建、读取、写入、控制设备等。
Parameters(参数):包含特定于请求的参数,例如读取、写入、控制设备的参数。
FileObject:表示与该请求关联的文件对象。
DeviceObject:指向执行此请求的设备对象。
CompletionRoutine(完成例程):请求完成后调用的例程。
Context(上下文):保存 I/O 操作的上下文信息。

驱动程序在执行 I/O 请求时,通过 IoCompleteRequest 完成请求,确保状态和结果反馈给调用方。这适用于成功完成或遇到错误的情况,从而帮助系统准确跟踪和管理请求的执行情况

给驱动程序添加处理 I/O请求的功能

#include <ntddk.h>

void ProcessPowerUnload(PDRIVER_OBJECT);

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT, PIRP Irp);
NTSTATUS ProcessPowerDeviceControl(PDEVICE_OBJECT, PIRP Irp);

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    KdPrint(("ProcessPower: DriverEntry\n"));
    KdPrint(("Registry path: %wZ\n", RegistryPath));

    DriverObject->DriverUnload = ProcessPowerUnload;

    RTL_OSVERSIONINFOW vi = { sizeof(vi) };
    NTSTATUS status = RtlGetVersion(&vi);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed in RtlGetVersion (0x%x)\n", status));
        return status;
    }

    KdPrint(("Windows version: %u.%u.%u\n", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber));
    DriverObject->MajorFunction[IRP_MJ_CREATE] = ProcessPowerCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = ProcessPowerCreateClose;
    //DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ProcessPowerDeviceControl;

    UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\BaimaoPower");
    PDEVICE_OBJECT DeviceObject;
    status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
    if (!NT_SUCCESS(status)) {
    KdPrint(("Failed in IoCreateDevice (0x%X)\n", status));
    return status;
    }

    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");
    status = IoCreateSymbolicLink(&symLink, &devName);
    if (!NT_SUCCESS(status)) {
    IoDeleteDevice(DeviceObject);
    KdPrint(("Failed in IoCreateSymbolLink (0x%x)\n", status));
    return status;
    }

    return STATUS_SUCCESS;
}

void ProcessPowerUnload(PDRIVER_OBJECT DriverObject) {
    KdPrint(("ProcessPower: Unload\n"));
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");
    IoDeleteSymbolicLink(&symLink);
    IoDeleteDevice(DriverObject->DeviceObject);

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT, PIRP Irp) {  //用于处理设备驱动程序中的“创建”和“关闭”请求
    Irp->IoStatus.Status = STATUS_SUCCESS;    // 将 I/O 请求包 (IRP) 的状态设置为成功
    Irp->IoStatus.Information = 0;            // 将 I/O 请求包中的信息字段设为 0,表示没有返回的额外数据
    IoCompleteRequest(Irp, 0);                // 调用 IoCompleteRequest 完成 I/O 请求并通知系统
    return STATUS_SUCCESS;                    // 返回成功状态
}

}

PDEVICE_OBJECT:这是一个指向 DEVICE_OBJECT 结构的指针

PIRP Irp:这是一个指向 IRP(I/O 请求包)结构的指针。IRP 是 Windows 内核模式中用于描述 I/O 请求的结构体,包含请求的类型、数据缓冲区、请求状态等信息

F7编译代码

image.png
现在创建一个客户端,右击解决方案,选择新建一个项目

image.png
选择控制台应用程序

image.png
写入客户端代码,来打开设备文件

#include <windows.h>
#include <stdio.h>

int main() {
    HANDLE hDevice = CreateFile(
        L"\\\\.\\BaimaoPower",      // 设备文件路径
        GENERIC_WRITE | GENERIC_READ, // 读写权限
        0,                            // 不共享
        nullptr,                      // 默认安全属性
        OPEN_EXISTING,                // 打开已存在的设备
        0,                            // 默认属性
        nullptr                       // 无模板文件
    );

    if (hDevice == INVALID_HANDLE_VALUE) { // 检查句柄是否有效
        printf("Error opening device (%u)\n", GetLastError()); // 输出错误信息
        return 1;
    }

    CloseHandle(hDevice); // 关闭句柄
}

编译后打了两个断点调试,可以看到程序成功打开了baimaopower设备

image.png

image.png

编写客户端程序

继续驱动程序的开发

#include <ntddk.h>
#include "input.h"  //添加编写的头文件

void ProcessPowerUnload(PDRIVER_OBJECT);

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT, PIRP Irp);
NTSTATUS ProcessPowerDeviceControl(PDEVICE_OBJECT, PIRP Irp);

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    KdPrint(("ProcessPower: DriverEntry\n"));
    KdPrint(("Registry path: %wZ\n", RegistryPath));

    DriverObject->DriverUnload = ProcessPowerUnload;

    RTL_OSVERSIONINFOW vi = { sizeof(vi) };
    NTSTATUS status = RtlGetVersion(&vi);
    if (!NT_SUCCESS(status)) {
        KdPrint(("Failed in RtlGetVersion (0x%x)\n", status));
        return status;
    }

    KdPrint(("Windows version: %u.%u.%u\n", vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber));
    DriverObject->MajorFunction[IRP_MJ_CREATE] = ProcessPowerCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = ProcessPowerCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ProcessPowerDeviceControl;

    UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\BaimaoPower");
    PDEVICE_OBJECT DeviceObject;
    status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
    if (!NT_SUCCESS(status)) {
    KdPrint(("Failed in IoCreateDevice (0x%X)\n", status));
    return status;
    }

    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");
    status = IoCreateSymbolicLink(&symLink, &devName);
    if (!NT_SUCCESS(status)) {
    IoDeleteDevice(DeviceObject);
    KdPrint(("Failed in IoCreateSymbolLink (0x%x)\n", status));
    return status;
    }

    return STATUS_SUCCESS;
}

void ProcessPowerUnload(PDRIVER_OBJECT DriverObject) {
    KdPrint(("ProcessPower: Unload\n"));
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\BaimaoPower");
    IoDeleteSymbolicLink(&symLink);
    IoDeleteDevice(DriverObject->DeviceObject);

NTSTATUS ProcessPowerCreateClose(PDEVICE_OBJECT, PIRP Irp) {
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, 0);
    return STATUS_SUCCESS;
}

NTSTATUS ProcessPowerDeviceControl(PDEVICE_OBJECT, PIRP Irp) { // 定义设备控制处理程序,接收设备对象指针和 I/O 请求包(IRP)指针。
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); // 获取当前的 I/O 堆栈位置,指向 IRP 中的设备控制参数。
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST; // 初始化状态为“无效的设备请求”。
    auto& dic = stack->Parameters.DeviceIoControl; // 引用 stack 中的设备控制参数。
    ULONG len = 0; // 声明并初始化一个无符号长整型变量 len,用于保存输出的字节长度。

    switch (dic.IoControlCode) { // 根据控制码执行不同的处理。
        case IOCTL_OPEN_PROCESS: // 当控制码为 IOCTL_OPEN_PROCESS(表示打开进程)时的处理逻辑。
            if (dic.Type3InputBuffer == nullptr || Irp->UserBuffer == nullptr) { // 检查输入缓冲区和用户缓冲区是否为 nullptr。
                status = STATUS_INVALID_PARAMETER; // 如果是,则设置状态为“无效参数”。
                break; // 退出 switch 语句。
            }

            if (dic.InputBufferLength < sizeof(ProcessPowerInput) || dic.OutputBufferLength < sizeof(ProcessPowerOutput)) { // 检查输入和输出缓冲区长度是否足够。
                status = STATUS_BUFFER_TOO_SMALL; // 如果不足,设置状态为“缓冲区太小”。
                break; // 退出 switch 语句。
            }

            auto input = (ProcessPowerInput*)dic.Type3InputBuffer; // 将输入缓冲区转换为 ProcessPowerInput 类型的指针。
            auto output = (ProcessPowerOutput*)Irp->UserBuffer; // 将用户缓冲区转换为 ProcessPowerOutput 类型的指针。

            OBJECT_ATTRIBUTES attr; // 声明一个对象属性结构体。
            InitializeObjectAttributes(&attr, nullptr, 0, nullptr, nullptr); // 初始化对象属性,无名称、无安全描述符等。

            CLIENT_ID cid = {}; // 声明并初始化客户端 ID 结构体。
            cid.UniqueProcess = (HANDLE)(ULONG_PTR)input->ProcessId; // 将输入中的进程 ID 赋值给客户端 ID 的 UniqueProcess。

            status = ZwOpenProcess(&output->hProcess, PROCESS_ALL_ACCESS, &attr, &cid); // 调用 ZwOpenProcess 以打开具有全部访问权限的进程,并返回句柄。
            if (NT_SUCCESS(status)) { // 检查打开进程是否成功。
                len = sizeof(*output); // 如果成功,将 len 设置为输出结构体的大小。
            }

            break; // 退出 switch 语句。
    }
  // 更新 IRP 的状态和信息字段。
  Irp->IoStatus.Status = status; // 将 IRP 的状态设置为操作的状态结果。
  Irp->IoStatus.Information = len; // 将 IRP 的信息字段设置为输出的字节长度。

  IoCompleteRequest(Irp, 0); // 标记 IRP 为已完成并将其返回给 I/O 管理器。
  return status; // 返回操作的最终状态。

}

f7编译驱动程序,再新建一个头文件,提供客户端和驱动程序相互通信的功能

image.png

image.png

#pragma once
// 防止头文件在同一文件中被多次包含。

#ifdef _KERNEL_MODE
#include <wdm.h>
#else
#include <Windows.h>
#endif

struct ProcessPowerInput {
    ULONG ProcessId;
};
// 定义了一个名为 `ProcessPowerInput` 的结构体,包含一个 `ULONG` 类型的 `ProcessId` 成员,用于存储进程 ID。

struct ProcessPowerOutput {
    HANDLE hProcess;
};
// 定义了一个名为 `ProcessPowerOutput` 的结构体,包含一个 `HANDLE` 类型的 `hProcess` 成员,用于存储进程句柄。

#define IOCTL_OPEN_PROCESS CTL_CODE(0x8000, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)
// 定义了一个宏 `IOCTL_OPEN_PROCESS`,用于创建一个 I/O 控制码。该控制码使用 `CTL_CODE` 宏生成,其中:
// - `0x8000` 是设备类型。
// - `0x800` 是函数码。
// - `METHOD_NEITHER` 表示不使用任何缓冲方法。
// - `FILE_ANY_ACCESS` 表示任何类型的访问权限均可使用该控制码。

继续编写客户端内容,它的作用是打开指定的进程,列出该进程中加载的模块信息,同时通过驱动程序与指定设备通信,尝试获取并输出该进程的模块信息

#include <windows.h>
#include <stdio.h>
#include <psapi.h>    // 包含进程状态 API 头文件。
#include "input.h"  //添加编写的头文件

// 定义一个函数,用于列出给定进程的模块。
void DumpProcessModules(HANDLE hProcess) {
    HMODULE h[4096];  // 声明一个数组用于存储模块的句柄。
    DWORD needed;     // 用于存储枚举模块时所需的字节数。

    // 使用 EnumProcessModulesEx 函数来获取进程中加载的模块。
    if (!EnumProcessModulesEx(hProcess, h, sizeof(h), &needed, LIST_MODULES_ALL)) {
        return;  // 如果枚举失败,直接返回。
    }

    DWORD count = needed / sizeof(HMODULE);  // 计算模块数量。
    printf("%u modules\n", count);  // 打印模块的数量。

    WCHAR name[MAX_PATH];  // 声明一个用于存储模块名称的缓冲区。

    // 循环遍历每个模块,输出其地址和名称。
    for (int i = 0; i < count; i++) {
        printf("Module: 0x%p ", h[i]);  // 打印模块的基地址。

        // 获取模块的文件名,如果成功则打印文件名。
        if (GetModuleBaseName(hProcess, h[i], name, _countof(name))) {
            printf("%ws", name);  // 打印模块名称。
        }
        printf("\n");  // 打印换行符。
    }
}

int main(int argc, const char* argv[]) { // 主函数,argc 表示命令行参数的数量,argv 是指向参数字符串数组的指针。
    if (argc < 2) {  // 检查命令行参数的数量是否少于 2(即只包含程序名,没有传入进程 ID)。
        printf("Usage: %s <pid>\n", argv[0]);  // 如果没有足够的参数,输出使用说明,%s 会被程序名称(argv[0])替换。
        return 0;  // 程序正常退出,返回值为 0。
    }

    int pid = atoi(argv[1]);  // 将程序传入的命令行参数(进程ID字符串)转换为整数类型,并将其赋值给变量 pid。

    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);  
    // 打开指定 PID 的进程,获取该进程的句柄。请求的访问权限为查询进程信息和读取进程内存。
    // 参数说明:
    // PROCESS_QUERY_INFORMATION:允许查询进程信息。
    // PROCESS_VM_READ:允许读取进程的内存。
    // FALSE:不继承句柄。
    // pid:要打开的进程 ID。

    if (hProcess) {  // 检查是否成功获取到进程句柄。
        DumpProcessModules(hProcess);   // 调用函数 DumpProcessModules,列出该进程中的模块。
        CloseHandle(hProcess);  // 关闭打开的进程句柄,释放资源。
        return 0;  // 成功打开并列出模块后,返回 0 表示程序成功执行。
}

    printf("Failed to open process with OpenProcess (%u)\n", GetLastError());  // 如果无法打开进程,打印错误信息并显示最后一个错误代码。

    HANDLE hDevice = CreateFile(L"\\\\.\\BaimaoPower",GENERIC_WRITE | GENERIC_READ,0,nullptr,OPEN_EXISTING,0,nullptr);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("Error opening device (%u)\n", GetLastError());
        return 1;
    }

    ProcessPowerInput input; // 声明一个 ProcessPowerInput 类型的结构体变量 input,用于传递给驱动的输入数据。
    input.ProcessId = atoi(argv[1]); // 将传入的进程 ID 参数(字符串)转换为整数,并存储在 input.ProcessId 中。

    ProcessPowerOutput output; // 声明一个 ProcessPowerOutput 类型的结构体变量 output,用于存储驱动返回的输出数据。
    DWORD bytes; // 声明一个 DWORD 类型的变量 bytes,用于存储 DeviceIoControl 返回的字节数。

    BOOL ok = DeviceIoControl(hDevice, IOCTL_OPEN_PROCESS, &input, sizeof(input), &output, sizeof(output), &bytes, nullptr);
    // 调用 `DeviceIoControl` 函数来发送 I/O 控制码请求。该函数参数解释如下:
    // - `hDevice`:设备句柄,用于标识目标设备。
    // - `IOCTL_OPEN_PROCESS`:定义的 I/O 控制码,指示将要执行的操作。
    // - `&input`:指向输入缓冲区的指针,包含用于驱动程序的输入数据(即进程 ID)。
    // - `sizeof(input)`:输入缓冲区的大小。
    // - `&output`:指向输出缓冲区的指针,用于接收驱动程序的输出数据(即进程句柄)。
    // - `sizeof(output)`:输出缓冲区的大小。
    // - `&bytes`:指向一个 `DWORD` 变量,用于存储实际返回的字节数。
    // - `nullptr`:用于重叠 I/O 的重叠结构指针,此处为空表示不使用异步操作。

    if (!ok) { // 检查 DeviceIoControl 是否成功,如果返回值为 FALSE 则表示失败。
        printf("Error: %u\n", GetLastError()); // 如果失败,输出错误代码。
        return 1; // 程序退出,返回 1 表示错误状态。
    }

    printf("Success!\n");

    DumpProcessModules(output.hProcess); // 调用 `DumpProcessModules` 函数,传入 `output.hProcess`,打印出此进程的模块信息。
    CloseHandle(output.hProcess);  // 调用 `CloseHandle` 函数,关闭 `output.hProcess` 句柄,释放进程资源。

    CloseHandle(hDevice);
}

f7编译程序,进入编译后程序所处的文件夹处打开终端

image.png
测试ConsoleApplication1.exe是否能正常运行

image.png
正常运行,打开Process Explorer随意找一个程序的pid进行测试,这里我用的是explorer程序进行测试

image.png

image.png
成功获取指定程序的dll模块,现在尝试获取受保护的程序dll模块,受保护的程序在Process Explorer中进行了红色标记

image.png

image.png
获取失败,显示无权限访问,是因为还没启动驱动程序,现在启动驱动程序后再次获取

image.png

image.png
成功获取受保护程序的dll模块,通过驱动程序,还能做到许多Process Explorer做不到的事,列如关闭defender进程等系统进程,具体的恶意利用文章之后会发布

引用进程缓冲区

在windows驱动开发中,引用用户模式下的进程缓冲区是一项复杂且容易出错的操作。因为驱动程序有时会在“任意线程上下文”中运行,这意味着驱动代码可能在不确定的进程上下文下执行,导致无法直接访问用户的内存地址空间。这种情况下,直接访问用户空间的缓冲区可能会产生严重的问题。

此外,即使在请求线程上下文中,应用程序的缓冲区也不一定安全。因为在多线程环境下,另一个线程可能在当前线程读取缓冲区之前已经释放或更改了该内存区域。这会导致驱动访问到无效或错误的数据,从而引发报错。

因此,在编写驱动程序时,开发人员通常会采用内核模式缓冲区或者使用探测(Probe)和锁定(Lock)等操作来确保缓冲区的安全访问,以避免在“任意线程上下文”中操作用户空间的内存区域。

用户空间和内核空间之间的数据传输是一个需要谨慎处理的环节。由于安全和稳定性原因,用户空间缓冲区通常不能直接在任意线程上下文或高优先级中被访问。I/O 系统因此提供了三种方式来处理用户缓冲区的数据传输需求:

  1. 缓冲 I/O:系统在内核中创建一个中间缓冲区,用于临时存储用户空间的数据。数据在用户空间和该中间缓冲区之间传输,使得数据的安全性和访问控制更加可靠。
  2. 直接 I/O:系统将用户空间的物理页直接映射到内核空间,这样数据可以直接在用户缓冲区和设备之间传输。这种方法效率较高,但需要注意同步问题和内存保护。
  3. 无缓冲 I/O:不依赖于系统提供的任何缓冲或映射机制。这种方式意味着驱动程序需要自行处理数据的安全性和传输的可靠性。 ```php
    DeviceObject->Flags |= DO_BUFFERED_IO; // DO = 设备对象
    
    在`DriverEntry` 函数中,可以通过设置 `DeviceObject->Flags` 标志为 `DO_BUFFERED_IO`,来指示驱动程序使用缓冲 I/O 模式。缓冲 I/O 是一种 I/O 处理机制,适用于读取(`IRP_MJ_READ`)和写入(`IRP_MJ_WRITE`)操作。使用缓冲 I/O 可以在用户空间和内核空间之间安全地传递数据,因为系统会在内核空间创建一个中间缓冲区来存储数据,避免直接访问用户缓冲区的风险。这在提高数据传输安全性和稳定性上起到关键作用
    
    

image.png
上图示展示了缓冲 I/O 机制在驱动程序中的数据传输过程。以下是图解:

  1. 用户空间 (User space):应用程序的数据缓冲区位于用户空间中。
  2. 内核空间 (Kernel space):驱动程序在内核空间中运行,无法直接访问用户空间的数据。这是为了保护系统的安全性和稳定性。
  3. RAM 中的中间缓冲区:缓冲 I/O 会将用户空间中的数据复制到内核空间中的缓冲区 (SystemBuffer),然后驱动程序可以安全地在内核空间中访问数据。
  4. 缓冲区 I/O 的流程:

当使用缓冲区 I/O 时,I/O 管理器会自动分配一个内核缓冲区,将用户空间的数据复制到这个内核缓冲区。

Irp->AssociatedIrp.SystemBuffer 指向这个内核缓冲区,驱动程序可以通过该指针读取或写入数据。

  1. q = Irp->AssociatedIrp.SystemBuffer:该变量 q 保存了 SystemBuffer 的地址,通过该地址可以访问复制后的数据。

这种机制确保了驱动程序在处理 I/O 请求时,能够安全且有效地访问数据,避免了跨越用户空间和内核空间带来的安全风险

Direct IO

Direct I/O 的特点:

在 Direct I/O 模式下,系统会将用户空间的内存页锁定,以防止在操作过程中被修改,从而允许驱动程序直接访问用户内存而无需进行额外的数据复制。

这种模式有助于提高数据传输效率,尤其适用于大规模数据传输的场景,例如文件系统驱动或网络驱动。

与 Buffered I/O 的区别:

与缓冲区 I/O (Buffered I/O) 不同,Direct I/O 不会在系统内存中创建中间缓冲区,而是直接操作用户内存。这样可以避免数据在用户空间和系统空间之间的多次复制。

但是,Direct I/O 的实现复杂度较高,因为驱动程序需要确保在内存页锁定状态下的正确访问,以避免潜在的访问冲突或内存保护问题

image.png
上图展示了在 Direct I/O 模式下,驱动程序如何通过内存描述符列表(MDL)访问用户空间的数据。

User Space(用户空间)和 Kernel Space(内核空间)之间的内存映射:

用户空间中的数据被映射到内核空间,使得内核驱动可以直接访问用户数据。
用户空间中的数据通过 MDL 锁定到物理内存,以防止用户数据在操作过程中被修改或换出。

MDL(内存描述符列表):

Irp->MdlAddress 指向一个内存描述符列表(MDL),用于描述用户空间缓冲区的物理页面。
MDL 通过内存页面锁定确保数据的稳定性,以便内核可以安全地访问用户数据。

MmGetSystemAddressForMdlSafe 函数:

该函数将 MDL 转换为系统空间的地址,使内核驱动程序能够直接访问用户数据。
调用 MmGetSystemAddressForMdlSafe(Irp->MdlAddress, ...) 可以获得 MDL 描述的数据的系统地址(q),从而可以直接对数据进行读写

简单来说, 用户空间数据通过 MDL 被映射到内核空间,并锁定在物理内存中。通过 MmGetSystemAddressForMdlSafe 函数,驱动程序可以直接访问用户数据,而无需拷贝到内核缓冲区

DeviceIoControl 缓冲区

DeviceIoControl 函数通常用于执行控制设备的操作,而不仅仅是读取或写入数据

BOOL DeviceIoControl(
    HANDLE hDevice,             // 设备或文件的句柄
    DWORD dwIoControlCode,      // 控制码(通常定义在 <winioctl.h> 中)
    PVOID lpInBuffer,           // 输入缓冲区指针,用于传递数据给设备
    DWORD nInBufferSize,        // 输入缓冲区的大小
    PVOID lpOutBuffer,          // 输出缓冲区指针,用于从设备接收数据
    DWORD nOutBufferSize,       // 输出缓冲区的大小
    PDWORD lpBytesReturned,     // 实际返回的字节数
    LPOVERLAPPED lpOverlapped   // 用于异步操作的重叠结构
);

在定义控制码时,必须使用 CTL_CODE 宏:

#define CTL_CODE(DeviceType, Function, Method, Access) \
    ((DeviceType) << 16) | ((Access) << 14) | \
    ((Function) << 2) | (Method)

DeviceType:设备类型,用于指定设备类别

Function:功能代码,用于定义控制码的具体功能

Method:数据传输方法,比如 METHOD_BUFFERED(缓冲方法)、METHOD_IN_DIRECT(直接输入)等

Access:访问权限,比如 FILE_ANY_ACCESS(任意访)或 FILE_READ_ACCESS

示例:

#define DEVICE_XYZ 0x8000                   // 设备类型(特定驱动)
#define IOCTL_XYZ_SOMETHING CTL_CODE(       \
    DEVICE_XYZ,                             \ // 设备类型标识符(自定义设备类型)
    0x800,                                  // 功能代码
    METHOD_BUFFERED,                        // 使用缓冲方法
    FILE_ANY_ACCESS)                        // 任意访问权限

image.png
上图为DeviceIoControl 函数中用于数据传输的四种不同的方法:

METHOD_BUFFERED:

输入缓冲区:Buffered(缓冲模式)
输出缓冲区:Buffered(缓冲模式)
说明:数据在用户空间和内核空间之间通过系统缓冲区传输,适合小数据传输,系统会自动将数据复制到内核缓冲区。

METHOD_IN_DIRECT:

输入缓冲区:Buffered(缓冲模式)
输出缓冲区:Direct(直接模式)
说明:输入数据使用缓冲模式,而输出数据使用直接模式,即通过内存描述列表(MDL)进行映射。

METHOD_OUT_DIRECT:

输入缓冲区:Buffered(缓冲模式)
输出缓冲区:Direct(直接模式)
说明:类似于 METHOD_IN_DIRECT,但主要用于输出数据的直接模式。

METHOD_NEITHER:

输入缓冲区:Neither(无缓冲)
输出缓冲区:Neither(无缓冲)
说明:内核不进行缓冲或直接处理,用户模式和内核模式需自己处理内存映射或指针,通常在高效数据处理和大量数据传输时使用

优先级提升驱动程序

在 Windows 系统中,线程优先级的管理和调整对于提升系统性能和资源优化起着关键作用。通过合适地设置进程和线程的优先级,开发者可以确保高优先级任务获得足够的 CPU 运算时间,Windows 系统中的线程优先级分为 32 个级别(0-31),通常将 0 优先级保留给零页线程,这个线程用于释放未使用的内存页,维持系统内存的可用性,高优先级线程适用于需要频繁响应或时间敏感的任务,而低优先级线程则适合后台任务或资源消耗较少的任务。

每个进程都有一个“优先级类”,该类决定了进程内线程的基础优先级,可以通过 SetPriorityClass API 设置进程的优先级类,如“高优先级”、“后台模式”等,从而间接影响该进程中所有线程的基础优先级,SetThreadPriority可以进一步调整线程的优先级,使其偏离进程的基础优先级,从而满足特定的任务需求。

在高负载的多任务系统中,优先级动态调整可以帮助关键任务获得更好的响应,同时减少不重要任务的 CPU 占用,在实时系统或需要短延迟的应用(例如音视频处理、游戏等)中,优先级的合理设置和动态调整尤为重要,以确保实时性能。

过多地提升优先级可能导致系统资源分配不均衡,甚至引起系统不稳定或其他任务无法响应。因此,建议合理且适度地使用 SetPriorityClass 和 SetThreadPriority。在编写系统级或内核级驱动时,开发者更需要了解线程优先级的分配,以保证驱动程序能高效且稳定地运行,不对用户态应用的响应造成负面影响

使用Process Explorer工具就能查看程序的优先级

image.png

image.png
上图表显示了不同优先级类(Priority Class)在Windows操作系统中的分布:

Realtime Priority Class(实时优先级类):

优先级范围:1631
最高优先级,适用于需要绝对优先执行的进程,但可能导致系统其他进程响应缓慢。
在图表中,用红色显示。

High Priority Class(高优先级类):

优先级范围:1315
用于需要比普通任务更高的优先级但不如实时任务重要的进程。
在图表中,用橙色显示。

Above Normal Priority Class(高于正常优先级类):

优先级范围:1012
比普通任务稍高的优先级,适用于一些性能敏感的进程。
在图表中,用橄榄绿色显示。

Normal Priority Class(正常优先级类):

优先级范围:59
默认优先级,适用于绝大多数普通应用程序。
在图表中,用绿色显示。

Below Normal Priority Class(低于正常优先级类):

优先级范围:34
低于普通任务的优先级,适用于一些可以延迟执行的任务。
在图表中,用蓝色显示。

Idle Priority Class(空闲优先级类):

优先级范围:12
最低优先级,用于仅在系统空闲时执行的任务。
在图表中,用青色显示。

驱动编写

新建一个项目,选择WDM

image.png
删除inf文件后新建源文件

image.png

image.png
创建一个名为“Booster”的内核设备对象,并提供基本的创建、关闭和卸载功能,然后可以用于接收用户模式的请求并设置指定线程的优先级

#include <ntifs.h>
#include <ntddk.h>
#include "C:\Users\baimao\source\repos\MyDriver2\MyDriver2\out.h" // 包含自定义头文件,用于访问驱动相关的定义

void BoosterUnload(PDRIVER_OBJECT DriverObject); // 声明一个名为 BoosterUnload 的函数,接受一个 PDRIVER_OBJECT 类型的参数
NTSTATUS BoosterCreateClose(PDEVICE_OBJECT, PIRP Irp); // 定义一个处理设备创建和关闭操作的函数,返回 NTSTATUS 类型的状态值
NTSTATUS BoosterDeviceControl(PDEVICE_OBJECT, PIRP Irp); // 声明处理设备控制请求的函数

extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING) {
    // 设置驱动程序卸载函数,在驱动程序被卸载时调用 `BoosterUnload` 进行清理
    DriverObject->DriverUnload = BoosterUnload;

    // 指定驱动程序的 `IRP_MJ_CREATE` 和 `IRP_MJ_CLOSE` 请求由 `BoosterCreateClose` 处理
    DriverObject->MajorFunction[IRP_MJ_CREATE] = BoosterCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = BoosterCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = BoosterDeviceControl; // 绑定处理设备控制请求的函数到 IRP_MJ_DEVICE_CONTROL

    // 声明一个指向设备对象的指针
    PDEVICE_OBJECT DeviceObject;
    // 定义设备的名称为 `\Device\Booster`
    UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\Booster");
    // 创建一个设备对象并存储在 `DeviceObject` 指针中
    NTSTATUS status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
    // 检查设备是否创建成功,如果失败则返回错误状态
    if (!NT_SUCCESS(status))
        return status;

    // 定义符号链接的名称为 `\??\Booster`
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Booster");
    // 创建符号链接,使用户模式可以通过 `\??\Booster` 访问该设备
    status = IoCreateSymbolicLink(&symLink, &devName);
    // 如果符号链接创建失败,则删除先前创建的设备对象并返回错误状态
    if (!NT_SUCCESS(status)) {
        IoDeleteDevice(DeviceObject);
        return status;
    }

    // 返回 `STATUS_SUCCESS` 表示驱动程序初始化成功
    return STATUS_SUCCESS;
}

void BoosterUnload(PDRIVER_OBJECT DriverObject) {
    // 定义一个 UNICODE_STRING 类型的变量 symLink,用于存储符号链接名称
    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Booster");

    // 删除指定的符号链接,以便在驱动卸载时清除该链接
    IoDeleteSymbolicLink(&symLink);

    // 删除驱动创建的设备对象,防止内存泄漏
    IoDeleteDevice(DriverObject->DeviceObject);
}
NTSTATUS BoosterCreateClose(PDEVICE_OBJECT, PIRP Irp) {
    Irp->IoStatus.Status = STATUS_SUCCESS;           // 设置 IRP 的状态为成功
    Irp->IoStatus.Information = 0;                   // 将信息字段设置为 0,表示没有额外信息
    IoCompleteRequest(Irp, IO_NO_INCREMENT);         // 完成 IRP 请求,通知 I/O 管理器不增加线程的优先级
    return STATUS_SUCCESS;                           // 返回成功状态
}
NTSTATUS BoosterDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    auto stack = IoGetCurrentIrpStackLocation(Irp); // 获取当前 IRP 的堆栈位置
    auto status = STATUS_INVALID_DEVICE_REQUEST; // 初始化状态为无效设备请求
    auto& dic = stack->Parameters.DeviceIoControl; // 获取设备控制参数的引用

    switch (dic.IoControlCode) { // 根据控制码选择处理操作
        case IOCTL_SET_PRIORITY: // 如果控制码为 IOCTL_SET_PRIORITY
            if (dic.InputBufferLength < sizeof(ThreadData)) { // 检查输入缓冲区长度是否小于 ThreadData 结构的大小
                status = STATUS_BUFFER_TOO_SMALL; // 如果缓冲区太小,设置状态为 STATUS_BUFFER_TOO_SMALL
                break; // 跳出 switch
            }

            auto data = (ThreadData*)Irp->AssociatedIrp.SystemBuffer; // 将系统缓冲区转换为 ThreadData 类型的指针
            if (data == nullptr) { // 如果数据指针为空
                status = STATUS_INVALID_PARAMETER; // 设置状态为 STATUS_INVALID_PARAMETER
                break; // 跳出 switch
            }

            if (data->Priority < 1 || data->Priority > 31) { // 检查优先级是否在有效范围内(1到31)
                status = STATUS_INVALID_PARAMETER; // 如果优先级无效,设置状态为 STATUS_INVALID_PARAMETER
                break; // 跳出当前代码块
            }

            PETHREAD Thread;
            status = PsLookupThreadByThreadId(ULongToHandle(data->ThreadId), &Thread); // 根据线程ID查找线程对象
            if (!NT_SUCCESS(status)) // 检查查找线程的操作是否成功
                break; // 如果失败,跳出当前代码块

            KeSetPriorityThread((PKTHREAD)Thread, data->Priority); // 设置线程的优先级
            ObDereferenceObject(Thread); // 释放线程对象引用

            break; // 结束 case IOCTL_SET_PRIORITY 分支
    }

    Irp->IoStatus.Status = status; // 将状态写入 IRP 的状态字段
    Irp->IoStatus.Information = 0; // 设置返回的字节数为 0
    IoCompleteRequest(Irp, IO_NO_INCREMENT); // 完成 IRP 请求,不增加优先级
    return status; // 返回状态
}

f7编译

image.png

客户端编写

新建一个项

image.png
选择控制台应用,写入代码, 这个客户端程序用于通过驱动程序将指定线程的优先级提升到给定的级别

#include "C:\Users\baimao\source\repos\MyDriver2\MyDriver2\out.h" // 包含自定义头文件,用于访问驱动相关的定义
#include <Windows.h>
#include <stdio.h>

int main(int argc, const char* argv[]) {
    if (argc < 3) { // 检查命令行参数是否足够
        printf("Usage: boost <tid> <priority>\n"); // 提示使用方法
        return 0; // 退出程序
    }

    int tid = atoi(argv[1]); // 将第一个参数转换为线程ID
    int priority = atoi(argv[2]); // 将第二个参数转换为优先级

    HANDLE hDevice = CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); 
    // 打开驱动设备对象,用于通信
    if (hDevice == INVALID_HANDLE_VALUE) { // 检查设备是否成功打开
        printf("Error opening device (%u)\n", GetLastError()); // 输出错误信息
        return 1; // 返回错误状态
    }
    int tid = atoi(argv[1]); // 将第一个命令行参数转换为整数,表示线程ID
    int priority = atoi(argv[2]); // 将第二个命令行参数转换为整数,表示优先级

    HANDLE hDevice = CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
    // 尝试打开设备 \\.\Booster,用于向驱动程序发送控制请求
    if (hDevice == INVALID_HANDLE_VALUE) { // 检查是否成功打开设备
        printf("Error opening device (%u)\n", GetLastError()); // 输出错误信息
        return 1; // 返回错误状态
    }

    ThreadData data; // 定义一个 ThreadData 结构,用于存储线程ID和优先级
    data.Priority = priority; // 设置 ThreadData 结构的优先级字段
    data.ThreadId = tid; // 设置 ThreadData 结构的线程ID字段

    DWORD bytes; // 定义一个 DWORD 变量用于接收实际传输的字节数
    BOOL ok = DeviceIoControl(hDevice, IOCTL_SET_PRIORITY, &data, sizeof(data), nullptr, 0, &bytes, nullptr);
    // 调用 DeviceIoControl 函数,将 ThreadData 结构发送到驱动程序以设置线程优先级
    if (!ok) { // 检查 DeviceIoControl 是否成功
        printf("Error in DeviceIoControl (%u)\n", GetLastError()); // 输出错误信息
        return 1; // 返回错误状态
    }
    printf("Success!!!\n"); // 输出成功信息,表示优先级设置成功
    CloseHandle(hDevice); // 关闭设备句柄,释放资源
}

再创建一个头文件,提供客户端和驱动程序相互通信的功能

image.png

#pragma once
#ifdef _KERNEL_MODE
#include <wdm.h>
#else
#include <Windows.h>
#endif

#define IOCTL_SET_PRIORITY CTL_CODE(0x8000, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) 
// 定义一个 I/O 控制码 IOCTL_SET_PRIORITY,用于设置线程优先级。
// CTL_CODE 宏生成控制码,参数依次为:
// 0x8000:设备类型,通常用户定义的设备类型使用 0x8000 以上的值。
// 0x800:函数代码,指定设备支持的操作。
// METHOD_BUFFERED:数据传输方式,表示使用缓冲传输方式。
// FILE_ANY_ACCESS:访问权限,表示任何用户模式程序都可以调用该控制码。

struct ThreadData {
    ULONG ThreadId; // 线程ID,用于标识要调整优先级的线程
    int Priority;   // 要设置的优先级值
};

f7编译客户端程序

image.png

部署和测试驱动程序

现在在虚拟机中运行编译的程序,要在没有安装Visual Studio的机子上运行编写的驱动程序,需要改变一下设置,右击客户端项目,选择属性

image.png
改为多线程调试,应用后重新编译客户端程序即可

image.png
打开存储编译后程序的文件夹,移动这四个文件到虚拟机即可

image.png
将系统设置为测试签名模式以允许加载未签名的驱动程序,执行命令后需重启虚拟机

bcdedit /set testsigning on

最后创建一个名为 booster 的内核模式服务(驱动程序),并指定驱动程序文件路径为 C:\Users\aptking\Desktop\驱动\MyDriver2.sys,然后启动驱动程序

sc create booster type= kernel binPath= C:\Users\aptking\Desktop\驱动\MyDriver2.sys
sc start booster

image.png
现在随意挑选一个进程,尝试提高进程优先级,未提升前,当前pid为8804的线程优先级为8

image.png
现在执行编写的程序,提升线程优先级为15

image.png
成功改变线程的优先级

  • 发表于 2025-01-14 10:00:02
  • 阅读 ( 2083 )
  • 分类:二进制

0 条评论

请先 登录 后评论
cike_y
cike_y

8 篇文章

站长统计