windows-kernel-programming 中文读后感-第四章

本章主要讲:

  • 介绍
  • 驱动程序初始化
  • 客户端代码
  • 创建和关闭调度例程
  • DeviceIoControl 调度例程
  • 安装和测试

完整代码参考这里:

驱动开发 第四章--干货篇_还在水里游的博客-CSDN博客

介绍

Window进程是有优先级的,一共5个大类,6个级别,共31个值,如下图:

调用示例:

THREAD_PRIORITY_LOWEST (-2),
THREAD_PRIORITY_BELOW_NORMAL (-1), 
THREAD_PRIORITY_NORMAL (0),
THREAD_PRIORITY_ABOVE_NORMAL (+1),
THREAD_PRIORITY_HIGHEST (+2). 

THREAD_PRIORITY_IDLE (-Sat) and THREAD_PRIORITY_TIME_CRITICAL (+Sat).

SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS);
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);

很明显很难用,不能连续的设置1-31,所以本章想创建一个驱动程序来绕过这些限制,并允许将线程的优先级设置为任意数字,而不管其进程优先级如何。

 驱动初始化

创建一个PriorityBooster.cpp文件,初始化代码:

#include <ntddk.h>
extern "C" NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
    return STATUS_SUCCESS;
}

大多数软件驱动程序需要在 DriverEntry 中执行以下操作:

  • 设置卸载程序
  • 设置驱动程序支持的调度例程
  • 创建一个设备对象
  • 创建到设备对象的符号链接

一旦执行了所有这些操作,驱动程序就准备好接受请求了。 第一步是添加一个 Unload 例程并从驱动程序对象指向它。 这是带有 Unload 例程的新 DriverEntry:

#include <ntddk.h>
// prototypes
void PriorityBoosterUnload(_In_ PDRIVER_OBJECT DriverObject);
// DriverEntry
extern "C" NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
    DriverObject->DriverUnload = PriorityBoosterUnload;
    return STATUS_SUCCESS;
}
void PriorityBoosterUnload(_In_ PDRIVER_OBJECT DriverObject) {
}

当我们在 DriverEntry 中执行需要撤消的实际工作时,我们将根据需要将代码添加到 Unload 例程中。

接下来,我们需要设置我们想要支持的调度例程。 实际上,所有驱动程序都必须支持 IRP_MJ_CREATE 和 IRP_MJ_CLOSE,否则将无法为此驱动程序打开任何设备的句柄。 所以我们将以下内容添加到 DriverEntry:

DriverObject->MajorFunction[IRP_MJ_CREATE] = PriorityBoosterCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = PriorityBoosterCreateClose;

 我们将 Create 和 Close 主要函数指向同一个例程。 这是因为,正如我们稍后将看到的,他们实际上会做同样的事情:简单地批准请求。 在更复杂的情况下,这些可能是单独的函数,在 Create 情况下,驱动程序可以(例如)检查调用者是谁,并且只让批准的调用者成功打开设备。 所有主要函数都有相同的原型(它们是函数指针数组的一部分),因此我们必须为 PriorityBoosterCreateClose 添加一个原型。 这些函数的原型如下:

NTSTATUS PriorityBoosterCreateClose(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp);

该函数必须返回 NTSTATUS 并接受指向设备对象的指针和指向 I/O 请求包 (IRP) 的指针。 对于所有类型的请求,IRP 是存储请求信息的主要对象。 我们将在第 6 章更深入地研究 IRP,但我们将在本章后面讨论基础知识,因为我们需要它来完成我们的驱动程序。

将信息传递给Driver

从用户模式客户端的角度来看,它可以使用三个基本功能: WriteFile、ReadFile 和 DeviceIoControl。 出于我们驱动程序的目的,我们可以使用 WriteFile 或 DeviceIoControl。 读取没有意义,因为我们将信息传递给驱动程序,而不是来自驱动程序。

那么哪个更好,WriteFile 还是 DeviceIoControl? 这主要是一个偏爱问题,但这里的一般约定是,如果它真的是一个写操作(逻辑上),就使用 Write;对于其他任何事情 - DeviceIoControl 是首选,因为它是一种将数据传入和传出驱动程序的通用机制。

