Windows内核

内核API的使用

关于内存检查的API

1
2
3
MmIsAddressValid();//查表检测该页是否有效  一般是0x12345678 & 0xffff000 ,操作系统申请没有一个字节,而是以页为单位
ProbeForRead(); //只能检测UserBuffer,而且要用异常处理,而且参数为0的时候不干事
ProbeForWrite();//只能检测UserBuffer,而且要用异常处理,而且参数为0的时候不干事

关于字符串的API

Rtl(Runtime Library)开头的函数命名源自 Windows 操作系统的运行时库(Runtime Library),这些库提供了一组基本函数,用于支持操作系统和驱动程序的开发。Rtl 函数主要用于以下几个方面:

  1. 字符串操作:包括 Unicode 和 ANSI 字符串的初始化、复制、比较、转换等操作。
  2. 内存管理:提供内存分配、释放、复制等基本功能。
  3. 其他基础操作:如列表操作、异常处理等。
1
2
3
4
5
6
7
8
9
10
RtlInitUnicodeString();//初始化一个 UNICODE_STRING 结构体。
RtlCopyUnicodeString();//将一个 UNICODE_STRING 结构体的内容复制到另一个 UNICODE_STRING 结构体中。
RtlAppendUnicodeToString();//将一个以 null 结尾的宽字符字符串追加到 UNICODE_STRING 结构体中。
RtlAppendUnicodeStringToString();//将一个 UNICODE_STRING 结构体的内容追加到另一个 UNICODE_STRING 结构体中。
RtlCompareUnicodeString();//比较两个 UNICODE_STRING 结构体的内容。
RtlAnsiTringToUnicodeString();//将一个 ANSI 字符串转换为 Unicode 字符串。
RtlFreeUnicodeString();//释放由 RtlAnsiStringToUnicodeString 或其他类似函数分配的 UNICODE_STRING 结构体的内存。
RtlStringCbCopyW(); //拷贝俩字符串
RtlStringCcbLengthW();//计算字符串长度
RtlZeroMemory();//这个可以初始化缓冲区

观察这个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void String()
{
// 第一个字符串操作
UNICODE_STRING unicode_str;
RtlInitUnicodeString(&unicode_str, L"Hello");
// 初始化字符串的长度和最大长度
unicode_str.Length = (USHORT)(wcslen(L"Hello") * sizeof(WCHAR));
unicode_str.MaximumLength = sizeof(L"Hello");
NTSTATUS status = RtlAppendUnicodeToString(&unicode_str, L" world");
if (!NT_SUCCESS(status))
KdPrint(("First Append Function Fail\n"));
else
KdPrint(("First Append Function Success!\n"));
}

为什么这个代码会报错???

1721095375796

因为UNICODE_STRING存的字符串放在一个常量里面,也就是把L“Hello”这个字符串放到UNICODE_STRING这个结构体里面,这是只读的,不能更改的

所以我们要改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void String()
{
// 第二个字符串操作
UNICODE_STRING unicode_str2;
WCHAR Buffer2[100] = L"Hello";
RtlInitUnicodeString(&unicode_str2, Buffer2);
// 初始化字符串的长度和最大长度
unicode_str2.Length = (USHORT)(wcslen(Buffer2) * sizeof(WCHAR));
unicode_str2.MaximumLength = sizeof(Buffer2);
status = RtlAppendUnicodeToString(&unicode_str2, L" World");
if (!NT_SUCCESS(status))
KdPrint(("Second Append Function Fail\n"));
else
KdPrint(("Second Append Function Success!\n"));
}

注意注意:UNICODE_STRING是没有\x00的概念的,因为在这个结构体里面,是给了Length的,一切以这个为标准

执行层API(ExXxx)

1
2
3
4
ExAllocatePool //分配内存池中的内存。
ExAllocatePoolWithTag //分配带标签的内存池中的内存。标签用于帮助调试和内存泄漏检测。
ExFreePoolWithTag //释放之前使用 ExAllocatePoolWithTag 分配的内存。
ExFreePool//释放之前使用 ExAllocatePool 分配的内存。
1
2
3
4
PVOID ExAllocatePool(
POOL_TYPE PoolType,
SIZE_T NumberOfBytes
);

