首页
社区
课程
招聘
[原创]X86内核笔记_5_句柄
2021-12-9 17:11 14139

[原创]X86内核笔记_5_句柄

2021-12-9 17:11
14139

0.前言

在实际对抗中,攻击者往往第一件事就是一个OpenProcess,然后执行注入、内存读写等操作。OpenProcess会返回一个叫做句柄的东西。而防守方通常对某些进程的句柄非常敏感,通过各种手段使OpenProcess失败或对返回的句柄进行处理,从而将恶意行为扼杀在摇篮里。本章通过学习句柄的一系列知识来体会下句柄防护在实际攻防对抗中的重要性。

1.句柄的获取

当调用OpenProcess时,会传入进程ID、权限等参数。此时系统会通过进程ID去全局句柄表中搜索进程对象,然后把找到的进程对象插入到调用OpenProcess的进程私有句柄表中,再将插入后的索引返回给调用者,这个索引就是句柄

 

线程的句柄获取方式与进程相同。

2.全局句柄表

全局句柄表中存储着当前系统所有进程及线程的对象信息。且全局句柄表中仅存储进程、线程信息。

全局句柄表宏观结构

进程ID、线程ID,每一个ID对应全局句柄表中一个成员,每个ID都是4的整数倍。但全局句柄表每个成员占8字节。宏观体现如下:(假设全局句柄表地址为0x12345670)

 

image-20211207154746596

 

当进程、线程数量过多时,一张表的512项显然不够,这时全局句柄表地址最后一位会变成1,代表额外增加一层,宏观体现如下:(全局句柄表地址0x12345671,实际地址为0x12345670,最后一位仅代表层级)

 

image-20211207155559537

 

当双层结构也不够存储时,又会增加一层。结构如下:

 

image-20211207160132932

实战查询进程对象

在虚拟机中随便打开个进程,查看该进程的PID,如dbgview:540(十进制)

 

image-20211207160403498

 

全局句柄表存在名为PspCidTable的全局变量中,windbg中查看该变量。

1
2
3
4
5
6
7
8
9
kd> dd PspCidTable
83f7bf34  8d404008 00000000 80000020 00000101
83f7bf44  80000320 80000024 00000000 00000000
83f7bf54  00000000 00000000 00000000 00000113
83f7bf64  00000000 00000000 83f2c35a 00000000
83f7bf74  00000000 00000000 00000000 00000008
83f7bf84  00000000 83f7bf88 83f7bf88 00000000
83f7bf94  00000000 00000000 00000000 00000000
83f7bfa4  00000000 807c8c38 807c4c38 00000000

句柄表实质是个名为_HANDLE_TABLE的结构,windbg中查看该结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kd> dt _HANDLE_TABLE 8d404008
ntdll!_HANDLE_TABLE
   +0x000 TableCode        : 0x9a8d6001
   +0x004 QuotaProcess     : (null)
   +0x008 UniqueProcessId  : (null)
   +0x00c HandleLock       : _EX_PUSH_LOCK
   +0x010 HandleTableList  : _LIST_ENTRY [ 0x8d404018 - 0x8d404018 ]
   +0x018 HandleContentionEvent : _EX_PUSH_LOCK
   +0x01c DebugInfo        : (null)
   +0x020 ExtraInfoPages   : 0n0
   +0x024 Flags            : 1
   +0x024 StrictFIFO       : 0y1
   +0x028 FirstFreeHandle  : 0xf98
   +0x02c LastFreeHandleEntry : 0x9a8d7808 _HANDLE_TABLE_ENTRY
   +0x030 HandleCount      : 0x255
   +0x034 NextHandleNeedingPool : 0x1000
   +0x038 HandleCountHighWatermark : 0x304

第一个成员TableCode值为0x9a8d6001,最后一位为1,说明有两层结构。第一层表中每个成员占4字节,每个成员都指向一张表。windbg查看第一层。

