Module 1: EDR Hooking & The Problem
Why calling NtAllocateVirtualMemory normally is like walking through a metal detector.
Why This Module?
Before understanding Hell's Gate (by am0nsec & RtlMateusz), you need to understand the problem it solves. Modern Endpoint Detection and Response (EDR) products hook critical Windows API functions in userland to inspect every sensitive operation your code performs. This module explains exactly how that hooking works, why it catches attackers, and why direct syscalls became necessary.
The Windows API Call Chain
When your program calls a Windows API function, the call travels through multiple layers before reaching the kernel. Understanding this chain is critical because EDRs insert themselves at a strategic point in the middle.
Normal API Call Flow
VirtualAlloc()VirtualAllocNtAllocateVirtualMemorysyscall
The Win32 API (kernel32.dll, kernelbase.dll) provides the documented, user-friendly interface. These functions perform parameter validation and then call down to ntdll.dll, the lowest user-mode layer. The Nt* functions in ntdll.dll contain tiny stubs that execute the syscall instruction to transition into the kernel. The kernel then dispatches the request to the appropriate kernel function via the System Service Descriptor Table (SSDT).
How EDR Hooking Works
EDR products inject a DLL into every process at startup (often via a kernel driver callback on process creation). This DLL modifies the first bytes of critical ntdll.dll functions in memory, replacing them with a JMP instruction that redirects execution into the EDR's own inspection code.
ASM; BEFORE hooking - clean NtAllocateVirtualMemory stub
NtAllocateVirtualMemory:
4c 8b d1 mov r10, rcx ; save 1st param
b8 18 00 00 00 mov eax, 0x18 ; SSN = 0x18 (Win10 1903)
0f 05 syscall ; transition to kernel
c3 ret
; AFTER EDR hooks - first bytes overwritten
NtAllocateVirtualMemory:
e9 xx xx xx xx jmp EDR_Hook_Func ; 5-byte relative JMP
b8 18 00 00 00 mov eax, 0x18 ; (partially clobbered)
0f 05 syscall
c3 ret
When your code calls NtAllocateVirtualMemory, instead of executing the syscall stub, execution jumps to the EDR's hook function. The EDR inspects the parameters (what memory are you allocating? what permissions? which process?), makes a decision (allow, block, or alert), and then either calls the original function or denies the request.
What EDRs Actually Inspect
EDR hooks are not just logging calls. They perform deep inspection: checking if the target process is a different process (injection), whether the memory permissions include RWX (shellcode staging), whether the caller is a known legitimate module, and correlating multiple calls into behavioral chains. A single NtAllocateVirtualMemory + NtWriteVirtualMemory + NtCreateThreadEx sequence in a remote process is a classic injection pattern that will immediately trigger an alert.
The Inline Hook Mechanism
The most common hooking technique used by EDRs is the inline hook (also called a trampoline hook). Here is how it works step by step:
| Step | Action | Detail |
|---|---|---|
| 1 | Save original bytes | EDR copies the first 5+ bytes of the target function to a trampoline buffer |
| 2 | Write JMP | EDR overwrites those bytes with JMP rel32 (E9 + 4-byte offset) pointing to its handler |
| 3 | Intercept call | When the function is called, execution jumps to the EDR handler |
| 4 | Inspect & decide | EDR analyzes parameters, context, and caller. Decides allow/block/alert |
| 5 | Call trampoline | If allowed, EDR calls the trampoline (saved original bytes + JMP back) to execute the real function |
Hooked API Call Flow
NtAllocateVirtualMemory()EDR Hook
(params, caller, behavior)
→ real syscall
Which Functions Get Hooked?
EDRs do not hook every function in ntdll.dll -- that would destroy performance. They focus on security-sensitive Nt* functions. Common targets include:
C// Memory operations (shellcode staging)
NtAllocateVirtualMemory // Allocate memory in a process
NtWriteVirtualMemory // Write data to process memory
NtProtectVirtualMemory // Change memory permissions (e.g., RW -> RX)
NtMapViewOfSection // Map shared memory (alternative injection)
// Thread/process creation (code execution)
NtCreateThreadEx // Create a new thread (remote thread injection)
NtQueueApcThread // Queue APC to a thread (APC injection)
NtResumeThread // Resume a suspended thread
NtCreateProcess/Ex // Create a new process
// Handle operations
NtOpenProcess // Open a handle to another process
NtDuplicateObject // Duplicate handles across processes
// File and registry (persistence, payload drop)
NtCreateFile // Create or open files
NtWriteFile // Write to files
The Key Insight
All EDR userland hooks exist in user-mode memory inside your own process. The EDR DLL and its JMP patches are in your address space. This means your code has full read access to the hooked bytes -- and theoretically, write access too. This is the fundamental weakness that Hell's Gate and other direct syscall techniques exploit: if you can figure out the System Service Number (SSN) yourself, you can issue the syscall instruction directly without ever touching the hooked ntdll stub.
Why Not Just Unhook ntdll?
One common evasion technique is to simply restore the original bytes of ntdll.dll functions by reading a clean copy from disk. While this works, it has several drawbacks:
| Approach | Pros | Cons |
|---|---|---|
| Read ntdll from disk | Full clean copy of all stubs | File I/O is logged; re-mapping ntdll is detectable via ETW or kernel callbacks |
Read ntdll from KnownDlls | Avoids disk I/O | Still requires NtOpenSection + NtMapViewOfSection which may be hooked |
| Overwrite hooked bytes | Simple implementation | EDR may monitor for writes to ntdll pages; periodic integrity checks catch this |
| Direct syscalls (Hell's Gate) | Never touches hooked code at all | Need to resolve SSN; syscall from non-ntdll region is detectable by kernel instrumentation |
Hell's Gate takes the approach of reading the SSN from the ntdll stub bytes (or neighboring stubs if hooked) and then executing the syscall instruction from its own assembly stub. The hooked function is never called at all -- the entire ntdll trampoline is bypassed.
The Hell's Gate Philosophy
Instead of fighting the hook (unhooking, re-mapping), Hell's Gate reads through the hook. The EDR overwrites the first bytes of the stub, but the SSN (in the mov eax, SSN instruction) is often still intact at an offset beyond the JMP patch. Even when the SSN itself is clobbered, neighboring clean stubs can be used to calculate the target SSN. Once you have the number, you invoke the syscall instruction yourself. The hook is irrelevant because you never execute through it.
Pop Quiz: EDR Hooking Fundamentals
Q1: Where in the API call chain do EDR inline hooks typically reside?
Q2: What is the primary advantage of Hell's Gate over ntdll unhooking?
Q3: An EDR hook replaces the first 5 bytes of NtCreateThreadEx with E9 xx xx xx xx. What instruction is this?