Windows XP 句柄表
ʕ •ᴥ•ʔ ɔ:
1 内核对象
1.1 内核对象
首先需要说一下,内核对象的讲解可以参考《Windows内核原理与实现》章节2.5.1,我这里提炼几点之前不太清楚的知识点:
每一个对象都被包含两部分:对象头(_OBJECT_HEADER)和对象体(Body)。所有对象的对象头都具有统一的格式,其定义见 base\ntos\inc\ob.h。
1 | typedef struct _OBJECT_HEADER { |
然而_OBJECT_HEADER与Body并不是整个object的全部,实际上在object header前面还有optional headers与pool header,一个完全的windows object应该是这样的:
- _POOL_HEADER
- _OBJECT_QUOTA_CHARGES (optional)
- _OBJECT_HANDLE_DB (optional)
- _OBJECT_NAME (optional)
- _OBJECT_CREATOR_INFO (optional)
- _OBJECT_HEADER
- body
1 | kd> dt _OBJECT_HEADER |
对象的两种引用方式:
在内核中直接通过对象地址来引用,
ObReferenceObjectByPointer使PointerCount计数减1。通过句柄来引用对象。使用
ObpIncrementHandleCount使HandleCount加1,使用ObpDecrementHandleCount使HandleCount减1。PointerCount:指针计数,它记录了内核本身(也包括驱动程序)引用该对象的次数,一般0环使用。HandleCount:句柄计数,它记录了有多少个句柄引用此对象,这些句柄可能出现在不同的进程中,一般3环使用。
可参考:4Windows内核情景分析—内核对象。
1.2 _OBJECT_HEADER
关于_OBJECT_HEADER.Type.DefaultObject需要进行说明,主要参考《Windows内核情景分析第6.3章节》(《Windows内核原理与实现》没说清楚):
1 | kd> dt _OBJECT_HEADER |
_OBJECT_HEADER.Type.DefaultObject这个字段或者是指针,或者是位移量:
- 指针: 指向一个默认的、全局的可等待对象
ObpDefaultObject。直接可等待对象的DefaultObject == 0,说明就是目标对象本身。 - 位移量:指向该类对象(数据结构)内部的一个可等待对象(例如_FILE_OBJECT中的事件Event)。
具体的请看《Windows XP 多核同步 3.2.1DefaultObject》
1.3 _DISPATCHER_HEADER
每个可被等待的对象都有成员_DISPATCHER_HEADER,如_KEVENT(事件)、_KMUTANT(互斥体)的第一个成员:
1 | kd> dt _KEVENT |
可等待对象分为:直接可等待对象间接可等待对象。
- 直接可等待对象:
_DISPATCHER_HEADER结构为内核对象的第一个成员。 - 间接可等待对象:内核对象第一个成员不是
_DISPATCHER_HEADER结构,而是往该内核对象内嵌入一个直接可等待对象,所以实际可等待的是嵌入于其内部的事件对象。
⚠️注意:_DISPATCHER_HEADER.Type对应的枚举值如下( \base\ntos\inc\ke.h):
1 | typedef enum _KOBJECTS { |
在分析KeWaitForSingleObject(0x00423F61)时涉及。
1.4 _KOBJECTS
1 |
|
1.5 OBJECT_TYPE
在Windows中,每一种对象都需要有一个对应的类型对象(即_OBJECT_HEADER.Type),其定义为:
1 |
|
系统定义的对象种类是有限的,WRK 支持 31 种对象,各有一个全局的 POBJECT_TYPE 变量指向其类型对象,表 2.4 列出了这 31 种对象,读者可以在 WRK 源代码中找到它们的定义和用法。(Windows内核原理与实现)

如互斥体:
1 | POBJECT_TYPE ExMutantObjectType; |
在函数NtCreateMutant等都会用到。
2 进程句柄表
句柄值:
| 句柄值 | 含义 |
|---|---|
| 0 | ActiveThreads = KTHREAD.ApcState.Process.ActiveThreads - ActiveThreads != 1,结束当前线程。 - ActiveThreads == 1,直接返回,也就是不能结束当前进程的最后一个线程。 在分析 NtTerminateThread(IN HANDLE ThreadHandle OPTIONAL,IN NTSTATUS ExitStatus)时逆得的。 |
| -1 | 代表当前进程 |
| -2 | 代表当前线程 |
| 一个为4倍数的正数 | 句柄表中的索引 |
| 负数 | 其值的绝对值为System进程中的句柄值,ObpKernelHandleTable,仅限于内核模式的函数可以引用。 |
2.1 进程句柄表的含义
只有进程这个内核对象才有句柄表,其他内核对象没有句柄表。
进程内核对象A自己创建或者打开的内核对象的句柄在自己的句柄表里,但是A自己的句柄不在自己的句柄表里,在父进程的句柄表里,但是A主线程的句柄值在自己的句柄表里。
每个进程对象都有一张属于自身进程的句柄表,当这个进程创建或者打开一个内核对象时,将获得一个句柄,通过这个句柄可以访问内核对象。
句柄表存储在零环,一个进程使用了几个句柄,在该进程的句柄表中就会存储几个句柄。
句柄存在的目的是为了避免在应用层直接修改内核对象。
每一个句柄值存储时是8字节,高4字节是句柄值,低4字节是对应内核对象的地址。
所有的句柄所对应的内核对象,都包含在
_OBJECT_HEADER中,真正的内核对象保存在_OBJECT_HEADER +0x018 body的位置(如ERPOCESS)。1
2
3
4
5
6
7
8
9
10
11
12
13
14kd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Type : Ptr32 _OBJECT_TYPE
+0x00c NameInfoOffset : UChar
+0x00d HandleInfoOffset : UChar
+0x00e QuotaInfoOffset : UChar
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD //内核对象
每一个对象都由两部分构成:对象头和对象体。所有对象的对象头都具有统一的格式,其定义见 base\ntos\inc\ob.h。(还可参见Windows内核原理与实现2.5.1)
句柄值和对应内核对象地址的关系:$$Index\ =\ 句柄值/4$$
$$内核对象\_OBJECT\_HEADER地址\ =\ TableCode[句柄值/4]$$
$$内核对象地址\ =\ \_OBJECT\_HEADER\ +\ 0x18$$
实验:
找个进程查看
1
2
3
4
5kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 86744da0 SessionId: 0 Cid: 0cfc Peb: 7ffdf000 ParentCid: 0b7c
DirBase: 06e40360 ObjectTable: e1df7c60 HandleCount: 68.
Image: 0418_Handle_1.e查找该进程句柄表
1
2
3
4
5kd> dt _EPROCESS 0x863746e0
ntdll!_EPROCESS
...
+0x0c4 ObjectTable : 0xe1df7c60 _HANDLE_TABLE
...查找句柄表的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kd> dt _HANDLE_TABLE 0xe1df7c60
ntdll!_HANDLE_TABLE
+0x000 TableCode : 0xe1e4b000
+0x004 QuotaProcess : 0x86744da0 _EPROCESS
+0x008 UniqueProcessId : 0x00000cfc Void
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0x80565ba8 - 0xe3270a9c ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0n0
+0x030 FirstFree : 0x704
+0x034 LastFree : 0
+0x038 NextHandleNeedingPool : 0x800
+0x03c HandleCount : 0n68
+0x040 Flags : 0
+0x040 StrictFIFO : 0y0在三环写程序
0418_Handle_1.exe打印一个句柄看看。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int _tmain(int argc, _TCHAR* argv[])
{
DWORD PID;
HANDLE hPro = NULL;
HWND hwnd = FindWindowA(NULL, "无标题 - 记事本");
GetWindowThreadProcessId(hwnd, &PID);
for (int i = 0; i < 10; i++)
{
hPro = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE |
PROCESS_VM_READ, TRUE, PID);
printf("句柄:%x\n", hPro);
}
SetHandleInformation(hPro, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
getchar();
return 0;
}输出其中一个句柄值为0x708,索引 = 0x708/4 = 0x1c2。
1
2
3
4
5
6kd> dq 0xe1e4b000+0x1c2*8
e1e4be10 0200003a`860ab773 0000003a`860ab773
e1e4be20 0000003a`860ab773 0000003a`860ab773
e1e4be30 0000003a`860ab773 0000003a`860ab773
e1e4be40 0000003a`860ab773 0000003a`860ab773
e1e4be50 0000003a`860ab773 0000003a`860ab773查看进程是谁
1
2
3
4
5kd> dt _EPROCESS 860ab770+0x18
ntdll!_EPROCESS
...
+0x174 ImageFileName : [16] "notepad.exe"
...
进程句柄表有以下这些属性:
进程句柄表的位置:
_EPROCESS.ObjectTable(+0x0c4).TableCode;每个进程的句柄表是一个
_HANDLE_TABLE结构(在base/ntos/inc/ex.h),每一个0x40 bytes大小。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21typedef struct _HANDLE_TABLE {
ULONG_PTR TableCode; //指向句柄表的存储结构
struct _EPROCESS *QuotaProcess; //句柄表所属的进程
HANDLE UniqueProcessId; //创建/打开此句柄的进程的ID,用于回调函数
EX_PUSH_LOCK HandleTableLock[HANDLE_TABLE_LOCKS];//HANDLE_TABLE_LOCKS=4
//句柄表锁,仅在句柄表扩展时使用
LIST_ENTRY HandleTableList; //所有的句柄表形成一个链表
//链表头为全局变量HandleTableListHead
EX_PUSH_LOCK HandleContentionEvent; //若在访问句柄时发生竞争,则在此推锁上等待
PHANDLE_TRACE_DEBUG_INFO DebugInfo; //调试信息,仅当调试句柄时才有意义
LONG ExtraInfoPages; //审计信息所占用的页面数量
ULONG FirstFree; //空闲链表表头的句柄索引
ULONG LastFree; //最近被释放的句柄索引,用于FIFD类型空闲链表
ULONG NextHandleNeedingPool; //下一次句柄表扩展的起始句柄索引
LONG HandleCount; //正在使用的句柄表项的数量
union {
ULONG Flags; //标志域
BOOLEAN StrictFIFO : 1; //是否使用FIF0风格的重用,即先释放先重用
};
} HANDLE_TABLE, *PHANDLE_TABLE;句柄表TableCode里面每一项句柄表的项是一个
_HANDLE_TABLDE_ENTRY,每一项8bytes大小。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18typedef struct _HANDLE_TABLE_ENTRY {
union {
PVOID Object; //指向句柄所代表的对象
ULONG ObAttributes; //最低三位有特别含义,参见OBJ_HANDLE_ATTRIBUTES宏定义
PHANDLE_TABLE_ENTRY_INFO InfoTable;//各个句柄表页面的第一个表项
ULONG_PTR Value; //使用此成员指向一张表
};
union {
union {
ACCESS_MASK GrantedAccess; //访问掩码
struct {
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};
LONG NextFreeTableEntry;
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;解析如下:(注意
_HANDLE_TABLE_ENTRY对应的是地址,不是句柄值)1
2
3
4
5
6
7
8
9
10kd> dt _HANDLE_TABLE_ENTRY 0xe1e4be10
nt!_HANDLE_TABLE_ENTRY
+0x000 Object : 0x860ab773 Void
+0x000 ObAttributes : 0x860ab773
+0x000 InfoTable : 0x860ab773 _HANDLE_TABLE_ENTRY_INFO
+0x000 Value : 0x860ab773
+0x004 GrantedAccess : 0x200003a
+0x004 GrantedAccessIndex : 0x3a
+0x006 CreatorBackTraceIndex : 0x200
+0x004 NextFreeTableEntry : 0n33554490
可参考的文章:
- Windows内核-句柄
- 句柄表篇——总结与提升
- Windows内核学习笔记之进程(下)
- 《Windows内核原理与实现》
2.2 句柄表TableCode
句柄表是一个多层结构,最多有三层,最少有一层。层数由TableCode的低2位的值来判断,低2位为0时为1层,为1时为2层,为2时为3层。
句柄表有几层是动态创建的,先回使用一层的,句柄变多了容不下了,就会动态创建二级句柄表(Windows内核原理与实现3.4)。
TableCode结构为:_HANDLE_TABLE。TableCode每一项的结构为:_HANDLE_TABLE_ENTRY,每一项为8字节。
打印0418_Handle_1.exe的句柄表:
1 | kd> dt _HANDLE_TABLE 0xe1df7c60 |
得到TableCode = 0xe1e4b000 ,需要注意最后二位(Windows内核原理与实现_WRK):
00:0,单层句柄表,4KB = 4096bytes/8bytes = 512项。
01:1,二层句柄表,(4KB/4bytes)*(4KB/8bytes) = 1024*512项。
10:2,三层句柄表,1024*1024*512项。
![1.png]()
该字段的低两位清零后的地址为句柄表的最高层表的首地址。
2.3 句柄项_HANDLE_TABLE_ENTRY
打印0418_Handle_1.exe的句柄表中句柄值为0x708项(索引 = 0x708/4 = 0x1c2)。
1 | kd> dq 0xe1e4b000+0x1c2*8 |
TableCode结构为:_HANDLE_TABLE。TableCode每一项的结构为:_HANDLE_TABLE_ENTRY,每一项为8字节。
句柄值:
| 句柄值 | 含义 |
|---|---|
| 0 | ActiveThreads = KTHREAD.ApcState.Process.ActiveThreads - ActiveThreads != 1,结束当前线程。 - ActiveThreads == 1,直接返回,也就是不能结束当前进程的最后一个线程。 在分析 NtTerminateThread(IN HANDLE ThreadHandle OPTIONAL,IN NTSTATUS ExitStatus)时逆得的。 |
| -1 | 代表当前进程 |
| -2 | 代表当前线程 |
| 一个为4倍数的正数 | 句柄表中的索引 |
| 负数 | 其值的绝对值为System进程中的句柄值,ObpKernelHandleTable,仅限于内核模式的函数可以引用。 |
下面对64位拆分解析,主要参考海哥、火哥和文章Windows内核-句柄。

| 划分 | 位数 | 含义 |
|---|---|---|
| 56-64bit | 8位 | 该8位中的高四位始终为0,后四位标志比如写成函数SetHandleInformation()函数是否设定了HANDLE_FLAG_PROTECT_FROM_CLOSE标志位,如果设定了这四位的值为0x2。 |
| 32-55bit | 24位 | 通过获得句柄的函数得到的访问掩码,在进程中就是OpenProcess(dwDesiredAccess,BInheritHandle,dwProcessId)中的dwDesiredAccess。 |
| 0-31bit | 32位 | 内核对象的首地址,后三位用0填充。 |
| 0-3bit | 3位 | 分别表示三种权限句柄通用权限。 |
需要特别说明的是0-3bit:
类似TableCode,_HANDLE_TABLE_ENTRY每一项的低3位在查地址时也需要清零,低3位由宏OBJ_HANDLE_ATTRIBUTES来定义(base/ntos/ob/obp.h):
1 |
- bit 0:
OBJ_PROTECT_CLOSE,指示关闭该对象时是否产生一个审计事件。海哥说恒等于1。 - bit 1:
OBJ_INHERIT,表示是否可以被该进程创建的子进程继承。1:可被继承,0:不可被继承。可以在三个地方修改:- 在打开句柄时函数的OpenProcess(dwDesiredAccess,BInheritHandle,dwProcessId),中第二个参数的指定。
- 采用SetHandleInformation(tempHandle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)来增加权限。
- 直接修改句柄表项的内容。
- bit 2:
OBJ_AUDIT_OBJECT_CLOSE,已经被取缔为表示句柄表项的锁标志了,如果为1表示句柄表项被锁住了。海哥说恒等于0
2.4 内核句柄表ObpKernelHandleTable
system进程的句柄表可以用全局变量ObpKernelHandleTable来表示,也叫内核句柄表。ObpKernelHandleTable中的句柄只有在内核模式下才可以被引用(仅限于内核模式的函数可以引用),但可以位于任何一个进程环境中。所以,这些句柄实际上是跨进程环境的。(在Windows内核原理与实现3.4有提过)
内核句柄表并不是全局句柄表:
- 内核句柄表:
ObpKernelHandleTable - 全局句柄表:
PspCidTable
当我们的句柄为负值时,操作系统就会使用ObpKernelHandleTable这个表。
先来欣赏一下内核句柄表:
1
2
3
4
5
6
7
8
9kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 867b5830 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 06e40020 ObjectTable: e1000de0 HandleCount: 278.
Image: System
kd> dd ObpKernelHandleTable
80563f38 e1000de0 00000000 00000000 00000000
80563f48 00000000 00000000 00000000 00000000内核句柄表
ObjectTable = 0xe1000de0 = ObpKernelHandleTable。解析一下内核句柄表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kd> dt _HANDLE_TABLE 0xe1000de0
ntdll!_HANDLE_TABLE
+0x000 TableCode : 0xe1777001
+0x004 QuotaProcess : (null)
+0x008 UniqueProcessId : 0x00000004 Void
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0xe134da34 - 0x80565ba8 ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0n0
+0x030 FirstFree : 0xb00
+0x034 LastFree : 0
+0x038 NextHandleNeedingPool : 0x1000
+0x03c HandleCount : 0n278
+0x040 Flags : 0
+0x040 StrictFIFO : 0y0得到
TableCode = 0xe1777001,低两位为01,说明是二级句柄表。查看一下二层句柄表的内容(每项4字节,而不是8字节):
1
2
3kd> dd 0xe1777000
e1777000 e1004000 e1778000 00000000 00000000
e1777010 00000000 00000000 00000000 00000000里面是两张表,拿第一张来看看。
查看第一章表内容:
1
2
3
4
5
6kd> dq 0xe1004000
e1004000 fffffffe`00000000 001f0fff`867b5819
e1004010 00000000`867b4d91 000f003f`e1316151
e1004020 00000000`e100e429 0002001f`e133a479
e1004030 00020019`e1319161 00020019`e1318441
e1004040 00020019`e1315639 00020019`e132b791我尝试了一下,原来内核句柄表也像进程句柄表一样有
_OBJECT_HEADER,原因就是内核句柄表是进程system的句柄表。随便拿个
_HANDLE_TABLE_ENTRY项0xe1004020解析看看:1
2
3
4
5
6
7
8
9
10kd> dt _HANDLE_TABLE_ENTRY 0xe1004020
nt!_HANDLE_TABLE_ENTRY
+0x000 Object : 0xe100e429 Void
+0x000 ObAttributes : 0xe100e429
+0x000 InfoTable : 0xe100e429 _HANDLE_TABLE_ENTRY_INFO
+0x000 Value : 0xe100e429
+0x004 GrantedAccess : 0
+0x004 GrantedAccessIndex : 0
+0x006 CreatorBackTraceIndex : 0
+0x004 NextFreeTableEntry : 0n0这里解析
Object这个成员,对应_OBJECT_HREADER看看就知道是啥内核对象了。解析
_OBJECT_HREADER,将0xe100e429后三位清零:1
2
3
4
5
6
7
8
9
10
11
12
13
14kd> dt _OBJECT_HEADER 0xe100e428
nt!_OBJECT_HEADER
+0x000 PointerCount : 0n3
+0x004 HandleCount : 0n1
+0x004 NextToFree : 0x00000001 Void
+0x008 Type : 0x867ae680 _OBJECT_TYPE
+0x00c NameInfoOffset : 0x10 ''
+0x00d HandleInfoOffset : 0 ''
+0x00e QuotaInfoOffset : 0 ''
+0x00f Flags : 0 ''
+0x010 ObjectCreateInfo : 0x00000001 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x00000001 Void
+0x014 SecurityDescriptor : (null)
+0x018 Body : _QUAD这里关注的是成员:
Type。解析
Type:1
2
3
4
5
6
7
8
9
10
11
12
13
14kd> dt _OBJECT_TYPE 0x867ae680
ntdll!_OBJECT_TYPE
+0x000 Mutex : _ERESOURCE
+0x038 TypeList : _LIST_ENTRY [ 0x867ae6b8 - 0x867ae6b8 ]
+0x040 Name : _UNICODE_STRING "Key"
+0x048 DefaultObject : 0x80563ec0 Void //指本类对象默认使用的同步事件对象
+0x04c Index : 0x14
+0x050 TotalNumberOfObjects : 0x39e
+0x054 TotalNumberOfHandles : 0x39f
+0x058 HighWaterNumberOfObjects : 0x3af
+0x05c HighWaterNumberOfHandles : 0x3b0
+0x060 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x0ac Key : 0x2079654b
+0x0b0 ObjectLocks : [4] _ERESOURCE可以看到成员
Name = Key,内核对象属于键盘类。值得注意的是
Type.TypeInfo这个成员里面的最后几个成员,里面很多都是回调函数,对象管理器通过这些函数来管理这些对象,如果我们把函数地址挂到上面,当调用相应的内核函数的时候,就被我们的函数HOOK了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22kd> dt _OBJECT_TYPE_INITIALIZER
ntdll!_OBJECT_TYPE_INITIALIZER
+0x000 Length : Uint2B
+0x002 UseDefaultObject : UChar
+0x003 CaseInsensitive : UChar
+0x004 InvalidAttributes : Uint4B
+0x008 GenericMapping : _GENERIC_MAPPING
+0x018 ValidAccessMask : Uint4B
+0x01c SecurityRequired : UChar
+0x01d MaintainHandleCount : UChar
+0x01e MaintainTypeList : UChar
+0x020 PoolType : _POOL_TYPE
+0x024 DefaultPagedPoolCharge : Uint4B
+0x028 DefaultNonPagedPoolCharge : Uint4B
+0x02c DumpProcedure : Ptr32 void
+0x030 OpenProcedure : Ptr32 long
+0x034 CloseProcedure : Ptr32 void
+0x038 DeleteProcedure : Ptr32 void
+0x03c ParseProcedure : Ptr32 long
+0x040 SecurityProcedure : Ptr32 long
+0x044 QueryNameProcedure : Ptr32 long
+0x048 OkayToCloseProcedure : Ptr32 unsigned char
3 全局句柄表
3.1 全局句柄表PspCidTable
全局句柄表里:只有所有的进程、线程的句柄,没有其他内核对象。
每个进程都会可能有很多线程,在OS中所有的线程TID和所有进程的PID是不会重复的。
全局句柄表:PspCidTable 指向。
全局句柄表:
- 所有的进程和线程无论无论是否打开,都在这个表中。
- 每个进程和线程都有一个唯一的编号:PID和TID,这两个值其实就是全局句柄表中的索引。
ETHREAD有一个_CLIENT_ID字段包含了线程的唯一ID和线程对应的进程ID。- 全局句柄表中,4是第一个句柄索引值,它被分配给
System进程的PID(因为System进程是第一个通过PspCreateProcess函数创建的进程)。0是专门保留给空闲进程的,它并非通过ExCreateHandle函数而获得。
全局句柄表和进程句柄表的相同点与区别:
相同点:
TableCode低2位含义相同,都是一样的采用动态分层,最多支持三层(512*1024*1024项)。
不同点:
- 全局句柄表由全局变量
PspCidTable指向,只保存所有进程及线程句柄值。 - 内核句柄表(
system进程句柄表)由ObpKernelHandleTable指向。 - 全局句柄表里面的内核对象地址值为内核对象的
Body而非_OBJECT_HEADER。 - PspCidTable 中存放的对象是系统中所有的进程线程对象,其索引就是 PID/4 和 TID/4。
- PspCidTable 中存放的直接是对象体(EPROCESS 和 ETHREAD),而每个进程私有的句柄表则存放的是对象头(OBJECT_HEADER)。
- PspCidTable:是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来注意访问对象时要掩掉低三位,每个进程私有的句柄表是双链连接起来的,实际上
ZwQuerySystemlnformation枚举系统句柄时就是走的这条双链,隐藏进程的话,这条链也是要断掉的~~在 遍历进程活动链表(ActiveProcessLinks)、DKOM 隐藏进程时,还要把隐藏进程的句柄表从链表中摘去。
3.2 查看notepad.exe进程全局句柄
查看进程notepad.exe(PID = 2292)在全局句柄表的详细情况:
2292/4 = 0x23D。
第一级句柄表512 = 0x200项,说明notepad.exe进程的句柄在第二级表中,在第二级表的索引值 = 0x23D - 0x200 = 0x3D
使用全局变量
PspCidTable:1
2
3kd> dd PspCidTable
805649c0 e1002c58 00000002 00000000 00000000
805649d0 00000000 00000000 00000000 00000000查看
_HANDLE_TABLE:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kd> dt _HANDLE_TABLE 0xe1002c58
ntdll!_HANDLE_TABLE
+0x000 TableCode : 0xe1040001
+0x004 QuotaProcess : (null)
+0x008 UniqueProcessId : (null)
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0xe1002c74 - 0xe1002c74 ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0n0
+0x030 FirstFree : 0x69c
+0x034 LastFree : 0xf40
+0x038 NextHandleNeedingPool : 0x1000
+0x03c HandleCount : 0n431
+0x040 Flags : 1
+0x040 StrictFIFO : 0y1可以看到
TableCode = 0xe1040001,低两位为01,是二级句柄表。将
TableCode低位清零,查看句柄表(每项4字节,而不是8字节):1
2
3kd> dd 0xe1040000
e1040000 e1005000 e1041000 00000000 00000000
e1040010 00000000 00000000 00000000 00000000进程notepad.exe在第二张表
0xe1041000中。查看notepad.exe句柄值:
1
2
3kd> dq 0xe1041000+0x3D*8
e10411e8 00000000`860ab789 0000096c`00000000
e10411f8 00000b3c`00000000 00000000`8627eba9将内核对象低3位清零,验证一下是否正确:
1
2
3
4
5
6
7
8
9
10
11
12
13kd> dt _EPROCESS 0x860ab788
ntdll!_EPROCESS
...
+0x174 ImageFileName : [16] "notepad.exe"
...
或者使用_EPROCESS偏移0x1f4来查看 //_SE_AUDIT_PROCESS_CREATION_INFO
kd> dd 0x860ab788+0x1f4
860ab97c 863cb638 6de818ce 01d84aee 00010000
860ab98c 000004a2 000003a9 000000c9 00000032
...
kd> du 0x863cb638
863cb638 "hj뙀蘼\Device\HarddiskVolume1\WIND"
863cb678 "OWS\system32\notepad.exe"说明是正确的。
这里需要特别注意,如果使用第一级表查索引
0x23D是不对的,因为第一级表最大只有0x200项,错误如下:1
2
3kd> dq 0xe1005000+0x23D*8
e10061e8 ffffffff`ffffffff ffffffff`ffffffff
e10061f8 ffffffff`ffffffff ffffffff`ffffffff如果要看
_OBJECT_HEADER也很简单,减去0x18即可,在第一级表中随便拿个查看一下: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
33kd> dq 0xe1005000
e1005000 fffffffe`00000000 00000000`867b5831
e1005010 00000000`867b5449 00000000`867b4da9
...
kd> dt _OBJECT_HEADER 0x867b5448-0x18 //低3位先清零
nt!_OBJECT_HEADER
+0x000 PointerCount : 0n2
+0x004 HandleCount : 0n0
+0x004 NextToFree : (null)
+0x008 Type : 0x867b5ca0 _OBJECT_TYPE
+0x00c NameInfoOffset : 0 ''
+0x00d HandleInfoOffset : 0 ''
+0x00e QuotaInfoOffset : 0 ''
+0x00f Flags : 0x22 '"'
+0x010 ObjectCreateInfo : 0x80564960 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x80564960 Void
+0x014 SecurityDescriptor : 0xe10019a6 Void
+0x018 Body : _QUAD
...
kd> dt _OBJECT_TYPE 0x867b5ca0
ntdll!_OBJECT_TYPE
+0x000 Mutex : _ERESOURCE
+0x038 TypeList : _LIST_ENTRY [ 0x867b5cd8 - 0x867b5cd8 ]
+0x040 Name : _UNICODE_STRING "Thread"
+0x048 DefaultObject : (null)
+0x04c Index : 6
+0x050 TotalNumberOfObjects : 0x19c
+0x054 TotalNumberOfHandles : 0x2e3
+0x058 HighWaterNumberOfObjects : 0x1ca
+0x05c HighWaterNumberOfHandles : 0x325
+0x060 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x0ac Key : 0x65726854
+0x0b0 ObjectLocks : [4] _ERESOURCE可以得知是线程。
3.3 常用的查询函数
进程和线程的查询,主要是以下几个函数,按照给定的PID、TID或Handle值从PspCidTable从查找相应的进线程对象(grep --color=auto -rns "PsLookupProcessByProcessId" *):
| 函数 | IN 输入值 | OUT 输出值 | 源码路径 |
|---|---|---|---|
| NTSTATUS ObReferenceObjectByHandle( IN HANDLE Handle, IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL ); |
Handle | Object | base\ntos\ob\obref.c |
| PHANDLE_TABLE_ENTRY ExpLookupHandleTableEntry ( IN PHANDLE_TABLE HandleTable, IN EXHANDLE Handle ) |
base/ntos/ex/handle.c | ||
| NTSTATUS PsLookupProcessThreadByCid( IN PCLIENT_ID Cid, OUT PEPROCESS *Process OPTIONAL, OUT PETHREAD *Thread ); |
Cid | PEPROCESS、PETHREAD | base\ntos\ps\pscid.c |
| NTSTATUS PsLookupProcessByProcessId( IN HANDLE ProcessId, OUT PEPROCESS *Process ) |
PID | PEPROCESS | base/ntos/ps/pscid.c |
| NTSTATUS PsLookupThreadByThreadId( IN HANDLE ThreadId, OUT PETHREAD *Thread ) |
TID | PETHREAD | base/ntos/ps/pscid.c |
MmGetSystemRoutineAddress(“UNICODE函数名”):查找ntoskrnel中导出函数的地址,原因就是:
- 导出表中有的函数是无法直接使用的,需要使用函数指针转换地址来使用。
- 当IAT表被挂钩子了,如果在程序中使用API,是从IAT获取的地址。但是使用
MmGetSystemRoutineAddress的话,是从ntoskrnel去拿的地址。
检查内核API是否被挂钩子:重载一份内核,然后逐个API地址对比。(PCHunter)
3.4 练习:打印全局句柄表的所有内核对象类型
编写程序,通过全局句柄表PspCidTable,遍历所有进程(包括隐藏进程)。打印全局句柄表中内核对象的所有类型。
一、需要解决的问题:
1、如何通过找到全局句柄表?
2、如何判断是否是进程?
代码思路/步骤如下:
ntkrnlpa.exe的导出表里面没有全局变量PsdCidTable,网上有以下两种方式来查找PsdCidTable:使用
KPCR:在_KPCR.KdVersionBlock+0x80即为PsdCidTable。fs:[34] +0x80。1
2
3
4
5
6
7
8
9PHANDLE_TABLE PspCidTable;
__asm
{
mov eax,fs:[0x34]
mov eax,[eax+0x80]
mov eax,[eax]
mov PspCidTable,eax
}
DbgPrint("PspCidTable=0x%08X\n",PspCidTable);测试过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17kd> r fs
fs=00000030
kd> r gdtr
gdtr=8003f000
kd> dq 0x8003f000+6*8
8003f030 ffc093df`f0000001 0040f300`00000fff
kd> dt _KPCR 0xffdff000
nt!_KPCR
...
+0x034 KdVersionBlock : 0x8054e2b8 Void
...
kd> dd 0x8054e2b8+0x80
8054e338 805649c0 00000000 80566d70 00000000
kd> dd 0x805649c0
805649c0 e1002c58 00000002 00000000 00000000
kd> dd PspCidTable
805649c0 e1002c58 00000002 00000000 00000000
方法二:全局搜索一下看哪个API使用到变量
PspCidTable,然后直接使用。这种方法非常不稳定,也不能保证每次开机地址都一样。在IDA中快捷键X交叉看一下。
![3.png]()
1
2
3
4
5
6
7
8ULONG PspCidTable;
ULONG TableCode;
PspCidTable = **(PULONG*)((ULONG)PsLookupThreadByThreadId + 0x18 +0x2);
//push _PspCidTable
//FF 35 C0 C9 48 00
//push --> FF 35,PspCidTable --> 0x0048c9c0
TableCode = *(PULONG)PspCidTable;方法三:PspCidTable攻与防。
由于我的虚拟机里面的全局句柄表是二层的,所以三层的就不写了。关于全局句柄表的解析和查询可以参考函数ExpLookupHandleTableEntry源码。
代码如下:
1 |
|