1
2
3
4
5
6
7
8
9
10
kd> dd 0x9a8d6000
ReadVirtual: 9a8d6000 not properly sign extended
9a8d6000  8d405000 9a8d7000 00000000 00000000
9a8d6010  00000000 00000000 00000000 00000000
9a8d6020  00000000 00000000 00000000 00000000
9a8d6030  00000000 00000000 00000000 00000000
9a8d6040  00000000 00000000 00000000 00000000
9a8d6050  00000000 00000000 00000000 00000000
9a8d6060  00000000 00000000 00000000 00000000
9a8d6070  00000000 00000000 00000000 00000000

上文dbgview进程ID为540,每个进程、线程ID都是4的倍数,因此540除以4等于135,索引从0开始,也就是第136个。每个表存储521个进程、线程信息,因此dbgview存在第一张表中。使用windbg命令查看第一张表索引为135的成员:

1
2
3
4
5
6
7
8
9
10
kd> dq 8d405000+8*0x87
ReadVirtual: 8d405438 not properly sign extended
8d405438  00000000`87fd0571 00000000`87ac9b31
8d405448  00000000`87ac9609 00000000`87ac7cc1
8d405458  00000000`87ad1971 00000000`87ad21c1
8d405468  00000000`87ad8bf9 00000000`87ad8281
8d405478  00000000`87ad98b1 00000000`87ad5031
8d405488  0000090c`00000000 00000000`877a7b01
8d405498  000003bc`00000000 000003f4`00000000
8d4054a8  00000af4`00000000 00000000`87b51031

找到的成员为0000000087fd0571,最后3个二进制位需要清空,就变成了0000000087fd0570,这个地址就是dbgview进程结构的地址:

索引为135的8字节成员同时也是一个名为 _HANDLE_TABLE_ENTRY 的结构,使用dt _HANDLE_TABLE_ENTRY 8d405000+8*0x87 也能得到进程结构体地址。这里直接看低4字节数据是因为全局句柄表中没有权限划分,因此高4字节必然为0。

 

image-20211207161847950

遍历全局句柄表

上文通过指定进程PID查询到了进程结构体,但全局句柄表中既有进程也有线程,反向查询时单凭一个结构体首地址无法得知这个结构体是进程的还是线程的。

Object_header

每个内核对象地址前面都有一个对象头结构,描述了这个对象的类型。对象头结构占0x18字节。

 

在windbg中查看全局句柄表中索引为1的对象头信息:

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
kd> dq 8d405000    //0~511句柄表
ReadVirtual: 8d405000 not properly sign extended
8d405000  fffffffe`00000000 00000000`865e8739
8d405010  00000000`865e8461 00000000`86617021
8d405020  00000000`8661f021 00000000`865e9a49
8d405030  00000000`86637d49 00000000`86637a71
8d405040  00000000`86633d49 00000000`86633a71
8d405050  00000000`8662fd49 00000000`8662fa71
8d405060  00000000`8661fd49 00000000`8661fa71
8d405070  00000000`8661bd49 00000000`8661ba71
 
kd> dt _OBJECT_HEADER 865e8738-0x18  //记得清空后三个二进制位
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n149
   +0x004 HandleCount      : 0n3
   +0x004 NextToFree       : 0x00000003 Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : 0x7 ''
   +0x00d TraceFlags       : 0 ''
   +0x00e InfoMask         : 0 ''
   +0x00f Flags            : 0x2 ''
   +0x010 ObjectCreateInfo : 0x83f6fcc0 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : 0x83f6fcc0 Void
   +0x014 SecurityDescriptor : 0x8d404d96 Void
   +0x018 Body             : _QUAD

其中TypeIndex成员指明了这个内核对象的类型,这个成员是内核对象类型表的索引值,内核对象类型表存储在全局变量ObTypeIndexTable中,每个成员占4个字节,每个成员都是一个名为_OBJECT_TYPE的结构体。使用windbg查看该内核对象具体类型信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kd> dd ObTypeIndexTable
83f7d900  00000000 bad0b0b0 865466a0 865465d8 
83f7d910  86546510 865e8ed0 865e8d90 865e8cc8    //上面索引是7,所以是这个值
83f7d920  865e8c00 865e8b38 865e8a70 865e83b8
83f7d930  8660f4d0 8660f408 8660f340 866379c8
83f7d940  86637900 86637838 866339c8 86633900
83f7d950  86633838 8662f9c8 8662f900 8662f838
83f7d960  8661f9c8 8661f900 8661f838 8661b9c8
83f7d970  8661b900 8661b838 865f78a0 8662b6e0
 
