avatar

Catalog
全局句柄表

基本概念

  1. 进程的句柄表是私有的,每个进程都有一个自己的句柄表
  2. 系统拥有一个全局的句柄表,通过全局变量PspCidTable获取到全局句柄表的地址
  3. 每个进程和线程都有一个唯一的编号:PIDCID,这两个值就是该内核对象在全局句柄表中的索引

与局部句柄表的差异

  1. 全局句柄表的句柄直接指向内核对象;局部句柄表的句柄指向_OBJECT_HEADER,所以在运算时还需加上0x18字节
  2. 全局句柄表只有进程和线程这俩内核对象的句柄;局部句柄表包含了当前进程所有打开的内核对象,不限于进程和线程,还有互斥体等。

全局句柄表结构

全局句柄表的结构取决于TableCode的低2位的值:

  • 取0x00时:最多可以存储512个句柄(4KB/8Byte = 512)
  • 取0x01时:第一级可以存1024个地址,第二级用来存句柄。最多可存1024x512个句柄
  • 取0x10时:第一级可以存1024个地址,每个第二级的表也存1024个地址,第三级表存句柄。最多可存1024x1024x512个句柄

具体如下图所示:

查找全局句柄表

常规方式

下面按照步骤,通过Windbg实现查找全局句柄表中的进程句柄

  1. 在虚拟机中打开计算器,并在任务管理器中确定进程PID的值

  2. 根据全局变量PspCidTable,定位_HANDLE_TABLE结构体

  3. 根据_HANDLE_TABLE中的TableCode字段,以及PID的值(用于索引),来确定句柄的位置。注意事项如下:

    1)观察TableCode后2位的值,判断是否需要跨表

    2)3环PID是四字节的值,但是句柄在内核是8字节,因此寻址时需要PID/4*8

  4. 根据句柄的低四字节,找到对应的内核对象。注意事项如下:

    1)全局句柄表的句柄是直接指向内核对象结构体的,所以不需要加0x18字节

    2)根据低四字节查找时,需清空后3bit属性位再进行运算

代码查找思路

由于PspCidTable无法直接引用,在代码实现的过程中,需要一些其它技巧

1)特征搜索

PsLookupProcessThreadByCid()
PsLookupProcessByProcessId()
PsLookupThreadByThreadId()

这三个函数都会根据PID或者CID去查找全局句柄表。在这个过程中,就会用到PspCidTable,这样我们可以通过Hook这三个函数,就可以借用PspCidTable这个值,找到全局句柄表了。由于这三个函数都是未导出的,所以需要通过特征搜索先进行定位,特征搜索可以参考之前的文章

2)KPCR

KPCR有一个成员,KdVersionBlock。这个成员非常奇妙,在他指向地址(+0x80)位置处有一个地址,这个地址指向的就是PspCidTable。具体如下图所示:

因此我们可以在驱动中采用如下手法。获取到PspCidTable

Code
1
2
3
4
5
6
7
PULONG PspCidTable
_asm {
mov eax, fs:[0x34]
mov eax, [eax + 0x80]
mov eax, [eax]
mov PspCidTable, eax
}

当然,这个KdVersionBlock指向的地址还能找到一些其它的值:

  • +0x10:KernelBase
  • +0x18/+0x70:PsLoadedModuleList
  • +0x78:PsActiveProcessHead
  • +0x88:ExpSystemResourcesList

遍历全局句柄表

有了获取PspCidTable的方法后,我们就可以试着遍历一下全局句柄表,例如打印每个进程的进程名。

代码实现

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

VOID Driver_Unload(PDRIVER_OBJECT pDriver);
VOID TraverseProcess();

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING RegistryPath) {

TraverseProcess();

pDriver->DriverUnload = Driver_Unload;

return STATUS_SUCCESS;
}

VOID TraverseProcess() {
PULONG PspCidTable, TableCode;
PUCHAR pEPROCESS;
_asm {
mov eax, fs:[0x34]
mov eax, [eax + 0x80]
mov eax, [eax]
mov PspCidTable, eax
mov eax, [eax]
mov TableCode, eax
}
DbgPrint("%x, %x\n", PspCidTable, TableCode);
for (int i = 0; i < 500; i++) {
TableCode = TableCode + 2;
// DbgPrint("Index: %d, TableCode: %x \n",i, *TableCode);
if (*TableCode != 0) {
pEPROCESS = (PUCHAR)((*TableCode) & 0xfffffff8);
DbgPrint("%s\n", (PULONG)(pEPROCESS + 0x174));
}
}
}

VOID Driver_Unload(PDRIVER_OBJECT pDriver) {
DbgPrint("Unload Success!\n");
}

实验效果

由于代码比较草率,并没有过滤掉线程结构体,所以会有较多的乱码,但仍然可以打印出所有的进程

参考资料

参考书籍:《Windows内核原理与实现》——潘爱民

参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?t=143&p=68

参考文章:

  1. https://blog.csdn.net/weixin_42052102/article/details/83479303
  2. https://blog.csdn.net/qq_41988448/article/details/104945311
  3. https://www.cnblogs.com/joneyyana/p/12595525.html

参考笔记:张嘉杰,时间刺客.

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/04/26/%E5%85%A8%E5%B1%80%E5%8F%A5%E6%9F%84%E8%A1%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