由于更改线程的优先级不是纯粹的写入操作,我们使用 DeviceIoControl。 该函数具有以下原型:

BOOL WINAPI DeviceIoControl(
	_In_ HANDLE hDevice,
	_In_ DWORD dwIoControlCode,
	_In_reads_bytes_opt_(nInBufferSize) LPVOID lpInBuffer,
	_In_ DWORD nInBufferSize,
	_Out_writes_bytes_to_opt_(nOutBufferSize, *lpBytesReturned) LPVOID lpOutBuffer,
	_In_ DWORD nOutBufferSize,
	_Out_opt_ LPDWORD lpBytesReturned,
	_Inout_opt_ LPOVERLAPPED lpOverlapped);

DeviceIoControl 包含三个重要部分:

  • A control code
  • An input buffer
  • An output buffer

这意味着 DeviceIoControl 是一种与驱动程序通信的灵活方式。 可以支持几个控制代码,这需要与可选缓冲区一起传递不同的语义。 在驱动端,DeviceIoControl对应IRP_MJ_DEVICE_CONTROL主功能码。 让我们将它添加到调度例程的初始化中:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PriorityBoosterDeviceControl;

然后创建了一个PriorityBoosterCommon.h的文件,这里定义了一个结构体,用来保存线程 ID 和为其设置的优先级。结构体如下:

struct ThreadData {
    ULONG ThreadId;
    int Priority;
};

线程 ID 是 32 位无符号整数,因此我们选择 ULONG 作为类型(请注意,我们通常不能使用 DWORD——在用户模式头文件中定义的常见类型——因为它没有在内核模式头文件中定义。 另一方面,ULONG 在两者中都有定义)。

优先级应该是一个介于 1 和 31 之间的数字,所以一个简单的 32 位整数就可以了。

接下来我们需要定义一个控制代码。 您可能认为任何 32 位数字都可以,但事实并非如此。 控制代码必须使用 CTL_CODE 宏构建,该宏接受构成最终控制代码的四个参数。 CTL_CODE 定义如下:

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

以下是这些宏参数含义的简要说明:

  • DeviceType - 标识设备类型。这可以是 WDK 标头中定义的 FILE_DEVICE_xxx 常量之一,但这主要用于基于硬件的驱动程序。 对于像我们这样的软件驱动程序,这个数字并不重要。 尽管如此,Microsoft 的文档仍指定第 3 方的值应以 0x8000 开头。
  • Function - 表示特定操作的升序数字。 如果没有别的,这个数字在同一个驱动程序的不同控制代码之间必须是不同的。 同样,任何数字都可以,但官方文档说 3rd 方驱动程序应该以 0x800 开头。
  • Method - 控制代码中最重要的部分。它指示客户端提供的输入和输出缓冲区如何传递给驱动程序。 我们将在第 6 章详细处理这些值。对于我们的驱动程序,我们将使用最简单的值 METHOD_NEITHER。 我们将在本章后面看到它的效果。
  • Access - 指示此操作是针对驱动程序 (FILE_WRITE_ACCESS)、来自驱动程序 (FILE_READ_ACCESS) 还是双向 (FILE_ANY_ACCESS)。 典型的驱动程序只使用 FILE_ANY_ACCESS 并在 IRP_MJ_DEVICE_CONTROL 处理程序中处理实际请求。

鉴于以上信息,我们可以定义我们的单一控制代码如下:

#define PRIORITY_BOOSTER_DEVICE 0x8000
#define IOCTL_PRIORITY_BOOSTER_SET_PRIORITY CTL_CODE(PRIORITY_BOOSTER_DEVICE, \
0x800, METHOD_NEITHER, FILE_ANY_ACCESS)

我们在 DriverEntry 中有更多的初始化工作要做。 目前,我们没有任何设备对象,因此无法打开句柄并到达驱动程序。 典型的软件驱动程序只需要一个设备对象,并带有指向它的符号链接,以便用户模式客户端可以获得句柄。 创建设备对象需要调用 IoCreateDevice API,声明如下(为了清楚起见,省略/简化了一些 SAL 注释):

