深入解析Windows内核I/O系统:内核模式与用户模式的通信实现
Ba1_Ma0 发表于 四川 历史精选 763浏览 · 2024-11-15 03:44

IO请求包(IRP)

编写客户端程序

继续驱动程序的开发

#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编译驱动程序,再新建一个头文件,提供客户端和驱动程序相互通信的功能

#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编译程序,进入编译后程序所处的文件夹处打开终端


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


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


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


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


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

引用进程缓冲区

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

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

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

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

  1. 缓冲 I/O:系统在内核中创建一个中间缓冲区,用于临时存储用户空间的数据。数据在用户空间和该中间缓冲区之间传输,使得数据的安全性和访问控制更加可靠。
  2. 直接 I/O:系统将用户空间的物理页直接映射到内核空间,这样数据可以直接在用户缓冲区和设备之间传输。这种方法效率较高,但需要注意同步问题和内存保护。
  3. 无缓冲 I/O:不依赖于系统提供的任何缓冲或映射机制。这种方式意味着驱动程序需要自行处理数据的安全性和传输的可靠性。
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 可以在用户空间和内核空间之间安全地传递数据,因为系统会在内核空间创建一个中间缓冲区来存储数据,避免直接访问用户缓冲区的风险。这在提高数据传输安全性和稳定性上起到关键作用


上图示展示了缓冲 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 的实现复杂度较高,因为驱动程序需要确保在内存页锁定状态下的正确访问,以避免潜在的访问冲突或内存保护问题


上图展示了在 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)                        // 任意访问权限


上图为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工具就能查看程序的优先级


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

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

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

High Priority Class(高优先级类):

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

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

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

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

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

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

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

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

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

驱动编写

新建一个项目,选择WDM


删除inf文件后新建源文件


创建一个名为“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编译

客户端编写

新建一个项

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

#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); // 关闭设备句柄,释放资源
}

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

#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编译客户端程序

部署和测试驱动程序

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


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


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


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

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


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


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


成功改变线程的优先级

1 条评论
某人
表情
可输入 255
Bat-Hibara
2024-11-23 12:47 甘肃 0 回复

大佬大佬666