Bootstrap

全局句柄表 —— 遍历全局句柄表

简介

进程句柄表是私有的,每个进程都有自己的进程句柄表。除此之外,系统还有一个全局句柄表,全局变量 PspCidTable 指向该表。

全局句柄表存储的是操作系统中所有创建的进程、线程的句柄(不会出现重复)。每个进程和线程都有一个唯一的编号:PIDCID , 这两个值其实就是全局句柄表中的索引。

也就是说,即使我们实现了对进程、线程的断链,但是只要操作系统中创建了进程、线程,全局句柄表中就会记录其对应的句柄(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
在这里插入图片描述

;