参数

  • PoolType:指定要分配的内存池的类型。常见的值包括:
    • NonPagedPool:非分页内存池,内存始终驻留在物理内存中。
    • PagedPool:分页内存池,内存可以换出到磁盘。
    • NonPagedPoolMustSucceed:非分页内存池,分配失败时会引发系统崩溃。
  • NumberOfBytes:要分配的内存字节数。
1
2
3
4
5
PVOID ExAllocatePoolWithTag(
POOL_TYPE PoolType,
SIZE_T NumberOfBytes,
ULONG Tag
);

Tag:用于标识分配的内存块的四字符标识符。这个标签可以帮助开发者跟踪和调试内存使用。

1
2
3
4
5
6
7
8
PVOID pMemory2 = ExAllocatePoolWithTag(NonPagedPool, 1024, 'Tag1');
if (pMemory2 != NULL) {
// 使用 pMemory2 做一些事情
// ...

// 释放内存
ExFreePoolWithTag(pMemory2, 'Tag1');
}

Windbg用命令

1
!poolused

可以查看内核所有使用的内存池,此时Tag就有用了。如果没有Tag就找不到申请的内存,也不知道有没有释放,也不好定位

文件的表示

在 Windows 驱动程序开发中, 应用层和内核层在访问同一个文件时,路径的表示方式在应用层和内核层有所不同。了解这些不同的表示方法对正确处理文件路径至关重要。

应用层:c:\1.txt

内核层:\\??\\c:\\1.txt \\sysroot\\1.txt

设备路径格式(??\c:\1.txt)

在内核中,文件路径通常以 \\??\\ 前缀开头。这种表示方法被称为设备路径格式,用于表示绝对路径。

NT 路径格式(\Device\HarddiskVolumeX\Path 或 \SystemRoot\Path)

另一种常见的内核路径格式是 NT 路径格式,通常用于表示系统路径。

ZwQuerySymbolicLinkObject 是一个内核模式函数,用来查询符号链接绑定哪个设备路径( 通常是 NT 路径格式 )

1
2
3
4
5
NTSTATUS ZwQuerySymbolicLinkObject(
HANDLE LinkHandle,
PUNICODE_STRING LinkTarget,
PULONG ReturnedLength
);

参数说明

  • LinkHandle:一个指向符号链接对象的句柄。这个句柄必须是通过适当的访问权限打开的。
  • LinkTarget:指向一个 UNICODE_STRING 结构,该结构接收符号链接对象所指向的目标路径。
  • ReturnedLength:可选参数,指向一个变量,该变量接收目标路径的实际长度(以字节为单位)。

QueryDosDevice是一个三环的函数,用来查询三环设备名对应在内核的路径:

例如我们查询c盘的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <windows.h>
#include <iostream>
using namespace std;
#ifdef _UNICODE
#define _tcout std::wcout
#else
#define _tcout std::cout
#endif
int main()
{
TCHAR Buf[100];
QueryDosDevice(L"C:", Buf, sizeof(Buf));
_tcout << Buf << endl;
return 0;
}

1721115197426

这样就可以在三环知道三环设备在内核对应的设备路径

用WinObj查出来也确实C盘就是对应这个设备

1721115625313

ZwQueryDirectoryFile 是一个 Windows 内核模式 API,用于查询指定目录中的文件信息。通过这个函数,驱动程序可以枚举目录中的文件和子目录,并检索它们的属性信息。

头文件ntifs要在ntddk前面

1721121316423

举例:找到\Device\HarddiskVolume1这个设备存的一个叫1.txt的文件,代码要咋写:

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
#include "header.h"
#define BUFFER_SIZE 256
typedef struct _DEVICE_EXTENSION {
CHAR Buffer[BUFFER_SIZE];
ULONG BufferLength;
} DEVICE_EXTENSION, * PDEVICE_EXTENSION;
TCHAR g_GlobalBuffer[BUFFER_SIZE] = { 0 }; // 全局缓冲区