NTSTATUS IoCreateDevice(
	_In_ PDRIVER_OBJECT DriverObject,
	_In_ ULONG DeviceExtensionSize,
	_In_opt_ PUNICODE_STRING DeviceName,
	_In_ DEVICE_TYPE DeviceType,
	_In_ ULONG DeviceCharacteristics,
	_In_ BOOLEAN Exclusive,
	_Outptr_ PDEVICE_OBJECT* DeviceObject);

IoCreateDevice 的参数描述如下:

  • DriverObject - 此设备对象所属的驱动程序对象。 这应该只是传递给 DriverEntry 函数的驱动程序对象。
  • DeviceExtensionSize - 除了 sizeof(DEVICE_- OBJECT) 之外将分配的额外字节。 用于将某些数据结构与设备相关联。 它对于仅创建单个设备对象的软件驱动程序不太有用,因为设备所需的状态可以简单地由全局变量管理。
  • DeviceName - 内部设备名称,通常在设备对象管理器目录下创建。
  • DeviceType - 与某些类型的基于硬件的驱动程序相关。 对于软件驱动程序,应使用值 FILE_DEVICE_UNKNOWN。
  • DeviceCharacteristics - 一组标志,与某些特定驱动程序相关。 如果软件驱动程序支持真正的命名空间(软件驱动程序很少使用,超出本书的范围),则它们指定零或 FILE_DEVICE_SECURE_OPEN。
  • Exclusive - 是否应允许多个文件对象打开同一设备?大多数驱动程序应该指定 FALSE,但在某些情况下 TRUE 更合适;它强制单个客户端连接到设备。
  • DeviceObject - 返回的指针,作为指向指针的指针传递。 如果成功,IoCreateDe 会从非分页池中分配结构并将结果指针存储在取消引用的参数中.

在调用 IoCreateDevice 之前,我们必须创建一个 UNICODE_STRING 来保存内部设备名称:

UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\PriorityBooster");
// RtlInitUnicodeString(&devName, L"\\Device\\ThreadBoost");

设备名称可以是任何名称,但应位于设备对象管理器目录中。 有两种方法可以用常量字符串初始化 UNICODE_STRING。 第一个是使用 RtlInitUnicodeString,它工作得很好。 但是 RtlInitUnicodeString 必须计算字符串中的字符数才能适当地初始化 Length 和 MaximumLength。 在这种情况下没什么大不了的,但有一种更快的方法 - 使用 RTL_CONSTANT_STRING 宏,它在编译时静态计算字符串的长度,这意味着它只能与常量字符串一起正常工作。 现在我们可以调用 IoCreateDevice 函数:

PDEVICE_OBJECT DeviceObject;
NTSTATUS status = IoCreateDevice(
	DriverObject // our driver object,
	0 // no need for extra bytes,
	& devName // the device name,
	FILE_DEVICE_UNKNOWN // device type,
	0 // characteristics flags,
	FALSE // not exclusive,
	& DeviceObject // the resulting pointer
);
if (!NT_SUCCESS(status)) {
	KdPrint(("Failed to create device object (0x%08X)\n", status));
	return status;
}

如果一切顺利,我们现在有一个指向设备对象的指针。 下一步是通过提供符号链接使用户模式调用者可以访问此设备对象。 以下行创建一个符号链接并将其连接到我们的设备对象:

UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\PriorityBooster");
status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status)) {
	KdPrint(("Failed to create symbolic link (0x%08X)\n", status));
	IoDeleteDevice(DeviceObject);
	return status;
}

IoCreateSymbolicLink 通过接受符号链接和链接的目标来完成工作。 请注意,如果创建失败,我们必须通过调用 IoDeleteDevice 撤消到目前为止所做的一切 - 在这种情况下只是设备对象已创建的事实。 更一般地,如果 DriverEntry 返回任何失败状态,则不调用 Unload 例程。 如果我们有更多的初始化步骤要做,我们必须记住在失败的情况下撤消所有的操作。 我们将在第 5 章看到一种更优雅的处理方式。 一旦我们建立了符号链接和设备对象,DriverEntry 可以返回成功,驱动程序现在准备好接受请求。 在我们继续之前,我们不能忘记卸载例程。 假设 DriverEntry 成功完成,Unload 例程必须撤消在 DriverEntry 中所做的任何事情。 在我们的例子中,有两件事:设备对象创建和符号链接创建。 我们将以相反的顺序撤消它们:

