简介
我们知道,当我们尝试在进程中创建、打开内核对象时,该内核对象的地址会作为句柄项存储在进程的句柄表中。
因此,我们可以通过遍历全局句柄表获取操作系统中的全部进程(如 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.exe
的 PID=700
,其只被系统进程 csrss.exe
所加载。而 csrss.exe
用于管理 Windows
图形相关任务,而被保护进程是一个图形化程序,因此这是正常的。
当我们使用 OllyDbg
附加 haha.exe
时,可以看到,即使 OllyDbg
实现了隐藏,但是我们依然可以发现它。