Module 3: System Service Numbers (SSN)
Every kernel function has a number. Know the number, open the gate.
The Central Concept
The System Service Number (SSN) is the integer that maps a user-mode Nt* function to its kernel-mode implementation. When you execute syscall with a specific value in EAX, the kernel dispatches to the corresponding function in the SSDT. Hell's Gate is fundamentally an SSN resolution engine -- everything else in the technique exists to discover and use these numbers at runtime.
What Is an SSN?
Each Nt* / Zw* function exported by ntdll.dll has a unique SSN. This number serves as an index into the kernel's System Service Descriptor Table (SSDT). The SSN is a simple unsigned integer, typically fitting in 12 bits (values range from 0 to roughly 470 on modern Windows builds).
The SSN for a function is embedded directly in the ntdll stub as an immediate value in the mov eax, <SSN> instruction:
ASM; NtAllocateVirtualMemory on Windows 10 21H2 (x64)
; SSN = 0x18 (24 decimal)
ntdll!NtAllocateVirtualMemory:
4c 8b d1 mov r10, rcx ; save 1st param
b8 18 00 00 00 mov eax, 0x18 ; SSN = 0x18
f6 04 25 08 03 fe
7f 01 test byte ptr [SharedUserData+0x308], 1
75 03 jnz +3 ; if hypervisor flag set (VBS/HVCI), use int 2e path
0f 05 syscall
c3 ret
cd 2e int 0x2e ; alternative path (VBS/HVCI)
c3 ret
SSNs Change Across Windows Versions
Microsoft does not guarantee stable SSNs. They change between major Windows versions, between builds, and even between service packs. A hardcoded SSN that works on Windows 10 1903 will produce wrong results (or BSOD) on Windows 10 21H2. This is precisely why Hell's Gate resolves SSNs dynamically at runtime rather than using a lookup table.
SSN Variation Across Versions
Here are some examples showing how SSNs differ across Windows builds. Note how the ordering is not always consistent -- Microsoft can insert new syscalls between existing ones, shifting everything above:
| Function | Win7 SP1 | Win10 1803 | Win10 1903 | Win10 21H2 | Win11 22H2 |
|---|---|---|---|---|---|
NtAllocateVirtualMemory | 0x15 | 0x18 | 0x18 | 0x18 | 0x18 |
NtWriteVirtualMemory | 0x37 | 0x3A | 0x3A | 0x3A | 0x3A |
NtCreateThreadEx | 0xA5 | 0xBB | 0xBD | 0xC1 | 0xC2 |
NtProtectVirtualMemory | 0x4D | 0x50 | 0x50 | 0x50 | 0x50 |
NtOpenProcess | 0x23 | 0x26 | 0x26 | 0x26 | 0x26 |
NtQueueApcThread | 0x44 | 0x47 | 0x47 | 0x47 | 0x47 |
Notice that some SSNs (like NtAllocateVirtualMemory at 0x18) remain stable across recent Windows 10 and 11 builds, while others (like NtCreateThreadEx) shift with each major version as new syscalls are added to the table.
The Nt* / Zw* Naming Convention
You will see two prefixes for system service functions: Nt and Zw. In user mode (ntdll.dll), they are identical -- both point to the same stub and the same SSN. The distinction matters only in kernel mode:
C// In ntdll.dll (user mode): Nt and Zw are the SAME function
// Both resolve to the same SSN and syscall stub
NtAllocateVirtualMemory == ZwAllocateVirtualMemory // same address
// In ntoskrnl.exe (kernel mode): they differ
// Nt* = direct call (no privilege check, assumes kernel caller)
// Zw* = sets PreviousMode = KernelMode, then dispatches through SSDT
// This is how kernel-mode code safely calls itself through the SSDT
For Hell's Gate, Use Nt* Names
When walking the ntdll Export Address Table to find syscall stubs, Hell's Gate searches for functions starting with "Zw" (because these are slightly easier to pattern-match as they all correspond to syscall stubs). However, since Nt* and Zw* are identical in user mode, either prefix works. The original Hell's Gate code uses Zw* when enumerating exports.
Static vs. Dynamic SSN Resolution
There are fundamentally two approaches to obtaining SSNs for direct syscalls:
| Approach | Description | Pros | Cons |
|---|---|---|---|
| Static (SysWhispers) | Compile-time table mapping function names to SSNs per OS version | Simple, no runtime parsing | Must ship tables for every Windows build; breaks on unknown versions; large static artifact |
| Dynamic (Hell's Gate) | Read SSNs from ntdll.dll stubs in memory at runtime | Works on any Windows version; no static tables; small footprint | Must handle hooked stubs; slightly more complex |
Hell's Gate pioneered the dynamic approach. At runtime, it reads the loaded ntdll.dll from the process's own memory, walks the Export Address Table to find Nt*/Zw* functions, and extracts the SSN directly from the mov eax, SSN instruction bytes in each stub. Since ntdll.dll is loaded into every Windows process, this information is always available.
SSN Ordering: The Alphabetical Property
A critical observation that Hell's Gate and its successors exploit: on every Windows version, the SSNs are assigned in alphabetical order of the Zw* function names. This means:
C// SSNs are assigned alphabetically by Zw* name:
// ZwAcceptConnectPort -> SSN 0x00
// ZwAccessCheck -> SSN 0x01
// ZwAccessCheckAndAuditAlarm -> SSN 0x02
// ...
// ZwAllocateVirtualMemory -> SSN 0x18
// ...
// ZwWriteVirtualMemory -> SSN 0x3A
// ...
// This means: if you know the SSN of one function,
// and you know the alphabetical order of Zw* names,
// you can CALCULATE any other SSN.
// Neighbor functions in the EAT (sorted by address) have SSNs
// that differ by exactly 1 from their alphabetical neighbors.
Why Alphabetical Order Matters
This property is what makes Halo's Gate and TartarusGate possible (Module 7). If a stub is hooked and its SSN is unreadable, you can look at an adjacent clean stub, read its SSN, and add or subtract a delta based on position. The alphabetical assignment guarantees that neighboring stubs in sorted order have sequential SSNs. But first, you need to understand how to read the SSN from a stub -- that is Module 4.
Pop Quiz: System Service Numbers
Q1: Why does Hell's Gate resolve SSNs dynamically rather than using a hardcoded table?
Q2: In user mode (ntdll.dll), what is the relationship between NtAllocateVirtualMemory and ZwAllocateVirtualMemory?
Q3: How are SSNs assigned to Nt*/Zw* functions on Windows?