驱动篇——内核编程基础
写在前面
??此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。??看此教程之前,问个问题,你明确学驱动的目的了吗?你的开发环境准备好了吗?上一节的内容学会了吗? 没有的话就不要继续了,请重新学习前面驱动篇的教程内容继续。
?
华丽的分割线
?
内核 API 的使用 ??在应用层编程我们可以使用
WINDOWS
提供的各种API
函数,只要导入头文件windows.h
就可以了。但是在内核编程的时候,微软为内核程序提供了专用的API
,只要在程序中包含相应的头文件就可以使用了,如:#include
,前提你必须安装了WDK
。??遇到不会的函数或者不知道如何使用函数怎么办?在应用层编程的时候,我们通过
MSDN
来了解函数的详细信息,在内核编程的时候,要使用WDK
自己的帮助文档。??然而
WDK
说明文档中只包含了内核模块导出的函数,对于未导出的函数,则不能直接使用。如果要使用未导出的函数,只要自己定义一个函数指针,并且为函数指针提供正确的函数地址就可以使用了。有两种办法都可以获取为导出的函数地址:特征码搜索和解析内核PDB
文件。对于第一种方法,每个函数不可能是一模一样的,它们的硬编码具有不同的特征,通过这个特定的独一无二的硬编码可以搜到我想要的函数。对于最后一种方法,我们思考一下WinDbg
为什么那么强大。为什么WinDbg
可以轻松分析一些结构体,或者函数名称?本质原因它有符号文件并且能够解析它,也就是PDB
文件。也就是为什么我们之前要为它配备符号文件路径。驱动基本数据类型 ??在内核编程的时候,强烈建议大家遵守
WDK
的编码习惯,建议不要这样写:unsigned long length;
,建议这样写:ULONG length
。??如下是
WDK
习惯与我们常规的习惯:WDK 习惯 | SDK 习惯 |
---|---|
ULONG | unsigned long |
PULONG | unsigned long* |
UCHAR | unsigned char |
PUCHAR | unsigned char* |
UINT | unsigned int |
PUNIT | unsigned int* |
VOID | void |
PVOID | void* |
NTSTATUS
类型,如:NTSTATUS PsCreateSystemThread();
NTSTATUS ZwOpenProcess();
NTSTATUS ZwOpenEvent();
??这个值能说明函数执行的结果,比如:
#define STATUS_SUCCESS 0x00000000//成功
#define STATUS_INVALID_PARAMETER 0xC000000D//参数无效
#define STATUS_BUFFER_OVERFLOW 0x80000005//缓冲区长度不够
??当你调用的内核函数,如果返回的结果不是
STATUS_SUCCESS
,就说明函数执行中遇到了问题,具体是什么问题,可以在ntstatus.h
文件中查看。内核异常处理 ??在内核中,一个小小的错误就可能导致蓝屏,比如:读写一个无效的内存地址。为了让自己的内核程序更加健壮,强烈建议大家在编写内核程序时,使用异常处理,降低蓝屏的可能性。不过错误大了该蓝屏的还是蓝屏。
??
Windows
提供了结构化异常处理机制,一般的编译器都是支持的,如下:__try{
//可能出错的代码
}
__except(filter_value) {
//出错时要执行的代码
}
??出现异常时,可根据
filter_value
的值来决定程序该如果执行,当filter_value
的值为:1??
EXCEPTION_EXECUTE_HANDLER(1)
:代码进入except
块2??
EXCEPTION_CONTINUE_SEARCH(0)
:不处理异常,由上一层调用函数处理3??
EXCEPTION_CONTINUE_EXECUTION(-1)
:回去继续执行错误处的代码常用的内核内存函数 ??对内存的使用,主要就是:申请、设置、拷贝以及释放。我们在编写3环的应用程序和内核对应的函数举例如下,具体使用请查看
MSDN
和WDK
的帮助文档:普通程序 | 内核中 |
---|---|
malloc | ExAllocatePool2 |
memset | RtlFillMemory |
memcpy | RtlMoveMemory |
free | ExFreePool |
malloc
对应的内核函数有很多,但是有很多已经被废弃掉了,下面是函数说明:??ExAllocatePool is obsolete and has been deprecated in Windows 10, version 2004. It has been replaced by ExAllocatePool2. For more information, see Updating deprecated > ExAllocatePool calls to ExAllocatePool2 and ExAllocatePool3.内核字符串 ??在编写3环程序我们经常用:
??When developing drivers for version of Windows prior to Windows 10, version 2004, use ExAllocatePoolZero.
CHAR(char)
/WCHAR(wchar_t)
来分别表示宅字符串和宽字符串,用0表示结尾。但是在内核中,我们常用:ANSI_STRING
/UNICODE_STRING
来分别表示宅字符串和宽字符串。它们的结构如下:??
ANSI_STRING
字符串:typedef struct _STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
}STRING;
??
UNICODE_STRING
字符串:typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaxmumLength;
PWSTR Buffer;
} UNICODE_STRING;
??为什么内核要用这样的字符串呢?主要是为了安全考虑。我们初学
C语言
的时候经常打印出烫烫烫
之类的字符串,那是因为它打印没用0结尾的字符串的结果。如果内核出现了这个问题,很容易导致蓝屏。故使用改结构体保证安全性。当然,处理这样的字符串内核就有专门处理的函数,接下来我将继续介绍。内核字符串常用函数 ??字符串常用的功能无非就是:创建、复制、比较以及转换等等。它们的函数如下,具体使用请查看
WDK
的帮助文档:ANSI_STRING | UNICODE_STRING |
---|---|
RtlInitAnsiString | RtlInitUnicodeString |
RtlCopyString | RtlCopyUnicodeString |
RtlCompareString | RtlCompareUnicoodeString |
RtlAnsiStringToUnicodeString | RtlUnicodeStringToAnsiString |
#include NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Chapter Driver By WingSummer,Unloaded Successfully!");
}NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("Chapter Driver By WingSummer,Loaded Successfully!");
DriverObject->DriverUnload = UnloadDriver;
return STATUS_SUCCESS;
}
DriverEntry ??
DriverEntry
是驱动程序的入口,如果驱动加载成功后,就像Dll
加载成功调用DllMain
函数一样,调用该函数。PDRIVER_OBJECT ??是指向
DRIVER_OBJECT
结构体的指针。一个驱动文件被加载后,它的完整信息将会返回给我们。我们来看看DRIVER_OBJECT
这个结构体存了什么,下面是头文件里面的定义:typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
??既然是讲解基础,我们就挑几个最重要的几个来讲解。不过为了方便学习驱动,我们对上面的代码进行小小的修改:
#include NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Chapter Driver By WingSummer,Unloaded Successfully!");
}NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("Chapter Driver By WingSummer,Loaded Successfully!");
DriverObject->DriverUnload = UnloadDriver;
DbgPrint("addr: %p", DriverObject);
return STATUS_SUCCESS;
}
??然后编译,让虚拟机加载这个驱动。如下图所示,然后我们得到了它的首地址:
文章图片
??然后我们再
dt
一下:kd> dt _DRIVER_OBJECT 89B7FA20
ntdll!_DRIVER_OBJECT
+0x000 Type: 0n4
+0x002 Size: 0n168
+0x004 DeviceObject: (null)
+0x008 Flags: 0x12
+0x00c DriverStart: 0xbab50000 Void
+0x010 DriverSize: 0x6000
+0x014 DriverSection: 0x89936678 Void
+0x018 DriverExtension: 0x89b7fac8 _DRIVER_EXTENSION
+0x01c DriverName: _UNICODE_STRING "\Driver\HelloDriver"
+0x024 HardwareDatabase : 0x80671ae0 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x028 FastIoDispatch: (null)
+0x02c DriverInit: 0xbab54000longHelloDriver!GsDriverEntry+0
+0x030 DriverStartIo: (null)
+0x034 DriverUnload: 0xbab51040voidHelloDriver!UnloadDriver+0
+0x038 MajorFunction: [28] 0x804f454alongnt!IopInvalidDeviceRequest+0
DriverStart
??驱动对象加载后的起始地址。
DriverSize
??驱动对象加载后的内存大小。
DriverSection
??它是一个存储目前所有已加载的驱动程序信息相关的
LDR_DATA_TABLE_ENTRY
结构体的双向循环链表。通过这个东西来实现把它们全部串起来,通过这个我们也可以进行遍历。我们通过WinDbg
来看看。我们先dt
一下我们自己编写的驱动的DriverSection
:kd> dt _LDR_DATA_TABLE_ENTRY 0x89936678
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x80554fc0 - 0x89b80d58 ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xffffffff - 0xffffffff ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x630069 - 0x0 ]
+0x018 DllBase: 0xbab50000 Void
+0x01c EntryPoint: 0xbab54000 Void
+0x020 SizeOfImage: 0x6000
+0x024 FullDllName: _UNICODE_STRING "\??\C:\Documents and Settings\wingsummer\桌面\HelloDriver.sys"
+0x02c BaseDllName: _UNICODE_STRING "HelloDriver.sys"
+0x034 Flags: 0x9104000
+0x038 LoadCount: 1
+0x03a TlsIndex: 0x49
+0x03c HashLinks: _LIST_ENTRY [ 0xffffffff - 0x1055c ]
+0x03c SectionPointer: 0xffffffff Void
+0x040 CheckSum: 0x1055c
+0x044 TimeDateStamp: 0xfffffffe
+0x044 LoadedImports: 0xfffffffe Void
+0x048 EntryPointActivationContext : (null)
+0x04c PatchInformation : 0x00650048 Void
??然后我们继续
dt
下一个成员:kd> dt _LDR_DATA_TABLE_ENTRY 0x89b80d58
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x89936678 - 0x89b45e98 ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xb8183850 - 0x1 ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0xe - 0x0 ]
+0x018 DllBase: 0xb817e000 Void
+0x01c EntryPoint: 0xb81a6105 Void
+0x020 SizeOfImage: 0x2b000
+0x024 FullDllName: _UNICODE_STRING "\SystemRoot\system32\drivers\kmixer.sys"
+0x02c BaseDllName: _UNICODE_STRING "kmixer.sys"
+0x034 Flags: 0x9104000
+0x038 LoadCount: 1
+0x03a TlsIndex: 0x74
+0x03c HashLinks: _LIST_ENTRY [ 0xffffffff - 0x2f580 ]
+0x03c SectionPointer: 0xffffffff Void
+0x040 CheckSum: 0x2f580
+0x044 TimeDateStamp: 0xe1786190
+0x044 LoadedImports: 0xe1786190 Void
+0x048 EntryPointActivationContext : (null)
+0x04c PatchInformation : 0x006d006b Void
??可以看出,我们可以通过这个链表实现遍历驱动程序的信息。
DriverName
??指示驱动对象的名字,是一个
_UNICODE_STRING
的结构体。DriverUnload
??驱动对象的卸载地址,如果存在则会调用它。它的定义:
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
其他 ??剩下的未介绍的成员,自己感兴趣的自行继续探索。
IRQL ??
IRQL
全称Interrupt Request Level
,即中断执行的优先级。它是Windows
自己定义的一套优先级方案,与CPU
无关,数值越大权限越高。中断包括了硬中断和软中断,硬中断是由硬件产生,而软中断则是完全虚拟出来的。处理器在一个IRQL
上执行线程代码,每个处理器的IRQL
决定了它如何处理中断,以及允许接收哪些中断。在同一处理器上,线程只能被更高级别IRQL
的线程能中断。每个处理器都有自己的中断IRQL
。常见的IRQL
级别有四个:Passive
、APC
、Dispatch
、DIRQL
。PASSIVE_LEVEL
是最低级别,没有被屏蔽的中断,线程执行用户模式,可以访问分页内存。APC_LEVEL
只有APC
级别的中断被屏蔽,可以访问分页内存。当有APC
发生时,处理器提升到APC
级别,就屏蔽掉其它APC
。DISPATCH_LEVEL
可以屏蔽DPC
(延迟过程) 和更低的中断,不能访问分页内存。因为只能处理分页内存,所以在这个级别,能够访问的API
大大减少。对于我们内核安全来讲,了解这些就够了,如下是IRQL
的示意图:文章图片
??在进行内核程序编写的时候,尤其注意
IRQL
这个东西。有很多的蓝屏因此而起。本节练习
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。??俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成。
1?? 编写驱动,申请一块内存,并在内存中存储
GDT
表的所有数据。然后在DebugView
中显示出来,最后释放内存。2?? 编写驱动,实现如下功能:
<1> 初始化一个字符串;
<2> 拷贝一个字符串;
<3> 比较两个字符串是否相等;
<4>
ANSI_STRING
与UNICODE_STRING
字符串相互转换;3?? 思考题:为什么
DISPATCH_LEVEL
不能访问分页内存。下一篇 【驱动篇——内核编程基础】??驱动篇——内核空间与内核模块
推荐阅读
- 急于表达——往往欲速则不达
- 慢慢的美丽
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量
- 2018年11月19日|2018年11月19日 星期一 亲子日记第144篇
- 2019-02-13——今天谈梦想()
- 考研英语阅读终极解决方案——阅读理解如何巧拿高分
- Ⅴ爱阅读,亲子互动——打卡第178天
- 低头思故乡——只是因为睡不着
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- 取名——兰