avatar

Catalog
内核编程基础

前面两篇文章分别介绍了驱动的编写以及调试驱动的方式,这一篇就对一些基础的概念以及注意事项做一个概括

内核API的使用

  • 在应用层编程我们可以使用Windows提供的各种API函数,只要导入头文件就可以了,但是在内核编程的时候,我们不能像在Ring3那样直接使用。微软为内核程序提供了专用的API,只要在程序中包含相应的头文件就可以使用了,例如:

    c
    1
    #include "ntddk.h"		//假定你已经正确安装了WDK
  • 在应用层编程的时候,我们通过MSDN来了解函数的详细信息,在内核编程的时候,主要借助于官方文档

未导出函数的使用

官方说明文档中只包含了内核模块导出的函数,对于未导出的函数,则不能直接使用。如果要使用未导出的函数,只要自己定义一个函数指针,并且为函数指针提供正确的函数地址就可以使用了。获取未导出函数地址的方法有如下两种:

  1. 特征码搜索
  2. 解析内核PDB文件

基本数据类型

  • 在内核编程的时候,尽量遵守WDK的编码习惯,不要这样写

    c
    1
    unsigned long length;
  • 尽量使用WDK自己的类型:

    c
    1
    2
    3
    4
    ULONG(unsigned long)          PULONG(unsigned long *)
    UCHAR(unsigned char) PUCHAR(unsigned char *)
    UINT32(unsigned int) PUNIT32(unsigned int *)
    VOID(void) PVOID(void *)

返回值

大部分内核函数的返回值都是NTSTATUS类型,如:

c
1
2
3
NTSTATUS PsCreateSystemThread();
NTSTATUS ZwOpenProcess();
NTSTATUS ZwOpenEvent();

这个类型的值用来说明函数执行的结果,例如:

c
1
2
3
STATUS_SUCCESS			0x00000000			//成功
STATUS_INVALID_PARAMETER 0xC000000D //参数无效
STATUS_BUFFER_OVERFLOW 0x80000005 //缓冲区长度不够

当你调用的内核函数,如果返回的结果不是STATUS_SUCCESS,就说明函数执行中遇到了问题,具体是什么问题,可以参考ntstatus.h文件

内核中的异常处理

在内核中,一个小小的错误就可能导致蓝屏,例如读写一个无效的内存地址。为了让自己的内核程序更加健壮,在编写内核程序时使用异常处理是非常有必要的。

Windows提供了结构化异常处理(SEH)机制,大部分编译器都有支持,大致如下:

c
1
2
3
4
5
6
__try {
//可能出错的代码
}
__except(filter_value){
//出错时要执行的代码
}

出现异常时,可根据filter_value的值来决定程序该如何执行,filter_value的值主要如下:

c
1
2
3
EXCEPTION_EXECUTE_HANDLER		1			//代码进入except块
EXCEPTION_CONTINUE_SEARCH 0 //不处理异常,由上一层调用函数处理
EXCEPTION_CONTINUE_EXECUTION -1 //回去继续执行错误处的代码

常用的内核内存函数

简要介绍一些对内存进行使用的功能函数:申请、设置、拷贝以及释放

C语言 内核中
malloc ExAllocatePool
memset RtlFillMemory
memcpy RtlMoveMemory
free ExFreePool

以ExAllocatePool为例,对应的语法为:

c
1
2
3
4
PVOID ExAllocatePool(
POOL_TYPE PoolType,
SIZE_T NumberOfBytes
);

其中POOL_TYPE的类型主要有

c
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
typedef enum _POOL_TYPE {
NonPagedPool, //这块内存不可以放到文件中,用于存放代码
NonPagedPoolExecute,
PagedPool, //这块内存不常用时可以放到文件中(硬盘),用于存放数据
NonPagedPoolMustSucceed,
DontUseThisType,
NonPagedPoolCacheAligned,
PagedPoolCacheAligned,
NonPagedPoolCacheAlignedMustS,
MaxPoolType,
NonPagedPoolBase,
NonPagedPoolBaseMustSucceed,
NonPagedPoolBaseCacheAligned,
NonPagedPoolBaseCacheAlignedMustS,
NonPagedPoolSession,
PagedPoolSession,
NonPagedPoolMustSucceedSession,
DontUseThisTypeSession,
NonPagedPoolCacheAlignedSession,
PagedPoolCacheAlignedSession,
NonPagedPoolCacheAlignedMustSSession,
NonPagedPoolNx,
NonPagedPoolNxCacheAligned,
NonPagedPoolSessionNx
} POOL_TYPE;

内核字符串

字符串种类

内核中的字符串主要有4种:

  1. CHAR(char)

  2. WCHAR(wchar_t)

  3. ANSI_STRING

    c
    1
    2
    3
    4
    5
    typedef struct _STRING {
    USHORT Length;
    USHORT MaximumLength;
    PCHAR Buffer;
    }STRING;
  4. UNICODE_STRING

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

    使用ANSI_STRING和UNICODE_STRING的好处是其结构包含最大长度,可以有效防止字符串越界崩溃的情况

字符串常用函数

字符串常用的功能无非就是:创建、复制、比较以及转换等等

ANSI_STRING字符串 UNICODE_STRING字符串
RtlInitAnsiString RtlInitUnicodeString
RtlCopyString RtlCopyUnicodeString
RtlCompareString RtlCompareUnicoodeString
RtlAnsiStringToUnicodeString RtlUnicodeStringToAnsiString

