Difficulty: Beginner

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:

FunctionWin7 SP1Win10 1803Win10 1903Win10 21H2Win11 22H2
NtAllocateVirtualMemory0x150x180x180x180x18
NtWriteVirtualMemory0x370x3A0x3A0x3A0x3A
NtCreateThreadEx0xA50xBB0xBD0xC10xC2
NtProtectVirtualMemory0x4D0x500x500x500x50
NtOpenProcess0x230x260x260x260x26
NtQueueApcThread0x440x470x470x470x47

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:

ApproachDescriptionProsCons
Static (SysWhispers)Compile-time table mapping function names to SSNs per OS versionSimple, no runtime parsingMust 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 runtimeWorks on any Windows version; no static tables; small footprintMust 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?

Microsoft does not guarantee stable SSNs. They change between Windows versions and builds. A dynamically resolved SSN works on any version because it reads the actual value from the loaded ntdll.dll.

Q2: In user mode (ntdll.dll), what is the relationship between NtAllocateVirtualMemory and ZwAllocateVirtualMemory?

In ntdll.dll (user mode), Nt* and Zw* prefixed functions are the same function at the same address. The distinction only matters in kernel mode, where Zw* functions set PreviousMode to KernelMode before dispatching through the SSDT.

Q3: How are SSNs assigned to Nt*/Zw* functions on Windows?

SSNs are assigned in alphabetical order of the Zw* names. This consistent ordering is what allows neighbor-based SSN calculation techniques like Halo's Gate to work.