Windows驱动开发入门

开始

到微软官网上下载并安装SDK和WDK,这俩内部版本号必须得对上,且安装时必须保持默认安装路径不变,新建Empty WDM Driver项目。当编译时出现Spectre相关错误,在项目设置中把这东西关掉。如果还是不能编译通过,把项目目录下的.inf文件从工程中移除。当某些警告被当作错误处理时,降低警告等级并关闭警告视为错误。

如果你的项目设置中没有链接器等选项(像我这样),恭喜你需要重装系统,或搞个虚拟机装吧!

WinDBG调试

Windows 7

别真机搞,弄个虚拟机,先在虚拟机设置中把打印机删除,再添加串行端口,使用名字“\\.\pipe\com_1”,并把轮询时中断CPU勾选上。当虚拟机为Windows 7时运行msconfig,引导中选择高级选项并勾选调试。Windows 10中要在设置的安全和更新选项卡的针对开发人员选项中开启开发人员模式。最后运行命令:

1
bcdedit /set testsigning on

打开WinDBG Preview并Attach to Kernel,选项选COM,勾选Pipe和Reconnect,Port输入“\\.\pipe\com_1”。

Windows 10

关机然后删掉打印机,新建串口,方法同上。Windows 10中要在设置的安全和更新选项卡的针对开发人员选项中开启开发人员模式。运行命令:

1
2
3
4
5
6
bcdedit /set testsigning on
bcdedit /set “{current}” bootmenupolicy Legacy
bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200
bcdedit /copy “{current}” /d “Debug”
bcdedit /debug “{<新建的启动配置的标识符>}” on
bcdedit /enum ;看看Windows启动加载器的debug选项是不是Yes

重启自动进入Windows启动管理器,进入Debug,即可被WinDBG附加。

加载驱动

Windows 7 x64

兄弟,别用。这个系统的驱动加载与签名校验都有大病。

Windows 10

写个驱动并编译试试:

1
2
3
4
5
6
7
8
9
10
11
12
#include<ntddk.h>
VOID DriverUnload(PDRIVER_OBJECT pDriver) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[%d] Unloading...\r\n", __LINE__);
return;
};
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING RegistryPath) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "RegistryPath = %S\r\n", RegistryPath->Buffer);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[%d] Loading...\r\n", __LINE__);
//...
pDriverObject->DriverUnload = DriverUnload; //设置驱动卸载入口函数
return STATUS_SUCCESS;
};

驱动想装载需要签名校验,有两种方法,一是尝试签个能用的证书,二是直接关掉Win10的签名校验机制。

第一种方法用软件Windows 64Signer,勾选上开启Test Signing启动选项然后重启,用它签名.sys即可。

第二种方法尝试在管理员下执行:

1
2
bcdedit /set testsigning on
bcdedit -set loadoptions DDISABLE_INTEGRITY_CHECKS

然后按着Shift点击开始菜单的重启,选择疑难解答->高级启动选项,重启,启动时选择倒数第二个选项(虚拟机可能汉字显示框框)。重启后桌面壁纸右下角应该显示测试模式即可。

然后以后每次都得在启动时选择带“DEBUG”的启动选项。

打开DebugView,菜单勾选上“Capture”的“Capture Win32”、“Capture Global Win32”、“Capture Kernel”等,再利用DriverMonitor进行加载即可,可以看到DebugView中的调试信息。

因为DSE保护机制,Windows 10开始任何驱动在测试模式下也得签名。这里修改_KLDR_DATA_TABLE_ENTRY中的Flags标志,来取消测试模式下的强制签名,当然正常模式下是无效的。

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
#include<ntddk.h>
BOOLEAN BypassCheckSign(PDRIVER_OBJECT pDriverObject) {
typedef struct _KLDR_DATA_TABLE_ENTRY {
LIST_ENTRY listEntry;
ULONG64 __Undefined1;
ULONG64 __Undefined2;
ULONG64 __Undefined3;
ULONG64 NonPagedDebugInfo;
ULONG64 DllBase;
ULONG64 EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING path;
UNICODE_STRING name;
ULONG Flags;
USHORT LoadCount;
USHORT __Undefined5;
ULONG64 __Undefined6;
ULONG CheckSum;
ULONG __padding1;
ULONG TimeDateStamp;
ULONG __padding2;
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection;
pLdrData->Flags = pLdrData->Flags | 0x20;
return TRUE;
};
VOID DriverUnload(PDRIVER_OBJECT pDriver) {
return;
};
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING RegistryPath) {
BypassCheckSign(Driver);
//...
pDriverObject->DriverUnload = DriverUnload; //设置驱动卸载入口函数
return STATUS_SUCCESS;
};

WinDBG初探

Attach to Kernel后,无论任何情况按一或两次F5或命令g即可继续被调试机的运行。例如调试驱动程序MyDriver1.sys,用DriverMonitor进行加载后,Alt+Delete进行中断,并命令bp MyDriver1+0x5000,其中0x5000是该.sys的入口点地址,随便找个PE分析工具都能看。重新运行几次驱动后可断在入口点。F11为步进,F10为步过,Shift+F11为运行到返回。

基础速成

字符串

双字节字符串定义:

1
2
3
4
5
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING *PUNICODE_STRING;

打印方法:

1
2
UNICODE_STRING str=RTL_CONSTANT_STRING(TEXT("xxx"));
DbgPrint("%wZ",&str); //估计DbgPrint不好使且必须为Passive中断级 可改用DbgPrintEx KdPrint只有在Debug版下输出

也有ANSI版的结构但极少使用。

UNICODE_STRING类型不能随便初始化,有两种方法,注意清零时Buffer一定要赋值为缓冲区指针,否则空指针访问异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
//法一
UNICODE_STRING str = { 0 };
WCHAR strBuf[128] = { 0 };
str.Buffer = strBuf;
wcscpy(str.Buffer, TEXT("xxx"));
str.Length = str.MaximumLength = wcslen(TEXT("xxx")) * sizeof(WCHAR);
//法二
UNICODE_STRING str;
str.Buffer = TEXT("xxx");
str.Length = str.MaximumLength = wcslen(TEXT("xxx")) * sizeof(WCHAR);
//法三
UNICODE_STRING str = { 0 };
RtlInitUnicodeString(&str, TEXT("xxx"));

字符串src拷贝到dst中,dst的缓冲区为dst_buf

1
2
3
4
5
UNICODE_STRING dst;
WCHAR dst_buf[256] = { 0 }; //需要用到内存分配 但还没学 先用定义缓冲区的方法
UNICODE_STRING src = RTL_CONSTANT_STRING(TEXT("xxx"));
RtlInitEmptyUnicodeString(&dst, dst_buf, 256 * sizeof(WCHAR));
RtlCopyUnicodeString(&dst, &src);

字符串连接,有两个API能用,这俩具体啥区别我也不知道:

1
2
3
NTSTATUS status = STATUS_SUCCESS;
status = RtlAppendUnicodeToString(&dst, &src); //缓冲区不够返回STATUS_BUFFER_TOO_SMALL
status = RtlAppendUnicodeStringToString(&dst, &src);

字符串打印,缓冲区不够直接截断并返回STATUS_BUFFER_OVERFLOW,最好是缓冲区多次扩展长度直到STATUS_SUCCESS为止:

1
2
3
4
5
6
7
8
NTSTATUS status=STATUS_SUCCESS;
UNICODE_STRING dst = { 0 };
WCHAR dst_buf[256] = { 0 };
UNICODE_STRING file_path = RTL_CONSTANT_STRING(TEXT("\\??\\C:\\xxx.xxx"));
RtlInitEmptyUnicodeString(&dst, dst_buf, 256 * sizeof(WCHAR));
USHORT file_size = 1024;
status = RtlStringCbPrintfW(dst.Buffer, 512 * sizeof(WCHAR), TEXT("%wZ -> %d\r\n"), &file_path, file_size);
dst.Length = wcslen(dst.Buffer) * sizeof(WCHAR); //通过RtlStringCbPrintfW打印的Buffer是以零结尾的 用wcslen问题不大

常用UNICODE_STRING字符串操作函数:

函数 描述
RtlInitUnicodeString 通过C字符串初始化
RtlCopyUnicodeString 复制,目标必须提前分配内存
RtlCompareUnicodeString 比较,可指明大小写是否无关
RtlEqualUnicodeString 比较相等,大小写相关
RtlAppendUnicodeStringToString 一个附加到另一个
RtlAppendUnicodeToString 一个UNICODE_STRING附加到另一个C字符串

C常用的也实现了:wcscpywcscatwcslenwcscpy_swcschrstrcpystrcpy_s

UNICODE_STRING与ANSI_STRING操作方法有:

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
#include <ntstrsafe.h>
// 定义内核字符串
ANSI_STRING ansi;
UNICODE_STRING unicode;
UNICODE_STRING str;
// 定义普通字符串
char* char_string = "hello";
wchar_t* wchar_string = (WCHAR*)"hello";
// 初始化字符串的多种方式
RtlInitAnsiString(&ansi, char_string);
RtlInitUnicodeString(&unicode, wchar_string);
RtlUnicodeStringInit(&str, L"hello");
// 改变原始字符串(乱码位置,此处仅用于演示赋值方式)
char_string[0] = (CHAR)"A";     // char类型每个占用1字节
char_string[1] = (CHAR)"B";
wchar_string[0] = (WCHAR)"A";     // wchar类型每个占用2字节
wchar_string[2] = (WCHAR)"B";
// 输出字符串 %Z
DbgPrint("输出ANSI: %Z \n", &ansi);
DbgPrint("输出WCHAR: %Z \n", &unicode);
DbgPrint("输出字符串: %wZ \n", &str);

NTSTATUS flag;
ULONG number;
UNICODE_STRING uncode_buffer_source = { 0 };
UNICODE_STRING uncode_buffer_target = { 0 };
// 字符串转为数字
RtlInitUnicodeString(&uncode_buffer_source, L"100");
flag = RtlUnicodeStringToInteger(&uncode_buffer_source, 10, &number);
if (NT_SUCCESS(flag))
DbgPrint("字符串 -> 数字: %d \n", number);
// 数字转为字符串
uncode_buffer_target.Buffer = (PWSTR)ExAllocatePool(PagedPool, 1024);
uncode_buffer_target.MaximumLength = 1024;
flag = RtlIntegerToUnicodeString(number, 10, &uncode_buffer_target);
if (NT_SUCCESS(flag))
DbgPrint("数字 -> 字符串: %wZ \n", &uncode_buffer_target);
// 释放堆空间
RtlFreeUnicodeString(&uncode_buffer_target);

UNICODE_STRING uncode_buffer_source = { 0 };
ANSI_STRING ansi_buffer_target = { 0 };
// 初始化 UNICODE 字符串
RtlInitUnicodeString(&uncode_buffer_source, L"hello");
// 转换函数
NTSTATUS flag = RtlUnicodeStringToAnsiString(&ansi_buffer_target,&uncode_buffer_source, TRUE);
if (NT_SUCCESS(flag))
DbgPrint("ANSI: %Z \n", &ansi_buffer_target);
// 销毁ANSI字符串
RtlFreeAnsiString(&ansi_buffer_target);

