From 3c90c2b00e9637ef30f21069ba6c76ea436fcde1 Mon Sep 17 00:00:00 2001 From: ek0ms savi0r Date: Wed, 10 Jun 2026 21:18:14 +0000 Subject: [PATCH] Upload files to "/" --- LICENSE | 11 +- Makefile | 56 ++++++ README.md | 511 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 569 insertions(+), 9 deletions(-) create mode 100644 Makefile diff --git a/LICENSE b/LICENSE index 3dc2a0c..cc20702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,6 @@ -MIT License +This is free software. Do what thou wilt shall be the whole of the code. -Copyright (c) 2026 ek0mssavi0r +No warranty. No liability. The authors are not responsible for what +you build with this. If you use it for something stupid, that's on you. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Hail Eris. Hail the kernel. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7541392 --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +# ⛧ Hades Gate — Makefile +# +# Targets: +# all — Build library and all examples +# lib — Build hades_gate.o only +# simple — Build simple_use.exe +# injector — Build injector.exe +# clean — Remove build artifacts +# +# Requirements: +# x86_64-w64-mingw32-gcc (MinGW cross-compiler) +# or MSVC tools via 'make CC=cl' +# +# Cross-compile for Windows from Linux: +# apt install mingw-w64 +# make CC=x86_64-w64-mingw32-gcc + +CC ?= x86_64-w64-mingw32-gcc +CFLAGS ?= -Os -masm=intel -fno-asynchronous-unwind-tables +LDFLAGS ?= -nostdlib -lkernel32 -lntdll +RM ?= rm -f + +SRC_DIR = src +EXA_DIR = examples +BLD_DIR = build + +SRC = $(SRC_DIR)/hades_gate.c +OBJ = $(BLD_DIR)/hades_gate.o +LIB = $(BLD_DIR)/hades_gate.lib + +.PHONY: all lib simple injector clean dirs + +all: dirs lib simple injector + +dirs: + @mkdir -p $(BLD_DIR) + +# Library object +lib: dirs + $(CC) $(CFLAGS) -c $(SRC) -o $(OBJ) + +# Static library (MinGW .a format) +$(LIB): lib + $(AR) rcs $(LIB) $(OBJ) + +# Simple usage example +simple: dirs lib + $(CC) $(CFLAGS) $(SRC) $(EXA_DIR)/simple_use.c -o $(BLD_DIR)/simple_use.exe $(LDFLAGS) + +# Full injector example +injector: dirs lib + $(CC) $(CFLAGS) $(SRC) $(EXA_DIR)/injector.c -o $(BLD_DIR)/injector.exe $(LDFLAGS) + +# Clean everything +clean: + $(RM) -r $(BLD_DIR) diff --git a/README.md b/README.md index ab63da5..5d7e213 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,510 @@ -# Hades_Gate +# ⛧ Hades Gate - Direct syscall construction from first principles \ No newline at end of file +### Direct syscall construction from first principles + +> "The gate to the underworld is open. Walk through it without knocking." + +--- + +## Abstract + +**Hades Gate** is a technique for constructing direct syscall stubs at runtime by resolving native API (Nt\*) function addresses from ntdll.dll via PEB walking, extracting their syscall numbers from the unhooked portions of their stubs, and synthesizing clean syscall instructions that bypass all userland EDR/AV function hooks. + +It does not hardcode syscall numbers. It does not rely on a pre-computed table. It derives everything from the running system at runtime, meaning it works across Windows versions without modification. + +--- + +## Relationship to Jake Swiz (0xXyc) + +Hades Gate is a **targeted extension** of Jake Swiz's Windows shellcoding research. + +### His Trilogy (the foundation) + +| Pillar | What it covers | +|--------|---------------| +| **[Fukahi Na Tekio](https://github.com/0xXyc/fukahi-na-tekio)** | CALL/POP XOR encoder with LFSR, polymorphism, static AV/EDR signature evasion. Replaces the broken FPU-based shikata_ga_nai for ARM64/Prism. | +| **[Windows Shellcoding In-Depth](https://churchofmalware.org/articles/windows-shellcoding-in-depth_md)** | The definitive public treatment of self-sufficient Windows shellcode. PEB walking → find kernel32 → parse exports → resolve Win32 API functions from scratch. | +| **[ASLR & NX/DEP Bypass](https://churchofmalware.org/articles/aslr-bypass_md)** | Linux ROP chain tutorial: GOT leaking, ret2libc, pwntools automation. Different OS, same foundational mindset. | + +### How Hades Gate extends it + +Jake's Shellcoding In-Depth guide walks the PEB to find **kernel32.dll** and resolves `WinExec` / `MessageBoxA` / `CreateProcess` through the Win32 API layer. This works — but every Win32 API call goes through `kernel32 → kernelbase → ntdll`, and ntdll is hooked by every EDR on the market. + +Hades Gate asks: *what if we point the same PEB walker at **ntdll** instead of kernel32?* Instead of resolving a Win32 function, we resolve `NtAllocateVirtualMemory` internally, extract its syscall number from the stub, and build a `mov eax, SSN; syscall; ret` stub that goes straight to the kernel. The Win32 layer — and its hooks — are never executed. + +``` +Jake's path: PEB walk → kernel32.dll → export scan → WinExec → shellcode runs +Hades Gate: PEB walk → ntdll.dll → export scan → SSN → direct syscall → EDR blind +``` + +Same engine. Different destination. His trilogy gives you the locomotion; Hades Gate takes that locomotion one DLL further and changes the outcome from "my code runs" to "my code runs without the EDR watching." + +**This repository exists because Jake published the foundation publicly. It's an arm of his work, not a replacement for it.** + +### References to Jake's original work: +- [Fukahi Na Tekio](https://github.com/0xXyc/fukahi-na-tekio) — Encoder with AV/EDR signature evasion +- [Windows Shellcoding In-Depth](https://churchofmalware.org/articles/windows-shellcoding-in-depth_md) — PEB walking & WinAPI resolution fundamentals +- [ASLR & NX/DEP Bypass](https://churchofmalware.org/articles/aslr-bypass_md) — Linux ROP chain methodology +- [Swiz Security Protocol](https://protocol.swizsecurity.com) — Full research catalog +- [Church of Malware — Our Blessed Connection: The Shellphone Sermon](https://churchofmalware.org/articles/Our_Blessed_Connection_md) — The article that introduced Jake's work to the congregation + +--- + +## Table of Contents + +1. [The Problem](#1-the-problem) +2. [Background: The Syscall Layer](#2-background-the-syscall-layer) +3. [How EDRs Hook You](#3-how-edrs-hook-you) +4. [The Technique](#4-the-technique) + - [Step 1: PEB Walk → ntdll Base](#step-1-peb-walk--ntdll-base) + - [Step 2: PE Parse → Export Resolution](#step-2-pe-parse--export-resolution) + - [Step 3: SSN Extraction](#step-3-ssn-extraction) + - [Step 4: Stub Synthesis](#step-4-stub-synthesis) + - [Step 5: Integration](#step-5-integration) +5. [Usage](#5-usage) +6. [Where and Why to Use It](#6-where-and-why-to-use-it) +7. [Variants and Bypasses](#7-variants-and-bypasses) +8. [Limitations and Detection](#8-limitations-and-detection) +9. [References](#9-references) +10. [License](#10-license) + +--- + +## 1. The Problem + +When your code calls `VirtualAllocEx`, the call chain looks like this: + +``` +your code → kernel32.dll → kernelbase.dll → ntdll.dll → kernel (ring 0) +``` + +Every single EDR and consumer AV on the market **hooks the Windows API at the userland level** — usually inside ntdll.dll, sometimes also kernelbase. They overwrite the first 5-16 bytes of each function with a `jmp` that redirects through their monitoring engine. Your call goes: + +``` +your code → kernel32 → kernelbase → ntdll (HOOKED) → EDR engine → kernel +``` + +The EDR sees: +- **What** function you're calling (NtAllocateVirtualMemory, NtCreateThreadEx, NtOpenProcess, etc.) +- **What arguments** you're passing (target PID, memory size, protection flags, etc.) +- **What called it** (stack trace back to your code) + +Once they have that data, detection is trivial: `call NtAllocateVirtualMemory with PAGE_EXECUTE_READWRITE` from a non-Microsoft binary? Alert. From shellcode? Alert. From a process that just decrypted itself? Alert. + +This is why your payload gets burned before the first byte executes. + +--- + +## 2. Background: The Syscall Layer + +On Windows (x64), system calls work like this: + +**Your process (ring 3) → ntdll stub → syscall instruction → kernel (ring 0)** + +Every kernel service is exposed through ntdll as a small assembly stub. For example, `NtAllocateVirtualMemory` looks like this in a clean, unhooked ntdll: + +```asm +mov r10, rcx ; 4C 8B D1 ; syscall clobbers RCX, save to R10 +mov eax, 0x0018 ; B8 18 00 00 00 ; syscall number for this function +syscall ; 0F 05 ; trap to kernel +ret ; C3 ; return +``` + +That `mov eax, 0x0018` — the `0x0018` is the **System Service Number (SSN)**, also called the syscall number. Each Nt\* function has a unique SSN. The kernel uses this number to dispatch to the right handler. + +The SSN is the key. If we know the SSN and we emit the `syscall` instruction ourselves, we never need to call ntdll's stub. We can go directly from our code to the kernel. + +**Why this works:** There is exactly one kernel. You cannot hook the kernel from userland (well, you could, but that's a different conversation involving kernel callbacks, ETW, and PatchGuard). The syscall instruction is an atomic trap. If you issue it with the correct SSN and arguments, the kernel will service your request regardless of what the EDR did to ntdll. + +--- + +## 3. How EDRs Hook You + +There are three common hooking strategies, ordered from most to least common: + +### 3.1 Inline Hooking (most common) + +The EDR writes a 5-byte `jmp` or `call` at offset 0 of the ntdll stub, redirecting to a trampoline in the EDR's own DLL. + +```asm +; Before hook (clean): + 4C 8B D1 mov r10, rcx + B8 18 00 00 00 mov eax, 0x18 + 0F 05 syscall + C3 ret + +; After hook: + E9 XX XX XX XX jmp edr_trampoline ; 5 byte jmp + B8 18 00 00 00 mov eax, 0x18 ; these bytes are still here + 0F 05 syscall + C3 ret +``` + +Notice the layout carefully: + +``` +Original stub (11 bytes): + [0] 4C mov r10, rcx + [1] 8B + [2] D1 + [3] B8 mov eax, SSN ← immediate starts here + [4] 18 ← SSN = 0x18 + [5] 00 + [6] 00 + [7] 00 + [8] 0F syscall + [9] 05 + [10] C3 ret + +5-byte JMP hook (what a simple detour looks like): + [0] E9 jmp edr_trampoline + [1] XX + [2] XX + [3] XX + [4] XX ← 5-byte jmp overwrites [0] through [4] + [5] 00 ← SSN upper bytes survive here + [6] 00 + [7] 00 + [8] 0F syscall + [9] 05 + [10] C3 ret +``` + +A **pure 5-byte jmp** (`E9 XX XX XX XX`) does overwrite byte [4] — the low byte of the SSN. If this were the only hooking method, reading byte [4] would fail. + +**In practice, it doesn't matter because almost no modern EDR uses a pure 5-byte jmp.** They use longer hook sequences that leave byte [4] untouched: + +| Hook type | Size | Byte layout | Overwrites [4]? | +|-----------|------|-------------|----------------| +| `jmp [rip+offset]` | 6 bytes | `FF 25 XX XX XX XX` | ❌ No (only [0-5]) | +| `call [rip+offset]` | 6 bytes | `FF 15 XX XX XX XX` | ❌ No (only [0-5]) | +| `mov rax, imm; jmp rax` | 13 bytes | `48 B8 XX ... XX FF E0` | ❌ No (only [0-12]) | +| `jmp rel32` | 5 bytes | `E9 XX XX XX XX` | ✅ **Yes** | + +The `jmp [rip+offset]` (6-byte) and `call [rip+offset]` (6-byte) forms are by far the most common in modern EDRs — Defender for Endpoint, SentinelOne, Cortex XDR, Sophos Intercept X, and Carbon Black all use these. The 5-byte `jmp rel32` is largely legacy or toy detour implementations. + +**If you encounter a 5-byte hook** (visible when bytes [0-4] read as `E9 XX XX XX XX`), the SSN at [4] is gone. Fall back to: + +1. **Scavenge the SSN higher bytes** — a 5-byte jmp leaves bytes [5-7] intact. The SSN is in the low byte, but you can reconstruct it from the known SSN ranges per Windows build, or +2. **Clean ntdll map** (Section 7.1) — read the clean DLL from disk and extract SSNs from the real stubs, or +3. **Suspended process method** — create a process suspended before the EDR attaches, read SSNs from its clean ntdll + +For 95%+ of real-world deployments, byte [4] is clean. The code reads it and moves on. + +### 3.2 Hooking via Detours (Microsoft Detours style) + +The EDR saves the original bytes elsewhere, patches with a `jmp`, and provides a "trampoline" to call the original. This is the most polite approach and the easiest to bypass — just don't use the trampoline. + +### 3.3 Replacement (least common) + +The EDR replaces the entire function body with a `jmp` to a completely fake function. The original stub is nowhere in memory. This breaks Hades Gate (and Hell's Gate, and most other techniques). The solution is to map a clean copy of ntdll from disk. + +--- + +## 4. The Technique + +### Step 1: PEB Walk → ntdll Base + +Every Windows process has a **Process Environment Block (PEB)** accessible at a fixed offset from the GS segment register: + +``` +x64: GS:[0x60] → PEB +x86: FS:[0x30] → PEB +``` + +The PEB contains a pointer to `PEB_LDR_DATA` (at offset 0x18), which contains a linked list of loaded modules. We traverse this list to find ntdll.dll and get its base address. + +**Why not GetModuleHandle?** It's hooked. The PEB is never patched by EDRs because touching it would crash the process. + +**The structure:** + +``` +GS:[0x60] → PEB + +0x18 → PEB_LDR_DATA + +0x20 → InMemoryOrderModuleList (LIST_ENTRY) + Flink → .exe (first) + Flink → ntdll.dll (second) + Flink → kernel32.dll (third) +``` + +Each `LDR_DATA_TABLE_ENTRY` has: +- `+0x10`: DllBase +- `+0x40`: BaseDllName (UNICODE_STRING) + +We iterate until we find the entry whose `BaseDllName` matches "ntdll.dll" (case-insensitive comparison), and read `DllBase` from offset 0x10. + +### Step 2: PE Parse → Export Resolution + +With ntdll's base address, we parse the PE headers: + +``` +DOS_HEADER → e_lfanew → NT_HEADERS → OptionalHeader + → DataDirectory[EXPORT] + → ExportDirectory +``` + +The export directory gives us: + +- **AddressOfNames**: array of RVA pointers to function name strings +- **AddressOfNameOrdinals**: maps name index → ordinal +- **AddressOfFunctions**: maps ordinal → function RVA + +We hash the target function name with FNV-1a, iterate through `AddressOfNames` until we find the match, resolve the ordinal, and get the function RVA from `AddressOfFunctions`. Adding the function RVA to the ntdll base gives us the function address in memory. + +### Step 3: SSN Extraction + +With the function address, we inspect the first N bytes of the stub. In a clean ntdll, the pattern is: + +```hex +4C 8B D1 [00-02] mov r10, rcx +B8 XX XX XX XX [03-07] mov eax, SSN +0F 05 [08-09] syscall +C3 [0A] ret +``` + +Even in a hooked stub, bytes [5-7] (the upper 24 bits of the `mov eax` immediate) are almost never overwritten because they're past any jmp/call hook preamble. On modern Windows (10+), syscall numbers fit in one byte (0x00-0xFF), so reading byte [4] or [5] gives us the SSN. + +**Edge cases handled:** +- If the hook is exactly 5 bytes (overwriting [0-4]), the SSN at [4] is clobbered. We detect this by checking if bytes [3-7] form a valid `mov eax` — if not, the SSN is at [5] (the `B8` at [3] was clobbered but the immediate at [5-7] survived). +- If the function is an export but NOT a syscall stub (e.g., `RtlAllocateHeap`), there's no SSN to extract and we return 0. +- If the function has no recognizable stub at all (EDR replaced it entirely), SSN is 0 and we fail gracefully. + +### Step 4: Stub Synthesis + +With the SSN in hand, we allocate executable memory and write: + +```asm +mov r10, rcx ; 4C 8B D1 — syscall calling convention +mov eax, SSN ; B8 XX 00 00 00 — syscall number +syscall ; 0F 05 — trap to kernel +ret ; C3 — return +``` + +This stub **never touches ntdll**. It goes directly from our allocated executable page into ring 0. The EDR's hooks are still sitting in ntdll, unexecuted, wondering where everyone went. + +### Step 5: Integration + +Chain these stubs for a complete unhooked injection flow: + +```c +void* hNtOpenProcess = hg_syscall("NtOpenProcess"); +void* hNtAllocateVirtualMemory = hg_syscall("NtAllocateVirtualMemory"); +void* hNtWriteVirtualMemory = hg_syscall("NtWriteVirtualMemory"); +void* hNtProtectVirtualMemory = hg_syscall("NtProtectVirtualMemory"); +void* hNtCreateThreadEx = hg_syscall("NtCreateThreadEx"); +void* hNtClose = hg_syscall("NtClose"); +``` + +Cast each to its NTAPI prototype and call directly. The EDR sees nothing. + +--- + +## 5. Usage + +### 5.1 Build + +```bash +# With MinGW cross-compiler: +x86_64-w64-mingw32-gcc -Os -masm=intel -c src/hades_gate.c -o hades_gate.o +x86_64-w64-mingw32-gcc -Os -masm=intel hades_gate.o your_code.c -o payload.exe + +# With MSVC (cl.exe): +cl /c /O1 src/hades_gate.c +link hades_gate.obj your_code.obj /OUT:payload.exe +``` + +### 5.2 Basic usage + +```c +#include "src/hades_gate.h" + +int main(void) { + // One-shot: resolve and build a clean syscall stub + void* stub = hg_syscall("NtAllocateVirtualMemory"); + if (!stub) return 1; + + // Cast to the proper NTAPI prototype + typedef NTSTATUS (NTAPI* fnNtAllocateVirtualMemory)( + HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG); + + fnNtAllocateVirtualMemory pNtAllocateVirtualMemory = + (fnNtAllocateVirtualMemory)stub; + + // Use it — EDR never fires + PVOID addr = NULL; + SIZE_T size = 0x1000; + NTSTATUS status = pNtAllocateVirtualMemory( + (HANDLE)-1, &addr, 0, &size, + MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE); + + // addr = RWX memory. The EDR has no idea this happened. + return 0; +} +``` + +### 5.3 Manual two-step (if you need to inspect the result) + +```c +HG_RESOLVED r = hg_resolve("NtAllocateVirtualMemory"); +if (r.ssn == 0) { + // Either the function wasn't found or it's not a syscall stub + return 1; +} + +printf("NtAllocateVirtualMemory is at %p, SSN = 0x%02X\n", + r.address, r.ssn); + +void* stub = hg_build_stub(r.ssn); +if (!stub) return 1; + +// stub is ready to call +``` + +### 5.4 Complete unhooked injection chain + +See `examples/injector.c` for a full implementation that opens a target process, allocates memory, writes shellcode, and executes it — all via direct syscalls. No Win32 API calls involved. + +--- + +## 6. Where and Why to Use It + +### Use Hades Gate when: + +- **You're writing shellcode or position-independent code** that cannot rely on import tables, runtime linking, or CRT initialization. The PEB walker gives you everything you need from nothing. + +- **You're building a loader or injector** that needs to survive on modern Windows with EDR present. Direct syscalls are the baseline requirement for any payload that doesn't want to be detected at the API call level. + +- **You're writing C2 implants** that need to dynamically resolve APIs at runtime without static IAT entries. Hades Gate's PEB walking + export parsing gives you dynamic resolution without calling `GetProcAddress` (which is also hooked). + +- **You need cross-version compatibility.** Because Hades Gate derives syscall numbers at runtime, the same binary works on Windows 10 1507, Windows 11 24H2, and everything in between. No hardcoded offset tables to maintain. + +### Know your enemy — what lurks below userland + +Hades Gate bypasses userland API hooks. There are three layers below that it **does not touch**. Here's how to handle each: + +#### Layer 1: Kernel callbacks (ETW, PsSetCreateProcessNotifyRoutine, etc.) + +Most EDRs register kernel callbacks that fire after the syscall completes. Direct syscalls don't avoid these — they happen in ring 0 regardless of how you called the kernel. + +**What they see:** the syscall number, the arguments, the calling process. +**What they don't see:** the Win32 function name, the call stack through ntdll. + +**How to mitigate:** +- Batch allocations into fewer, larger calls (reduces event volume) +- Chain shellcode delivery through reflective DLL loading instead of per-API calls +- Use `NtSetInformationProcess` to disable ETW for your process before injection calls +- Time your calls with realistic delays between them (an injection that completes in 2ms is obvious) +- Spoof the calling thread's start address so the kernel callback sees a legitimate entry point + +#### Layer 2: Secure Kernel / VBS + +Virtualization-Based Security runs a hypervisor below the kernel. It can intercept every syscall at the VMExit level. There is no userland bypass for this. + +**If VBS is enabled, direct syscalls still work** — they just don't help you hide from the hypervisor. The EDR watching from VBS sees every syscall with full fidelity. + +**How to deal with it:** +- Hades Gate still gives you cross-version compat and avoids userland hooks. It's not useless under VBS — it's just not invisible. +- Combine with ETW disable and call-spoofing to reduce the signal your process emits at userland, making it harder to distinguish from legitimate behavior even when VBS is watching. +- If absolute invisibility is required under VBS, you need hardware-level techniques (Secure Kernel bypasses) that are outside the scope of any userland tool. + +#### #### Layer 3: Full stub replacement (CrowdStrike Falcon, some SentinelOne configs) + +These EDRs don't just hook the first bytes — they overwrite the entire syscall stub with a jmp to a completely fake function. The SSN at byte [4] is gone. + +**How Hades Gate handles this:** +- Call `hg_resolve()` first, then `hg_verify_stub()`. If the stub doesn't look like a syscall stub, fall back to `hg_map_clean_ntdll()` + `hg_resolve_at()` (Section 7.1). +- `hg_verify_stub()` checks for the presence of `0F 05 C3` (syscall; ret) within the first 16 bytes. If absent, the EDR has replaced the stub. +- `hg_map_clean_ntdll()` maps a fresh copy of ntdll.dll from disk, then `hg_resolve_at()` extracts SSNs from the real stubs. + +```c +HG_RESOLVED r = hg_resolve("NtAllocateVirtualMemory"); +if (!hg_verify_stub(r.address)) { + // Stub appears replaced — try clean ntdll from disk + uintptr_t clean_base = hg_map_clean_ntdll(); + r = hg_resolve_at("NtAllocateVirtualMemory", clean_base); +} +void* stub = hg_build_stub(r.ssn); +``` + +### Bottom line + +Hades Gate is a userland hook bypass. Nothing more, nothing less. If the EDR is watching from ring 0 or below, direct syscalls are still useful — they eliminate the most common detection vector (function hooking) — but they are not a complete stealth solution. Pair with ETW disable, call spoofing, and behavioral timing for a fuller picture. + +--- + +## 7. Variants and Bypasses + +### 7.1 Clean-mapped nTDLL + +When the EDR replaces entire stubs instead of hooking them, read ntdll.dll from disk and map it as a clean copy. Then resolve SSNs from the clean copy. + +``` +Steps: + 1. NtOpenFile("\\??\\C:\\Windows\\System32\\ntdll.dll") + 2. NtCreateSection(..., SEC_IMAGE, ...) + 3. NtMapViewOfSection(...) → maps a CLEAN copy into memory + 4. Run hg_resolve against this clean base instead of in-memory ntdll + 5. Use the resolved SSNs to build stubs +``` + +The in-memory ntdll may have fake stubs, but the on-disk copy is always clean. + +**Updated API usage:** + +```c +// Before: only resolves from in-memory ntdll +HG_RESOLVED r = hg_resolve("NtAllocateVirtualMemory"); + +// Now: verify the stub is real, fall back to clean copy +if (!hg_verify_stub(r.address)) { + // EDR replaced the stub — map from disk + uintptr_t clean = hg_map_clean_ntdll(); + r = hg_resolve_at("NtAllocateVirtualMemory", clean); +} + +void* stub = hg_build_stub(r.ssn); +``` + +### 7.2 Indirect Syscalls + +Some EDRs (Cybereason, modern Defender ATP) hook the `syscall` instruction itself by patching `ntdll!KiFastSystemCall`. To bypass: + +1. Scan any signed Microsoft DLL (kernel32.dll, user32.dll, etc.) for bytes `0F 05 C3` (syscall + ret) +2. Redirect your stub's `syscall` to that gadget instead of embedding it + +Your stub becomes: +```asm +mov r10, rcx +mov eax, SSN +jmp gadget_addr ; jumps to a clean syscall;ret +``` + +The EDR hooks the `syscall` in ntdll, not in kernel32, so the gadget is clean. + +### 7.3 Random Access SSN Extraction + +Some EDRs use variable-length hooks that clobber different offsets. Use multiple extraction strategies: + +```c +// Strategy 1 (Hell's Gate / Hades Gate): read B8 XX at [3] +// Strategy 2 (Tartarus Gate): scan for B8 anywhere in first 16 bytes +// Strategy 3: if stub starts with FF (call), the real stub is elsewhere +// Strategy 4: byte-by-byte scan for 0F 05 C3, read SSN from preceding bytes +``` + +Try each strategy in order until you get a valid (non-zero, reasonable) SSN. + +### 7.4 Hardware Breakpoint Tear-down + +A small but helpful trick: before calling your synthesized stub, clear all hardware debug registers (DR0-DR3). Some EDRs use hardware breakpoints to monitor specific syscalls. + +```c +__writegsqword(0x10, 0); // Clear DR0 +__writegsqword(0x18, 0); // Clear DR1 +``` + +--- + + +> ⛧ *Hades Gate - Church of Malware - ek0ms - MCMLXXXIV* ⛧