一般在驱动中会通过DbgPrint函数格式化输出到DbgView上,上述字符串有如下对应关系:

c
1
2
3
4
"%s"---ANSI_STRING.Buffer
"%S"---UNICODE_STRING.Buffer
"%Z"---&ANSI_STRING
"wZ"---&UNICODE_STRING

打印GDT,IDT

在简要了解了内存函数和字符串函数后,来做两个实验巩固一下知识吧

第一个实验:申请一块内存,并在内存中存储GDT、IDT的所有数据,然后在DebugView中显示出来,最后释放内存

代码实现

原来比较简单,主要是熟悉函数的使用,这里就不详细分析了,直接上代码

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

VOID Driver_Unload(PDRIVER_OBJECT driver) {
DbgPrint("Unload Success!");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING RegistryPath) {
USHORT gdtl, idtl;
UINT32 idtr, gdtr;
UCHAR gdt[6], idt[6];
PULONG pGDT, pIDT;

_asm {
sgdt gdt
sidt idt
}

gdtl = *(PUSHORT)&gdt[0];
idtl = *(PUSHORT)&idt[0];
gdtr = *(PUINT32)&gdt[2];
idtr = *(PUINT32)&idt[2];

// DbgPrint("%2x, %2x, %8x, %8x\n", gdtl, idtl, gdtr, idtr);
gdtl++;
idtl++;

pGDT = ExAllocatePool(PagedPool, gdtl);
pIDT = ExAllocatePool(PagedPool, idtl);

if (pGDT == NULL || pIDT == NULL) {
DbgPrint("Allocate Failed!");
return STATUS_UNSUCCESSFUL;
}

RtlFillMemory(pGDT, gdtl, 0);
RtlFillMemory(pIDT, idtl, 0);

RtlMoveMemory(pGDT, (PUINT32)gdtr, gdtl);
RtlMoveMemory(pIDT, (PUINT32)idtr, idtl);

for (int i = 0; i < (gdtl/0x4); i += 0x4) {
DbgPrint("%08x: %08x`%08x\t%08x`%08x\n", gdtr+i, *(pGDT + i + 1), *(pGDT + i),
*(pGDT + i + 3), *(pGDT + i + 2));
}

for (int i = 0; i < (idtl/0x4); i += 0x4) {
DbgPrint("%08x: %08x`%08x\t%08x`%08x\n", idtr+i, *(pIDT + i + 1), *(pIDT + i),
*(pIDT + i + 3), *(pIDT + i + 2));
}

ExFreePool(pGDT);
ExFreePool(pIDT);

driver->DriverUnload = Driver_Unload;
return STATUS_SUCCESS;
}

测试结果

  1. 截取GDT部分内容 :
  2. 截取IDT部分内容:
  3. Windbg中打印部分内容验证结果是否正确:

字符串操纵

第二个实验,应用刚刚学习的字符串函数,操纵字符串实现如下功能:

  1. 初始化一个字符串
  2. 拷贝一个字符串
  3. 比较两个字符串是否相等
  4. 进行ANSI_STRING与UNICODE_STRING的转化

代码实现

c
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 

VOID Driver_Unload(PDRIVER_OBJECT driver);
VOID Manipulate();

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
DbgPrint("Load Driver");
Manipulate();
DriverObject->DriverUnload = Driver_Unload;
return STATUS_SUCCESS;
}

VOID Manipulate() {
ANSI_STRING aStr, bStr = {0}, cStr, dStr;
UNICODE_STRING uStr;
ULONG ul;

//Initialize
RtlInitAnsiString(&aStr, "TestANSI");
RtlInitAnsiString(&cStr, "TestCOMPARE");
RtlInitUnicodeString(&uStr, "TestUnicode");

DbgPrint("%x, %x, %s", aStr.Length, aStr.MaximumLength, aStr.Buffer);
DbgPrint("%x, %x, %s", cStr.Length, cStr.MaximumLength, cStr.Buffer);
DbgPrint("%x, %x, %s", uStr.Length, uStr.MaximumLength, uStr.Buffer);

//Copy
RtlCopyString(&bStr, &aStr);
DbgPrint("%x, %x, %s", bStr.Length, bStr.MaximumLength, bStr.Buffer);

//Compare
ul = RtlCompareString(&aStr, &cStr, 1);
DbgPrint("%d", ul);

//Transfer
RtlUnicodeStringToAnsiString(&dStr, &uStr, TRUE);
DbgPrint("%x, %x, %s", dStr.Length, dStr.MaximumLength, dStr.Buffer);
}


VOID Driver_Unload(PDRIVER_OBJECT driver) {
DbgPrint("Unload Success!");
}

测试结果

由结果可以看出,代码测试存在问题,请教了Joney,他猜测其原因在于,可能由于编译器的优化,定义的字符串结构体仅仅是引用了字符串常量(在常量区,无法写),并没有真正获取它,因而在进行拷贝操作时,无法获取另一个字符串结构体的内容。猜想没有去验证,因为我太懒了,字符串这东西,真的是麻烦,以后用到的话,会在研究一下的…….

参考教程:https://www.bilibili.com/video/av68700135?p=59

参考笔记:张嘉杰的笔记

参考代码:Joney的代码,葫芦娃救爷爷的代码

Author: cataLoc
Link: http://cata1oc.github.io/2020/04/09/%E5%86%85%E6%A0%B8%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