UNICODE_STRING uncode_buffer_source = { 0 };
ANSI_STRING ansi_buffer_target = { 0 };
// 初始化字符串
RtlInitString(&ansi_buffer_target, "hello");
// 转换函数
NTSTATUS flag = RtlAnsiStringToUnicodeString(&uncode_buffer_source,&ansi_buffer_target, TRUE);
if (NT_SUCCESS(flag))
DbgPrint("UNICODE: %wZ \n", &uncode_buffer_source);
// 销毁UNICODE字符串
RtlFreeUnicodeString(&uncode_buffer_source);

UNICODE_STRING uncode_buffer_source = { 0 };
ANSI_STRING ansi_buffer_target = { 0 };
char szBuf[1024] = { 0 };
// 初始化 UNICODE 字符串
RtlInitUnicodeString(&uncode_buffer_source, L"hello");
// 转换函数
NTSTATUS flag = RtlUnicodeStringToAnsiString(&ansi_buffer_target, &uncode_buffer_source, TRUE);
if (NT_SUCCESS(flag)) {
strcpy(szBuf, ansi_buffer_target.Buffer);
DbgPrint("输出char*字符串: %s \n", szBuf);
};
// 销毁ANSI字符串
RtlFreeAnsiString(&ansi_buffer_target);

UNICODE_STRING uncode_buffer_source = { 0 };
ANSI_STRING ansi_buffer_target = { 0 };
// 设置CHAR*
char szBuf[1024] = { 0 };
strcpy(szBuf, "hello lyshark");
// 初始化ANSI字符串
RtlInitString(&ansi_buffer_target, szBuf);
// 转换函数
NTSTATUS flag = RtlAnsiStringToUnicodeString(&uncode_buffer_source,
&ansi_buffer_target, TRUE);
if (NT_SUCCESS(flag))
DbgPrint("UNICODE: %wZ \n", &uncode_buffer_source);
// 销毁UNICODE字符串
RtlFreeUnicodeString(&uncode_buffer_source);

UNICODE_STRING uncode_buffer = { 0 };
wchar_t * wchar_string = L"hello";
uncode_buffer.MaximumLength = 1024; // 设置最大长度
uncode_buffer.Buffer = (PWSTR)ExAllocatePool(PagedPool, 1024); // 分配内存空间
uncode_buffer.Length = wcslen(wchar_string) * 2; // 设置字符长度 因为是宽字符,所以是字符长度的 2 倍
ASSERT(uncode_buffer.MaximumLength >= uncode_buffer.Length); // 保证缓冲区足够大,否则程序终止
RtlCopyMemory(uncode_buffer.Buffer, wchar_string, uncode_buffer.Length); // 将 wchar_string 中的字符串拷贝到 uncode_buffer.Buffer
uncode_buffer.Length = wcslen(wchar_string) * 2; // 设置字符串长度 并输出
DbgPrint("输出字符串: %wZ \n", uncode_buffer);
ExFreePool(uncode_buffer.Buffer); // 释放堆空间
uncode_buffer.Buffer = NULL;
uncode_buffer.Length = uncode_buffer.MaximumLength = 0;

UNICODE_STRING uncode_buffer[10] = { 0 };
wchar_t * wchar_string = L"hello";
int size = sizeof(uncode_buffer) / sizeof(uncode_buffer[0]);
DbgPrint("数组长度: %d \n", size);
for (int x = 0; x < size; x++) {
// 分配空间
uncode_buffer[x].Buffer = (PWSTR)ExAllocatePool(PagedPool, 1024);
// 设置长度
uncode_buffer[x].MaximumLength = 1024;
uncode_buffer[x].Length = wcslen(wchar_string) * sizeof(WCHAR);
ASSERT(uncode_buffer[x].MaximumLength >= uncode_buffer[x].Length);
// 拷贝字符串并输出
RtlCopyMemory(uncode_buffer[x].Buffer, wchar_string, uncode_buffer[x].Length);
uncode_buffer[x].Length = wcslen(wchar_string) * sizeof(WCHAR);
DbgPrint("循环: %d 输出字符串: %wZ \n", x, uncode_buffer[x]);
// 释放内存
ExFreePool(uncode_buffer[x].Buffer);
uncode_buffer[x].Buffer = NULL;
uncode_buffer[x].Length = uncode_buffer[x].MaximumLength = 0;
}

UNICODE_STRING uncode_buffer_source = { 0 };
UNICODE_STRING uncode_buffer_target = { 0 };
// 该函数可用于初始化字符串
RtlInitUnicodeString(&uncode_buffer_source, L"hello");
// 初始化target字符串,分配空间
uncode_buffer_target.Buffer = (PWSTR)ExAllocatePool(PagedPool, 1024);
uncode_buffer_target.MaximumLength = 1024;
// 将source中的内容拷贝到target中
RtlCopyUnicodeString(&uncode_buffer_target, &uncode_buffer_source);
// 输出结果
DbgPrint("source = %wZ \n", &uncode_buffer_source);
DbgPrint("target = %wZ \n", &uncode_buffer_target);
// 释放空间 source 无需销毁
// 如果强制释放掉source则会导致系统蓝屏,因为source是在栈上的
RtlFreeUnicodeString(&uncode_buffer_target);

UNICODE_STRING uncode_buffer_source = { 0 };
UNICODE_STRING uncode_buffer_target = { 0 };
// 该函数可用于初始化字符串
RtlInitUnicodeString(&uncode_buffer_source, L"hello");
RtlInitUnicodeString(&uncode_buffer_target, L"hello");
// 比较字符串是否相等
if (RtlEqualUnicodeString(&uncode_buffer_source, &uncode_buffer_target, TRUE))
DbgPrint("字符串相等 \n");
else
DbgPrint("字符串不相等 \n");

UNICODE_STRING uncode_buffer_source = { 0 };
UNICODE_STRING uncode_buffer_target = { 0 };
// 该函数可用于初始化字符串
RtlInitUnicodeString(&uncode_buffer_source, L"hello lyshark");
RtlInitUnicodeString(&uncode_buffer_target, L"HELLO LYSHARK");
// 字符串小写变大写
RtlUpcaseUnicodeString(&uncode_buffer_target, &uncode_buffer_source, TRUE);
DbgPrint("小写输出: %wZ \n", &uncode_buffer_source);
DbgPrint("变大写输出: %wZ \n", &uncode_buffer_target);
// 销毁字符串
RtlFreeUnicodeString(&uncode_buffer_target);

多线程安全性

一个函数的多线程安全性指的是一个函数在被调用过程中,还未返回时,又再次被调用情况下执行结果可靠性。

可能运行于多线程环境函数必须多线程安全,只运行于单线程环境的函数不需要。函数所有调用源只运行于同一单线程环境下则该函数也只运行在单线程环境下。函数其中一个调用源可能运行在多线程环境下或多个调用源可能运行于不同可并发多个线程环境下,且调用路径上没有采取多线程序列化强制措施时该函数可能运行在多线程环境下。若函数所有可能运行于多线程环境下调用路径上都有序列化强制措施,则运行在单线程环境下。只使用函数内部资源,完全不使用全局变量、静态变量或其他全局性资源时多线程安全的。对某个全局/静态变量所有访问被强制同步手段限制为同一时刻只有一个线程访问,即使使用全局/静态变量则不影响多线程安全性。

例如DriverEntryDriverUnload需要单线程,各种分发/完成/NDIS回调函数运行环境为多线程。

代码中断级(IRQL)

有Dispatch和Passive两种中断级,Dispatch比Passive级高。很多复杂功能API需要Passive级执行。调用路径上没有导致中断级改变的操作时,则与调用源中断级相同。调用路径上有获取自旋锁时中断级升高,有释放自旋锁时中断级下降。例如DriverEntryDriverUnload还有各种分发函数需要Passive级,完成函数、各种NDIS回调函数需要Dispatch级。升降方法如下,

1
2
3
4
5
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL,&oldIrql);
NT_ASSERT(KeGetCurrentIrql()==DISPATCH_LEVEL);
//...
KeLowerIrql(oldIrql);

提升IRQL后一定要在同一个函数中把它降下来,且保证它确实升或降了。其实IRQL升到DISPATCH_LEVEL及以上就没啥意义了,此时获得无限时间片,直到降回DISPATCH_LEVEL以下。任务管理器或Process Explorer中System interrupts或Interrupts伪进程显示花费在IRQL 2+上的CPU时间。

参数说明宏SAL

原语叫啥我忘了,反正与_In__Out_一样:

1
2
3
4
5
6
VOID NdisProtStatus(
IN NDIS_HANDLE ProtocolBindingContext,
IN NDIS_STATUS GeneralStatus,
__in_bcount(StatusBufferSize) IN PVOID StatusBuffer,//代表该参数字节长度由StatusBufferSize参数决定
IN UINT StatusBufferSize
);

预编译指令

默认代码在text段的PAGELK节,但一般可以把DriverEntry放到INIT节,INIT节初始化完毕后就被释放,不占内存。NDIS相关也可放到PAGE节,这个节在内存紧张时可被交换到磁盘上。但PAGE节函数不可在Dispatch级调用,否则可诱发缺页中断,且缺页中断处理不能在Dispatch级完成,可用PAGED_CODE宏测试,如果发现在Dispatch级程序直接报异常:

1
2
3
4
5
6
7
8
9
10
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE,SfAttachToMountedDevice)
NTSTATUS SfAttachToMountedDevice(IN PDEVICE_OBJECT DeviceObject, IN PDEVICE_OBJECT SFilterDeviceObject) {
PSFILTER_DEVICE_EXTENSION newDevExt = SFilterDeviceObject->DeviceExtension;
NTSTATUS status = STATUS_SUCCESS;
ULONG i = 0;

PAGED_CODE();
//...
};

内存

内存分配与释放,非分页内存永久在物理地址上而不会被交换到磁盘上,ExFreePool只能释放用ExAllocatePoolWithTag申请的内存,如果不释放则重启计算机前该内存将永久泄露。驱动可使用的有Paged Pool、NonPaged Pool、NonPagePoolNx。

1
2
3
4
5
6
7
8
9
10
11
NTSTATUS status=STATUS_SUCCESS;
UNICODE_STRING dst = { 0 };
UNICODE_STRING src = RTL_CONSTANT_STRING(TEXT("xxx"));
dst.Buffer = (PWCHAR)ExAllocatePoolWithTag(NonPagedPool, src.Length, 'MyTt'); //随便起名字
if (dst.Buffer == NULL)
status = STATUS_INSUFFICIENT_RESOURCES;
dst.Length = dst.MaximumLength = src.Length;
RtlCopyUnicodeString(&dst, &src);
ExFreePool(dst.Buffer);
dst.Buffer = NULL;
dst.Length = dst.MaximumLength = 0;

常用函数有:

函数 含义
ExAllocatePoolWithTag 从某内存池分配,带指明标记
ExAllocatePoolWithQuotaTag 同上,消耗当前进程分配额度
ExFreePool 释放,该函数知道内存从哪个内存池分配的

LIST_ENTRY结构

定义如下:

1
2
3
4
typedef struct _LIST_ENTRY{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
}LIST_ENTRY,*PLIST_ENTRY;

