Sleep obfuscation
Encrypts the process’s executable image for the duration of a sleep, so a memory scan during the sleep sees RC4 ciphertext instead of the payload.
Metadata
Section titled “Metadata”| ATT&CK | T1027 (Obfuscated Files or Information) |
| Stability | Experimental |
| Category | Preparation |
| YAML key | sleep_obfuscation |
| Introduced in | phase 6 (ekko variant) |
What it does
Section titled “What it does”Encrypts the executable image region of the current process for the duration of a sleep, so a memory scanner that inspects the process during the sleep sees RC4 ciphertext instead of the payload bytes it would signature-match against. When the sleep ends, the image is decrypted and restored to its original protections, and the pipeline continues (the Execution technique then runs the payload normally).
This phase ships the ekko variant (timer queue + ROP chain + NtContinue),
which is the canonical, best-documented sleep-obfuscation technique. foliage
(APC-based) and cronos (waitable timers) are recognized by the parser but
rejected with a “not implemented yet” error; they arrive in later phases.
YAML parameters
Section titled “YAML parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
variant | string | no | ekko | Mechanism: ekko | foliage | cronos (only ekko implemented) |
sleep_ms | u64 | no | 5000 | Length of the obfuscated sleep, in milliseconds |
encrypt_regions | list | no | [payload] | Region classes: [payload] | [payload, stack] | all (only [payload] implemented) |
YAML example
Section titled “YAML example”runtime: - technique: sleep_obfuscation params: variant: ekko sleep_ms: 5000 encrypt_regions: [payload]How it works
Section titled “How it works”sequenceDiagram
participant M as Main thread
participant T as Timer thread
M->>M: VirtualQuery image regions
M->>M: flip image to PAGE_READWRITE
M->>T: queue timer chain
T->>T: RtlCaptureContext
T->>T: NtContinue -> encrypt (RC4)
T->>T: WaitForSingleObject(sleep_ms)
T->>T: NtContinue -> decrypt
T->>T: VirtualProtect restore
T->>M: SetEvent
1. Prepare the image and capture the context
Section titled “1. Prepare the image and capture the context”The ekko sequence (all on CreateTimerQueueTimer timers flagged
WT_EXECUTEINTIMERTHREAD, so every step runs on the timer thread outside the
encrypted image):
VirtualQuerywalksimage_base .. image_base + SizeOfImageand records every committed run with its original protection.- The image is flipped
PAGE_READWRITE(once, on the calling thread). - A timer runs
RtlCaptureContextto capture a timer-thread context. The capture point matters:RtlCaptureContextstoresRspjust past the return address the timer dispatch left on that stack. - The captured context is copied into one continuation context per step. Each
step sets
Ripto the target function, the argument registers (Rcx,Rdx,R8,R9) to its parameters, andRsp = captured.Rsp - 8, so when the target returns, itsretlands on the dispatch’s return address and the hijacked timer thread unwinds cleanly (the ROP chain is the sequence of timer callbacks, each entering throughNtContinue).
2. Run the timer-queue ROP chain
Section titled “2. Run the timer-queue ROP chain”- The chain, queued with staggered due times:
SystemFunction032(RC4, resolved fromadvapi32) encrypts the image in ≤ 64 KB chunks. Its data parameter is astruct ustringwhoseLengthis aDWORD(u32), so a single call could cover the whole image; the fragment splits it anyway as a per-call safety margin. The key is the first 16 bytes of the per-build XChaCha key.WaitForSingleObject(NtCurrentProcess(), sleep_ms)performs the real sleep on the hijacked thread.SystemFunction032decrypts the same chunks, scheduled to fire after the sleep (the original PoC let them fire during it; that is its best-known flaw).VirtualProtectrestores each run to its original protection.SetEventwakes the main thread.
- The main thread waits on the event the whole time; no code executes from the image while it is encrypted. The queue is deleted and the technique returns.
All API and DLL names (NtContinue, RtlCaptureContext, SystemFunction032,
the timer-queue family, …) are resolved at runtime and obfuscated through
StringsConfig, so none of them reaches the import table.
What it evades
Section titled “What it evades”- Memory scans during the sleep. An EDR that walks the process’s
executable regions while the implant sleeps finds RC4 ciphertext (and,
during the crypto steps,
RWpages) instead of payload signatures. The payload itself is only ever mapped/executed after the sleep completes.
What detects it
Section titled “What detects it”- Timer callbacks (ETW): a thread pool repeatedly entering
NtContinuefromCreateTimerQueueTimercallbacks is a well-known ekko signature. - ROP chain anomalies: call stacks that return into the timer dispatch
instead of a normal caller, and thread contexts whose
Rspdoes not match the executing function’s frame. NtContinueorigin: threads resuming at addresses likeVirtualProtect/SetEventstraight fromNtContinue.- RW during the crypto steps: the image pages flip
RX → RW(visible toVirtualQuery/NtQueryVirtualMemorywatchers) for the duration of the encryption and decryption passes. - RC4 key reuse across chunks: every ≤ 64 KB chunk restarts the RC4 keystream with the same key, so equal-size chunks share keystream bytes (a cryptanalytic weakness; the threat model here is signature scans, not offline cryptanalysis).
- The RC4 key sits in the heap in cleartext during the whole sleep.
Measurements
Section titled “Measurements”- See
docs/measurements.md. Static metrics barely move (the payload is already encrypted at rest in.rdata); the value is runtime: during the sleep the image region is ciphertext — verifiable with a debugger (VirtualQuery+ memory dump inside the sleep window).
Implementation notes
Section titled “Implementation notes”- Variant: ekko only. The module structure (variant enum + one fragment) is ready for foliage and cronos.
- Timing option C (demo): the technique performs one encrypted sleep of
sleep_msand returns; the payload then runs once, after the sleep. The operational form — encrypting during the payload’s own sleeps — requires hookingSleep/SleepExin the target, which is a future phase. - Region scope: this phase encrypts the whole image
(
image_base + SizeOfImage), the original Ekko region. Note that at Preparation time the decrypted payload buffer still lives on the heap (it is only mapped into executable regions by the Execution technique afterwards), so this MVP demonstrates the mechanism, not the operational region set; the VirtualQuery-based enumeration of the payload’s mapped RX/RWX regions is the follow-up. - CONTEXT alignment: windows-rs 0.58 exposes the x64
CONTEXTwith the correct field offsets but only 8-byte alignment; the fragment wraps it in#[repr(align(16))](same workaround as theGet/SetThreadContextfragments). No layout divergence beyond that was found. - Stable-Rust ROP: no
global_asm!is needed. The “chain” is the timer queue itself: each step is an independentNtContinuecontinuation context (a plain struct copy withRip/Rsp/argument registers patched), queued with staggered due times. Function pointers are transmuted to the callback ABI. SystemFunction032is dynamic: it resolves fromadvapi32at runtime; if resolution fails, the stub errors withCryptFunctionNotFound(a pure ChaCha20 fallback in the stub is possible but not needed — the crate is already linked).SystemFunction032takes astruct ustring, not aUNICODE_STRING: itsLength/MaximumLengthfields areDWORDs (u32). Modeling them as theUSHORTs of aUNICODE_STRINGtruncates the image and smears the twou16halves of each length into one garbageu32(this was the original bug).- The sleep is real: the delay step is
WaitForSingleObjecton a timer thread;std::thread::sleepcannot be used (it would block the calling thread, which must stay parked in the event wait).
References
Section titled “References”- Original Ekko technique and paper: C5pider, https://github.com/Cracked5pider/Ekko
- MDSec Nighthawk study that first documented the mechanism: Austin Hudson, https://suspicious.actor/2022/05/05/mdsec-nighthawk-study.html
- Rust port used as reference material (reimplemented, not copied): memN0ps, https://github.com/memN0ps/ekko-rs
- RustRedOps sleep-obfuscation material: https://github.com/joaoviictorti/RustRedOps