void PriorityBoosterUnload(_In_ PDRIVER_OBJECT DriverObject) {
	UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\PriorityBooster");
	// delete symbolic link
	IoDeleteSymbolicLink(&symLink);
	// delete device object
	IoDeleteDevice(DriverObject->DeviceObject);
}

客户端代码

此时值得编写用户模式客户端代码。 我们为客户需要的一切都已经定义好了。 将一个新的控制台桌面项目添加到名为 Booster(或您选择的其他名称)的解决方案中。 Visual Studio 向导应在 Visual Studio 2017 中创建一个源文件 (Visual Studio 2019) 和两个预编译头文件 (pch.h、pch.cpp)。 您现在可以放心地忽略预编译的头文件。 在 Booster.cpp 文件中,删除默认的“hello, world”代码并添加以下声明:

#include <windows.h>
#include <stdio.h>
#include "..\PriorityBooster\PriorityBoosterCommon.h"

请注意,我们包含了由驱动程序创建并与客户端代码共享的公共头文件。 更改主函数以接受命令行参数。 我们将使用命令行参数接受线程 ID 和优先级,并请求驱动程序将线程的优先级更改为给定值。

int main(int argc, const char* argv[]) {
	if (argc < 3) {
		printf("Usage: Booster <threadid> <priority>\n");
		return 0;
	}

接下来我们需要打开我们设备的句柄。 CreateFile 的“文件名”应该是前面带有“\\.\”的符号链接。整个调用应如下所示:

HANDLE hDevice = CreateFile(L"\\\\.\\PriorityBooster", GENERIC_WRITE,
	FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
return Error("Failed to open device");
The Error function simply prints some text with the last error occurred :
int Error(const char* message) {
	printf("%s (error=%d)\n", message, GetLastError());
	return 1;
}

CreateFile 调用应在其 IRP_MJ_CREATE 调度例程中到达驱动程序。 如果此时未加载驱动程序——这意味着没有设备对象和符号链接——我们将得到错误号 2(未找到文件)。 现在我们有了设备的有效句柄,是时候设置对 DeviceIoControl 的调用了。 首先,我们需要创建一个 ThreadData 结构并填写详细信息:

ThreadData data;
data.ThreadId = atoi(argv[1]); // command line first argument
data.Priority = atoi(argv[2]); // command line second argument

现在我们准备好调用 DeviceIoControl 并在之后关闭设备句柄:

DWORD returned;
BOOL success = DeviceIoControl(hDevice,
	IOCTL_PRIORITY_BOOSTER_SET_PRIORITY, // control code
	&data, sizeof(data), // input buffer and length
	nullptr, 0, // output buffer and length
	&returned, nullptr);
if (success)
printf("Priority change succeeded!\n");
else
Error("Priority change failed!");
CloseHandle(hDevice);

DeviceIoControl 通过调用 IRP_MJ_DEVICE_CONTROL 主函数例程到达驱动程序。 至此,客户端代码完成。 剩下的就是实现我们在驱动程序端声明的调度例程。

创建和关闭调度程序

现在我们准备实现驱动程序定义的三个调度例程。 迄今为止最简单的是创建和关闭例程。 所需要做的就是以成功状态完成请求。 这是完整的创建/关闭调度例程实现:

Use_decl_annotations_
NTSTATUS PriorityBoosterCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
	UNREFERENCED_PARAMETER(DeviceObject);
	Irp->IoStatus.Status = STATUS_SUCCESS;
	Irp->IoStatus.Information = 0;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);
	return STATUS_SUCCESS;
}

每个调度例程都接受目标设备对象和一个 I/O 请求包 (IRP)。 我们不太关心设备对象,因为我们只有一个,所以它必须是我们在 DriverEntry 中创建的那个。 另一方面,IRP 非常重要。 我们将在第 6 章深入探讨 IRP,但现在我们需要快速浏览一下 IRP。

