本章带着写第一个驱动程序,能和用户程序通信的第一个程序
4.1 简介
本章要实现一个能够设置线程优先级为任何值并且不受进程优先级的限制的功能,避开 Windows API 设置优先级只能设置几个值的限制
4.2 驱动程序初始化
通常多数驱动都要在 DriverEntry 中进行如下操作:
- 设置 Unload 例程
- 设置本驱动程序支持的分发例程:通过 DeviceIoControl 进行通信,需要设置控制号,输入缓冲区和输出缓冲区
- 创建设备对象:调用
IoCreateDevice
进行创建设备对象的时候- 参数里的对象名是内部的设备名称,用户模式无法直接访问,需要通过创建该设备对象的符号链接,用户模式从符号链接进行访问
- 设备名称可以任意取,但必须位于 Device 目录下
- 该 API 会从非分页内存池里分配内存创建设备对象
- 创建指向设备对象的符号链接:
- 符号链接是将一个名称符号通过句柄表与设备对象进行连接,用户模式通过符号链接获得句柄号,从而通过句柄号进行操作设备对象
通信用的数据结构和控制号定义:
#pragma once
#include <ntifs.h>
// 参数:
// DeviceType: 设备类型,第三方驱动从0x8000开始
// Function: 用来指明操作类型,对于第三方驱动从0x800开始
// Method: 指明用户提供的输入输出缓冲区是如何传入驱动的
// Access: 指明是到达驱动程序还是来自驱动程序
#define CTL_CODE(DeviceType,Function,Method,Access)(\
((DeviceType) << 16) | ((Access)<<14) | ((Function) << 2) | Method)
#define PRIORITY_BOOSTER_DEVICE 0x8000
#define IOCTL_PRIORITY_BOOSTER_SET_PRIORITY CTL_CODE(PRIORITY_BOOSTER_DEVICE,0x800,METHOD_NEITHER,FILE_ANY_ACCESS)
typedef struct _ThreadData {
ULONG ThreadId; // 需要修改的线程的ID
int Priority; // 需要修改的优先级
}ThreadData, * PThreadData;
初始化代码:
#include <ntifs.h>
#include <ntddk.h>
#include "PBCommon.h"
#define DEVICE_NAME L"\\Device\\PriorityBooster"
#define SYMBLE_LINK_NAME L"\\??\\PriorityVooster"
VOID DriverUnload(PDRIVER_OBJECT pDriverObject) {
// 还原驱动所做的操作
// 删除符号链接
UNICODE_STRING symLink = RTL_CONSTANT_STRING(SYMBLE_LINK_NAME);
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(pDriverObject->DeviceObject);
KdPrint(("Bye World!\n"));
}
EXTERN_C NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pReg) {
// 指定Unload例程
pDriverObject->DriverUnload = DriverUnload;
// 操作设备对象至少需要CREATE和CLOSE操作分发例程
pDriverObject->MajorFunction[IRP_MJ_CREATE] = PriorityBoosterCreateClose;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = PriorityBoosterCreateClose;
// 通过设备对象使得信息能从R3传入驱动,需要使用DeviceIoControl,该API通知的是DEVICE_CONTROL
// 通过这种方法进行通信需要控制码和输入缓冲区
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PriorityBoosterDeviceControl;
// 创建设备对象,经典的驱动程序只需要一个设备对象,通过符号链接指向它即可
UNICODE_STRING devName = RTL_CONSTANT_STRING(DEVICE_NAME);
PDEVICE_OBJECT pDeviceObject = NULL;
NTSTATUS ntStatus = IoCreateDevice(pDriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject);
// 提供符号链接
UNICODE_STRING symLink = RTL_CONSTANT_STRING(SYMBLE_LINK_NAME);
ntStatus = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(ntStatus)) {
// 如果失败了,需要回退之前的操作,因为DriverEntry中返回了非成功的状态,DriverUnload是不会被调用执行的
KdPrint(("Failed to create symbolic link (0x%08x)\n",ntStatus));
IoDeleteDevice(pDeviceObject);
return ntStatus;
}
KdPrint(("Hello World!\n"));
return STATUS_SUCCESS;
}
4.3 客户程序代码
主要内容都写在注释里了,没啥好说的,直接看代码吧:
#include <stdio.h>
#include <Windows.h>
#define CTL_CODE(DeviceType,Function,Method,Access)(\
((DeviceType) << 16) | ((Access)<<14) | ((Function) << 2) | Method)
#define PRIORITY_BOOSTER_DEVICE 0x8000
#define IOCTL_PRIORITY_BOOSTER_SET_PRIORITY CTL_CODE(PRIORITY_BOOSTER_DEVICE,0x800,METHOD_NEITHER,FILE_ANY_ACCESS)
typedef struct _ThreadData {
ULONG ThreadId; // 需要修改的线程的ID
int Priority; // 需要修改的优先级
}ThreadData, * PThreadData;
int Error(const char* message) {
printf("%s (error=%d)\n",message,GetLastError());
return 1;
}
int main(int argc,const char* argv[])
{
if (argc < 3) {
printf("Usage: Booster <threadid> <priority>\r\n");
return 0;
}
// 打开设备对象的句柄,该函数调用会触发IRP_MJ_CREATE分发例程,如果驱动未加载,则此处会--文件未找到
HANDLE hDevice = CreateFileW(L"\\\\.\\PriorityVooster",GENERIC_ALL,FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
// 构造缓冲区结构
ThreadData data = { 0 };
data.ThreadId = atoi(argv[1]);
data.Priority = atoi(argv[2]);
// 这里函数触发的是IRP_MJ_DEVICE_CONTROL分发例程
DWORD ret = 0;
BOOL success = DeviceIoControl(hDevice, IOCTL_PRIORITY_BOOSTER_SET_PRIORITY, &data, sizeof(data), nullptr, 0, &ret, nullptr);
if (success) {
printf("Priority change succedded!\n");
}
else {
Error("Priority change failed!");
}
// 这里函数触发的是IRP_MJ_CLOSE分发例程
CloseHandle(hDevice);
return 0;
}
4.4 Create 和 Close 分发实例
每个分发实例都接受目标设备对象和 IRP 请求包作为参数,不管 IRP 的创建者是谁,驱动程序的目的就是处理 IRP 请求
IRP 总是会伴随着 IO_STACK_LOCATION
到来,从用户模式传入的数据都是通过该结构获得
要处理 Irp 请求给与返回,见代码:
_Use_decl_annotations_
NTSTATUS
PriorityBoosterCreateClose(
_In_ struct _DEVICE_OBJECT* DeviceObject,
_Inout_ struct _IRP* Irp
) {
Irp->IoStatus.Status = STATUS_SUCCESS; // 指用什么状态完成该请求
Irp->IoStatus.Information = 0; // 不同请求有不同含义,这里请求需要设置为0
IoCompleteRequest(Irp,IO_NO_INCREMENT); // 完成IRP响应,会把IRP传回给创建者(IO管理器),然后管理器通知客户程序操作完成情况
return STATUS_SUCCESS;
}
4.5 DeviceIoControl 分发实例
该实例在 4.4 的最简单处理的基础上,新增了对 IO_STACK_LOCATION
结构的获取,以及对控制码的处理
该结构的主要成员 Parameters 是一个巨大的联合体,包含了每一种 IRP 请求,一般来说,对应的 IRP 处理函数收到的就是对应的请求数据
这里如果参数和控制码传入均为正确,则通过参数传入的线程 ID 获取通过 PsLookupThreadByThreadId
线程结构,然后使用 KeSetPriorityThread
去设置线程优先级,这里获取线程结构的时候需要使用宏 ULongToHandle
去将线程 id 转换成句柄类型,因为 HANDLE 在 64 位系统下是 64 位的,而用户模式提供的线程 ID 总是 32 位的,所以需要进行转换
其他内容见代码注释吧:
_Use_decl_annotations_
NTSTATUS
PriorityBoosterDeviceControl(
_In_ struct _DEVICE_OBJECT* DeviceObject,
_Inout_ struct _IRP* Irp
) {
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto status = STATUS_SUCCESS;
// 筛选控制码
switch (stack->Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_PRIORITY_BOOSTER_SET_PRIORITY: {
// do the work
// 判断接收到的缓冲区大小是否足够
if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(ThreadData)) {
status = STATUS_BUFFER_TOO_SMALL;
break;
}
// 获取参数
auto data = (PThreadData)stack->Parameters.DeviceIoControl.Type3InputBuffer;
if (data == nullptr) {
status = STATUS_INVALID_PARAMETER;
break;
}
// 参数合法性检查
if (data->Priority < 1 || data->Priority>31) {
status = STATUS_INVALID_PARAMETER;
break;
}
// 将线程id转换成句柄,然后通过API获取线程结构,该API会增加线程结构的引用计数,需要手动减少
PETHREAD pEthread;
status = PsLookupThreadByThreadId(ULongToHandle(data->ThreadId), &pEthread);
if (!NT_SUCCESS(status)) {
break;
}
// 修改优先级
KeSetPriorityThread(pEthread, data->Priority);
// 手动减少引用计数
ObDereferenceObject(pEthread);
break;
}
default: {
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
}
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
4.6 安装与测试
通过驱动安装程序安装成功驱动之后,可以通过软件 DeviceTree 查看到驱动信息,如下图所示,根据驱动名称(好像一般是驱动文件名),找到驱动的设备链表,这里还可以看到支持的 IRP 请求,这里知道设备对象的名称是:\Device\PriorityBooster
要与驱动通信,一定要有设备对象,然后创建符号链接,用户模式通过符号链接去访问驱动设备对象,接下来通过设备名称去找对应的符号链接:可以看到这里的符号链接名称是 PriorityVooster
以上是如何查看已经安装启动的驱动的相关信息,接下来是运行演示一下刚刚写的驱动
首先,找一个线程,这里我就随便找了个测试用进程来演示,线程 id 是 7988,当前线程动态优先级是 10
运行用户程序之后,可见,该线程的动态优先级已经变成了 27,实验成功!