Files
Necropolis-C2/docs/evasion-chain.md
T
2026-07-07 04:39:43 +01:00

5.2 KiB

Evasion Chain

Architecture

Evasion lives in two layers:

  • Go binary — reflective PE loader, sleep obfuscation, cgocall bridge
  • Zig DLL — syscall dispatch, API resolution, AMSI/ETW/EDR evasion, module stomping

Loading Phase

Reflective DLL Loading

How the Go loader reads PE headers, allocates memory via VirtualAlloc, copies sections, applies relocations, resolves imports, sets section protections, resolves exports, calls DllMain and init_evasion. No file on disk, no LoadLibrary callback.

Initialization Phase

init_evasion → syscall.init_syscall

  • Walks PEB to find ntdll base
  • Scans ntdll for 0F 05 C3 (syscall;ret) gadgets, stores addresses in pool
  • Seeds PRNG
  • Caches exception directory for HAL's Gate fallback
  • Builds FreshyCalls table: walks ntdll export directory, collects Nt* exports, sorts by RVA, SSN = position. Immune to inline hooks. Reference: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/
  • Scans ntdll for 41 FF E2 (jmp r10) gadget — used as CFG-safe IC neutralizer

api.ensure()

  • Resolves ~100 Windows API function pointers by walking PEB → module export tables
  • kernel32, ntdll, advapi32, winhttp, bcrypt
  • Used by evasion functions for non-syscall operations (VirtualProtect, LoadLibrary, etc.)

Evasion Phase

stomp_evasion

  • Copies DLL .text into a signed Microsoft DLL (CryptoAPI/dwrite/msvcp_win)
  • Changes target protection to RWX, copies, restores to RX
  • Zeros original PE headers (DOS header + NT headers)
  • Memory scanners see legitimate signed DLL code at execution addresses
  • Base passed from Go (reflective DLL not in PEB module list)

patch_etw (three-tier)

  • Tier 1: NtTraceControl to stop EDR ETW providers (MDE, kernel-process, security-mitigations)
  • Tier 2: Hardware breakpoint on EtwEventWrite via VEH + debug registers
  • Tier 3: RET patch fallback — writes 0xC3 to EtwEventWrite's first byte
  • On Windows 10/11 where NtTraceControl is forwarded to api-ms-win DLLs, extract_ssn skips Method 1 (forward RVA guard) and falls through to HAL's Gate Method 2

patch_amsi

  • Loads amsi.dll, resolves AmsiScanBuffer address
  • Installs Vectored Exception Handler (VEH) as first-in-chain
  • Sets DR0 hardware breakpoint to AmsiScanBuffer address
  • Sets Dr7 to enable DR0 locally
  • SuspendThread → SetThreadContext → ResumeThread (proper thread context modification per MS docs)
  • On hit: VEH handler sets RAX=0 (AMSI_RESULT_CLEAN), sets RIP to return address, pops stack
  • No bytes modified in amsi.dll — invisible to EDR tamper detection

remove_edr_callbacks

  • Escalates SeDebugPrivilege (best-effort, silently skips if fails)
  • Calls NtSetInformationProcess with InfoClass=40 (ProcessInstrumentationCallback)
  • CFG-aware: on CFG-enabled systems (Win10 1709+), kernel refuses null callback pointer. Uses jmp r10 gadget address instead — IC fires but immediately returns, neutralizing it. Reference: https://cirosec.de/en/news/windows-instrumentation-callbacks-part-4/
  • On non-CFG systems: sets Callback=NULL, removing the EDR's instrumentation callback
  • Runs LAST in startup chain (the call itself is an IoC — but nothing follows it)
  • Defanged on Windows 11 23H2+ (restricted to kernel-mode callers)

Sleep Phase

EvasionSleep (Go-side)

  • Gated by dllLoaded flag (prevents protection before DLL fully initialized)
  • Pre-saved .text bounds from section scan during loading
  • VirtualProtect(DLL .text, PAGE_NOACCESS) — DLL code unreadable during idle
  • time.Sleep(duration) — Go runtime, not DLL code
  • VirtualProtect(restore original protection) — DLL executable again
  • DLL .text at different address than Go binary .text — Go code executes fine during protected period

Syscall Dispatch

FreshyCalls: SSN Extraction

  • Walks ntdll's export directory, collects all Nt* exports with real code addresses
  • Sorts by RVA ascending — SSN = position in sorted order
  • Immune to inline hooking: EDRs can patch stub bytes but cannot change linker RVA order
  • HAL's Gate fallback: binary search exception directory for forwarded exports not in ntdll's direct export table
  • Reference: https://0xdbgman.github.io/posts/edr-internals-research-and-bypass/

Indirect Syscall Execution

  • Random gadget from g_syscall_addrs pool (0F 05 C3 in ntdll)
  • hells_gate assembly: encrypts SSN, stores gadget address globally
  • hell_descent assembly: decrypts SSN into eax, sets up r10 (first arg), jumps to gadget
  • Gadget's syscall runs with eax=SSN — kernel processes it
  • Gadget's ret returns to hell_descent caller, unwinding through normal call chain
  • Never calls the hooked export stub — EDR sees a syscall from a random ntdll address

No Call Stack Spoofing

  • Go's cgocall requires clean stack returns
  • NtContinue-based ROP chains corrupt Go's goroutine scheduler
  • All dispatch is non-spoofed

Key Fixes Applied

  • PE struct offsets corrected (SizeOfImage at 56, not 36)
  • imageImportDescriptor corrected to 20 bytes
  • @intCast→ntstatus helper (usize to NTSTATUS via @bitCast)
  • extract_ssn forwarded export guard (prevents @intCast panic)
  • CONTEXT_DEBUG_REGISTERS = 0x100010 (was 0x10, missing AMD64 flag)
  • SuspendThread/ResumeThread before SetThreadContext
  • stomp_evasion(base) parameter from Go
  • dllLoaded flag guards EvasionSleep