kd> dt _OBJECT_TYPE 865e8cc8
ntdll!_OBJECT_TYPE
   +0x000 TypeList         : _LIST_ENTRY [ 0x865e8cc8 - 0x865e8cc8 ]
   +0x008 Name             : _UNICODE_STRING "Process"   //可以得知全局句柄表中索引为1的元素存的是个进程结构体。
   +0x010 DefaultObject    : (null)
   +0x014 Index            : 0x7 ''
   +0x018 TotalNumberOfObjects : 0x2b
   +0x01c TotalNumberOfHandles : 0xf2
   +0x020 HighWaterNumberOfObjects : 0x31
   +0x024 HighWaterNumberOfHandles : 0x10d
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER
   +0x078 TypeLock         : _EX_PUSH_LOCK
   +0x07c Key              : 0x636f7250
   +0x080 CallbackList     : _LIST_ENTRY [ 0x865e8d48 - 0x865e8d48 ]

3.进程保护

思路:

  • 根据指定的PID在全局句柄表中查找该进程的 _HANDLE_TABLE_ENTRY 结构
  • 将 _HANDLE_TABLE_ENTRY 结构清空

效果:

 

无法打开目标进程。

4.私有句柄表

全局句柄表中仅存储所有进程和线程。类似互斥体、事件、信号量这些东西的句柄都存储在调用者进程自身的私有句柄表中。私有句柄表中还存储OpenProcess、OpenThread返回的句柄和从父进程继承的句柄。

 

使用windbg随便查看一个进程的EPROCESS结构,在偏移0xF4处有一个成员名为ObjectTable,其类型为 _HANDLE_TABLE 。与全局句柄表类型结构一致,查找方式也一致。这个句柄表就是该进程的私有句柄表。

 

image-20211209111243659

 

该句柄表结构

 

image-20211209111343484

 

该句柄表结构成员信息:

  • 进程打开的句柄很少,因为TableCode最后一位为0,只有一层表。
  • 私有句柄表中存储了该句柄表所属的进程,通过QuotaProcess与UniqueProcessId成员描述。
  • 句柄表成员中有锁结构,Windows在操作句柄表时会加锁防止冲突。
    • HandleLock锁用于修改私有句柄表结构(_HANDLE_TABLE)。
    • HandleContentionEvent锁用于修改私有句柄表中每个句柄结构(_HANDLE_TABLE_ENTRY)。
  • HandleTableList成员是一个双向链表,里面存储所有进程的私有句柄表。(此处断链会PG)
  • LastFreeHandleEntry:存储最后一次被释放的句柄。
  • HandleCount:该私有句柄表中共有多少个句柄。该数值不精确,可以随意更改。

查看句柄表内每个句柄:

1
2
3
4
5
6
7
8
9
10
kd> dq 0xab8c1000
ReadVirtual: ab8c1000 not properly sign extended
ab8c1000  fffffffe`00000000 00000003`9461f671
ab8c1010  00100020`87cb3a41 00100020`866e6259
ab8c1020  00000009`b20b0f01 001f0001`87cab941
ab8c1030  00000009`a9ab01b1 00020019`a8cbe019
ab8c1040  001f0003`880565a1 000f003f`b31ac699
ab8c1050  00020019`b31abd39 00100003`8870ee69
ab8c1060  00100003`869877e9 00000001`b31b2f11
ab8c1070  00000804`87ce0b29 021f0003`880b2e91

第一个句柄为-2,代表当前线程。在3环调用GetCurrentProcess会返回-1代表当前进程,GetCurrentThread返回-2代表当前线程。

 

继续查看第二个句柄结构:

