句柄表 ——遍历句柄表实现反调试

简介

我们知道,当我们尝试在进程中创建、打开内核对象时,该内核对象的地址会作为句柄项存储在进程的句柄表中。

因此,我们可以通过遍历全局句柄表获取操作系统中的全部进程(如 OllyDbg 就实现了进程隐藏,在任务管理器中是无法看到该进程的),然后再遍历每个进程的私有句柄表,检测其私有句柄表中是否存储着被保护进程的内核对象地址 _EPROCESS,就可以得知被保护进程是否被其他进程附加,以达到反调试的目的。

当然通过不断将被保护进程的 _EPROCESS.DebugPort 清零,就可以强制实现进程无法被附加调试。

可参考这篇文章:
清空 _EPROCESS 结构体中的 DebugPort ,实现进程无法被附加调试

Code

此处只给出驱动程序的部分关键函数代码,完整代码可参考
_EPROCESS断链 —— 实现进程内核隐藏


关键代码如下:

NTSTATUS WkNtAntiDebug(ULONG uPid)
{
	//__asm {
	//	pushad
	//	pushfd
	//	int 3
	//	popfd
	//	popad
	//}
	BOOLEAN bIsFound = FALSE;

	// 被保护进程的 _EPROCESS 地址
	PVOID pProtectedProcessStructAddr = 0;
	BOOLEAN bIsEnumProcessHandleTable = FALSE;	// 不进行进程私有句柄表的遍历

	// 获取全局句柄表的地址
	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

		// 进程的私有句柄表
		PVOID pHANDLE_TABLE;
		PVOID pHandleItmeAddrOfProcess;	// 句柄表项, 指向 _OBJECT_HEADER
		ULONG uHandleCountOfProcess;	// 私有句柄表的句柄数量
		PINT64 pTableCodeOfProcess;	// 进程的私有句柄表地址
		ULONG uIndex;	// 进程的私有句柄表中的句柄索引

		// 声明字符串
		UNICODE_STRING unicode_Process;
		//UNICODE_STRING unicode_Thread;

		// 初始化字符串
		RtlInitUnicodeString(&unicode_Process, L"Process");
		//RtlInitUnicodeString(&unicode_Thread, L"Thread");

		DbgPrint("正在进程反调试检测.\n");
	EnumProcessHandleTable:	// 开始遍历进程私有句柄表
		for (index = 0, uOffset = 0; index < uHandleCount; TableCodep++, uOffset++)
		{
			// 清除句柄项的属性, 获取地址
			pHandleItmeAddr = *TableCodep & 0xfffffff8;
			if (pHandleItmeAddr == 0x00)
			{
				continue;
			}

			// 获取句柄的类型
			pOBJECT_HEADER = (POBJECT_HEADER)((ULONG)pHandleItmeAddr - 0x18);
			WkPOBJECT_TYPE pWkPOBJECT_TYPE = pOBJECT_HEADER->Type;
			ULONG uHandleNumb = uOffset * 4;


			// 该句柄是进程句柄
			if (!RtlCompareUnicodeString(&pWkPOBJECT_TYPE->Name, &unicode_Process, TRUE))
			{
				if (!bIsEnumProcessHandleTable)	// 不进行遍历进程私有句柄表
				{
					if (uHandleNumb == uPid)
					{
						DbgPrint("被保护的进程名: %s.\n", (PUCHAR)pHandleItmeAddr + 0x174);
						pProtectedProcessStructAddr = pOBJECT_HEADER;
						TableCodep = (PINT64)(*(PULONG)PspCidTable & 0xfffffff8);
						bIsEnumProcessHandleTable = !bIsEnumProcessHandleTable;
						goto EnumProcessHandleTable;
					}

					else
					{
						index++;
						continue;
					}
				}

				pHANDLE_TABLE = *(PULONG*)((ULONG)pHandleItmeAddr + 0xc4);
				pTableCodeOfProcess = *(PINT64*)pHANDLE_TABLE;
				uHandleCountOfProcess = *(PULONG)((ULONG)pHANDLE_TABLE + 0x3c);

				// 遍历进程的私有句柄表
				uIndex = 0;
				do
				{
					// 清除句柄项的属性, 获取地址
					pHandleItmeAddrOfProcess = *pTableCodeOfProcess & 0xfffffff8;
					pTableCodeOfProcess++;
					if (pHandleItmeAddrOfProcess == 0x00)
					{
						continue;
					}

					if (pHandleItmeAddrOfProcess == pProtectedProcessStructAddr)
					{
						DbgPrint("调试进程: %s.\n", (PUCHAR)pHandleItmeAddr + 0x174);
						DbgPrint("调试进程PID: %u 活动的线程数: %u.\n", *(PULONG)((ULONG)pHandleItmeAddr + 0x84), *(PULONG)((ULONG)pHandleItmeAddr + 0x1a0));
						bIsFound = TRUE;
						break;
					}

					uIndex++;
				} while (uIndex < uHandleCountOfProcess);
			}
			
			index++;
		}

		DbgPrint("反调试检测结束.\n");
		if (bIsFound)
			return STATUS_SUCCESS;
		else
			break;
	}

	case 1:	// 两级结构
	{
		DbgPrint("全局句柄表采用的是两级结构, 未处理!\n");
		break;
	}

	case 2:	// 三级结构
	{
		DbgPrint("全局句柄表采用的是三级结构, 未处理!\n");
		break;
	}
	}

	return STATUS_ABANDONED;
}

可以看到,被保护的进程 haha.exePID=700 ,其只被系统进程 csrss.exe 所加载。而 csrss.exe 用于管理 Windows 图形相关任务,而被保护进程是一个图形化程序,因此这是正常的。
句柄表 ——遍历句柄表实现反调试_第1张图片

当我们使用 OllyDbg 附加 haha.exe 时,可以看到,即使 OllyDbg 实现了隐藏,但是我们依然可以发现它。
句柄表 ——遍历句柄表实现反调试_第2张图片

你可能感兴趣的:(Windows内核,安全,内核,底层应用开发,windows)