NTSTATUS FindFileInDirectory(HANDLE directoryHandle, UNICODE_STRING targetFileName)
{
NTSTATUS status;
IO_STATUS_BLOCK ioStatusBlock;
PVOID buffer;
ULONG bufferLength = 1024;
BOOLEAN restartScan = TRUE;

buffer = ExAllocatePoolWithTag(PagedPool, bufferLength, 12138);
if (!buffer) {
return STATUS_INSUFFICIENT_RESOURCES;
}

while (TRUE)
{
status = ZwQueryDirectoryFile( //ZwQueryDirectoryFile 例程返回有关给定文件句柄指定的目录中文件的各种信息。
directoryHandle,
NULL,
NULL,
NULL,
&ioStatusBlock,
buffer,
bufferLength,
FileDirectoryInformation,
TRUE,
NULL,
restartScan
);

if (status == STATUS_NO_MORE_FILES) {
break;
}

if (!NT_SUCCESS(status)) {
ExFreePool(buffer);
return status;
}


PFILE_DIRECTORY_INFORMATION fileInfo = (PFILE_DIRECTORY_INFORMATION)buffer;
do {
UNICODE_STRING fileName;
fileName.Buffer = fileInfo->FileName;
fileName.Length = (USHORT)fileInfo->FileNameLength;
fileName.MaximumLength = (USHORT)fileInfo->FileNameLength;

if (RtlCompareUnicodeString(&fileName, &targetFileName, TRUE) == 0) {
KdPrint(("Found file: %s\n", fileName.Buffer));
ExFreePool(buffer);
return STATUS_SUCCESS;
}

if (fileInfo->NextEntryOffset == 0) {
break;
}
fileInfo = (PFILE_DIRECTORY_INFORMATION)((PUCHAR)fileInfo + fileInfo->NextEntryOffset);
} while (TRUE);

restartScan = FALSE;

}

ExFreePool(buffer);
return STATUS_OBJECT_NAME_NOT_FOUND;
}


// DriverEntry
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
KdBreakPoint();
OBJECT_ATTRIBUTES objAttr;
UNICODE_STRING volumeName;
UNICODE_STRING targetFileName;
HANDLE directoryHandle;
IO_STATUS_BLOCK ioStatusBlock;
DriverObject->DriverUnload = DriverUnload;
KdPrint(("Create!\n"));

RtlInitUnicodeString(&volumeName, L"\\??\\C:\\"); //或者L"\\Device\\HarddiskVolume1\\"
RtlInitUnicodeString(&targetFileName, L"1.txt");

InitializeObjectAttributes(&objAttr, &volumeName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL); //InitializeObjectAttributes宏初始化不透明的OBJECT_ATTRIBUTES结构,该结构将对象句柄的属性指定给打开句柄的例程。

NTSTATUS status = ZwOpenFile(
&directoryHandle,
FILE_LIST_DIRECTORY | SYNCHRONIZE,
&objAttr,
&ioStatusBlock,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT
);

if (NT_SUCCESS(status)) {
status = FindFileInDirectory(directoryHandle, targetFileName);
ZwClose(directoryHandle);

if (NT_SUCCESS(status)) {
DbgPrint("File '1.txt' found on the volume.\n");
}
else {
DbgPrint("File '1.txt' not found on the volume.\n");
}
}
else {
DbgPrint("Failed to open directory: %x\n", status);
}



return STATUS_SUCCESS;
}
// DriverUnload
VOID DriverUnload(PDRIVER_OBJECT DriverObject) {
DriverObject;
KdPrint(("Unload\n"));

}

数据结构API

在内核编程中,不能使用标准模板库(STL)

内存管理

  • 内核的内存分配要求更高:内核中需要严格控制内存的分配和释放,任何内存泄漏都会导致系统崩溃。而STL中的容器在进行动态内存分配时并不能保证其内存分配策略完全符合内核的要求。
  • 分页问题:内核中的某些代码运行在高IRQL(如 DISPATCH_LEVEL)时不能访问分页内存,而STL的实现可能会在这些地方使用分页内存。

实时性和确定性

  • 性能问题:STL的一些操作(如动态内存分配、迭代器操作等)在用户模式下可能是可以接受的,但在内核模式下这些操作可能会导致不可预测的延迟。
  • 中断处理:在内核中运行的代码有时需要在中断处理程序中运行,这要求这些代码必须非常高效且不能阻塞,而STL的很多操作并不满足这些要求。

