avatar

Catalog
内核空间&内核模块

对编写基础的驱动有所了解后,我们来进一步了解一下内核,本篇会介绍两个概念,内核空间以及内核模块,先从内核空间说起。

内核空间

内核空间的概念,我们还是比较熟悉的。这里我们主要关注一点,就是不同进程低2G内存空间对应的物理页往往是不同的,而在高2G内存空间对应的物理页往往是相同的。如图: 这一要素,主要运用于跨进程读取内存等手法,之前的文章也提到过,这里我们来验证一下这个理论:

  1. 第一步,写一个驱动,获取全局变量的地址,程序比较简单,就不贴代码了,直接上图
  2. 第二步运行驱动,在DebugView中可以看到该全局变量的线性地址:0xbac93000
  3. 然后我们随机打开一个应用,这里以记事本为例。打开后,在Windbg中,找到记事本的Cr3查看该进程在线性地址0xbac93000处的值为多少 可以发现,我们在记事本这个进程中查看0xbac93000这个线性地址对应的物理页时,它所存着的值恰好是我们在另一个驱动中定义的全局变量的值。这正说明了,不同进程在高2G中对应的物理页是相同的。(这里解释一下 .process 这个指令的作用,0xaaaabbbb对应着某个进程的进程结构体的地址,.process aaaabbbb这个指令就相当于切换到这个进程,之后所访问的地址,都是这个进程地址空间中的地址)

内核模块

基本概念

有了内核空间的概念,这里介绍另一个概念,内核模块。看图

  • 硬件种类繁多,不可能做出一个兼容所有硬件的内核,所以,微软提供规定的接口格式,让硬件驱动人员按照规定的格式编写“驱动程序”。当然,并不是每个驱动程序一定需要关联一个硬件,也可以仅仅是一个程序,就如同我们之前所写的。
  • 这些驱动程序,每一个都是一个模块,称为“内核模块”,都可以加载到内核中,也都遵守PE结构。本质上,任意一个.sys文件与内核文件(例如ntoskrnl.exe)没有区别。

有了上述概念后,我们可以打个比喻,内核空间(高2G),相当于一个进程;而各个加载到内核中的内核模块,就相当于加载到进程中的DLL;内核模块提供0环的函数实现以及硬件的程序驱动,DLL为这个进程提供额外功能。这样就好理解了。

DRIVER_OBJECT

在之前编写的驱动程序中,入口函数总会传递一个参数,它的类型是PDRIVER_OBJECT,说明这是一个指向DRIVER_OBJECT的指针,而DRIVER_OBJECT正是驱动模块对应的结构体,来描述该驱动的相关信息。 由图,这里着重介绍四个比较关键的字段:

  • DriverStart(+0x00C):驱动模块在内核中的地址
  • DriverSize(+0x010):驱动模块在内核中的大小
  • DriverName(+0x01C):驱动模块在内核中的名字
  • DriverSection(+0x014):这个位置存的是一个指针,指向一个_LDR_DATA_TABLE_ENTRY结构体

LDR_DATA_TABLE_ENTRY

驱动在内核中也属于内核模块,该结构体描述了内核模块的相关信息,同时包含串着所有内核模块的双向链表,通过该链表,可以遍历所有内核模块。

  • InLoadOrderLinks(+0x000):串着所有内核模块的双向链表
  • DllBase(+0x018):当前内核模块的基址
  • SizeOfImage(+0x020):当前内核模块的大小
  • FullDllName(+0x024):当前内核模块的完整路径
  • BaseDllName(+0x02C):当前内核模块的模块名

内核模块遍历

有了上面这些基础后呢,就可以自己实现内核模块遍历的功能了。这里先说一下思路:

  1. 首先我们写一个驱动,驱动函数的入口会传一个PDRIVER_OBJECT这个参数,我们就可以利用这个参数,获取到自身驱动的DRIVER_OBJECT
  2. 有了DRIVER_OBJECT结构体后,就可以通过其偏移0x014位置处的值,找到LDR_DATA_TABLE_ENTRY结构体,该结构体的第一个元素,就是串着所有内核模块的双向链表
  3. 写一个循环,遍历这个双向链表,打印出相应的信息

下面附上代码:

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

VOID Driver_Unload(PDRIVER_OBJECT driver);
VOID EnumKernelModule(PDRIVER_OBJECT driver);

NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING RegistryPath) {

DbgPrint("Driver is running!\n");
EnumKernelModule(driver);

driver->DriverUnload = Driver_Unload;

return STATUS_SUCCESS;
}

VOID EnumKernelModule(PDRIVER_OBJECT driver) {
UINT32 ds, dsCurrent;

DbgPrint("_Driver_Object: %x\n", driver);
DbgPrint("_LDR_DATA_TABLE_ENTRY: %x\n", driver->DriverSection);

ds = (UINT32)(driver->DriverSection);
dsCurrent = ds;
DbgPrint("Name: %wZ, Base: %x, Size: %x\n", (PUINT32)(ds + 0x2c), *(PUINT32)(ds + 0x18), *(PUINT32)(ds + 0x20));

while (1) {
ds = *(PUINT32)ds;
if (ds == dsCurrent) {
break;
}
DbgPrint("Name: %wZ, Base: %x, Size: %x\n", (PUINT32)(ds + 0x2c), *(PUINT32)(ds + 0x18), *(PUINT32)(ds + 0x20));
}
}

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

观察实验结果:

这里主要打印了模块名,基址以及大小。观察结构可以发现,内核模块中不仅包含驱动文件,还有一些系统的dll,exe。因为它们本质上都是PE结构,所以不论是驱动文件还是内核文件,在内核空间中,都是一种内核模块。

关系梳理

之前,在学习完KPCR后,对进程结构体,线程结构体,KPCR进行了简单的关系梳理,不过需要指出一点,在后面分析线程切换函数SwapContext时也指出了,线程寻找进程主要用的是KTHREAD+0x44偏移处的Process,而不是用ETHREAD+0x220的偏移处的EPROCESS,不过呢,通常情况下,这两个地方都可以用。

现在又学习完了驱动、内核相关的结构体,来看看它们之间的关系如何(在已知驱动的情况下):

遍历内核模块:PDRIVER_OBJECT -> DRIVER_OBJECT -> DriverSection -> _LDR_DATA_TABLE_ENTRY -> InLoadOrderLinks

遍历驱动模块名:PDRIVER_OBJECT -> DRIVER_OBJECT -> DriverSection -> _LDR_DATA_TABLE_ENTRY -> InLoadOrderLinks -> DllBase(模块名.sys)

参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=60

参考文章:https://blog.csdn.net/qq_41988448/article/details/103514007

参考笔记:张嘉杰的笔记

参考代码:葫芦娃救爷爷,Joney,张嘉杰

Author: cataLoc
Link: http://cata1oc.github.io/2020/04/10/%E5%86%85%E6%A0%B8%E7%A9%BA%E9%97%B4%E5%92%8C%E5%86%85%E6%A0%B8%E6%A8%A1%E5%9D%97/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