Drunkmars's Blog

基于全局句柄表发现隐藏进程

字数统计: 2.7k阅读时长: 12 min
2022/03/16

我们知道在0环进行PEB断链可以达到隐藏进程的效果,但是这只是作为权限维持的一种方法,如果要想完美的隐藏进程几乎是不可能的,本文就基于全局句柄表PsdCidTable,来找到隐藏进程的效果。

句柄表

什么是句柄?

当一个进程创建或者打开一个内核对象时,将获得一个句柄,通过这个句柄可以访问内核对象。

为什么要有句柄?

句柄存在的目的是为了避免在应用层直接修改内核对象。

我们假设一个场景,如果直接返回内核地址给应用层,我们可以在应用层随意修改内核地址,当我们修改的地址没有访问权限的时候,操作系统就会蓝屏,所以为了安全起见,只给应用层一个句柄,再通过这个句柄去找到真实的内核地址,就可以有效防止蓝屏的情况出现

句柄表项每个占8字节,一个页4KB,所以一个页能存储512个句柄表项,当进程中的句柄数量超过512,句柄表就会以分级形式存储,最多三级

句柄表的结构如下:

image-20220316100523106

我们编写一个程序,得到一些句柄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Handle_Table.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <tchar.h>

int _tmain(int argc, _TCHAR* argv[])
{
DWORD PID;
HANDLE hPro = NULL;
HWND hwnd = FindWindowA(NULL, "计算器");
GetWindowThreadProcessId(hwnd, &PID);

for (int i = 0; i < 600; 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;
}

首先说一下如何定位到句柄表,首先找到_EPROCESS的0x0c4偏移有一个_HANDLE_TABLE结构

image-20220316100747765

通过_HANDLE_TABLE结构的地址找到句柄表

image-20220316100816814

查看一下

image-20220316100829319

这里找最后一个对应地址

image-20220316100840995

用640/4得到190偏移,这里因为inter设置句柄表的储存是8个字节一组,所以这里需要*8

image-20220316100907603

我们得到句柄表里面的值为02000002`85dc0d8b,这里b拆分开为1011,将后3位清0可以得到85dc0d88

image-20220316101137251

这里因为每个链表之前都有一个OBJECT_HEADER结构,所以需要加上0x18才能定位到真正的链表

image-20220316101220991

通过句柄表后4字节的值即可定位到当前程序的EPROCESS

image-20220316101234632

特别留意 TableCode 的第2位,它表明了句柄表的结构,如果第2位是01,表示现在句柄表有两级, TableCode 指向的表存储了 4KB / 4 = 1024 个句柄表的地址,每个地址指向一个句柄表。

我们构造超过512个句柄,看看 TableCode 的低2位是否是01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Handle_Table2.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <tchar.h>

int _tmain(int argc, _TCHAR* argv[])
{
DWORD PID;
HANDLE hPro = NULL;
HWND hwnd = FindWindowA(NULL, "计算器");
GetWindowThreadProcessId(hwnd, &PID);

for (int i = 0; i < 600; 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;
}

image-20220316100929466

全局句柄表

全局变量 PspCidTable 存储了全局句柄表 _HANDLE_TABLE 的地址

全局句柄表存储了所有 EPROCESS ETHREAD 和进程的句柄表不同,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER

除此之外,和进程句柄表就没什么不同了,结构也是可以分为1、2、3级。

1464/4转十六进制得到16E

image-20220316101319177

这里 0xe1000cc0 低位是0,就只有一级

image-20220316101303811

得到当前进程

image-20220316101330600

遍历PsdCidTable

这里我们了解了原理之后就可以编写程序来遍历所有的进程,首先要解决的一个问题就是该如何找到全局句柄表,在查阅资料后发现,有三个函数调用了PsdCidTable

1
2
3
PsLookupProcessByProcessId()
PsLookupProcessThreadByCid()
PsLookupThreadByThreadId()

这里我们直接去看一下PsLookupProcessByProcessId的反汇编,可以看到有一个push PspCidTable地址的操作,那么这里我们就直接通过定位PsLookupProcessByProcessId加偏移的方法去定位PsdCidTable,这里因为系统的原因可能结构会有所不同,所以更完美的方法就是通过特征码去定位,这里我就使用偏移的方法定位

image-20220316101546218

通过计算偏移为26

image-20220316101720840

那么就可以定位到PspCidTable结构

1
2
PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26);
DbgPrint("PspCidTable = %x\n", PspCidTable);

定位到结构之后我们取出对应地址里面的值

1
2
TableCode = *(PULONG)PspCidTable;
DbgPrint("TableCode = %x\n", TableCode);

我们首先要判断是几级句柄表,就是通过最后一位是0、1、2来判断,那么这里就可以与0x03相与,然后消除标志位

1
2
TableLevel = TableCode & 0x03; // 句柄表等级
TableCode = TableCode & ~0x03; // 清除等级标志位

我们知道一级句柄表的范围是0-512,那么这里就可以写一个for循环来进行遍历操作

1
for (i = 0; i < 512; i++)

首先通过MmIsAddressValid判断一下地址是否可用,否则会发生蓝屏的风险,如果可用就继续遍历

1
if (MmIsAddressValid(TableLevel1[i].Object))

然后用RtlCompareUnicodeString进行判断,如果为EPROCESS结构则直接打印出进程名,如果为ETHREAD结构则打印出地址和进程名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RtlInitUnicodeString(&ProcessString, L"Process");
RtlInitUnicodeString(&ThreadString, L"Thread");

HandleAddr = ((ULONG)(TableLevel1[i].Object) & ~0x03);
pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);

if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0)
{
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);
}
else
{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr);
}

如果是二层句柄表则再加一个for循环即可

1
2
3
4
5
6
7
for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel2)[i]))
{
for (j = 0; j < 512; j++)
{
if (MmIsAddressValid(TableLevel2[i][j].Object))

三层句柄表的话使用三个for循环进行遍历

1
2
3
4
5
6
7
8
9
10
11
for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel3)[i]))
{
for (j = 0; j < 1024; j++)
{
if (MmIsAddressValid((PVOID)((PULONG*)TableLevel3)[i][j]))
{
for (k = 0; k < 512; k++)
{
if (MmIsAddressValid(TableLevel3[i][j][k].Object))

完整代码如下

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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#include <ntifs.h>

typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
UINT16 LoadCount;
UINT16 TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
ULONG CheckSum;
ULONG TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

typedef struct _HANDLE_TABLE_ENTRY {

//
// The pointer to the object overloaded with three ob attributes bits in
// the lower order and the high bit to denote locked or unlocked entries
//

union {

PVOID Object;

ULONG ObAttributes;

PHANDLE_TABLE_ENTRY_INFO InfoTable;

ULONG_PTR Value;
};

//
// This field either contains the granted access mask for the handle or an
// ob variation that also stores the same information. Or in the case of
// a free entry the field stores the index for the next free entry in the
// free list. This is like a FAT chain, and is used instead of pointers
// to make table duplication easier, because the entries can just be
// copied without needing to modify pointers.
//

union {

union {

ACCESS_MASK GrantedAccess;

struct {

USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};

LONG NextFreeTableEntry;
};

} HANDLE_TABLE_ENTRY, * PHANDLE_TABLE_ENTRY;

typedef struct _OBJECT_TYPE {
ERESOURCE Mutex;
LIST_ENTRY TypeList;
UNICODE_STRING Name;
PVOID DefaultObject;
ULONG Index;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
OBJECT_TYPE_INITIALIZER TypeInfo;
#ifdef POOL_TAGGING
ULONG Key;
#endif //POOL_TAGGING
ERESOURCE ObjectLocks[ OBJECT_LOCK_COUNT ];
} OBJECT_TYPE, * POBJECT_TYPE;

typedef struct _OBJECT_HEADER {
LONG PointerCount;
union {
LONG HandleCount;
PVOID NextToFree;
};
POBJECT_TYPE Type;
UCHAR NameInfoOffset;
UCHAR HandleInfoOffset;
UCHAR QuotaInfoOffset;
UCHAR Flags;
union {
POBJECT_CREATE_INFORMATION ObjectCreateInfo;
PVOID ObjectCreateInfo;
PVOID QuotaBlockCharged;
};

PSECURITY_DESCRIPTOR SecurityDescriptor;
QUAD Body;
} OBJECT_HEADER, * POBJECT_HEADER;

VOID DriverUnload(PDRIVER_OBJECT pDriver);
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING reg_path);

ULONG PspCidTable;

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING reg_path)
{
typedef HANDLE_TABLE_ENTRY* L1P;
typedef volatile L1P* L2P;
typedef volatile L2P* L3P;

int i, j, k;
ULONG TableCode, TableLevel;

L1P TableLevel1;
L2P TableLevel2;
L3P TableLevel3;

UNICODE_STRING ProcessString, ThreadString;
ULONG HandleAddr;
PEPROCESS pEprocess;
PCHAR ImageFileName;
POBJECT_HEADER pObjectHeader;


PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26); // 找到PspCidTable的地址
DbgPrint("PspCidTable = %x\n", PspCidTable);

TableCode = *(PULONG)PspCidTable;
DbgPrint("TableCode = %x\n", TableCode);

TableLevel = TableCode & 0x03; // 句柄表等级
TableCode = TableCode & ~0x03; // 清除等级标志位
DbgPrint("TableLevel = %x\n", TableLevel);
DbgPrint("New_TableCode = %x\n", TableCode);

RtlInitUnicodeString(&ProcessString, L"Process");
RtlInitUnicodeString(&ThreadString, L"Thread");

switch (TableLevel)
{
case 0:
{
DbgPrint("\n一级句柄表\n");
TableLevel1 = (L1P)TableCode;

for (i = 0; i < 512; i++)
{
if (MmIsAddressValid(TableLevel1[i].Object))
{
HandleAddr = ((ULONG)(TableLevel1[i].Object) & ~0x03);
pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);

if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0)
{
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);
}
else
{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr);
}
}
}
break;
}

case 1:
{
DbgPrint("\n二级句柄表\n");
TableLevel2 = (L2P)TableCode;

for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel2)[i]))
{
for (j = 0; j < 512; j++)
{
if (MmIsAddressValid(TableLevel2[i][j].Object))
{
HandleAddr = ((ULONG)(TableLevel2[i][j].Object) & ~0x03);
pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);

if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0)
{
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);
}
else
{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr);
}
}
}
}
}
break;
}

case 2:
{
DbgPrint("\n三级句柄表\n");
TableLevel3 = (L3P)TableCode;

for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel3)[i]))
{
for (j = 0; j < 1024; j++)
{
if (MmIsAddressValid((PVOID)((PULONG*)TableLevel3)[i][j]))
{
for (k = 0; k < 512; k++)
{
if (MmIsAddressValid(TableLevel3[i][j][k].Object))
{
HandleAddr = ((ULONG)(TableLevel3[i][j][k].Object) & ~0x03);
pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);

if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0)
{
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);
}
else
{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr);
}
}
}
}
}

}
}
break;
}

}

pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
DbgPrint("DriverUnload successfully!\n");
}

实现效果

首先安装驱动

image-20220316105344582

然后启动即可遍历全局句柄表

image-20220316105532085

这里我们可以看到notepad.exe这个进程

image-20220316110021797

这里为了看一下效果,使用PEB断链隐藏一下notepad进程

image-20220316105813025

启动驱动断链成功

image-20220316110106266

然后在任务管理器和cmd里面都看不到notepad.exe这个进程

image-20220316110135787

然后再启动遍历全局句柄表的驱动

image-20220316110207457

可以看到notepad.exe进程,那么这里就可以证明,系统并不是通过PEB找双向链表去定位到进程的,而是通过全局句柄表来寻找进程,也就是说我们通过PEB断链进行进程隐藏只能进行表面上的隐藏,要实现真正的隐藏就需要将某个进程从全局句柄表里面摘除,但是这里如果将进程从全局句柄表里面摘除就有可能发生不稳定的情况,这又是另外一个知识点了,这里就不拓展延伸了。

image-20220316110235182

CATALOG
  1. 1. 句柄表
  2. 2. 全局句柄表
  3. 3. 遍历PsdCidTable
  4. 4. 实现效果