来了解一下Windows内核的句柄表。

句柄

句柄就类似 Linux 的文件描述符,指示了某个进程在内核对象的偏移,内核可以通过这个下标找到对应的内核对象。

1
2
3
4
HANDLE g_hMutex = ::CreateMutex( NULL , FALSE, "XYZ");
HANDLE g_hMutex = ::OpenMutex( MUTEX_ALL_ACCESSFALSE, "XYZ");
HANDLE g_hEvent = ::CreateEvent( NULL, TRUE, FALSE, NULL);
HANDLE g_hThread = ::CreateThread( NULL, 0, Proc,NULL, 0, NULL);

句柄是给3环用的,而不是给内核用的 。所以在写驱动的时候,不要搞句柄花里胡哨的东西。Windows所有涉及句柄的API,一旦到了真正函数实现的部分,就立刻使用ObReferenceObjectByHandle把它转化为真正的指向内核对象的指针。

句柄表

之前看进程结构体里面,其实就有一个成员叫句柄表 ObjectTable

1
2
3
4
5
6
kd> dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x0f0 ExceptionPortState : Pos 0, 3 Bits
+0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE
+0x0f8 Token : _EX_FAST_REF

句柄表结构

句柄表也是有一个专门的结构维护的,来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kd> dt _HANDLE_TABLE
ntdll!_HANDLE_TABLE
+0x000 TableCode : Uint4B
+0x004 QuotaProcess : Ptr32 _EPROCESS
+0x008 UniqueProcessId : Ptr32 Void
+0x00c HandleLock : _EX_PUSH_LOCK
+0x010 HandleTableList : _LIST_ENTRY
+0x018 HandleContentionEvent : _EX_PUSH_LOCK
+0x01c DebugInfo : Ptr32 _HANDLE_TRACE_DEBUG_INFO
+0x020 ExtraInfoPages : Int4B
+0x024 Flags : Uint4B
+0x024 StrictFIFO : Pos 0, 1 Bit
+0x028 FirstFreeHandle : Uint4B
+0x02c LastFreeHandleEntry : Ptr32 _HANDLE_TABLE_ENTRY
+0x030 HandleCount : Uint4B
+0x034 NextHandleNeedingPool : Uint4B
+0x038 HandleCountHighWatermark : Uint4B

最低两位表示句柄表的级数,例如最低两位位为 0 表示该句柄表存放的是真正的句柄,如果为 01 或者 10 则表示指向了一个句柄表,如下图所示:

顺表介绍一下句柄在句柄表中的结构,一个句柄项占 8 字节,通常来说一个句柄表会占一个页(4KB),所以一个句柄表最多容纳 512 个句柄项。

①:这一块共计两个字节,高位字节是给SetHandleInformation这个函数用的,比如写成如下形式,那么这个位置将被写入0x02

1
SetHandleInformation(Handle,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);

HANDLE_FLAG_PROTECT_FROM_CLOSE宏的值为0x00000002,取最低字节,最终 ① 这块是0x0200

②:这块是访问掩码,是给OpenProcess这个函数用的,具体的存的值就是这个函数的第一个参数的值。

③ 和 ④ 这两个块共计四个字节,其中bit0-bit2存的是这个句柄的属性,其中bit2bit0默认为01; bit1表示的函数是该句柄是否可继承,OpenProcess的第二个参数与bit1有关,bit31-bit3则是存放的该内核对象在内核中的具体的地址。

挂在进程下的句柄表就叫私有句柄表,除此之外内核还维护一张全局句柄表

私有句柄表

私有句柄表有很多类型的句柄,文件句柄、进程句柄、线程句柄、快照句柄等等。

这里以 OpenProcess 打开的句柄为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
#include<stdio.h>
#include<windows.h>

int main()
{
DWORD pid=0;
scanf("%d",&pid);
HANDLE hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
printf("HANDLE=%p",hProcess);
getchar();
getchar();
return 0;
}

因为发现 win7 的镜像自带的任务管理器居然没有显示 pid,所以使用 CE 查看。

这里获取了 explorer.exe 的句柄,返回值为 0x2C