IRP 是一种代表请求的半文档化结构,通常来自Executive 中的一个管理器:I/O 管理器、即插即用管理器或电源管理器。 使用简单的软件驱动程序,很可能就是 I/O 管理器。 不管 IRP 的创建者是谁,驱动程序的目的都是处理 IRP,这意味着查看请求的详细信息并执行完成它需要做的事情。

对驱动程序的每个请求总是包装在一个 IRP 中,无论是创建、关闭、读取还是任何其他 IRP。 通过查看 IRP 的成员,我们可以了解请求的类型和详细信息(从技术上讲,调度例程本身是根据请求类型指向的,因此在大多数情况下您已经知道请求类型)。 值得一提的是,IRP 永远不会单独到达。它伴随着一个或多个 IO_STACK_LOCATION 类型的结构。 在像我们的驱动程序这样的简单情况下,只有一个 IO_STACK_LOCATION。 在我们上面或下面有过滤器驱动程序的更复杂的情况下,存在多个 IO_STACK_LOCATION 实例,一个用于设备堆栈中的每一层。 (我们将在第 6 章更彻底地讨论这个问题)。

简而言之,我们需要的一些信息在基本 IRP 结构中,一些在设备堆栈中我们“级别”的 IO_STACK_LOCATION 中。 在创建和关闭的情况下,我们不需要查看任何成员。我们只需要在其 IoStatus 成员(类型为 IO_STATUS_BLOCK)中设置 IRP 的状态,该成员有两个成员:

  • Status - 指示此请求将完成的状态。
  • Information - 一个多态成员,在不同的请求中表示不同的东西。 在创建和关闭的情况下,零值就可以了。

为了真正完成 IRP,我们调用 IoCompleteRequest。 这个函数有很多事情要做,但基本上它会将 IRP 传播回它的创建者(通常是 I/O 管理器),并且该管理器会通知客户端操作已完成。 第二个参数是驱动程序可以提供给其客户端的临时优先级提升值。 在大多数情况下,零值是最好的(IO_NO_INCREMENT 定义为零),因为请求是同步完成的,所以调用者没有理由获得优先级提升。 同样,第 6 章提供了有关此功能的更多信息。

最后要做的操作是返回与放入 IRP 的状态相同的状态。 这可能看起来像一个无用的重复,但它是必要的(原因将在后面的章节中更清楚)。

DeviceIoControl 调度例程

这是问题的症结所在。 到目前为止,所有驱动程序代码都导致了这个调度例程。 这是将给定线程设置为请求优先级的实际工作。 我们需要检查的第一件事是控制代码。 典型的驱动程序可能支持许多控制代码,因此如果无法识别控制代码,我们希望立即使请求失败:

_Use_decl_annotations_
NTSTATUS PriorityBoosterDeviceControl(PDEVICE_OBJECT, PIRP Irp) {
	// get our IO_STACK_LOCATION
	auto stack = IoGetCurrentIrpStackLocation(Irp); // IO_STACK_LOCATION*
	auto status = STATUS_SUCCESS;
	switch (stack->Parameters.DeviceIoControl.IoControlCode) {
	case IOCTL_PRIORITY_BOOSTER_SET_PRIORITY:
		// do the work
		break;
	default:
		status = STATUS_INVALID_DEVICE_REQUEST;
		break;
	}

获取任何 IRP 信息的关键是查看与当前设备层关联的 IO_STACK_LOCATION 内部。 调用 IoGetCurrentIrpStackLocation 会返回一个指向正确 IO_STACK_LOCATION 的指针。 在我们的例子中,实际上只有一个 IO_STACK_LOCATION,但无论如何调用 IoGetCurrentIrpStackLocation 是正确的调用。

IO_STACK_LOCATION 中的主要成分是一个名为Parameters 的巨大联合成员,它拥有一组结构,每个结构对应一种IRP。 在 IRP_MJ_DEVICE_CONTROL 的情况下,要查看的结构是 DeviceIoControl。 在该结构中,我们可以找到客户端传达的信息,例如控制代码、缓冲区及其长度。

switch 语句使用 IoControlCode 成员来确定我们是否理解控制代码。 如果没有,我们只需将状态设置为成功以外的其他值,然后跳出 switch 块。 我们需要的最后一段通用代码是在 switch 块之后完成 IRP,无论它是否成功。 否则,客户端将不会收到完成响应:

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

无论状态如何,我们只需完成 IRP。 如果控制代码未被识别,那将是故障状态。 否则,这将取决于在我们确实识别控制代码的情况下所做的实际工作。

最后一部分是最有趣和最重要的:做改变线程优先级的实际工作。 第一步是检查我们收到的缓冲区是否足够大,可以包含一个 ThreadData 对象。 指向用户提供的输入缓冲区的指针在 Type3InputBuffer 成员中可用,输入缓冲区长度为 InputBufferLength:

if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(ThreadData)) {
	status = STATUS_BUFFER_TOO_SMALL;
	break;
}

