Sleep obfuscation, from first principles

Ekko, Foliage, and timer-queue sleep obfuscation compared under the same memory scanner. The writeup is the thesis; the harness is proof.

Oct 12, 2026·16 min read·Research difficulty·Lab derived
Share
  • #Sleep Obfuscation
  • #Ekko
  • #Foliage
  • #Implants
  • #EDR Evasion

§Why sleep obfuscation exists at all

A beacon that sleeps 60 seconds between check-ins is invisible to a memory scanner for 59 of those seconds only if its code and data are not sitting in plaintext in the process address space. EDR memory scanners (Moneta, PE-sieve, YARA in-memory) walk every committed region looking for known implant signatures, RWX pages, and unbacked executable memory. If your implant's `.text` and config are in plaintext while sleeping, you lose.

Sleep obfuscation is the family of techniques that encrypt the implant in memory during sleep and restore it before the next callback. The three dominant approaches are Ekko, Foliage, and timer-queue-based (also called "WaitForSingleObject" / APC-based). This post implements all three in a single harness and measures them against the same scanner, so the comparison is honest rather than vibes.

§The shared building block: VirtualProtect + XOR

Every sleep-obfuscation scheme needs two primitives:

  1. 01Flip the implant's pages from RX to RW so they can be written.
  2. 02XOR (or RC4/AES) the bytes in place.
  3. 03Flip back to RX.
  4. 04Sleep.
  5. 05Reverse on wake.

The encryption key must not live in the same region as the payload, and the page-flip must happen through a legitimate API call so the EDR's kernel callback (PsSetLoadImageNotifyRoutine, ObRegisterCallbacks on thread creation) does not flag an anomalous transition.

§Ekko: timer + APC

Ekko (by Cracked5pider) uses a Windows timer queue (`CreateTimerQueueTimer`) to schedule an APC that runs the encrypt/sleep/decrypt sequence inside the implant's own thread. The trick is that the APC runs in the context of a thread the EDR already trusts (the beacon thread), so the page flips look like normal thread activity.

c
#include <windows.h>
#include <stdio.h>

static unsigned char g_key[16] = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
                                   0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50 };

void xor_region(void *base, size_t len) {
    unsigned char *p = (unsigned char *)base;
    for (size_t i = 0; i < len; i++) p[i] ^= g_key[i % 16];
}

void ekko_sleep(DWORD ms) {
    // 1. flip implant pages RW
    DWORD old;
    VirtualProtect(g_implant_base, g_implant_size, PAGE_READWRITE, &old);
    // 2. encrypt
    xor_region(g_implant_base, g_implant_size);
    // 3. sleep while encrypted
    Sleep(ms);
    // 4. decrypt
    xor_region(g_implant_base, g_implant_size);
    // 5. flip back RX
    VirtualProtect(g_implant_base, g_implant_size, PAGE_EXECUTE_READ, &old);
}

The full Ekko implementation queues the encrypt and decrypt as separate timer callbacks so the sleep happens in between, and uses `NtContinue` to resume execution at the original RIP. The harness in this repo (`harness/ekko.c`) implements the complete version.

§Foliage: waitable timer + APC

Foliage (by Austin Hudson) replaces the timer queue with a waitable timer (`CreateWaitableTimer`) and queues an APC via `QueueUserAPC`. The advantage is that the APC fires in the target thread's context without a separate timer thread, which reduces the number of threads EDR sees. The encrypt/decrypt happens in the APC callback; the main thread blocks on `WaitForSingleObject` on the timer.

c
void foliage_sleep(DWORD ms) {
    HANDLE timer = CreateWaitableTimer(NULL, TRUE, NULL);
    LARGE_INTEGER due;
    due.QuadPart = -( (LONGLONG)ms * 10000 );
    SetWaitableTimer(timer, &due, 0, encrypt_apc, NULL, FALSE);
    WaitForSingleObject(timer, INFINITE);
    // decrypt happens in a second APC queued on wake
    CloseHandle(timer);
}

§Timer-queue (the "classic" variant)

The simplest: create a timer queue, schedule encrypt → sleep → decrypt as three separate callbacks. Easiest to read, hardest to hide, because three callbacks in a timer queue thread is a recognizable pattern.

§Measuring against the scanner

I ran all three against PE-sieve (with `--imp` and YARA) and Moneta on a Windows 11 VM with Defender for Endpoint. Results:

The residual hit in Ekko/Foliage comes from the timer queue thread's own stack and the fact that the page transition (RW → RX) is still observable via ETW. The fix is to avoid the second VirtualProtect by using a single RWX page (which itself is a detection) — a genuine trade-off with no free lunch.

§Compile

bash
# MSVC
cl /O2 /GS- ekko.c /link /OUT:ekko.exe

# MinGW
x86_64-w64-mingw32-gcc -O2 -s ekko.c -o ekko.exe -lntdll

§Detection notes (for defenders)

  • ETW: `Microsoft-Windows-Kernel-Memory` events for `VirtualProtect` on a region whose base is unbacked.
  • Thread stacks containing `NtContinue` / `RtlUserThreadStart` with a timer callback.
  • Timer queue threads in processes that should not have them (e.g. a normal user app creating timer queues at high frequency).

§Takeaway

Sleep obfuscation is not invisibility. It is a tax on the scanner: you pay in complexity and thread anomalies, and you buy a reduction in static signature hits. Foliage is the cleanest of the three on thread profile; Ekko is the most documented. Pick based on your threat model, not on which blog post you read last. "

MJ

Written by

MJ — offensive security researcher

I break things in a lab so defenders can build better controls. Everything published here is lab-derived, responsibly disclosed, and documented with the detection path in mind.

Next dispatch

One long-form entry, roughly twice a month.

Root cause, reproduction, and the detection path — the same standard every time.