前面两篇文章分别介绍了驱动的编写以及调试驱动的方式,这一篇就对一些基础的概念以及注意事项做一个概括
内核API的使用
未导出函数的使用 官方说明文档中只包含了内核模块导出的函数,对于未导出的函数 ,则不能直接使用。如果要使用未导出的函数 ,只要自己定义一个函数指针 ,并且为函数指针提供正确的函数地址 就可以使用了。获取未导出函数地址的方法 有如下两种:
特征码搜索
解析内核PDB文件
基本数据类型
返回值 大部分内核函数的返回值都是NTSTATUS类型 ,如:
1 2 3 NTSTATUS PsCreateSystemThread () ;NTSTATUS ZwOpenProcess () ;NTSTATUS ZwOpenEvent () ;
这个类型的值用来说明函数执行的结果,例如:
1 2 3 STATUS_SUCCESS 0x00000000 STATUS_INVALID_PARAMETER 0xC000000D STATUS_BUFFER_OVERFLOW 0x80000005
当你调用的内核函数,如果返回的结果不是STATUS_SUCCESS,就说明函数执行中遇到了问题,具体是什么问题,可以参考ntstatus.h文件
内核中的异常处理 在内核中,一个小小的错误就可能导致蓝屏,例如读写一个无效的内存地址。为了让自己的内核程序更加健壮,在编写内核程序时 ,使用异常处理 是非常有必要的。
Windows提供了结构化异常处理(SEH) 机制,大部分编译器都有支持,大致如下:
1 2 3 4 5 6 __try { } __except(filter_value){ }
出现异常时,可根据filter_value的值来决定程序该如何执行,filter_value的值主要如下:
1 2 3 EXCEPTION_EXECUTE_HANDLER 1 EXCEPTION_CONTINUE_SEARCH 0 EXCEPTION_CONTINUE_EXECUTION -1
常用的内核内存函数 简要介绍一些对内存进行使用的功能函数:申请、设置、拷贝以及释放
C语言
内核中
malloc
ExAllocatePool
memset
RtlFillMemory
memcpy
RtlMoveMemory
free
ExFreePool
以ExAllocatePool为例,对应的语法为:
1 2 3 4 PVOID ExAllocatePool ( POOL_TYPE PoolType, SIZE_T NumberOfBytes ) ;
其中POOL_TYPE的类型主要有
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种:
CHAR(char)
WCHAR(wchar_t)
ANSI_STRING
1 2 3 4 5 typedef struct _STRING { USHORT Length; USHORT MaximumLength; PCHAR Buffer; }STRING;
UNICODE_STRING
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上,上述字符串有如下对应关系:
1 2 3 4 "%s" ---ANSI_STRING.Buffer"%S" ---UNICODE_STRING.Buffer"%Z" ---&ANSI_STRING"wZ" ---&UNICODE_STRING
打印GDT,IDT 在简要了解了内存函数和字符串函数后,来做两个实验巩固一下知识吧
第一个实验:申请一块内存,并在内存中存储GDT、IDT的所有数据,然后在DebugView中显示出来,最后释放内存
代码实现 原来比较简单,主要是熟悉函数的使用,这里就不详细分析了,直接上代码
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 ]; 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; }
测试结果
截取GDT部分内容 :
截取IDT部分内容:
Windbg中打印部分内容验证结果是否正确:
字符串操纵 第二个实验,应用刚刚学习的字符串函数,操纵字符串实现如下功能:
初始化一个字符串
拷贝一个字符串
比较两个字符串是否相等
进行ANSI_STRING与UNICODE_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 #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; 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); RtlCopyString(&bStr, &aStr); DbgPrint("%x, %x, %s" , bStr.Length, bStr.MaximumLength, bStr.Buffer); ul = RtlCompareString(&aStr, &cStr, 1 ); DbgPrint("%d" , ul); 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的代码,葫芦娃救爷爷的代码