您可能想知道,访问提供的缓冲区是否真的合法。 由于这个缓冲区在用户空间,我们必须在客户端进程的上下文中。 我们确实是这样,因为调用者是客户端的线程本身,它转换到了第 1 章中描述的内核模式。

接下来,我们可以假设缓冲区足够大,所以我们将其视为 ThreadData:

auto data = (ThreadData*)stack->Parameters.DeviceIoControl.Type3InputBuffer;

如果指针为 NULL,那么我们应该中止:

if (data == nullptr) {
	status = STATUS_INVALID_PARAMETER;
	break;
}

接下来看看优先级是否在1到31的合法范围内,如果不是则中止:

if (data->Priority < 1 || data->Priority > 31) {
	status = STATUS_INVALID_PARAMETER;
	break;
}

我们越来越接近我们的目标。 我们要使用的 API 是 KeSetPriorityThread,原型如下:

KPRIORITY KeSetPriorityThread(
	_Inout_ PKTHREAD Thread,
	_In_ KPRIORITY Priority);

KPRIORITY 类型只是一个 8 位整数。 线程本身由指向 KTHREAD 对象的指针标识。 KTHREAD 是内核管理线程的方式之一。 它完全没有文档记录,但这里的重点是我们从客户端获得线程 ID,并且需要以某种方式获取指向内核空间中真正线程对象的指针。 可以通过 ID 查找线程的函数被恰当地命名为 PsLookupThreadByThreadId。 为了得到它的定义,我们需要添加另一个#include:

 #include <ntifs.h>

请注意,您必须在 <ntddk.h> 之前添加此 #include,否则会出现编译错误。 现在我们可以把我们的线程 ID 变成一个指针:

PETHREAD Thread;
status = PsLookupThreadByThreadId(ULongToHandle(data->ThreadId), &Thread);
if (!NT_SUCCESS(status))
    break;

这段代码有几个重点:

  • 查找函数被输入为接受一个句柄而不是某种ID。 那么它是句柄还是ID?这是一个键入为句柄的 ID。 原因与进程和线程 ID 的生成方式有关。 这些是从全局私有内核句柄表生成的,因此句柄“值”是实际的 ID。 ULongToHandle 宏提供了必要的转换以使编译器满意。 (请记住,在 64 位系统上 HANDLE 是 64 位的,但客户端提供的线程 ID 始终是 32 位。)
  • 结果指针类型为 PETHREAD 或指向 ETHREAD 的指针。 同样,ETHREAD 完全没有文档记录。 无论如何,我们似乎遇到了问题,因为 KeSetPriorityThread 接受 PKTHREAD 而不是 PETHREAD。 事实证明这些是相同的,因为 ETHREAD 的第一个成员是 KTHREAD(该成员名为 Tcb)。 我们将在下一章使用内核调试器时证明这一切。 最重要的是,我们可以在需要时安全地将 PKTHREAD 切换为 PETHREAD,反之亦然。
  • PsLookupThreadByThreadId 可能因多种原因而失败,例如非法线程 ID 或已终止的线程。 如果调用失败,我们只需使用函数返回的任何状态退出开关。