这里需要注意的是,句柄表项指向的仍然不是真正的内核对象,而是一个 _OBJECT_HEADER 结构,在里面可以区分该句柄是进程句柄,线程句柄还是其它句柄。

1
2
3
4
5
6
7
kd>!process 0 0
PROCESS 887fc888 SessionId: 1 Cid: 0634 Peb: 7ffdc000 ParentCid: 0614
DirBase: 401d3000 ObjectTable: 92303268 HandleCount: 567.
Image: explorer.exe
PROCESS 87d73450 SessionId: 1 Cid: 0d30 Peb: 7ffd9000 ParentCid: 070c
DirBase: 7480b000 ObjectTable: 923c7920 HandleCount: 17.
Image: Test1.exe

可以快速得到该进程的句柄表位置,然后找到该句柄表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kd> dt _HANDLE_TABLE 923c7920
nt!_HANDLE_TABLE
+0x000 TableCode : 0xa4cfc001
+0x004 QuotaProcess : 0x87d73450 _EPROCESS
+0x008 UniqueProcessId : 0x00000d30 Void
+0x00c HandleLock : _EX_PUSH_LOCK
+0x010 HandleTableList : _LIST_ENTRY [ 0xa4c25708 - 0xab485c68 ]
+0x018 HandleContentionEvent : _EX_PUSH_LOCK
+0x01c DebugInfo : (null)
+0x020 ExtraInfoPages : 0n0
+0x024 Flags : 0
+0x024 StrictFIFO : 0y0
+0x028 FirstFreeHandle : 0x30
+0x02c LastFreeHandleEntry : 0xa4cb3ff8 _HANDLE_TABLE_ENTRY
+0x030 HandleCount : 0x11
+0x034 NextHandleNeedingPool : 0x1800
+0x038 HandleCountHighWatermark : 0x11

TableCode 的最低两位为 1,说明是二级的句柄表,需要索引一次,也就是将句柄下标 /512 得到二级句柄表所在的下标,取512模得到二级句柄表内的偏移。

0x2C 的实际下标是 0x2C/4=11, 那么 11/512=011%512=11,即可知道应该找第一个二级句柄表,表内偏移为 11*8 (每个句柄项大小为 8)。

1
2
3
4
5
6
7
8
9
kd> dd 0xa4cfc000
a4cfc000 ab7c1000 a4d01000 a4cb3000 00000000
a4cfc010 00000000 00000000 00000000 00000000
a4cfc020 00000000 00000000 00000000 00000000
a4cfc030 00000000 00000000 00000000 00000000
a4cfc040 00000000 00000000 00000000 00000000
a4cfc050 00000000 00000000 00000000 00000000
a4cfc060 00000000 00000000 00000000 00000000
a4cfc070 00000000 00000000 00000000 00000000

三级句柄表同理可得。其实跟分页是同理的,例如常见的 10-10-12 是二级页表 2-9-9-12 是三级页表。不过需要注意的是,32位的系统中,每个二级句柄表可以存下 1024 个句柄指针,所以三级句柄表可能实际跟二级句柄表又有些许差异,不过影响不大。不过这里有一个小技巧,句柄表初始化的时候,在 AccessMode 的位置上会显示后一个句柄的实际值。

1
2
3
4
5
6
7
8
9
10
kd> dq ab7c1000
ReadVirtual: ab7c1000 not properly sign extended
ab7c1000 fffffffe`00000000 00000009`ab552a69
ab7c1010 00000003`98453741 00100020`8889c259
ab7c1020 001f0003`87c461f9 001f0001`882bfc31
ab7c1030 001f0001`86a20ee9 00100020`86a28be1
ab7c1040 00000009`ab5a0c39 00020019`ab61a201
ab7c1050 00000001`ab6c69e1 001fffff`887fc871
ab7c1060 00000034`00000000 00000038`00000000
ab7c1070 0000003c`00000000 00000040`00000000

可以很清楚看到后面连成片的 0x340x380x3c 等,0x34 所在的位置句柄值就是 0x30,同理,前面就找到了 0x2C 的句柄

