简介
进程句柄表是私有的,每个进程都有自己的进程句柄表。除此之外,系统还有一个全局句柄表,全局变量 PspCidTable
指向该表。
全局句柄表存储的是操作系统中所有创建的进程、线程的句柄(不会出现重复)。每个进程和线程都有一个唯一的编号:PID
和 CID
, 这两个值其实就是全局句柄表中的索引。
也就是说,即使我们实现了对进程、线程的断链,但是只要操作系统中创建了进程、线程,全局句柄表中就会记录其对应的句柄(PID/CID),仍然可以通过全局句柄实现对线程、进程的遍历。
EnumPspCidTable
我们可以通过遍历全局句柄表,可实现对操作系统中的 进程/线程 的枚举遍历。
此处只给出驱动程序的部分关键函数代码,完整代码可参考
_EPROCESS断链 —— 实现进程内核隐藏
测试代码如下:
//******************
// 其他驱动代码略去
//*****************
// 遍历全局句柄表
NTSTATUS WkNtEnumPspCidTable()
{
// 获取全局句柄表的地址
PVOID PspCidTable = **(PVOID**)((ULONG)PsLookupProcessByProcessId + 26);
ULONG uHandleCount = *(PULONG)((ULONG)PspCidTable + 0x3c); // 句柄数量
DbgPrint("全局句柄数量: %u.\n", uHandleCount);
PINT64 TableCodep = *(PVOID*)PspCidTable;
// 判断全局句柄表的结构类型
UCHAR uTableType = (ULONG)TableCodep & 0x3;
TableCodep = (PUINT64)((ULONG)TableCodep & 0xfffffff8);
switch (uTableType)
{
ULONG index; // 句柄表项的有效索引
ULONG uOffset; // 句柄表项的偏移
case 0: // 单级结构
{
DbgPrint("全局句柄表采用的是单级结构.\n");
// 遍历全局句柄表
PVOID pHandleItmeAddr; // 句柄表项指向的 _OBJECT_HEADER.Body
POBJECT_HEADER pOBJECT_HEADER; // 句柄表项指向的 _OBJECT_HEADER
// 声明字符串
UNICODE_STRING unicode_Process;
UNICODE_STRING unicode_Thread;
// 初始化字符串
RtlInitUnicodeString(&unicode_Process, L"Process");
RtlInitUnicodeString(&unicode_Thread, L"Thread");
for (index = 0, uOffset = 0; index < uHandleCount; TableCodep++, uOffset++)
{
// 清除句柄项的属性, 获取地址
pHandleItmeAddr = *TableCodep & 0xfffffff8;
if (pHandleItmeAddr == 0x00)
{
continue;
}
//DbgPrint("内核对象的地址: %x index: %u\n", pHandleItmeAddr, index);
// 获取句柄的类型
pOBJECT_HEADER = (POBJECT_HEADER)((ULONG)pHandleItmeAddr - 0x18);
WkPOBJECT_TYPE pWkPOBJECT_TYPE = pOBJECT_HEADER->Type;
ULONG uHandleNumb = uOffset * 4;
__try {
// 该句柄是进程句柄
if (!RtlCompareUnicodeString(&pWkPOBJECT_TYPE->Name, &unicode_Process, TRUE))
{
DbgPrint("句柄类型: Process, 句柄号: %u.\n", uHandleNumb);
DbgPrint("进程名: %s, PID: %u 活动的线程数: %u.\n", (PUCHAR)pHandleItmeAddr + 0x174, *(PULONG)((PUCHAR)pHandleItmeAddr + 0x84), *(PULONG)((PUCHAR)pHandleItmeAddr + 0x1a0));
}
// 该句柄是线程句柄
if (!RtlCompareUnicodeString(&pWkPOBJECT_TYPE->Name, &unicode_Thread, TRUE))
{
DbgPrint("句柄类型: Thread, 句柄号: %u.\n", uHandleNumb);
PEPROCESS pEPROCESS = *(PVOID*)((ULONG)pHandleItmeAddr + 0x220);
DbgPrint("父进程名: %s, PID: %u, ThreadID: %u.\n", (PUCHAR)pEPROCESS + 0x174, *(PULONG)((PUCHAR)pHandleItmeAddr + 0x1ec), *(PULONG)((PUCHAR)pHandleItmeAddr + 0x1f0));
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("code wrong!\n");
}
index++;
}
break;
}
case 1: // 两级结构
{
DbgPrint("全局句柄表采用的是两级结构, 未处理!\n");
break;
}
case 2: // 三级结构
{
DbgPrint("全局句柄表采用的是三级结构, 未处理!\n");
break;
}
}
return STATUS_ABANDONED;
}
效果图:
可以看到,此时驱动程序可以正确的枚举操作系统中的每个线程和进程。
发现隐藏进程
此处,我以 haha.exe
为例,实现进程的隐藏,再遍历全局句柄表发现被隐藏的该进程。
此时可以看到,成功实现对 PID=1584
的进程的隐藏。
通过 EnumPspCidTable
的遍历,成功发现了 haha.exe
。