【火哥学习笔记】句柄表

局部句柄表

局部句柄表是在每个进程中存在的,因为一个进程难免需要用到另一个进程,或者其他非该进程下的线程,或者其他EVENT等结构。但是结构是存在内核空间中的,为了避免在用户层直接误操作修改了内核层的数据导致蓝屏,提出了使用句柄的方法。实际上是现在内核中找到对象,然后放在了进程空间的局部句柄表中,而句柄就是为了在表中找到某个句柄的索引,这个索引是4的倍数对齐的,并且句柄项的大小为8字节,所以最终的索引应该为base + pid/4*8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<process.h>
int main()
{
DWORD PID;
HWND calc = FindWindow(NULL,"计算器"); // 通过窗口名找到窗口进程的PID
GetWindowThreadProcessId(calc,&PID);
if(PID)
{
HANDLE pro = OpenProcess(PROCESS_ALL_ACCESS,false,PID); // 通过PID打开进程得到句柄
printf("%x\n",pro);
}
return 0;
}

然后找test进程地址

1
2
3
4
Failed to get VadRoot
PROCESS 81faa020 SessionId: 0 Cid: 0170 Peb: 7ffd8000 ParentCid: 03ec
DirBase: 02940300 ObjectTable: e1a57490 HandleCount: 94.
Image: test.exe