1
2
3
4
5
6
7
8
9
10
kd> dt _HANDLE_TABLE_ENTRY ab8c1000+8
ntdll!_HANDLE_TABLE_ENTRY
   +0x000 Object           : 0x9461f671 Void
   +0x000 ObAttributes     : 0x9461f671
   +0x000 InfoTable        : 0x9461f671 _HANDLE_TABLE_ENTRY_INFO
   +0x000 Value            : 0x9461f671
   +0x004 GrantedAccess    : 3
   +0x004 GrantedAccessIndex : 3
   +0x006 CreatorBackTraceIndex : 0
   +0x004 NextFreeTableEntry : 3
  • Object:对应的内核结构体的ObjectHeader结构体(实际结构体地址大多数需要+0x18),最后3位为权限。
    • 在32位下,对象头结构为0x18大小。
    • 在64位下,大部分对象头结构为0x18大小,但部分对象如文件,对象头结构大小需要计算。
      • 计算方法:该内核结构对应的_OBJECT_TYPE结构下的DefaultObject成员,即为对象头结构大小。
    • 权限(低3个二进制位):
      • 最后一位:锁位。为0代表加锁了。如进程句柄将该位清0,当尝试结束进程,系统会等待锁被释放,导致无法结束进程,即使恢复为1也无法结束。
      • 倒数第二位:继承位。为1代表句柄可继承,为0代表不可继承。对应OpenProcess一类API的第二个参数。
      • 倒数第三位(现在已经被转移到从最高位开始第6位,从0开始):可否被关闭位。为1,句柄无法被关闭(CloseHandle)。
  • GrantedAccess:访问权限。在打开进程、文件、线程时,有一个参数为访问权限,这里对应的就是传入的权限。

5.进程防降权

进程保护思路

一些公司为了防止自身进程被操作,会遍历系统中所有进程的私有句柄表。如果发现某个进程的私有句柄表有自己的进程(判断进程名、进程ID、CR3、进程结构体地址等手段),就清空该私有句柄表的自身句柄的权限位(_HANDLE_TABLE_ENTRY->GrantedAccess)。这样导致该句柄不再具有读写等权限,从而保护自身进程。

进程提权(防降权)思路

由于我们的进程打开目标进程后,私有句柄表内的句柄权限被循环清空,导致无法读写目标进程的内存或只能查看很短的时间。因此我们需要进行句柄的替换。

  1. 构建EPROCESS结构。将目标进程的EPROCESS结构复制到我们构建的那块内存中。
  2. 将新构建的EPROCESS结构中的PID、进程名清空或修改。
  3. 将目标进程的第一级页表的内容复制到一块新内存中,并将新构建的EPROCESS结构的DirectoryTableBase改为新内存的物理地址,从而实现CR3的替换。
  4. 修改我们自身进程的私有句柄表中的目标进程的句柄结构的Object为新构建的EPROCESS结构。
  5. 此时我们再拿着新的句柄操作的进程与目标进程是同一个进程空间,但是指向的进程结构却是不同的。

需注意句柄结构中Object成员包含0x18大小的对象头部数据,在复制时要将目标进程的对象头结构一同复制。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞3
打赏
分享
最新回复 (6)
雪    币: 2390
活跃值: (9210)
能力值: ( LV13,RANK:385 )
在线值:
发帖
回帖
粉丝
TkBinary 5 2021-12-9 17:30
2
0
谢谢大佬分享.写的详细不错.
雪    币: 39
活跃值: (4172)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
ookkaa 2021-12-9 18:06
3
0
marik
雪    币: 14
活跃值: (948)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
妮可 2023-4-28 22:48
4
0
为啥我替换了对象类型会随机变化呢?cr3一起替换还会蓝屏
雪    币: 14
活跃值: (948)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
妮可 2023-4-29 01:24
5
0
妮可 为啥我替换了对象类型会随机变化呢?cr3一起替换还会蓝屏
知道了 TypeIndex是根据object加密的,单纯复制头部数据不行,要重新计算TypeIndex的值写入
雪    币: 2
活跃值: (392)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
饥愚 2023-12-25 17:52
6
0
妮可 知道了 TypeIndex是根据object加密的,单纯复制头部数据不行,要重新计算TypeIndex的值写入
我也这样伪造句柄表了。但是用CE附加的时候就会蓝屏,读写数据是正常的,兄弟你遇见这种情况没有
雪    币: 310
活跃值: (1917)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
niuzuoquan 2023-12-25 19:23
7
0
mark
游客
登录 | 注册 方可回帖
返回