1
001fffff`887fc871

可以发现 AccessMode0xFFFF 也就是 PROCESS_ALL_ACCESS 的赋值,最高三位未赋予实际意义(也就是说0x1FFF就足以表示 PROCESS_ALL_ACCESS),取何值不影响。

将取出来的值去掉最低三位拿到的地址就是 887fc870,这是一个 _OBJECT_HEADER 结构,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd> dt _OBJECT_HEADER 887fc870
nt!_OBJECT_HEADER
+0x000 PointerCount : 0n317
+0x004 HandleCount : 0n8
+0x004 NextToFree : 0x00000008 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : 0x7 ''
+0x00d TraceFlags : 0 ''
+0x00e InfoMask : 0x8 ''
+0x00f Flags : 0 ''
+0x010 ObjectCreateInfo : 0x8874e040 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x8874e040 Void
+0x014 SecurityDescriptor : 0x922ba4aa Void
+0x018 Body : _QUAD

这里的 TypeIndex 可以辨认该句柄是什么类型的句柄,据说早年是用一个字符串标识的,个人感觉可能是浪费内存了就使用这个 TypeIndex 去存储类型,其中 +0x18 的偏移就是句柄对应的 EPROCESS 的结构。

1
2
3
4
5
kd> dt _EPROCESS 887fc870+0x18
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x16c ImageFileName : [15] "explorer.exe"
+0x17b PriorityClass : 0x2 ''

成功找到对应的 EPROCESS 结构,可以看到,这个地址就是 explorer.exe 进程实际 EPROCESS 结构的地址(可在 !process 0 0 命令输出)。

全局句柄表

全局句柄表指针由内核的全局变量 PspCidTable 维护,故此全局句柄表因此又被称为CID句柄表

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
kd> dd PspCidTable
83d7ad54 8e801070 00000000 80000020 00000101
83d7ad64 800002e0 80000024 00000000 00000000
83d7ad74 00000000 00000000 00000000 00000113
83d7ad84 00000000 00000000 83d2d482 00000000
83d7ad94 00000000 00000000 00000000 00000008
83d7ada4 00000000 83d7ada8 83d7ada8 00000000
83d7adb4 00000000 00000000 00000000 00000000
83d7adc4 00000000 80dc8c38 80dc4c38 00000000
kd> dt _HANDLE_TABLE 8e801070
ntdll!_HANDLE_TABLE
+0x000 TableCode : 0x8e804000
+0x004 QuotaProcess : (null)
+0x008 UniqueProcessId : (null)
+0x00c HandleLock : _EX_PUSH_LOCK
+0x010 HandleTableList : _LIST_ENTRY [ 0x8e801080 - 0x8e801080 ]
+0x018 HandleContentionEvent : _EX_PUSH_LOCK
+0x01c DebugInfo : (null)
+0x020 ExtraInfoPages : 0n0
+0x024 Flags : 1
+0x024 StrictFIFO : 0y1
+0x028 FirstFreeHandle : 0x178
+0x02c LastFreeHandleEntry : 0x8e8041e0 _HANDLE_TABLE_ENTRY
+0x030 HandleCount : 0x1a7
+0x034 NextHandleNeedingPool : 0x800
+0x038 HandleCountHighWatermark : 0x1a7
kd> dq 0x8e804000
8e804000 fffffffe`00000000 00000000`868e5901
8e804010 00000000`868e5629 00000000`86944431
8e804020 00000000`86944921 00000000`86940d49
8e804030 00000000`86940931 00000000`86934d49
8e804040 00000000`86934a71 00000000`86930d49
8e804050 00000000`86930a71 00000000`8691cd49
8e804060 00000000`8691ca71 00000000`8690cd49
8e804070 00000000`8690ca71 00000000`868f4d49

全局句柄表顾名思义不依附于任何一个进程。每个进程和线程都有一个唯一的编号:PIDTID,这两个值其实就是全局句柄表中的索引,统称CID。进程和线程的查询,主要是以下三个函数,按照给定的PIDTIDPspCidTable从查找相应的进线程对象:

1
2
3
PsLookupProcessThreadByCid(x, x, x);
PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process);
PsLookupThreadByThreadId(HANDLE ThreadId, PETHREAD *Thread);

参考文献