现在我们终于准备好改变优先级了。 但是等等——如果在最后一次调用成功之后线程被终止,就在我们设置它的新优先级之前呢?请放心,这实际上不可能发生。 从技术上讲,线程可以在该点终止,但这不会使我们的指针悬空。 这是因为查找函数如果成功,会增加内核线程对象的引用计数,因此在我们显式减少引用计数之前它不会死掉。 这是进行优先级更改的调用:

KeSetPriorityThread((PKTHREAD)Thread, data->Priority);

现在剩下要做的就是减少线程对象的引用;否则,我们手上有泄漏,只有在下次系统启动时才能解决。 完成这一壮举的函数是 ObDereferenceObject:

ObDereferenceObject(Thread);

我们完成了!作为参考,这里是完整的 IRP_MJ_DEVICE_CONTROL 处理程序,有一些小的外观变化:

_Use_decl_annotations_
NTSTATUS PriorityBoosterDeviceControl(PDEVICE_OBJECT, PIRP Irp) {
	// get our IO_STACK_LOCATION
	auto stack = IoGetCurrentIrpStackLocation(Irp); // IO_STACK_LOCATION*
	auto status = STATUS_SUCCESS;
	switch (stack->Parameters.DeviceIoControl.IoControlCode) {
	case IOCTL_PRIORITY_BOOSTER_SET_PRIORITY: {
		// do the work
		auto len = stack->Parameters.DeviceIoControl.InputBufferLength;
		if (len < sizeof(ThreadData)) {
			status = STATUS_BUFFER_TOO_SMALL;
			break;
		}

		auto data = (ThreadData*)stack->Parameters.DeviceIoControl.Type3InputBuffer;
		if (data == nullptr) {
			status = STATUS_INVALID_PARAMETER;
			break;
		}
		if (data->Priority < 1 || data->Priority > 31) {
			status = STATUS_INVALID_PARAMETER;
			break;
		}

		PETHREAD Thread;
		status = PsLookupThreadByThreadId(ULongToHandle(data->ThreadId), &Thread);
		if (!NT_SUCCESS(status))
			break;

		KeSetPriorityThread((PKTHREAD)Thread, data->Priority);
		ObDereferenceObject(Thread);
		KdPrint(("Thread Priority change for %d to %d succeeded!\n",data->ThreadId, data->Priority));
		break;
	}
	default:
		status = STATUS_INVALID_DEVICE_REQUEST;
		break;
	}
	Irp->IoStatus.Status = status;
	Irp->IoStatus.Information = 0;
	IoCompleteRequest(Irp, IO_NO_INCREMENT);
	return status;
}

安装和测试

至此我们就可以成功构建驱动和客户端了。 我们的下一步是安装驱动程序并测试其功能。 您可以在虚拟机上尝试以下操作,或者如果您有足够的勇气 - 在您的开发机上尝试。

首先,让我们安装驱动程序。 打开一个提升的命令窗口并使用 sc.exe 工具进行安装,就像我们在第 2 章中所做的那样:

sc create booster type= kernel binPath= c:\Test\PriorityBooster.sys

确保 binPath 包含生成的 SYS 文件的完整路径。 示例中驱动程序(助推器)的名称是创建的注册表项的名称,因此必须是唯一的。 它不必与 SYS 文件名相关。 现在我们可以加载驱动了:

sc start booster

如果一切顺利,驱动程序将成功启动。 为了确保这一点,我们可以打开 WinObj 并查找我们的设备名称和符号链接。 图 4-1 显示了 WinObj 中的符号链接。

现在我们终于可以运行客户端可执行文件了。 图 4-2 显示了 cmd 的 Process Explorer 中的一个线程。 exe 进程选择作为我们希望将优先级设置为新值的示例。

 使用线程 ID 和所需优先级运行客户端(根据需要替换线程 ID):

booster 768 25

如果您在尝试运行可执行文件时遇到错误,您可能需要将运行时库设置为静态库而不是 DLL。 转到项目属性、C++ 节点、代码生成,然后选择多线程调试。

总结

我们已经了解了如何从头到尾构建一个简单但完整的驱动程序。 我们创建了一个用户模式客户端来与驱动程序进行通信。 在下一章中,我们将解决调试问题,这是我们在编写可能不符合我们预期的驱动程序时必须要做的事情。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值