初始化与增加一个链表节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LIST_ENTRY my_list_head;
VOID MyFileInforInit(VOID) { //程序入口初始化调用
InitializeListHead(&my_list_head);
return;
};
typedef struct {
LIST_ENTRY list_entry; //最好LIST_ENTRY是第一个成员
PFILE_OBJECT file_object;
PUNICODE_STRING file_name;
LARGE_INTEGER file_length;
}MY_FILE_INFOR,*PMY_FILE_INFOR;
NTSTATUS MyFileInforAppendNode(PFILE_OBJECT file_object, PUNICODE_STRING file_name, PLARGE_INTEGER file_length) { //非线程安全的写法
PMY_FILE_INFOR my_file_infor = (PMY_FILE_INFOR)ExAllocatePoolWithTag(PagedPool, sizeof(MY_FILE_INFOR), 'MyTt');
if (my_file_infor == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
my_file_infor->file_object = file_object;
my_file_infor->file_name = file_name;
my_file_infor->file_length = file_length;
InsertHeadList(&my_list_head, (PLIST_ENTRY)&my_file_infor);
return STATUS_SUCCESS;
};

遍历链表:

1
2
3
4
for (PLIST_ENTRY p = my_list_head.Flink; p != &my_list_head.Flink; p = p->Flink) { //最后一个指向头节点
PMY_FILE_INFOR elem = CONTAINING_RECORD(p, MY_FILE_INFOR, list_entry);
//...
};

链表操作方法有:

函数 含义
InitializeListHead 初始化一个链表头部,前后向指针都指向自身
InsertHeadList 链表头部插入一项
InsertTailList 链表尾部插入一项
IsListEmpty 链表是否为空
RemoveHeadList 链表头部移去一项
RemoveTailList 链表尾部移去一项
RemoveEntryList 链表中移除指定一项
ExInterlockedInsertHeadList 用指定自旋锁,原子化从链表头部插入一项
ExInterlockedInsertTailList 用指定自旋锁,原子化从链表尾部插入一项
ExInterlockedRemoveHeadList 用指定自旋锁,原子化从链表头部移除一项

长长整型

LARGE_INTEGER类似于__int64

高32位为HighPart,低32位为LowPart,整个64位为QuadPart

1
2
3
4
5
6
LARGE_INTEGER a = { 0 }, b = { 0 };
a.QuadPart = 100;
a.QuadPart *= 100;
b.QuadPart = a.QuadPart;
if(b.QuadPart>1000)
//...

线程同步与互锁

驱动能用的互锁函数如下:

函数 描述
InterlockedIncrement(16/64) 对32/16/64位整数原子化加一
InterlockedDecrement(16/64) 对32/16/64位整数原子化减一
InterlockedAdd(64) 原子化将一个32/64位整数加到一个变量上
InterlockedExchanged(8/16/64) 原子化交换俩32/8/16/64位整数
InterlockedCompareExchange(64/128) 原子化比较一个变量与一个值,喜爱你高等则将提供的值交换到变量中并返回TRUE,否则将当前值放入变量中并返回FALSE

自旋锁

基本操作:

1
2
3
4
5
6
7
KSPIN_LOCK my_spin_lock; //只能是静态/全局变量或在堆上申请 要不每个函数都不一样

KeInitializeSpinLock(&my_spin_lock);
KIRQL irql = 0;
KeAcquireSpinLock(&my_spin_lock,&irql); //获得自旋锁 中断级别提高 其他线程被中断在这里
//需要单线程化的操作...
KeReleaseSpinLock(&my_spin_lock,&irql); //释放自旋锁

给LIST_ENTRY结构加锁:

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
LIST_ENTRY my_list_head;
KSPIN_LOCK my_list_lock;
VOID MyFileInforInit(VOID) { //程序入口初始化调用
InitializeListHead(&my_list_head);
KeInitializeSpinLock(&my_list_head);
return;
};
typedef struct {
LIST_ENTRY list_entry; //最好LIST_ENTRY是第一个成员
PFILE_OBJECT file_object;
PUNICODE_STRING file_name;
LARGE_INTEGER file_length;
}MY_FILE_INFOR,*PMY_FILE_INFOR;
NTSTATUS MyFileInforAppendNode(PFILE_OBJECT file_object, PUNICODE_STRING file_name, PLARGE_INTEGER file_length) { //非线程安全的写法
PMY_FILE_INFOR my_file_infor = (PMY_FILE_INFOR)ExAllocatePoolWithTag(PagedPool, sizeof(MY_FILE_INFOR), 'MyTt');
if (my_file_infor == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
my_file_infor->file_object = file_object;
my_file_infor->file_name = file_name;
my_file_infor->file_length = file_length;
ExInterlockedInsertHeadList(&my_list_head, (PLIST_ENTRY)&my_file_infor, &my_list_lock);
//移除一个节点并返回到my_file_infor中
my_file_infor=ExInterlockedRemoveHeadList(&my_list_head, &my_list_lock);
return STATUS_SUCCESS;
};

队列自旋锁使用KLOCK_QUEUE_HANDLE结构定义,除了获取和释放外,其他操作跟原子自旋锁一模一样,这是队列自旋锁的区别点:

1
2
3
4
5
6
KSPIN_LOCK my_Queue_SpinLock = { 0 };
KeinitializeSpinLock(&my_Queue_SpinLock);
KLOCK_QUEUE_HANDLE my_lock_queue_handle = { 0 };
KeAcquireInStackQueuedSpinLock(&my_Queue_SpinLock, &my_lock_queue_handle); //函数名意思不是自旋锁要放到栈上 而只是暗示中断级别要Dispatch级
//...
KeReleaseInStackQueuedSpinLock(&my_lock_queue_handle);

异常处理

可直接用SEH,这里略,但一定要在__finally中进行资源释放,否则可能出现释放前被异常打断。也可利用C++ 11 RAII即析构函数避免这种情况:

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
template<typename T=void>
struct kunique_ptr{
kunique_ptr(T* p=nullptr):_p(p){}
kunique_ptr(const kunique_ptr&)=delete;
kunique_ptr& operator=(const kunique_ptr&)=delete;
kunique_ptr(kunique_ptr&& other):_p(other._p){
other._p=nullptr;
}
kunique_ptr& operator=(kunique_ptr&& other){
if(&other!=this){
Release();
_p=other._p;
other._p=nullptr;
}
return *this;
}
~kunique_ptr(){
Release();
}
operator bool()const{
return _p!=nullptr;
}
T* operator->()const{
return _p;
}
T& operator*()const{
return *_p;
}
void Release(){
if(_p)
ExFreePool(_p);
}
private:
T* _p;
};
//运用如下:
struct MyData{
ULONG Data1;
HANDLE Data2;
};
void foo(void){
kunique_ptr<MyData> data((MyData*)ExAllocatePool(PagedPool,sizeof(MyData)));
data->Data1=10;
//此时自动释放缓冲区
return;
};

分发器对象/可等待对象

这些对象分为有信号和无信号两种状态,可等待的是因为线程可在这样的对象上等待直至他们变成有信号,等待期间不消耗CPU周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NTSTATUS KeWaitForSingleObject(
_In_ PVOID Object, //要等待的对象 可用ObReferenceObjectByHandle通过句柄获得对象
_In_ KWAIT_REASON WaitReason, //等待的原因 一般Executive 等待用户请求位UserRequest
_In_ KPROCESSOR_MODE WaitMode, //一般KernelMode
_In_ BOOLEAN Alertable, //等待过程中线程是否处于警报状态 允许传递用户模式APC 一般FALSE
_In_opt_ PLARGE_INTEGER Timeout //等待的超时值 NULL不指定直到对象变为有信号 单位100纳秒 负值表示相对是兼职 正值从1601年1月1日物业起绝对时间值
); //STATUS_SUCCESS等待完成 对象状态变为有信号 STATUS_TIMEOUT等待完成 超时已到
NTSTATUS KeWaitForMultipleObjects(
_In_ ULONG Count, //等待对象数目
_In_reads_(Count) PVOID Object[], //等待对象指针数组
_In_ WAIT_TYPE WaitType, //WaitAll等所有对象变为有信号 WaitAny有一个对象有信号就行
_In_ KWAIT_REASON WaitReason,
_In_ KPROCESSOR_MODE WaitMode,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout,
_Out_opt_ PKWAIT_BLOCK WaitBlockArray //用于对等待操作内部管理的结构数组
); //同上

WaitBlockArray参数:若对象数目小于等于THREAD_WAIT_OBJECTS即3则该参数可选,内核用每个线程里内建的数组,若对象数高于该数则驱动必须从非分页池中分配正确大小结构,并在等待结束后释放他们。

互斥量

互斥量在自由时为有信号态,一旦某线程调用等待函数并成功,该互斥量变为无信号态,该线程称为该互斥量拥有者。若某个线程是一个互斥量的拥有者,则此线程是唯一能释放该互斥量的线程。一个互斥量能多次被同一线程获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
KMUTEX MyMutex;
LIST_ENTRY DataHead;
void Init(void){
KeInitializeMutex(&MyMutex,0); //必须非分页池
return;
};
void DoWork(void){
KeWaitForSingleObject(&MyMutex,Executive,KernelMode,FALSE,nullptr);
__try{
//...
}
__finally{
KeReleaseMutex(&MyMutex,FALSE);
}
return;
};

或创建一个互斥量包装器:

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
struct Mutex{
void Init(void){
KeInitializeMutex(&_mutex,0);
};
void Lock(){
KeWaitForSingleObject(&_mutex,Executive,KernelMode,FALSE,nullptr);
}
void Unlock(){
KeReleaseMutex(&_mutex,FALSE);
}
private:
KMUTEX _mutex;
};
template<typename TLock>
struct AutoLock{
AutoLock(TLock& lock):_lock(lock){
lock,Lock();
}
~AutoLock(){
_lock.Unlock();
}
private:
TLocks _lock;
};

//使用:
Mutex MyMutex;
void Init(void){
MyMutex.Init();
return;
};
void DoWork(void){
AutoLock<Mutex> locker(MyMutex);
//...
};

若一个线程没有释放互斥量就死亡了,内核会显式释放该互斥量。下一个企图获得该互斥量的线程会在等待函数中接收STATUS_ABANDONED返回值,表示该互斥量被抛弃。但其他线程可正常获取该互斥量。

快速互斥量是正式互斥量的一个替代,提供更好的性能。快速互斥量不能递归获取,否则造成死锁。快速互斥量被获取后,IRQL会提升到APC_LEVEL,阻止该线程APC传递。快速互斥量无法指定超时值,只能无限等待。快速互斥量包装器实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FastMutex{
public:
void Init();
void Lock();
void Unlock();
private:
FAST_MUTEX _mutex;
};
void FastMutex::Init(){
ExInitializeFastMutex(&_mutex);
};
void FastMutex::Lock(){
ExAcquireFastMutex(&_mutex);
};
void FastMutex::Unlock(){
ExReleaseFastMutex(&_mutex);
};

信号量

信号量一般用于限制某些东西,比如队列长度。信号量最大值和初始值用KeInitializeSemaphore完成,一般初始值设为最大值。当内部值大于零时,信号量处于有信号态。此时用KeWaitForSingleObject的线程结束等待,信号量值减一。直到值达到零,信号量变为无信号态。KeReleaseSemaphore对信号量值加某个值,但一般用不上。

事件

事件封装一个布尔型标志,真为有信号,假为无信号。事件分为通知事件和同步时间,类型在初始化事件时指定。通知事件被设置后释放搜索正等待的线程,事件状态保持有信号直至被显式重置。同步时间被设置后最多释放一个线程,释放后该事件自动回到无信号状态。分配KEVENT结构,用KeInitializeEvent初始化,事件类型有NitificationEvent或SynchronizationEvent,初始事件状态为Signaled或non-signaled。用KeSetEvent设置事件为有信号,用KeResetEventKeClearEvent重置该事件为无信号,后者较快。

执行体资源

当访问共享资源的线程声明为读时,别的声明为读的线程能够并发读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ERESOURCE resource;
void WriteData(){
KeEnterCriticalRegion(); //禁止内核APC
ExAcquireResourceExclusiveList(&resource,TRUE); //获取排他锁用于写
//可写入数据
ExReleaseResource(&resource); //释放执行体资源
KeLeaveCriticalRegion();
};
//另一种合并写法
void WriteData(){
ExEnterCriticalRegionAndAcquireResourceExclusive(&resource);
//...
ExReleaseResourceAndLeaveCriticalRegion(&resource);
};

此外还可用KeAcquireResourceSharedLite获取共享锁。释放此资源占用内存前须先用ExDeleteResourceLite将此资源从内核资源列表中移除:

1
2
3
NTSTATUS ExDeleteResourceLite(
_Inout_ PRESOURCE Resource
);

驱动程序对象

即DRIVER_OBJECT,其MajorFunction字段的主功能代码有:

主功能 含义
IRP_MJ_CREATE 创建,由于ZwCreateFileCreateFile
IRP_MJ_CLOSE 关闭,由于ZwCloseFileCloseFile
IRP_MJ_READ 读,由于ZwReadFileReadFile
IRP_MJ_WRITE 写,由于ZwWriteFileWriteFile
IRP_MJ_DEVICE_CONTROL 驱动程序调用,由于ZwDeviceIoControlFileDeviceIoControlFile
IRP_MJ_INTERNAL_DEVICE_CONTROL 同上,仅限内核模式调用
IRP_MJ_PNP 即插即用相关
IRP_MJ_POWER 电源管理相关

没指定的默认指向IopInvalideDeviceRequest,返回错误状态表示不支持。为了能够打开驱动程序设备对象的句柄,至少要支持前俩。例如Process Explorer设备的符号链接为“GLOBAL??\PROCEXP152”,打开方法为:

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

驱动程序用IoCreateDevice创建设备对象,返回DRIVER_OBJECT结构的DeviceObject字段中。若创建多个设备对象则组成个单链表,NextDevice指向下一个设备对象,最后一个指向NULL。

线程优先级

线程优先级有:

优先级类别 THREAD_PRIORITY_IDLE THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL(默认) THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_IDLE THREAD_PRIORITY_TIME_CRITICAL 备注
空闲 1 2 3 4 5 6 15
低于普通 1 4 5 6 7 8 15
普通 1 6 7 8 9 10 15
高于普通 1 8 9 10 11 12 15
1 11 12 13 14 15 15 只有6个值
实时 16 22 23 24 25 26 31 16~31都能用

例如设置优先级为:

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

自旋锁

当IRQL在DISPATCH_LEVEL及以上更高级别时,无法将线程放入等待状态,由此引入自旋锁,它是内存中一个简单位。当CPU想获取自旋锁但它当前不自由时,CPU会在自旋锁上自旋,等待它被另一个CPU释放。获取自旋锁时需要IRQL为DISPATCH_LEVEL,不是的话要提升,释放自旋锁要把IRQL降回来。

创建自旋锁需要从非分页池分配一个KSPIN_LOCK结构,并用KeInitializeSpinLock初始化,至于未被拥有的状态。获取自旋锁后一定要在同一个函数中释放它,否则有死锁风险。

获取函数 释放函数 备注
KeAcquireSpinLock KeReleaseSpinLock
KeAcquireSpinLockAtDpcLevel KeReleaseSpinLockFromDpcLevel 只能在DISPATCH_LEVEL调用,常在DPC例程中使用
KeAcquireInterruptSpinLock KeReleaseInterruptSpinLock 用于ISR和别的函数同步,常在硬件驱动中使用
KeAcquireInStackQueuedSpinLock KeReleaseInStackQueuedSpinLock 排队自旋锁,先进先出,只用于DISPATCH_LEVEL
ExInterlockedXxx 用于操作LIST_ENTRY链表,IRQL提升到HIGH_LEVEL

工作项目

为显式创建一个线程,可用PsCreateSystemThreadIoCreateSystemThread创建分离执行线程。其中IoCreateSystemThread允许将一个设备对象或驱动程序对象关联到线程,让I/O系统为对象增加一个引用,使线程执行时驱动不会过早卸载。驱动创建的线程必须用PsTerminateSystemThread终止自身。创建线程适合需要后台长时间运行的代码,对于时间有限的操作应该用内核提供的线程池,在系统工作线程里执行代码。

工作项目为在系统线程池中排队的函数。驱动可分配和初始化工作项目,使其指向驱动希望执行的函数,随后让它在线程池排队。工作项目在PASSIVE_LEVEL执行。工作项目有两种方法创建和初始化工作项目。

第一种方法:用IoAllocateWorkItem分配和初始化工作项目,该函数指向不透明IO_WORKITEM指针,之后用IoFreeWorkItem释放它。第二种方法:用IoSizeofWorkItem提供的大小动态分配一个IO_WORKITEM结构,然后用IoInitializeWorkItem,用完后用IoUninitializeWorkItem。需要确保工作项目在排队或执行时,驱动没被卸载。

IoQueueWorkItem将工作项目排队:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void IoQueueWorkItem(
_Inout_ PIO_WORKITEM IoWorkItem, //工作项目
_In_ PIO_WORKITEM_ROUTINE WorkerRoutine, //回调函数
_In_ WORK_QUEUE_TYPE QueueType, //排队类型
_In_opt_ PVOID Context //驱动定义值
);
typedef enum _WORK_QUEUE_TYPE{ //系统线程池有多个队列 基于服务这些工作项目的线程优先级进行区分
CriticalWorkQueue, //优先级13
DelayedWorkQueue, //12 理论上必须使用这个
HyperCriticalWorkQueue, //15
NormalWorkQueue, //8
BackgroundWorkQueue, //7
RealTimeWorkQueue, //18
SuperCriticalWorkQueue, //14
MaximumWorkQueue,
CustomPriorityWorkQueue=32
} WORK_QUEUE_TYPE;
IO_WORKITEM_ROUTINE WorkItem;
void WorkItem( //回调函数原型
_In_ PDEVICE_OBJECT DeviceObject,
_In_opt_ PVOID Context
);

I/O请求包

入门

IRP总伴随一个或多个I/O栈位置IO_STACK_LOCATION结构。当一个IRP被分配时,调用者必须指明有多少I/O栈位置需要跟IRP一起分配,这些I/O栈位置紧跟IRP后面,I/O栈位置数量就是设备栈中设备对象的数量。I/O栈位置其中一个是给驱动用的,可用IoGetCurrentIrpStackLocation获取。

设备对象可被命名,CreateFile接收指向设备对象名的符号链接。设备栈中每个设备实际是个DEVICE_OBJECT结构,通过IoCreateDevice创建。

PDO物理设备对象由总线驱动创建,总线驱动是负责特定总线的驱动,如PCI、USB等,该PDO设备表明有默写设备位于此总线的设备槽中。FDO功能设备对象由驱动创建。FiDO过滤设备对象是由驱动创建的可选过滤设备。

设备节点创建事件序列为:PCI总线驱动pci.sys识别某个槽中有设备,它用IoCreateDevice创建一个PDO来表示这个事实。PCI总线驱动通知P&P管理器在它总线上发生变化,P&P管理器请求由总线驱动管理的PDO列表。现在P&P管理器的工作是发现并装载该新PDO驱动,并向总线驱动发送查询请求硬件设备ID。然后P&P管理器查询HKLM\System\CurrentControlSet\Enum\PCI\<硬件ID>,若驱动以前装载过,则在该位置注册,P&P管理器就可装载改驱动。驱动装载并用IoCreateDevice创建FDO,并用IoAttachDeviceToDeviceStack将自身置于前一层次PDO之上。

上述注册表路径下的Service值指向HKLM\System\CurrentControlSet\Services\<服务名>中的实际驱动,所有驱动必须在这里注册。

过滤驱动等收到不感兴趣的IRP时,选择直接向下传递IRP,此时需要先用IoSkipCurrentIrpStackLocation确保下一个设备能看到当前设备同一个I/O栈位置,再用IoAttachDeviceToDeviceStack获取下层设备对象,并用IoCallDriver传递下层设备对象和IRP。这是因为I/O管理器只初始化第一个I/O栈位置,驱动须为下一个驱动初始化下一个I/O栈位置,常用IoCopyIrpStackLocationToNext,但这里直接传递的话用IoSkipCurrentIrpStackLocation就行。

下层驱动完成请求时,除底层外任何一层都能再向下传递前用IoSetCompletion设置一个I/O完成例程。异步IRP时,驱动用IoMarkIrpPending将此IRP挂起,并从分发例程返回STATUS_PENDING,最终驱动完成。

IRP中部分常用字段:

字段 描述
MdlAddress 指向可选的内存描述符列表MDL,描述RAM缓冲区,用于直接I/O
AssociatedIrp 联合体。SystemBuffer最常见,指向系统分配的非分页池缓冲区,用于有缓冲I/O。IrpCount为与主IRP的关联IRP个数。MasterIrp本关联IRP的主IRP
IoStatus 包含NT_STATUS型Status和ULONG_PTR型Information字段,对于读写IRP,Information含义是操作传输字节数
UserEvent 指向KEVENT的指针。客户程序调用为异步时由客户程序提供。用户模式该事件在OVERLAPPED结构中通过HANDLE提供
UserBuffer 原始缓冲区指针
Cancel Routine 指向取消例程的指针。例如用用户模式CancelIoCancelIoEx

IO_STACK_LOCATION中常用字段:

字段 含义
MajorFunction IRP主功能代码
MinorFunction IRP次功能代码
Parameters 联合体,每个结构适用特定某个操作
FileObject 与此IRP关联的FILE_OBJECT,一般用不到
DeviceObject 与此IRP关联的设备对象,一般用不到
CompletionRoutine 完成例程
Context 传递给完成例程的参数,如果有的话

观察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
3: kd> !irpfind
Using a machine size of ffefc pages to configure the kd cache

*** CacheSize too low - increasing to 81 MB

Max cache size is : 85876736 bytes (0x14798 KB)
Total memory in cache : 30527 bytes (0x1e KB)
Number of regions cached: 123
75 full reads broken into 77 partial reads
counts: 65 cached/12 uncached, 84.42% cached
bytes : 40999 cached/7104 uncached, 85.23% cached
** Transition PTEs are implicitly decoded
** Prototype PTEs are implicitly decoded

Scanning large pool allocation table for tag 0x3f707249 (Irp?) (ffff80834c999000 : ffff80834c9f9000)

Irp [ Thread ] irpStack: (Mj,Mn) DevObj [Driver] MDL Process
ffff80834a173720 [0000000000000000] Irp is complete (CurrentLocation 3 > StackCount 2) 0x0000000000000000
...
3: kd> !irp ffff80834a173720
Irp is active with 2 stacks 3 is current (= 0xffff80834a173880)
Mdl=ffff80834f83aac0: No System Buffer: Thread 00000000: Irp is completed. Pending has been returned
cmd flg cl Device File Completion-Context
[N/A(0), N/A(0)]
0 0 00000000 00000000 00000000-00000000

Args: 00000000 00000000 00000000 00000000
[IRP_MJ_INTERNAL_DEVICE_CONTROL(f), N/A(0)]
0 0 ffff80834a250050 00000000 fffff8040b525260-ffff80834a9c5390
\Driver\stornvme
Args: 00000000 00000000 0x0 ffff80834a2501a0

Irp Extension present at 0xffff80834f598280:
3: kd> dt _IRP ffff80834a173720
nt!_IRP
+0x000 Type : 0n6
+0x002 Size : 0x358
+0x004 AllocationProcessorNumber : 1
+0x006 Reserved : 0
+0x008 MdlAddress : 0xffff8083`4f83aac0 _MDL
+0x010 Flags : 0
+0x018 AssociatedIrp : <anonymous-tag>
+0x020 ThreadListEntry : _LIST_ENTRY [ 0xffff8083`4a173740 - 0xffff8083`4a173740 ]
+0x030 IoStatus : _IO_STATUS_BLOCK
+0x040 RequestorMode : 0 ''
+0x041 PendingReturned : 0x1 ''
+0x042 StackCount : 2 ''
+0x043 CurrentLocation : 3 ''
+0x044 Cancel : 0 ''
+0x045 CancelIrql : 0 ''
+0x046 ApcEnvironment : 1 ''
+0x047 AllocationFlags : 0x44 'D'
+0x048 UserIosb : (null)
+0x050 UserEvent : (null)
+0x058 Overlay : <anonymous-tag>
+0x068 CancelRoutine : (null)
+0x070 UserBuffer : (null)
+0x078 Tail : <anonymous-tag>

常见IRP主功能码如下:

名称 调用者 含义
IRP_MJ_CREATE CreateFile 请求一个句柄
IRP_MJ_CLEANUP CloseHandle 关闭句柄时取消悬挂的IRP
IRP_MJ_CLOSE CloseHandle 关闭句柄
IRP_MJ_READ ReadFile 从设备得到数据
IRP_MJ_WRITE WriteFile 传送数据到设备
IRP_MJ_DEVICE_CONTROL DeviceIoControl IOCTL宏控制操作
IRP_MJ_INTERNAL_DEVICE_CONTROL 内核控制操作
IRP_MJ_QUERY_INFORMATION GetFileSize 得到文件长度
IRP_MJ_SET_INFORMATION SetFileSize 设置文件长度
IRP_MJ_FLUSH_BUFFERS FlushFileBuffersFlushConsoleInputBufferPurgeComm 写输出缓冲区或丢弃输入缓冲区
IRP_MJ_SHUTDOWN InitiateSystemShutdown 系统关闭

分发例程

所有分发例程原型定义相同:

1
2
3
4
typedef NTSTATUS DRIVER_DISPATCH(
_In_ PDEVICE_OBJECT DeviceObject,
_Inout_ PIRP Irp
);

一旦驱动决定处理IRP,意味着它不会将请求向下传递给另一个驱动,它最终必须完成它。否则句柄将泄露,请求线程无法终止,导致僵尸进程。若完成实在分发例程本身中实现,则例程必须用IoCompleteRequest返回与IRP中相同的状态。

1
2
3
4
5
6
7
NTSTATUS MyDispatchRoutine(PDEVICE_OBJECT,PIRP Irp){
//...
Irp->IoStatus.Status=STATUS_XXX;
Irp->IoStatus.Information=传输字节数; //错误时设为0
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_XXX; //不要再访问IRP结构了 可能已被释放
};

若完成时选择给线程增加优先级,则线程会以新优先级执行一个事件量度单位,之后优先级减一,又在这个降低后的优先级上获得另一个时间量度单位,直到优先级降低到原数值为止。优先级提高后不会超过15,顶多到15,若原优先级超过15则不起作用。

访问用户缓冲区

当IRQL为PASSIVE_LEVEL时,可正常处理页面错误,且线程是请求者,指针在此处理过程上下文中有效。但若IRQL为DISPATCH_LEVEL时,无法处理页面错误,且若执行DPC时,执行线程任意,指针在当前处理器进程中没有意义。由此引入缓冲I/O和直接I/O。

缓冲I/O

为支持缓冲I/O,需要先设置一个标志:

1
DeviceObejct->Flags|=DO_BUFFERED_IO;

步骤为:I/O管理器从非分页池中分配一个与用户缓冲区一样大的缓冲区,将指针保存到IRP的AssociatedIrp->SystemBuffer中,大小可从I/O栈位置的Parameters.Read.LengthParameters.Write.Length中获得。若为写请求,I/O管理器将用户缓冲区复制到系统缓冲区。此时驱动的分发例程被调用,驱动可直接使用系统缓冲区指针。当驱动完成该IRP时,对读请求而言,I/O管理器把系统缓冲区复制回用户缓冲区,复制到小为IRP的IoStatus.Information。最终I/O管理器释放系统缓冲区。

缓冲I/O便于使用,且总涉及复制,最好用于不超过一页的小缓冲区。

直接I/O

直接I/O允许在任何IRQL和线程中访问用户缓冲区,而不需要复制。设置标志:

1
DeviceObject->Flags|=DO_DIRECT_IO;

步骤为:I/O管理器先确认用户缓冲区合法,并利用页面错误将其装入物理内存。然后I/O管理器用MmProbeAndLockPages将缓冲区锁定在内存中而不被换出。接着I/O管理器构造一个内存描述符列表MDL,记录如何将缓冲区映射到RAM,地址保存到IRP的MdlAddress字段中。之后驱动用MmGetSystemAddressForMdlSafe将物理内存中映射的用户缓冲区二次映射到系统地址,系统地址在任何进程上下文均合法。驱动完成请求后,I/O管理器移除系统地址映射,用MmUnlockPages释放MDL并将用户缓冲区解锁。

MmGetSystemAddressForMdlSafe参数是一个MDL和一个页面优先级,后者为MM_PAGE_PRIORITY枚举类型。一般页面优先级设为NormalPagePriority,也可能用LowPagePriority或HighPagePriority,表示映射的重要程度。若该函数失败则返回NULL,表示空闲的系统页表很少或耗尽了,此时驱动必须用STATUS_INSUFFICIENT_RESOURCES状态完成IRP。

无I/O

当设备对象标志中没设置缓冲I/O和直接I/O,则使用无I/O方式。表示驱动不会从I/O管理器获得任何帮助,怎么处理用户缓冲区完全取决于驱动自身。

IRP_MJ_DEVICE_CONTROL用户缓冲区

用户模式DeviceIoControl原型如下:

1
2
3
4
5
6
7
8
9
10
BOOL DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode, //I/O控制代码
PVOID lpInBuffer, //输入缓冲区
DWORD nInBufferSize,
PVOID lpOutBuffer, //输出缓冲区
DWORD nOutBufferSize,
PDWORD lpdwBytesReturned,
LPOVERLAPPED lpOverlapped
);

控制代码由提供给CTL_CODE宏的四个参数组成:

1
CTL_CODE(DeviceType,Function,Method,Access)

其中Method选项有:

  • METHOD_NEITHER表示不需要I/O管理器帮助,不需要任何缓冲区,此时指向用户输入缓冲区的指针保存在当前I/O栈位置的Parameters.DeviceIoControl.Type3InputBuffer字段中,输出缓冲区保存在IRP的UserBuffer字段中。
  • METHOD_BUFFERED表示输入输出缓冲区均使用缓冲I/O。请求开始时,I/O管理器用输入输出缓冲区最大值从非分页池中分配系统缓冲区,然后将输入缓冲区复制到系统缓冲区,并调用IRP_MJ_DEVICE_CONTROL分发例程。请求完成后I/O管理器将系统缓冲区复制到输出缓冲区,复制的字节数为IRP的IoStatus.Information字段,系统缓冲区指针为IRP的AssociatedIrp.SystemBuffer
  • METHOD_IN_DIRECT和METHOD_OUT_DIRECT都是指输入缓冲区用缓冲I/O,输出缓冲区使用直接I/O。但前者输出缓冲区可读,后者可写。

其他主题

这里有各种未公开API:https://github.com/winsiderss/phnt

过滤驱动

IoAttachDevice将一个设备层叠到另一个设备上。该函数只能附加到设备栈顶,若TargetDevice参数不在栈顶,则自动改为附加对应栈顶的设备,并从AttachedDevice处返回实际附加的设备。

1
2
3
4
5
NTSTATUS IoAttachDevice(
PDEVICE_OBJECT SourceDevice, //要附加的设备对象
_In_ PUNICODE_STRING TargetDevice, //被附加的命名的设备对象
_Out_ PDEVICE_OBJECT* AttachedDevice //返回实际附加的对象
);

I/O管理器使用的I/O方式由顶部设备决定,所以附加时要复制原顶层设备的DO_BUFFERED_IO或DO_DIRECT_IO标志值。这里名称指的是WinObj的Device目录下的,如“\Device\ProcExp152”,忽略大小写。

此外还有IoAttachDeviceToDeviceStackIoAttachDeviceToDeviceStackSafe,这两个函数接收被附加设备对象而不是设备名。

内核代码可用IoGetDeviceObjectPointer通过设备名得到一个设备对象和一个此设备打开的文件对象。该函数返回的文件对象的引用计数会增加,要记得ObDereferenceObject以防文件对象泄露,返回的设备对象能作为IoAttachDeviceToDeviceStack(Safe)参数使用。

1
2
3
4
5
6
NTSTATUS IoGetDeviceObjectPointer(
_In_ PUNICODE_STRING ObjectName,
_In_ ACCESS_MASK DesiredAccess, //FILE_READ_DATA等
_Out_ PFILE_OBJECT* FileObject,
_Out_ PDEVICE_OBJECT* DeviceObject
);

新设备因为附加在设备栈顶部,所以得注册全部主功能:

1
2
3
DriverObject->DriverUnload = DevMonUnload;
for (auto& func : DriverObject->MajorFunction)
func = HandleFilterFunction; //该函数必须调用下层驱动

当对于不关心的请求要用IoCallDriver直接转发给下层设备,在此之前要准备好下一个I/O栈位置供下一层驱动使用。这里IoSkipCurrentIrpStackLocation减小内部IRP的I/O栈位置指针,并用IoCallDriver增加这个指针,这样下一层驱动看到这层同样I/O栈位置,不需任何复制。

1
2
IoSkipCurrentIrpStackLocation(Irp);
status=IoCallDriver(LowerDeviceObject,Irp);

驱动应再设备节点创建时调用附加回调函数,该函数设置如下:

1
DriverObject->DriverExtension->AddDevice=FilterAddDevice;

该回调在一个新的、属于本驱动的硬件设备被即插即用系统标识出来时被调用,原型为:

1
2
3
4
NTSTATUS AddDeviceRoutine(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PDEVICE_OBJECT PhysicalDeviceObject //栈底部的物理设备对象
);

例如注册回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NTSTATUS FilterAddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject) {
// example for the book only - not used in this driver
struct DeviceExtension {
PDEVICE_OBJECT LowerDeviceObject;
};
PDEVICE_OBJECT DeviceObject;
auto status = IoCreateDevice(DriverObject, sizeof(DeviceExtension), nullptr, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject); //第二个参数为请求I/O管理器与DEVICE_OBJECT一起分配一个额外缓冲区的大小 设备扩展机制
if (!NT_SUCCESS(status))
return status;
auto ext = (DeviceExtension*)DeviceObject->DeviceExtension;
status = IoAttachDeviceToDeviceStackSafe(DeviceObject, PhysicalDeviceObject, &ext->LowerDeviceObject);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(DeviceObject);
return status;
}
// copy some info from the lower device
DeviceObject->Flags |= (ext->LowerDeviceObject->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO));
// DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING; 注释掉以向PnP管理器表明设备已准备好
DeviceObject->Flags |= DO_POWER_PAGABLE; //电源IRP必须在IRQL<DISPATCH_LEVEL到达
DeviceObject->DeviceType = ext->LowerDeviceObject->DeviceType;
return status;
}

对于不基于硬件的设备,上述AddDevice回调不会被调用。这时可以自行创建一个设备对象,并可在任意时刻附加到过滤器。

用下层设备指针调用IoDetachDevice移除附加了的过滤器。当用户从系统中拔掉一个设备,硬件驱动会受到一个IRP_MJ_PNP、带有IRP_MN_REMOVE_DEVICE次功能代码的IRP请求,表示硬件不存在了,整个设备节点都不再需要,将要被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
NTSTATUS FilterDispatchPnp(PDEVICE_OBJECT fido,PIRP Irp){
auto ext=(DeviceExtension*)fido->DeviceExtension;
auto stack=IoGetCurrentIrpStackLocation(Irp);
UCHAR minor=stack->MinorFunction;
IoSkipCurrentIrpStackLocation(Irp);
auto status=IoCallDriver(ext->LowerDeviceObject,Irp);
if(minor==IRP_MN_REMOVE_DEVICE){
IoDeleteDevice(LowerDeviceObject);
IoDeleteDevice(fido);
}
return status;
}

硬件设备的处理很复杂,为了避免各种竞争条件,引入移除锁IO_REMOVE_LOCK对象。该结构管理着一个当前正处理的IRP引用计数和一个在I/O计数到达零和有一个移除操作正在进行时发送信号的事件。

驱动作为设备扩展一部分或全局变量分配此结构,用IoInitializeRemoveLock进行初始化。对每个IRP,驱动在将其传到下层设备前用IoAcquireRemoveLock获取移除锁,STATUS_DELETE_PENDING失败则表示有一个一处操作正在进行,驱动应立即返回。一旦下层驱动完成了IRP,则用IoReleaseRemoveLock释放移除锁。处理IRP_MN_REMOVE_DEVICE时,在脱离并删除设备前用IoReleaseRemoveLockAndWait,该函数在所有其他IRP均不再处理时成功返回。

例如向下传递请求的通用分发例程和IRP_MJ_PNP处理代码应修改如:

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
struct DeviceExtension{
IO_REMOVE_LOCK RemoveLock;
PDEVICE_OBJECT LowerDeviceObject;
};
NTSTATUS FilterGenericDispatch(PDEVICE_OBJECT DeviceObject,PIRP Irp){
auto ext=(DeviceExtension*)DeviceObject->DeviceExtension;
auto status=IoAcquireRemoveLock(&ext->RemoveLock,Irp);
if(!NT_SUCCESS(status)){ //STATUS_DELETE_PENDING
Irp->IoStatus.Status=status;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return status;
};
IoSkipCurrentIrpStackLocation(Irp);
status=IoCallDriver(ext->LowerDeviceObject,Irp);
IoReleaseRemoveLock(&ext->RemoveLock,Irp);
return status;
};
NTSTATUS FilterDispatchPnp(PDEVICE_OBJECT fido,PIRP Irp){
auto ext=(DeviceExtension*)fido->DeviecExtension;
auto status=IoAcuqireRemoveLock(&ext->RemoveLock,Irp);
if(!NT_SUCCESS(status)){ //STATUS_DELETE_PENDING
Irp->IoStatus.Status=status;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return status;
};
auto stack=IoGetCurrentIrpStackLocation(Irp);
UCHAR minor=stack->MinorFunction;
IoSkipCurrentIrpStackLocation(Irp);
auto status=IoCallDriver(ext->LowerDeviceObject,Irp);
if(minor==IRP_MN_REMOVE_DEVICE){
IoReleaseRemoveLockAndWait(&ext->RemoveLock,Irp);
IoDetachDevice(ext->LowerDeviceObject);
IoDeleteDevice(fido);
}
else
IoReleaseRemoveLock(&ext->RemoveLock,Irp);
return status;
};

驱动挂钩

可用ObReferrenceObjectByName通过名称定位到任何驱动:

1
2
3
4
5
6
7
8
9
10
NTSTATUS ObReferenceObjectByName(
_In_ PUNICODE_STRING ObjectPath,
_In_ ULONG Attributes,
_In_opt_ PACCESS_STATE PassedAccessState,
_In_opt_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_TYPE ObjectType,
_In_ KPROCESSOR_MODE AccessMode,
_Inout_opt_ PVOID ParseContext,
_Out_ PVOID* Object
);

用法如下:

1
2
3
4
5
6
UNICODE_STRING name;
RtlInitUnicodeString(&name,L"\\driver\\kbdclass");
PDRIVER_OBJECT driver;
auto status=ObReferenceObjectByName(&name,OBJ_CASE_INSENSITIVE,nullptr,0,*IoDriverObjectType,KernelMode,nullptr,(PVOID*)driver);
if(NT_SUCCESS(status))
ObDereferenceObject(driver);

挂钩后驱动可替换主功能指针、卸载例程、增加设备例程等。必须保存好原函数指针,以便需要时解除挂钩。替换操作必须是原子操作。

1
2
3
for(int j=0;j<=IRP_MJ_MAXIMUM_FUNCTION;j++)
InterlockedExchangePointer((PVOID*)&driver->MajorFunction[j],MyHookDispatch);
InterlockedExchangePointer((PVOID*)&driver->DriverUnload,MyHookUnload);

更多信息去看https://github.com/zodiacon/DriverMon。

实战

例子1

初始化:

1
2
3
4
5
6
7
8
#include <ntddk.h>
extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject,_In_ PUNICODE_STRING RegistryPath){
DriverObject->DriverUnload=PriorityBoosterUnload; //待会儿再写
DriverObject->MajorFunction[IRP_MJ_CREATE]=PriorityBoosterCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE]=PriorityBoosterCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=PriorityBoosterDeviceControl;
return STATUS_SUCCESS;
};

新建一个PriorityBoosterCommon.h,随后要被用户模式客户程序使用。这里要定义驱动期望从客户程序获得的数据结构,和修改线程优先级的控制代码。客户程序传递给驱动程序的信息为:

1
2
3
4
struct ThreadData{
ULONG ThreadId; //PID
int Priority;
};

自定义控制代码要用CTL_CODE宏。DeviceType为设备蕾西格尼,第三方值从0x8000开始。Function指明操作,数值随意但从0x800开始,同一驱动中不能撞。Method指明客户程序提供的I/O缓冲区怎样传递到驱动程序,最简单用METHOD_NEITHER。Access指明该操作是FILE_WRITE_ACCESS到达驱动程序、FILE_READ_ACCESS来自驱动程序还是FILE_ANY_ACCESS都有,通常直接FILE_ANY_ACCESS和IRP_MJ_DEVICE_CONTROL。

1
2
#define PRIORITY_BOOSTER_DEVICE 0x8000
#define IOCTL_PRIORITY_BOOSTER_SET_PRIORITY CTL_CODE(PRIORITY_BOOSTER_DEVICE,0X800,METHOD_NEITHER,FILE_ANY_ACCESS)

创建设备对象:

1
2
3
4
5
6
7
8
9
NTSTATUS IoCreateDevice(
_In_ PDRIVER_OBJECT DriverObject, //设备对象所属驱动程序对象
_In_ ULONG DeviceExtensionSize, //除DEVICE_OBJECT所需大小外额外分配的字节数
_In_opt_ PUNICODE_STRING DeviceName, //内部设备名
_In_ DEVICE_TYPE DeviceType, //硬件驱动相关 软件驱动直接FILE_DEVICE_UNKNOWN
_In_ ULONG DeviceCharacteristics, //特殊驱动相关 一般直接0
_In_ BOOLEAN Exclusive, //是否允许多个文件对象打开同一设备
_Outptr_ PDEVICE_OBJECT* DeviceObject //返回设备对象指针
);

为了存放内部设备名,需要创建个UNICODE_STRING:

1
2
3
4
//法一
UNICODE_STRING devName=RTL_CONSTANT_STRING(L"\\Device\\PriorityBooster");
//法二
RtlInitUnicodeString(&devName,L"\\Device\\ThreadBoost");

创建设备对象:

1
2
3
4
5
6
7
8
9
PDEVICE_OBJECT DeviceObject;
NTSTATUS status=IoCreateDevice(DriverObject,0,&devName,FILE_DEVICE_UNKNOWN,0,FALSE,&DeviceObject);
UNICODE_STRING symLink=RTL_CONSTANT_STRING(L"\\??\\PriorityBooster");
status=IoCreateSymbolicLink(&symLink,&devName);
if(!NT_SUCCESS(status)){
DbgPrintEx(DPFLTR_IHVDRIVER_ID,DPFLTR_ERROR_LEVEL,"[%d]0x%08X\r\n", __LINE__,status);
IoDeleteDevice(DeviceObject);
return status;
};

卸载:

1
2
3
4
5
6
void PriorityBoosterUnload(_In_ PDRIVER_OBJECT DriverObject){
UNICODE_STRING symLink=RTL_CONSTANT_STRING(L"\\??\\PriorityBooster");
IoDeleteSymblicLink(&symLink);
IoDeleteDevice(DriverObject->DeviceObject);
return;
};

对于用户模式客户端,要先引用PriorityBoosterCommon.h。 然后打开设备句柄,CreateFile会调用IRP_MJ_CREATE分发例程,若驱动程序未加载则得到错误代码2,即文件未找到。

1
2
3
4
5
#include "..\PriorityBooster\PriorityBoosterCommon.h"
//...
HANDLE hDevice=CreateFile(L"\\\\.\\PriorityBooster",GENERIC_WRITE,FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
if(hDevice==INVALID_HANDLE_VALUE)
return 1;

再用DeviceIoControl调用IRP_MJ_DEVICE_CONTROL主功能例程到达驱动程序。

1
2
3
4
5
6
7
8
9
10
ThreadData data;
data.ThreadId=atoi(argv[1]);
data.Priority=atoi(argv[2]);
DWORD returned;
BOOL success=DeviceIoControl(hDevice,IOCTL_PRIORITY_BOOSTER_SET_PRIORITY,&data,sizeof(data),nullptr,0,&returned,nullptr);
if(success)
printf("Priority change succeeded!\n");
else
printf("Priority change failed!\n");
CloseHandle(hDevice);

驱动的创建和关闭回调函数如下。IRP从不单独到来,它总有一个或多个IO_STACK_LOCATION结构相伴。对于该请求,在IRP的IoStatus字段,有Status和Information俩成员,前者指明用什么状态完成该请求,Information用途很多这里0就行。IoCompleteRequest来完成该IRP,它将IRP传送回创建者I/O管理器,然后由管理器通知客户程序操作完成。

1
2
3
4
5
6
7
_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); //第二个参数为客户程序优先级临时提升数值 一般0就行 没必要得到一个优先级提升
return STATUS_SUCCESS;
};

接下来是I/O控制分发例程,若有未识别控制代码则请求失败。IO_STACK_LOCATION有个Parameters联合体成员,每类IRP都有个对应的结构体。IoGetCurrentIrpStackLocation可返回一个指向正确的IO_STACK_LOCATION指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
_Use_decl_annotations_ NTSTATUS PriorityBoosterDeviceControl(PDEVICE_OBJECT,PIRP Irp){
auto stack=IoGetCurrentIrpStackLocation(Irp);
auto status=STATUS_SUCCESS;
switch(stack->Parameters.DeviceIoControl.IoControlCode){
case IOCTL_PRIORITY_BOOSTER_SET_PRIORITY:
//...
break;
default:
status=STATUS_INVALID_DEVICE_REQUEST;
break;
};
//...
};

在该回调函数结束部分,有如下完成IRP的过程,无论成功与否都要返回。

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

在上面回调中,最重要的部分还没写:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(stack->Parameters.DeviceIoControl.InputBufferLength<sizeof(ThreadData)){ //检查接收缓冲区是否足够大能容纳一个ThreadData对象 不够大肯定有问题
status=STATUS_BUFFER_TOO_SMALL;
break;
};
auto data=(ThreadData)stack->Parameters.DeviceIoControl.Type3InputBuffer; //获取用户提供的输入缓冲区指针
if(data==nullptr){ //指针为NULL则终止
status=STATUS_INVALIDE_PARAMETER;
break;
};
if(data->Priority<1||data->Priority>31){ //看看优先级是否在合法范围内
status=STATUS_INVALID_PARAMETER;
break;
};

要用的是下面这个API:

1
2
3
4
KPRIORITY KeSetPriorityThread(
_Inout_ PKTHREAD Thread, //获得该线程KTHREAD的对象指针
_In_ KPRIORITY Priority
);

接着如下。HANDLE类型在x64下为64位的,客户程序提供的线程ID是32位的,需要ULongToHandle宏来转。

1
2
3
4
5
6
7
#include <ntifs.h> //必须在ntddk.h之前
PETHREAD Thread; //ETHREAD的第一个成员就是KTHREAD
status=PsLookupThreadByThreadId(ULongToHandle(data->TheadId),&Thread); //通过线程ID获取内核空间线程对象指针
if(!NT_SUCCESS(status))
break;
KeSetPriorityThread((PKTHREAD)Thread,data->Priority);
ObDereferenceObject(Thread); //减少引用计数 否则产生泄露

驱动安装与加载如下。若发生运行时错误,则在代码生成选项改为多线程调试。

1
2
sc create booster type=kernel binPath=C:\PriorityBooster.sys
sc start booster

例子2

驱动:

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
#include "pch.h"
#include "ZeroCommon.h"
#define DRIVER_PREFIX "Zero: "
// prototypes
void ZeroUnload(PDRIVER_OBJECT DriverObject);
DRIVER_DISPATCH ZeroCreateClose, ZeroRead, ZeroWrite, ZeroDeviceControl;
// globals
long long g_TotalRead, g_TotalWritten;
// DriverEntry
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = ZeroUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = ZeroCreateClose;
DriverObject->MajorFunction[IRP_MJ_READ] = ZeroRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = ZeroWrite;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ZeroDeviceControl;
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\Zero");
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");
PDEVICE_OBJECT DeviceObject = nullptr;
auto status = STATUS_SUCCESS;
auto symLinkCreated = false;
do {
status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
if (!NT_SUCCESS(status)) {
KdPrint((DRIVER_PREFIX "failed to create device (0x%08X)\n", status));
break;
}
DeviceObject->Flags |= DO_DIRECT_IO;
status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status)) {
KdPrint((DRIVER_PREFIX "failed to create symbolic link (0x%08X)\n", status));
break;
}
symLinkCreated = true;
} while (false);
if (!NT_SUCCESS(status)) {
if (symLinkCreated)
IoDeleteSymbolicLink(&symLink);
if (DeviceObject)
IoDeleteDevice(DeviceObject);
}
return status;
}
// implementation
NTSTATUS CompleteIrp(PIRP Irp, NTSTATUS status = STATUS_SUCCESS, ULONG_PTR info = 0) {
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = info;
IoCompleteRequest(Irp, 0);
return status;
}
void ZeroUnload(PDRIVER_OBJECT DriverObject) {
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\Zero");
IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
NTSTATUS ZeroCreateClose(PDEVICE_OBJECT, PIRP Irp) {
return CompleteIrp(Irp);
}
NTSTATUS ZeroRead(PDEVICE_OBJECT, PIRP Irp) {
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto len = stack->Parameters.Read.Length;
if (len == 0)
return CompleteIrp(Irp, STATUS_INVALID_BUFFER_SIZE);
auto buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); //因为是直接I/O
if (!buffer)
return CompleteIrp(Irp, STATUS_INSUFFICIENT_RESOURCES);
memset(buffer, 0, len);
//g_TotalRead += len;
InterlockedAdd64(&g_TotalRead, len);
return CompleteIrp(Irp, STATUS_SUCCESS, len);
}
NTSTATUS ZeroWrite(PDEVICE_OBJECT, PIRP Irp) {
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto len = stack->Parameters.Write.Length;
//g_TotalWritten += len;
InterlockedAdd64(&g_TotalWritten, len);
return CompleteIrp(Irp, STATUS_SUCCESS, len); //使用了整个缓冲区
}
NTSTATUS ZeroDeviceControl(PDEVICE_OBJECT, PIRP Irp) {
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto& dic = stack->Parameters.DeviceIoControl;
if (dic.IoControlCode != IOCTL_ZERO_GET_STATS)
return CompleteIrp(Irp, STATUS_INVALID_DEVICE_REQUEST);
if (dic.OutputBufferLength < sizeof(ZeroStats))
return CompleteIrp(Irp, STATUS_BUFFER_TOO_SMALL);
auto stats = (ZeroStats*)Irp->AssociatedIrp.SystemBuffer;
stats->TotalRead = g_TotalRead;
stats->TotalWritten = g_TotalWritten;
return CompleteIrp(Irp, STATUS_SUCCESS, sizeof(ZeroStats));
}

客户端:

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
#include "pch.h"
int Error(const char* msg) {
printf("%s: error=%d\n", msg, ::GetLastError());
return 1;
}
int main() {
HANDLE hDevice = ::CreateFile(L"\\\\.\\Zero", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
return Error("failed to open device");
// test read
printf("Test read\n");
BYTE buffer[64];
for (int i = 0; i < sizeof(buffer); ++i)
buffer[i] = i + 1;
DWORD bytes;
BOOL ok = ::ReadFile(hDevice, buffer, sizeof(buffer), &bytes, nullptr);
if (!ok)
return Error("failed to read");
if (bytes != sizeof(buffer))
printf("Wrong number of bytes\n");
long total = 0;
for (auto n : buffer)
total += n;
if (total != 0)
printf("Wrong data\n");
// test write
printf("Test write\n");
BYTE buffer2[1024]; // contains junk
ok = ::WriteFile(hDevice, buffer2, sizeof(buffer2), &bytes, nullptr);
if (!ok)
return Error("failed to write");
if (bytes != sizeof(buffer2))
printf("Wrong byte count\n");
::CloseHandle(hDevice);
}

例子3

设备监视器。这里写一个通用驱动,能作为过滤器附加到设备对象上,允许拦截发送到几乎任何设备的请求。还可以配合用户模式客户程序,增加或删除要过滤的设备。

为了方便管理所有正在过滤的设备,这里创建一个辅助类,目的是加入和删除需要的过滤设备。这是每个设备用该结构表示:

1
2
3
4
5
struct MonitoredDevice {
UNICODE_STRING DeviceName; //设备名
PDEVICE_OBJECT DeviceObject; //过滤器设备对象
PDEVICE_OBJECT LowerDeviceObject; //被附加的下层设备对象
};

辅助类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int MaxMonitoredDevices = 32;
class DevMonManager {
public:
void Init(PDRIVER_OBJECT DriverObject);
NTSTATUS AddDevice(PCWSTR name);
int FindDevice(PCWSTR name) const;
bool RemoveDevice(PCWSTR name);
void RemoveAllDevices();
PDEVICE_OBJECT CDO;
private:
bool RemoveDevice(int index);
private:
MonitoredDevice Devices[MaxMonitoredDevices];
int MonitoredDeviceCount;
FastMutex Lock;
PDRIVER_OBJECT DriverObject;
};
struct DeviceExtension { //设备扩展结构
PDEVICE_OBJECT LowerDeviceObject; //设备对象
};

其中DevMonManager::AddDevice完成附加到设备:

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
NTSTATUS DevMonManager::AddDevice(PCWSTR name) {
AutoLock locker(Lock); //获取互斥量
if (MonitoredDeviceCount == MaxMonitoredDevices) //数组是否已用完
return STATUS_TOO_MANY_NAMES;
if (FindDevice(name) >= 0) //要处理的设备是否已被过滤了
return STATUS_SUCCESS;
for (int i = 0; i < MaxMonitoredDevices; i++) {
if (Devices[i].DeviceObject == nullptr) { //找一个未使用的数组元素 保存新建的过滤器信息
UNICODE_STRING targetName;
RtlInitUnicodeString(&targetName, name);
PFILE_OBJECT FileObject;
PDEVICE_OBJECT LowerDeviceObject = nullptr;
auto status = IoGetDeviceObjectPointer(&targetName, FILE_READ_DATA, &FileObject, &LowerDeviceObject); //获取想要过滤的设备对象指针
if (!NT_SUCCESS(status)) {
KdPrint(("Failed to get device object pointer (%ws) (0x%8X)\n", name, status));
return status;
}
PDEVICE_OBJECT DeviceObject = nullptr;
WCHAR* buffer = nullptr;
do {
status = IoCreateDevice(DriverObject, sizeof(DeviceExtension), nullptr, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject); //创建新过滤器设备对象并初始化
if (!NT_SUCCESS(status))
break;
// allocate buffer to copy device name
buffer = (WCHAR*)ExAllocatePoolWithTag(PagedPool, targetName.Length, DRIVER_TAG);
if (!buffer) {
status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
auto ext = (DeviceExtension*)DeviceObject->DeviceExtension;
DeviceObject->Flags |= LowerDeviceObject->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO);
DeviceObject->DeviceType = LowerDeviceObject->DeviceType;
Devices[i].DeviceName.Buffer = buffer;
Devices[i].DeviceName.MaximumLength = targetName.Length;
RtlCopyUnicodeString(&Devices[i].DeviceName, &targetName);
Devices[i].DeviceObject = DeviceObject;
status = IoAttachDeviceToDeviceStackSafe(DeviceObject/*filter device object*/, LowerDeviceObject/*target device object*/, &ext->LowerDeviceObject/*result*/); //附加
if (!NT_SUCCESS(status))
break;
Devices[i].LowerDeviceObject = ext->LowerDeviceObject;
// hardware based devices require this
DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
DeviceObject->Flags |= DO_POWER_PAGABLE;
MonitoredDeviceCount++;
} while (false);
if (!NT_SUCCESS(status)) { //清理
if (buffer)
ExFreePool(buffer);
if (DeviceObject)
IoDeleteDevice(DeviceObject);
Devices[i].DeviceObject = nullptr;
}
if (LowerDeviceObject)
// dereference - not needed anymore
ObDereferenceObject(FileObject);
return status;
}
}
// should never get here
NT_ASSERT(false);
return STATUS_UNSUCCESSFUL;
}

对于移除过滤设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool DevMonManager::RemoveDevice(PCWSTR name) {
AutoLock locker(Lock);
int index = FindDevice(name);
if (index < 0)
return false;
return RemoveDevice(index);
}
void DevMonManager::RemoveAllDevices() {
AutoLock locker(Lock);
for (int i = 0; i < MaxMonitoredDevices; i++)
RemoveDevice(i);
}
bool DevMonManager::RemoveDevice(int index) {
auto& device = Devices[index];
if (device.DeviceObject == nullptr)
return false;
ExFreePool(device.DeviceName.Buffer);
IoDetachDevice(device.LowerDeviceObject);
IoDeleteDevice(device.DeviceObject);
device.DeviceObject = nullptr;
MonitoredDeviceCount--;
return true;
}

FindDevice用于在数组中通过名称定位设备:

1
2
3
4
5
6
7
8
9
10
int DevMonManager::FindDevice(PCWSTR name) const {
UNICODE_STRING uname;
RtlInitUnicodeString(&uname, name);
for (int i = 0; i < MaxMonitoredDevices; i++) {
auto& device = Devices[i];
if (device.DeviceObject && RtlEqualUnicodeString(&device.DeviceName, &uname, TRUE))
return i;
}
return -1;
}

DriverEntry例程相当标准,可以选择加入移除锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DevMonManager g_Data;
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING) {
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\KDevMon");
PDEVICE_OBJECT DeviceObject;
auto status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &DeviceObject);
if (!NT_SUCCESS(status))
return status;
UNICODE_STRING linkName = RTL_CONSTANT_STRING(L"\\??\\KDevMon");
status = IoCreateSymbolicLink(&linkName, &devName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(DeviceObject);
return status;
}
//DriverObject->DriverExtension->AddDevice = FilterAddDevice;
DriverObject->DriverUnload = DevMonUnload;
for (auto& func : DriverObject->MajorFunction)
func = HandleFilterFunction;
//for (int i = 0; i < ARRAYSIZE(DriverObject->MajorFunction); i++)
// DriverObject->MajorFunction[i] = HandleFilterFunction;
g_Data.CDO = DeviceObject;
g_Data.Init(DriverObject);
return status;
}

卸载例程中,删除符号链接和CDO,比国内脱离所有激活的过滤器:

1
2
3
4
5
6
7
8
void DevMonUnload(PDRIVER_OBJECT DriverObject) {
UNREFERENCED_PARAMETER(DriverObject);
UNICODE_STRING linkName = RTL_CONSTANT_STRING(L"\\??\\KDevMon");
IoDeleteSymbolicLink(&linkName);
NT_ASSERT(g_Data.CDO);
IoDeleteDevice(g_Data.CDO);
g_Data.RemoveAllDevices();
}

HandlerFilterFunction分发例程:

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
NTSTATUS HandleFilterFunction(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
if (DeviceObject == g_Data.CDO) { //若目标设备为CDO
switch (IoGetCurrentIrpStackLocation(Irp)->MajorFunction) {
case IRP_MJ_CREATE:
case IRP_MJ_CLOSE:
return CompleteRequest(Irp);
case IRP_MJ_DEVICE_CONTROL:
return DevMonDeviceControl(DeviceObject, Irp); //增加和移除过滤器
}
return CompleteRequest(Irp, STATUS_INVALID_DEVICE_REQUEST); //不支持的操作
}
//不是CDO则为过滤器
auto ext = (DeviceExtension*)DeviceObject->DeviceExtension; //取得设备扩展以获得下层设备访问
auto thread = Irp->Tail.Overlay.Thread; //挖出发出IRP的线程 得到调用者的线程和进程ID
HANDLE tid = nullptr, pid = nullptr;
if (thread) {
tid = PsGetThreadId(thread);
pid = PsGetThreadProcessId(thread);
}
auto stack = IoGetCurrentIrpStackLocation(Irp);
DbgPrint("driver: %wZ: PID: %d, TID: %d, MJ=%d (%s)\n", &ext->LowerDeviceObject->DriverObject->DriverName, HandleToUlong(pid), HandleToUlong(tid), stack->MajorFunction, MajorFunctionToString(stack->MajorFunction)); //MajorFunctionToString是自己实现的代码 功能号转字符串
IoSkipCurrentIrpStackLocation(Irp); //把请求原样传递下去
return IoCallDriver(ext->LowerDeviceObject, Irp);
}
NTSTATUS CompleteRequest(PIRP Irp, NTSTATUS status, ULONG_PTR information) {
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = information;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}

控制代码的请求处理:

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
#define IOCTL_DEVMON_ADD_DEVICE CTL_CODE(0x8000, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_DEVMON_REMOVE_DEVICE CTL_CODE(0x8000, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_DEVMON_REMOVE_ALL CTL_CODE(0x8000, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)
#define IOCTL_DEVMON_START_MONITOR CTL_CODE(0x8000, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS)
#define IOCTL_DEVMON_STOP_MONITOR CTL_CODE(0x8000, 0x804, METHOD_NEITHER, FILE_ANY_ACCESS)
#define IOCTL_DEVMON_ADD_DRIVER CTL_CODE(0x8000, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_DEVMON_REMOVE_DRIVER CTL_CODE(0x8000, 0x806, METHOD_BUFFERED, FILE_ANY_ACCESS)
NTSTATUS DevMonDeviceControl(PDEVICE_OBJECT, PIRP Irp) {
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto status = STATUS_INVALID_DEVICE_REQUEST;
auto code = stack->Parameters.DeviceIoControl.IoControlCode;
switch (code) {
case IOCTL_DEVMON_ADD_DEVICE:
case IOCTL_DEVMON_REMOVE_DEVICE: {
auto buffer = (WCHAR*)Irp->AssociatedIrp.SystemBuffer;
auto len = stack->Parameters.DeviceIoControl.InputBufferLength;
if (buffer == nullptr || len > 512) {
status = STATUS_INVALID_BUFFER_SIZE;
break;
}
buffer[len / sizeof(WCHAR) - 1] = L'\0';
if (code == IOCTL_DEVMON_ADD_DEVICE)
status = g_Data.AddDevice(buffer);
else {
auto removed = g_Data.RemoveDevice(buffer);
status = removed ? STATUS_SUCCESS : STATUS_NOT_FOUND;
}
break;
}
case IOCTL_DEVMON_REMOVE_ALL: {
g_Data.RemoveAllDevices();
status = STATUS_SUCCESS;
break;
}
}
return CompleteRequest(Irp, status);
}

用户程序为:

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
int wmain(int argc, const wchar_t* argv[]) { //请求如\device\procexp152、\device\keyboardclass0、\device\mup等
if (argc < 2)
return Usage();
auto& cmd = argv[1];
HANDLE hDevice = ::CreateFile(L"\\\\.\\kdevmon", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hDevice == INVALID_HANDLE_VALUE)
return Error("Failed to open device");
DWORD bytes;
if (::_wcsicmp(cmd, L"add") == 0) {
if (!::DeviceIoControl(hDevice, IOCTL_DEVMON_ADD_DEVICE, (PVOID)argv[2], static_cast<DWORD>(::wcslen(argv[2]) + 1) * sizeof(WCHAR), nullptr, 0, &bytes, nullptr))
return Error("Failed in add device");
printf("Add device %ws successful.\n", argv[2]);
return 0;
}
else if (::_wcsicmp(cmd, L"remove") == 0) {
if (!::DeviceIoControl(hDevice, IOCTL_DEVMON_REMOVE_DEVICE, (PVOID)argv[2], static_cast<DWORD>(::wcslen(argv[2]) + 1) * sizeof(WCHAR), nullptr, 0, &bytes, nullptr))
return Error("Failed in remove device");
printf("Remove device %ws successful.\n", argv[2]);
return 0;
}
else if (::_wcsicmp(cmd, L"clear") == 0) {
if (!::DeviceIoControl(hDevice, IOCTL_DEVMON_REMOVE_ALL, nullptr, 0, nullptr, 0, &bytes, nullptr))
return Error("Failed in remove all devices");
printf("Removed all devices successful.\n");
}
else {
printf("Unknown command.\n");
return Usage();
}
return 0;
}

现在只能看到达的请求,而不能看请求的结果。这时应用IoSetCompletionRoutineEx来设置完成例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NTSTATUS IoSetCompetionRoutineEx(
_In_ PDEVICE_OBJECT DeviceObject,
_In_ PIRP Irp,
_In_ PIO_COMPLETION_ROUTINE CompletionRoutine,
_In_opt_ PVOID Context,
_In_ BOOLEAN InvokeOnSuccess, //TRUE则完成例程在IRP状态满足NT_SUCCESS宏时被调用
_In_ BOOLEAN InvokeOnError, //同上 不满足
_In_ BOOLEAN InvokeOnCancel //同上STATUS_CANCELLED
);
NTSTATUS CompletionRoutine( //完成例程原型
_In_ PDEVICE_OBJECT DeviceObject,
_In_ PIPR Irp,
_In_opt_ PVOID Context
)

完成例程会在调用IoCompleteRequest的线程中被调用。完成例程可检查IRP状态和缓冲区,可用IoGetCurrentIrpStackLocation获得信息等。

完成例程的返回值只有STATUS_MORE_PROCESSING_REQUIRED和其他。前者表示告诉I/O管理器停止把IRP沿着设备栈向上传递并取消,IRP已完成。这是驱动获得IRP所有权并必须再次用IoCompleteRequest

完成例程返回的任何其他状态值会继续把IRP沿着设备栈向上传递,可能调用上层驱动的其他完成例程。若下称设备将IRP标为待定,则驱动必须:

1
2
if(Irp->PendingReturned)
IoMarkingIrpPending(Irp);