驱动篇——内核编程基础

2021年11月22日 阅读数:3
这篇文章主要向大家介绍驱动篇——内核编程基础,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。因为系统内核的复杂性,故可能有错误或者不全面的地方,若有错误,欢迎批评指正,本教程将会长期更新。 若有好的建议,欢迎反馈。码字不易,若是本篇文章有帮助你的,若有闲钱,能够打赏支持个人创做。如想转载,请把个人转载信息附在文章后面,并声明个人我的信息和本人博客o'o地址便可,但必须事先通知我html

你若是是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。编程

  看此教程以前,问个问题,你明确学驱动的目的了吗?你的开发环境准备好了吗?上一节的内容学会了吗? 没有的话就不要继续了,请从新学习前面驱动篇的教程内容继续。windows


🔒 华丽的分割线 🔒api


内核 API 的使用

  在应用层编程咱们可使用WINDOWS提供的各类API函数,只要导入头文件windows.h就能够了。可是在内核编程的时候,微软为内核程序提供了专用的API,只要在程序中包含相应的头文件就可使用了,如:#include <ntddk.h>,前提你必须安装了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提供告终构化异常处理机制,通常的编译器都是支持的,以下:spa

__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环的应用程序和内核对应的函数举例以下,具体使用请查看MSDNWDK的帮助文档:

普通程序 内核中
malloc ExAllocatePoolWithTag
memset RtlFillMemory
memcpy RtlMoveMemory
free ExFreePool

  固然malloc对应的内核函数有不少,可是有不少已经被废弃掉了,下面是说明:

  The ExAllocatePool routine is obsolete, and is exported only for existing binaries. Use ExAllocatePoolWithTag instead.

  当咱们进行内存申请时,好比遇到ExAllocatePoolWithTag函数时,会有POOL_TYPE PoolType这个参数。那么什么是POOL_TYPE,咱们查一下WDK

typedef enum _POOL_TYPE {
  NonPagedPool,
  PagedPool,
  NonPagedPoolMustSucceed,
  DontUseThisType,
  NonPagedPoolCacheAligned,
  PagedPoolCacheAligned,
  NonPagedPoolCacheAlignedMustS
} POOL_TYPE;

  其实咱们用的成员也就前两项目NonPagedPoolPagedPool,分别申请非分页内存和分页内存。那么什么是非分页内存?什么是分页内存?咱们在前面介绍过申请的物理页并非永久属于你的,这个申请的页就是分页内存,也就是能够随时被操做系统撤走转到虚拟内存交换文件。而非分页内存就是告诉操做系统,不要把个人申请的物理页撤走,这就是我独享的物理页。操做系统就不会把它给撤走转到文件中了。

内核字符串

  在编写3环程序咱们常常用: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 RtlCompareUnicodeString
RtlAnsiStringToUnicodeString RtlUnicodeStringToAnsiString

代码细节解析

  上一篇教程咱们用了一段代码,用来测试驱动是否可以加载并执行,下面咱们就来解析它,上次使用的代码以下:

#include <ntddk.h>

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

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       : 0xbab54000     long  HelloDriver!GsDriverEntry+0
   +0x030 DriverStartIo    : (null)
   +0x034 DriverUnload     : 0xbab51040     void  HelloDriver!UnloadDriver+0
   +0x038 MajorFunction    : [28] 0x804f454a     long  nt!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级别有四个:PassiveAPCDispatchDIRQLPASSIVE_LEVEL是最低级别,没有被屏蔽的中断,线程执行用户模式,能够访问分页内存。APC_LEVEL只有APC级别的中断被屏蔽,能够访问分页内存。当有APC发生时,处理器提高到APC级别,就屏蔽掉其它APCDISPATCH_LEVEL能够屏蔽DPC(延迟过程) 和更低的中断,不能访问分页内存。由于只能处理分页内存,因此在这个级别,可以访问的API大大减小。对于咱们内核安全来说,了解这些就够了,以下是IRQL的示意图:

  在进行内核程序编写的时候,尤为注意IRQL这个东西。有不少的蓝屏所以而起。

本节练习

本节的答案将会在下一节进行讲解,务必把本节练习作完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。

  俗话说得好,光说不练假把式,以下是本节相关的练习。若是练习没作好,就不要看下一节教程了,越到后面,不作练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习很少,请保质保量的完成。

1️⃣ 编写驱动,申请一块内存,并在内存中存储GDT表的全部数据。而后在DebugView中显示出来,最后释放内存。

2️⃣ 编写驱动,实现以下功能:
<1> 初始化一个字符串;
<2> 拷贝一个字符串;
<3> 比较两个字符串是否相等;
<4> ANSI_STRINGUNICODE_STRING字符串相互转换;

3️⃣ 思考题:为何DISPATCH_LEVEL不能访问分页内存。

下一篇

  驱动篇——内核空间与内核模块