异常处理

  • 内核代码通常不支持C++异常:STL广泛使用异常来处理错误情况,但在内核中使用异常处理机制是不可行的,因为它可能会导致更多的问题。

内核提供了两个主要的数据结构API:链表和AVL树。

链表:

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
#include <ntddk.h>

// 定义一个结构体,该结构体包含一个LIST_ENTRY
typedef struct _MY_DATA {
LIST_ENTRY ListEntry;
int Data;
} MY_DATA, *PMY_DATA;

VOID UseLinkedList()
{
LIST_ENTRY ListHead;
PMY_DATA pData;
PLIST_ENTRY pListEntry;
// 初始化链表头
InitializeListHead(&ListHead);
// 插入元素
pData = (PMY_DATA)ExAllocatePool(NonPagedPool, sizeof(MY_DATA));
if (pData)
{
pData->Data = 1;
InsertTailList(&ListHead, &pData->ListEntry);
}
pData = (PMY_DATA)ExAllocatePool(NonPagedPool, sizeof(MY_DATA));
if (pData)
{
pData->Data = 2;
InsertTailList(&ListHead, &pData->ListEntry);
}
// 遍历链表
for (pListEntry = ListHead.Flink; pListEntry != &ListHead; pListEntry = pListEntry->Flink)
{
pData = CONTAINING_RECORD(pListEntry, MY_DATA, ListEntry);
DbgPrint("Data: %d\n", pData->Data);
}

// 删除并释放元素
while (!IsListEmpty(&ListHead))
{
pListEntry = RemoveHeadList(&ListHead);
pData = CONTAINING_RECORD(pListEntry, MY_DATA, ListEntry);
ExFreePool(pData);
}C++
}

AVL树

内核中的AVL树使用的是 RTL_AVL_TABLE 结构。下面是一个简单的示例,展示如何使用AVL树。

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
#include <ntddk.h>
#include <ntifs.h>

typedef struct _MY_DATA {
int Key;
// 其他数据字段
} MY_DATA, *PMY_DATA;

// AVL树比较函数
RTL_GENERIC_COMPARE_RESULTS NTAPI MyCompareRoutine(
_In_ struct _RTL_AVL_TABLE *Table,
_In_ PVOID FirstStruct,
_In_ PVOID SecondStruct
)
{
PMY_DATA first = (PMY_DATA)FirstStruct;
PMY_DATA second = (PMY_DATA)SecondStruct;

if (first->Key < second->Key)
return GenericLessThan;

if (first->Key > second->Key)
return GenericGreaterThan;

return GenericEqual;
}

// AVL树分配函数
PVOID NTAPI MyAllocateRoutine(
_In_ struct _RTL_AVL_TABLE *Table,
_In_ CLONG ByteSize
)
{
UNREFERENCED_PARAMETER(Table);
return ExAllocatePoolWithTag(NonPagedPool, ByteSize, 'mytg');
}

// AVL树释放函数
VOID NTAPI MyFreeRoutine(
_In_ struct _RTL_AVL_TABLE *Table,
_In_ PVOID Buffer
)
{
UNREFERENCED_PARAMETER(Table);
ExFreePool(Buffer);
}

VOID UseAvlTree()
{
RTL_AVL_TABLE AvlTable;
MY_DATA data1 = { 1 };
MY_DATA data2 = { 2 };
PMY_DATA pData;
BOOLEAN newElement;

// 初始化AVL树
RtlInitializeGenericTableAvl(
&AvlTable,
MyCompareRoutine,
MyAllocateRoutine,
MyFreeRoutine,
NULL
);

// 插入元素
RtlInsertElementGenericTableAvl(&AvlTable, &data1, sizeof(MY_DATA), &newElement);
RtlInsertElementGenericTableAvl(&AvlTable, &data2, sizeof(MY_DATA), &newElement);

// 查找元素
pData = (PMY_DATA)RtlLookupElementGenericTableAvl(&AvlTable, &data1);
if (pData)
{
DbgPrint("Found element with key: %d\n", pData->Key);
}

// 删除元素
RtlDeleteElementGenericTableAvl(&AvlTable, &data1);
}