局部句柄表结构 在_EPROCESS结构的+0xc4偏移处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kd> dt _EPROCESS 81faa020
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER 0x01d7976b`6df590c6
+0x078 ExitTime : _LARGE_INTEGER 0x0
+0x080 RundownProtect : _EX_RUNDOWN_REF
+0x084 UniqueProcessId : 0x00000170 Void
+0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x805648b8 - 0x81ee6828 ]
+0x090 QuotaUsage : [3] 0x578
+0x09c QuotaPeak : [3] 0x578
+0x0a8 CommitCharge : 0x67
+0x0ac PeakVirtualSize : 0xdbc000
+0x0b0 VirtualSize : 0xdbc000
+0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0xf89cd014 - 0x81ee6854 ]
+0x0bc DebugPort : 0x81ed58d0 Void
+0x0c0 ExceptionPort : 0xe1636c78 Void
+0x0c4 ObjectTable : 0xe1a57490 _HANDLE_TABLE
+0x0c8 Token : _EX_FAST_REF

继续查看_HANDLE_TABLE结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kd> dt _HANDLE_TABLE 0xe1a57490
nt!_HANDLE_TABLE
+0x000 TableCode : 0xe11d5000 // 这里就是局部句柄表的表首地址了
+0x004 QuotaProcess : 0x81faa020 _EPROCESS
+0x008 UniqueProcessId : 0x00000170 Void
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0x80565ba8 - 0xe171e0fc ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0n0
+0x030 FirstFree : 0x7c8
+0x034 LastFree : 0
+0x038 NextHandleNeedingPool : 0x800
+0x03c HandleCount : 0n94
+0x040 Flags : 0
+0x040 StrictFIFO : 0y0

根据计算公式直接找到句柄所在位置

1
2
3
kd> dq 0xe11d5000 + 7cc / 4 * 8
e11d5f98 001f0fff`82238d89 000f01ff`81eecf61 // 句柄结构一共8字节,高4位是属性,低四位才是对象头地址
e11d5fa8 000f037f`81e1cb51 021f0003`82274f01

这个地址是对象头地址,后3位是标志位需要抹去,抹去之后+0x18才是对象实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd> dt _OBJECT_HEADER 82238d88
nt!_OBJECT_HEADER
+0x000 PointerCount : 0n16
+0x004 HandleCount : 0n2
+0x004 NextToFree : 0x00000002 Void
+0x008 Type : 0x82350e38 _OBJECT_TYPE
+0x00c NameInfoOffset : 0 ''
+0x00d HandleInfoOffset : 0 ''
+0x00e QuotaInfoOffset : 0 ''
+0x00f Flags : 0x20 ' '
+0x010 ObjectCreateInfo : 0x820f0318 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : 0x820f0318 Void
+0x014 SecurityDescriptor : 0xe17da42c Void
+0x018 Body : _QUAD // 这里是对象实体

然后_EPROCESS查看可以找到ImageFileName

1
2
3
4
5
6
7
kd> dt _EPROCESS 82238d88 + 18
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
///... 中间省略
+0x170 Session : 0xf89cd000 Void
+0x174 ImageFileName : [16] "calc.exe" // 成功

全局句柄表

这个表里面只有进程和线程的句柄,并且所有的进程和线程无论无论是否打开,只要没有被终止,都在这个表中。

在任务管理器中就有具体的呈现,每个进程和线程都有一个唯一的编号:PIDCID 这两个值其实就是全局句柄表中的索引。

全局句柄表在内核的一个PspCidTable变量中,是_HANDLE_TABLE结构的地址。

1
2
3
kd> dd PspCidTable
805649c0 e1003c58 00000002 00000000 00000000
805649d0 00000000 00000000 00000000 00000000

之后就可以找到全局句柄表的首地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kd> dt _HANDLE_TABLE e1003c58
nt!_HANDLE_TABLE
+0x000 TableCode : 0xe1005000
+0x004 QuotaProcess : (null)
+0x008 UniqueProcessId : (null)
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0xe1003c74 - 0xe1003c74 ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0n0
+0x030 FirstFree : 0x1d0
+0x034 LastFree : 0x240
+0x038 NextHandleNeedingPool : 0x800
+0x03c HandleCount : 0n265
+0x040 Flags : 1
+0x040 StrictFIFO : 0y1

这里的TableCode是有讲究的,后两位为0的时候只有一层,后两位为1的时候有两层,后两位为2的时候有三层

一层的话就应该装512个句柄(一个table是一个页4bk,一个句柄项8字节),两层的话就是1024*512个句柄,三层的话就是1024*1024*521个句柄。总结一下就是只有最后一层是句柄项,其他层的都是某张表的首地址。

这里的话是TableCode低两位为0,只有一层,直接找,先查看calc.exePID,这里是十进制的

图片.png

然后直接查就行了

1
2
3
4
5
6
7
8
9
10
11
12
kd> dq 0xe1005000 + 630*2
e1005c60 00000000`82238da1 00000000`81ed9ad9
e1005c70 00000000`81fd37c1 00000000`81f5b639
// 这里82238da1的后三位一样是属性,需要抹掉,但是和局部句柄表不一样的是 这个地址直接是对象实体的首地址
kd> dt _EPROCESS 82238da0
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
// ... 省略
+0x170 Session : 0xf89cd000 Void
+0x174 ImageFileName : [16] "calc.exe" // 成功

句柄拆分

1
2
3
4
5
6
7
31~21	20~11	10~2	1~0
A B C D
一个句柄分为这四个部分
D是用于判断层数的 并且真正的索引会把这两位右移出去
当为一层的时候 ABC一共30位组成index去表中索引(一项8字节)
当为两层的时候 AB一共21位组成index索引第一层(一项4字节) C一共9位组成index索引第二层(一项8字节)
当为三层的时候 A一共11位组成index索引第一层(一项4字节) B一共10位组成index索引第二层(一项4字节) C一共9位组成index索引第三层(一项8字节)

作业

内容:用进程句柄表实现反调试

要求:知道是哪个进程调试我,并且关闭这个调试进程

目前BUG:可能某个系统进程里面也打开了目标进程,强制关闭系统进程直接蓝屏 ,添加白名单的方式可以解决这个问题但是每次出现的系统进程不一样。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#include <ntddk.h>

BOOLEAN FLAG = TRUE;
HANDLE hThread;

BOOLEAN FindPidTable(ULONG Process, char* name)
{
int i;
ULONG ObjectTable = *(PULONG)(Process + 0xc4);
ULONG TableCode = *(PULONG)(ObjectTable + 0x0);
char white[] = "csrss.exe";
char white2[] = "explorer.exe";
if (!strncmp(white, (char*)(Process + 0x174), strlen(white)))
{
return 0;
}
if (!strncmp(white2, (char*)(Process + 0x174), strlen(white2)))
{
return 0;
}
for (i = 0; i < 512; i++)
{
//DbgPrint("case[%d]\n", i);
ULONG value = *(PULONG)(TableCode + i * 8);
if (value < 0x80000000) continue;
ULONG Pro = (value & 0xfffffffc) + 0x18; // 局部句柄表
if (!strncmp(name,(char*)(Pro + 0x174), strlen(name)))
{
return 1;
}
}
return 0;

}
BOOLEAN CloseProcess(ULONG Process)
{
/*
思路是枚举PID 然后再全局句柄表里面找,通过进程名对比
全局句柄表在KPCR里面有
找到PID之后就可以通过 ZwOpenProcess 得到句柄
*/
ULONG PspCidTable;
_asm
{
mov eax, fs: [0x34]
mov eax, [eax + 0x80]
mov eax, [eax]
mov eax, [eax] //两次取值直接拿首地址
mov PspCidTable, eax
}
DbgPrint("PspCidTable : %x\n", PspCidTable);
ULONG PID = 0;
for (int i = 0; i < 0x800; i += 4)
{
ULONG value = *(PULONG)(PspCidTable + i * 2);
if (value < 0x80000000) continue;
ULONG Pro = (value & 0xfffffffc); /// 全局句柄表 不用+0x18的偏移
if (!strncmp((char*)(Process + 0x174), (char*)(Pro + 0x174), strlen((char*)(Process + 0x174))))
{
DbgPrint("PID: %d\n", i);
DbgPrint("Find Process: %s\n", (char*)(Pro + 0x174));
PID = i;
break;
}
}
if (PID == 0)
{
DbgPrint("未找到对应进程 ....错误\n");
return FALSE;
}

HANDLE handle = NULL;
CLIENT_ID client_id;
client_id.UniqueProcess = (HANDLE)PID;
client_id.UniqueThread = (HANDLE)0;
OBJECT_ATTRIBUTES attr = { sizeof(OBJECT_ATTRIBUTES), 0, NULL, NULL };
attr.Attributes = 0;
NTSTATUS ret = NtOpenProcess(&handle, PROCESS_ALL_ACCESS, &attr, &client_id);
if (!NT_SUCCESS(ret))
{
DbgPrint("NtOpenProcess 打开进程失败\n");
return FALSE;
}
ZwTerminateProcess(handle, 0);
ZwClose(handle);

return TRUE;
}

VOID FindAllProcess(char* name)
{

ULONG curProcess;
__asm
{
mov eax, dword ptr fs : [0x124] ;
mov ecx, [eax + 0x44];
mov curProcess, ecx;
}
PLIST_ENTRY plistProcess = (PLIST_ENTRY)(curProcess + 0x88); // _EPROCESS
//DbgPrint("curProcess : %X\n", curProcess);

while (plistProcess->Flink != (PLIST_ENTRY)(curProcess + 0x88))
{

ULONG nextProcess = ((ULONG)(plistProcess)) - 0x88; // 每次通过链表得到的地址是下一个进程链表的地址
plistProcess = plistProcess->Flink;
//DbgPrint("%s\n", (char*)(nextProcess + 0x174));

if (FindPidTable(nextProcess, name))
{
DbgPrint("进程[%s]打开了[%s]的句柄\n", (char*)(nextProcess + 0x174), name);
DbgPrint("正在结束该进程...\n");
if (CloseProcess(nextProcess))
{
DbgPrint("进程已被结束...\n");
}
else
{
DbgPrint("关闭进程失败\n");
}
}
}
}

VOID ThreadFunc(PVOID StartContext)
{
DbgPrint("Create Thread Success\n");
LARGE_INTEGER timer = { 0 };
timer.QuadPart = - 10 * 1000 * 1000;

while (FLAG)
{
KeDelayExecutionThread(KernelMode, FALSE, &timer);
FindAllProcess("test.exe");
}
DbgPrint("Thread End End End\n");
ZwClose(hThread);
}

VOID DriverUnload(PDRIVER_OBJECT driver)
{
DbgPrint("卸载了\n");
FLAG = FALSE;

LARGE_INTEGER timer = { 0 };
timer.QuadPart = -30 * 1000 * 1000;
KeDelayExecutionThread(KernelMode, FALSE, &timer);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
driver->DriverUnload = DriverUnload;
DbgPrint("加载了\n");
PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, ThreadFunc, NULL);
return STATUS_SUCCESS;
}