Hell’s Gate is a direct syscall technique on Windows that can bypass most EDR hooks at the Ring3 layer.
As early as 1997, there were techniques that programmatically resolved required function addresses by parsing the Export Address Table (EAT) of kernel32.dll, without relying on hardcoded function addresses.
Shortly after, Jackson (@jackson) released the SysWhispers Python script on GitHub. However, there was a problem — SysWhispers relied entirely on statically defined syscall numbers, which were heavily dependent on Mateusz Jurczyk’s (@j00ru) Windows X86-64 System Call Table.
Subsequently, a series of variants emerged with further improvements to the technique.
0x02 Recommended Projects
Here are several direct syscall projects, each with unique characteristics:
https://github.com/jthuraisamy/SysWhispers2: Unlike v1, non-hardcoded; resolves ntdll from PEB→LDR, sorts by API address, where the sorted position corresponds to the syscall number, reducing stub size
https://github.com/N4kedTurtle/HellsGatePoC: Parses ntdll.dll from disk to extract the specified API’s syscall stub for direct syscalls (avoids the in-process ntdll being replaced by EDR)
First, it’s essential to understand the concept of system calls in Windows:
In Windows, the process architecture is divided into two processor access modes — user mode and kernel mode. These two modes protect user applications from accessing and modifying critical system data. User applications (such as Chrome, Word, etc.) run in user mode, while system code (such as system services and device drivers) runs in kernel mode.
In kernel mode, the processor allows programs to access all system memory and all CPU instructions. Some x86 and x64 processors also use the term Ring Levels to distinguish between these two modes.
Processors that use Ring Level privilege modes define four privilege levels (Rings) to protect system code and data. The following diagram illustrates Ring Levels:
Windows only uses Ring 0 and Ring 3. Each program running at Ring 3 is assigned an independent virtual address space, isolated from one another, so one program cannot arbitrarily modify another program’s data. Ring 0 represents the kernel layer, where drivers and the Windows kernel operate, sharing a single memory space that stores a large number of internal data structures (such as handle tables). Processes, files, registry entries, threads, etc. can all be referred to as handles. To access these handles, one must first transition from user mode to kernel mode — this is where syscall instructions come in. In 64-bit mode, the syscall instruction is syscall; in 32-bit mode, it’s sysenter.
Here’s how a user-mode process specifically transitions to kernel mode. For example, when notepad.exe creates a text file, a user-to-kernel mode transition occurs:
When notepad.exe calls kernel32!CreateFileW for file creation, it internally jumps to ntdll!NtCreateFile, using ntdll to implement the transition between user and kernel mode. Let’s examine the implementation of ntdll!NtCreateFile:
This function is only 24 bytes long. 0x55 is the syscall number for NtCreateFile. After calling syscall to enter kernel mode, the corresponding kernel-mode function ntoskrnl!NtCreateFile is invoked based on the syscall number to complete the handle object access operation.
Therefore, based on ntdll’s implementation, the direct syscall approach can be simply summarized as:
Syscall numbers may change with each system update. You can look up the syscall numbers for specific systems in the Windows X86-64 System Call Table maintained by j00ru from Google Project Zero, or dynamically obtain syscall numbers by parsing the ntdll stored on disk or in loaded memory modules.
0x04 Technical Challenges
1. Syscall Numbers Differ Across Windows Versions
This can be resolved by directly parsing the export table of the on-disk ntdll.dll file, or by parsing the ntdll module loaded in memory via PEB→LDR:
By locating the function in the export table and disassembling it, we can determine the syscall number through the machine code:
2. How to Implement Direct Syscalls in 32-bit Programs
If running a 32-bit program on a 32-bit machine, it’s straightforward and basically the same as 64-bit. However, when implementing 32-bit compatibility on 64-bit systems, there’s an unavoidable issue: WOW64. For technical details and exploitation ideas regarding WOW64, refer to this presentation. POC project: https://github.com/aaaddress1/wowGrail.
There’s also a simpler method for quick 32-bit to 64-bit transition, see:
In short, on 64-bit systems there’s a call to fs:[0xC0] (wow64cpu!X86SwitchTo64BitMode), replacing the standard call to ntdll.KiFastSystemCall. Therefore, 64-bit syscalls can directly call this address: