
How three generations of call stack spoofers defeat the x64 unwinder, and where the detection still holds.
The previous post covered call stack spoofing: how an implant makes its call stack look legitimate at the moment an EDR inspects it. But that solves only half the problem. A memory scanner does not wait for the implant to call an API. It walks every process’s address space on a schedule, looking for executable code in regions that have no backing image on disk. If your beacon sleeps for sixty seconds between callbacks, a scanner running once per minute catches it in plaintext almost every time.
Call stack spoofing hides the who. Sleep obfuscation hides the what. I went into the timer-queue and APC machinery to understand how modern sleep obfuscation works at the primitive level, how three generations of the technique have evolved, and where the detection still holds. The short version: encrypting the beacon during sleep defeats content-based scanning, but the artifacts of the encryption itself (timer objects, permission flips, entropy anomalies) are durable detection surfaces that no amount of XOR can erase.
An EDR’s memory scanner enumerates the virtual memory regions of every process and inspects them for indicators of injected code. The scan has layers.
First, the scanner calls VirtualQueryEx in a loop, walking from address zero to the top of user-mode address space. For each region, it reads the Type field. MEM_IMAGE means the region is backed by a PE loaded from disk (a DLL or the process’s own EXE). MEM_PRIVATE means the region was allocated at runtime, typically via VirtualAlloc. A MEM_PRIVATE region with PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE permissions is the primary signal. Legitimate processes rarely allocate executable private memory. The CLR does it for JIT compilation. JavaScript engines do it. Most normal applications never do.
The contents check can be as simple as a YARA rule scanning for known beacon signatures (the Cobalt Strike config block, known function prologues, hardcoded C2 URLs) or as sophisticated as PE structure detection: looking for MZ/PE headers, export tables, or section alignment patterns inside private memory. Tools like hasherezade’s pe-sieve and forrest-orr’s Moneta automate this. They scan for PE artifacts, module stomping (where a loaded DLL’s in-memory content diverges from the file on disk), and executable private regions.
The timing matters. A beacon that sleeps for 60 seconds and is active for 200 milliseconds is scannable 99.7% of the time. The scanner almost always wins the race because the beacon is dormant for the vast majority of its lifecycle. This is the window that sleep obfuscation closes.
Sleep obfuscation is straightforward in concept: before the beacon sleeps, encrypt its own memory. When it wakes up, decrypt and continue. If a scanner walks the process during the sleep window, it finds a region filled with what looks like random bytes. No PE headers, no function prologues, no YARA hits. The content-based scan returns clean.
The challenge is in the execution. To encrypt yourself, you need code that runs the encryption. That code must be executable and unencrypted. If you leave a small decryption stub unencrypted in memory, the scanner can find the stub and use its bytes as a signature. The evolution of sleep obfuscation is the story of eliminating this stub.
Generation 0: plaintext sleep. The baseline. The beacon calls Sleep() or WaitForSingleObject() and its entire code section sits in memory as executable plaintext. Any memory scanner finds it trivially. This was the default before roughly 2021.
Generation 1: inline self-encryption. Before sleeping, the beacon calls VirtualProtect on its own code section to change PAGE_EXECUTE_READ to PAGE_READWRITE, XOR or RC4-encrypts the bytes using a key stored on the heap, calls Sleep(), then on wake decrypts, calls VirtualProtect to restore execute permission, and continues. This defeats content-based scanning during sleep. A YARA rule looking for the Cobalt Strike config block will not match because the bytes are encrypted.
But it has two problems. First, the code that performs the encryption and decryption must itself remain executable and unencrypted. This is the stub problem: a small region of code (typically 50 to 200 bytes) that encrypts everything else, calls Sleep, and then decrypts. The stub’s bytes are stable across executions and become a signature. Second, the VirtualProtect calls generate observable events. ETW’s Microsoft-Windows-Threat-Intelligence provider logs memory protection changes from protected processes. A private region flipping from RX to RW and back to RX in a regular pattern (every 30s, 60s) is a strong temporal signal. Early Cobalt Strike sleep masks (pre-4.7) and many custom loaders work this way.
Generation 2: timer and APC-based encryption (Ekko, Foliage). The key insight: do not encrypt yourself. Let the OS encrypt you.
Instead of having the beacon’s own code run the encryption, create a chain of OS callbacks (timer queue timers or APCs) where each callback uses NtContinue to redirect the OS thread’s execution to a different function. The beacon thread waits on an event while a separate OS thread (a thread pool worker for timers, or the same thread via alertable APCs) executes the encrypt-sleep-decrypt chain.
This eliminates the stub problem entirely. When the encryption runs, it runs as a Windows thread pool callback, not as beacon code. The beacon’s entire code section, including any would-be stub, is encrypted. The only unencrypted code in the picture is ntdll.dll and kernel32.dll, which are legitimate OS modules.
Ekko (C5pider, 2022) uses timer queue timers. Foliage (Austin Hudson, 2022) uses APCs queued to the current thread. Both use the same fundamental primitive: NtContinue, which loads a CONTEXT structure and sets the thread’s entire register state. The next section covers how Ekko builds its timer chain.
Generation 3: full context obfuscation. Ekko and Foliage encrypt the beacon’s code and data, but they leave other artifacts untouched: the thread’s start address still points to the beacon’s entry, the call stack during sleep may not be spoofed, and the heap may contain unencrypted strings or configuration data.
Gen 3 combines sleep obfuscation with call stack spoofing during sleep (covered in the previous post), thread start address spoofing, heap encryption (encrypting all heap allocations, not just the code section), and module stomping (loading the beacon over a legitimate DLL’s memory so VirtualQuery reports MEM_IMAGE instead of MEM_PRIVATE). Cobalt Strike 4.9+ with the user-defined reflective loader and sleep mask can achieve most of this. Nighthawk and Brute Ratel build it into the core.
The result is a beacon that, during sleep, looks like a legitimate DLL’s memory with modified contents (but the scanner cannot tell because the modification is encrypted), a clean thread start address, a spoofed call stack, and no readable strings on the heap. Detection shifts from finding the beacon’s code to finding its behavioral footprint: the timer objects, the permission flips, the entropy anomalies, and the thread state inconsistencies.
Ekko builds a chain of six timer queue timers, each with NtContinue as the callback function. NtContinue takes a pointer to a CONTEXT structure and a boolean. It loads the entire register state from the structure, effectively teleporting the thread to wherever RIP points with whatever arguments RCX, RDX, R8, R9 hold. Here is how the chain works.
Step 1: capture the current context. The beacon calls RtlCaptureContext to snapshot its own register state. This gives a baseline CONTEXT that it copies six times, one for each step in the chain.
Step 2: set up a wait event and timer queue.
HANDLE hEvent = CreateEventW(NULL, FALSE, FALSE, NULL);
HANDLE hTimerQueue = CreateTimerQueue();
Step 3: build six CONTEXT copies. On x64, the first four arguments go in RCX, RDX, R8, R9. So to call VirtualProtect(ImageBase, ImageSize, PAGE_READWRITE, &OldProt), you set:
// Context for VirtualProtect: RX -> RW
memcpy(&RopProtRW, &CtxThread, sizeof(CONTEXT));
RopProtRW.Rip = (DWORD64) VirtualProtect;
RopProtRW.Rcx = (DWORD64) ImageBase; // lpAddress
RopProtRW.Rdx = (DWORD64) ImageSize; // dwSize
RopProtRW.R8 = (DWORD64) PAGE_READWRITE; // flNewProtect
RopProtRW.R9 = (DWORD64) &OldProtect; // lpflOldProtect
RopProtRW.Rsp -= 8; // alignment
// Context for SystemFunction032 (RC4 encrypt)
memcpy(&RopMemEnc, &CtxThread, sizeof(CONTEXT));
RopMemEnc.Rip = (DWORD64) SystemFunction032;
RopMemEnc.Rcx = (DWORD64) &Data; // USTRING: {len, maxlen, buf}
RopMemEnc.Rdx = (DWORD64) &Key; // USTRING: {len, maxlen, buf}
RopMemEnc.Rsp -= 8;
// Context for WaitForSingleObject (the actual sleep)
memcpy(&RopDelay, &CtxThread, sizeof(CONTEXT));
RopDelay.Rip = (DWORD64) WaitForSingleObject;
RopDelay.Rcx = (DWORD64) hEvent;
RopDelay.Rdx = (DWORD64) SleepTimeMs;
RopDelay.Rsp -= 8;
// Contexts for decrypt, protect RX, and SetEvent follow
// the same pattern with mirrored arguments
SystemFunction032 is an undocumented export from advapi32.dll that implements RC4. Its two arguments are USTRING structures (Length, MaximumLength, Buffer). Because RC4 is a symmetric XOR stream cipher, calling it twice with the same key on the same data encrypts and then decrypts: the same function handles both directions.
Step 4: queue the timers.
// Each timer fires NtContinue with its own CONTEXT
// WT_EXECUTEINTIMERTHREAD forces serial execution on one thread
CreateTimerQueueTimer(&t1, hTimerQueue,
(WAITORTIMERCALLBACK) NtContinue, &RopProtRW,
100, 0, WT_EXECUTEINTIMERTHREAD); // RX -> RW
CreateTimerQueueTimer(&t2, hTimerQueue,
(WAITORTIMERCALLBACK) NtContinue, &RopMemEnc,
200, 0, WT_EXECUTEINTIMERTHREAD); // RC4 encrypt
CreateTimerQueueTimer(&t3, hTimerQueue,
(WAITORTIMERCALLBACK) NtContinue, &RopDelay,
300, 0, WT_EXECUTEINTIMERTHREAD); // sleep N ms
CreateTimerQueueTimer(&t4, hTimerQueue,
(WAITORTIMERCALLBACK) NtContinue, &RopMemDec,
400, 0, WT_EXECUTEINTIMERTHREAD); // RC4 decrypt
CreateTimerQueueTimer(&t5, hTimerQueue,
(WAITORTIMERCALLBACK) NtContinue, &RopProtRX,
500, 0, WT_EXECUTEINTIMERTHREAD); // RW -> RX
CreateTimerQueueTimer(&t6, hTimerQueue,
(WAITORTIMERCALLBACK) NtContinue, &RopSetEvt,
600, 0, WT_EXECUTEINTIMERTHREAD); // signal event
// Beacon thread waits here while the timer thread
// executes the full encrypt-sleep-decrypt chain
WaitForSingleObject(hEvent, INFINITE);
The WT_EXECUTEINTIMERTHREAD flag forces all callbacks to run on the timer queue’s dedicated thread, ensuring serial execution. Each timer fires at a staggered interval (100ms, 200ms, etc.), and NtContinue loads the prepared CONTEXT, teleporting the timer thread to the target function with the correct arguments already in the right registers.
Step 5: the beacon thread waits. The entire encrypt-sleep-decrypt cycle happens on the timer thread. The beacon thread just calls WaitForSingleObject(hEvent, INFINITE). When timer 6 fires and calls SetEvent, the beacon thread wakes up with its code decrypted and executable.
From a defender’s perspective, during sleep: the beacon’s code section is PAGE_READWRITE and encrypted (high entropy, no signatures). The beacon thread is waiting on a standard Windows event (clean call stack through WaitForSingleObject). The timer thread is a legitimate Windows thread pool worker (call stack through TppWorkerThread to RtlUserThreadStart). The only artifacts are the CONTEXT structures on the heap, the timer queue objects in the kernel, and the event handle.
Foliage achieves the same result with APCs instead of timers. It queues APC routines to the current thread using NtQueueApcThread, with NtContinue as each APC’s callback and a prepared CONTEXT as the argument. When the thread enters an alertable wait (SleepEx, WaitForSingleObjectEx), the APCs fire in order. The mechanism is different, the outcome is identical: the OS does the encryption using legitimate system functions, and no beacon code remains unencrypted during sleep.
The encryption hides the beacon’s content, but the act of encrypting leaves artifacts that no cipher can remove.
1. Timer and callback enumeration. Every Ekko-style chain creates kernel timer objects whose callback field points to NtContinue or RtlRestoreContext. A kernel driver or a tool with debug privileges can enumerate timer queue objects and inspect their callback addresses. NtContinue is a legitimate API, but it is almost never used as a timer callback in normal applications. A timer callback pointing to NtContinue is a near-certain indicator of Ekko-style sleep obfuscation. The tool Hunt-Sleeping-Beacons (thefLink) does exactly this: it enumerates threads in wait states, inspects their associated timer objects, and flags callbacks targeting context-manipulation functions.
2. Memory permission anomalies. Even after VirtualProtect changes the beacon’s memory from PAGE_EXECUTE_READ to PAGE_READWRITE, the region still exists and VirtualQuery still reports it. A MEM_PRIVATE region in RW with high Shannon entropy (above roughly 7.5 bits per byte, consistent with encrypted data) is suspicious. Normal applications do not have large blocks of high-entropy writable private memory. The AllocationProtect field in the MEMORY_BASIC_INFORMATION structure preserves the original protection at allocation time, which may still show PAGE_EXECUTE_READWRITE even after the downgrade.
3. The permission flip temporal pattern. If you monitor VirtualProtect calls over time via ETW-TI or a minifilter callback, a repeating pattern emerges: RX to RW on a private region, a delay matching the beacon’s sleep cycle, then RW back to RX on the same base address. No legitimate application does this in a regular cadence. The temporal signature is harder to suppress than any single event because eliminating it means eliminating the protection change entirely, which means giving up the permission downgrade that makes encryption possible.
4. Thread start address analysis. The beacon thread’s start address is queryable via NtQueryInformationThread with ThreadQuerySetWin32StartAddress. If the beacon was injected into a legitimate process, the start address points into MEM_PRIVATE memory, not into a loaded module. During sleep, the thread may have a clean call stack (it is waiting on an event), but its start address still reveals that it originated from unbacked memory. Legitimate threads start in MEM_IMAGE modules.
Gen 3 implementations address this by overwriting the start address via NtSetInformationThread or by using a trampoline that starts in a legitimate module. But most deployed sleep obfuscation implementations do not spoof the start address, making this a practical detection for the majority of real-world beacons.
5. Heap artifacts. The CONTEXT structures used for the timer chain live on the heap or in a separate allocation. These structures contain the RIP values pointing to VirtualProtect, SystemFunction032, WaitForSingleObject, and SetEvent, along with the arguments (the beacon’s base address, its size, the protection constants, the encryption key). A heap scan that searches for structures resembling a CONTEXT with RIP pointing to known API addresses is a valid detection. The encryption key itself, typically a USTRING structure with the key buffer, also lives on the heap.
6. Private memory existence. This is the simplest and most robust check. Regardless of encryption, regardless of permission changes, the memory allocation itself exists. A large MEM_PRIVATE allocation (100KB to 2MB, the typical range for beacon payloads) in a process that should not have one is an anomaly. Moneta’s workingset scan mode detects private executable regions regardless of current permissions by checking the original allocation protection. pe-sieve can flag the region even when its contents are encrypted, because the region’s metadata (type, size, allocation base) is structural, not content-dependent.
Tier 1: periodic memory scanning. Run pe-sieve or Moneta on a schedule across all processes. pe-sieve’s implant scanner mode flags MEM_PRIVATE regions with executable permissions and dumps them for offline analysis. Moneta’s workingset mode catches regions that were allocated with execute permissions even if those permissions were later downgraded. This catches Gen 0 always, Gen 1 during the brief wake window, and Gen 2-3 by flagging the suspicious private allocation even when its contents are encrypted.
Tier 2: sleeping beacon analysis. Run Hunt-Sleeping-Beacons on a regular cadence. It targets the sleep obfuscation pattern specifically: it enumerates threads in wait states, checks their call stacks, inspects timer objects for NtContinue callbacks, examines thread start addresses for private-memory origins, and flags threads whose return addresses point into stomped modules. This is the purpose-built tool for this exact problem and catches Gen 2 (Ekko, Foliage) directly.
Tier 3: ETW-TI VirtualProtect monitoring. If your EDR consumes the Microsoft-Windows-Threat-Intelligence provider (Microsoft Defender for Endpoint, CrowdStrike Falcon, Elastic Endpoint), monitor for VirtualProtect calls that change private memory from RX/RWX to RW and back. The temporal pattern (regular interval, same base address) is a high-fidelity signal. The Sigma rule below targets this.
Tier 4: kernel timer enumeration. For mature security teams with custom kernel drivers or access to live memory forensics: enumerate kernel timer objects and check their callback addresses. A timer callback pointing to NtContinue with a CONTEXT argument whose RIP is VirtualProtect or SystemFunction032 is a confirmed Ekko instance. This is the deepest check, and the hardest for an attacker to eliminate without abandoning the timer-based approach entirely.
The detection surface for sleep obfuscation is different from call stack spoofing. The previous post’s Sigma rule targeted CallTrace UNKNOWN in Sysmon Event ID 10 (unbacked return addresses). That rule still applies to the wake-up moment when the beacon decrypts and calls APIs, but it does not detect the sleep obfuscation mechanism itself.
The rule below targets the structural artifact that every sleep obfuscation technique must produce: the memory protection downgrade. To encrypt its own code, the beacon must change the code section from executable to writable. This VirtualProtect call is logged by the ETW Threat Intelligence provider and exposed by EDR platforms that consume it. The rule fires on the downgrade event (EXECUTE to READWRITE on private memory), with filters for legitimate JIT engines that produce the same call pattern.
The honest scoping: this rule requires ETW-TI or EDR kernel telemetry as the log source. Sysmon does not log VirtualProtect calls natively. If your environment runs Microsoft Defender for Endpoint, CrowdStrike Falcon, or Elastic Endpoint Security with kernel driver telemetry, the events are available. If not, use Tier 1 and Tier 2 from the section above (pe-sieve, Moneta, Hunt-Sleeping-Beacons) as your primary detection layer.
title: Sleep Obfuscation - Executable Private Memory Protection Downgrade
id: a7c3d2e1-8f4b-4a2d-9e6c-1b5d3f7a9c0e
status: experimental
description: |
Detects VirtualProtect calls that remove execute permission from private
(non-image-backed) memory regions. Sleep obfuscation techniques (Ekko,
Foliage, Cobalt Strike sleep mask) change beacon memory from
PAGE_EXECUTE_READ to PAGE_READWRITE before encrypting the payload during
sleep, then reverse the change on wake. This permission downgrade on
private executable memory is anomalous outside of JIT compilation engines.
Requires ETW Threat Intelligence provider or EDR kernel telemetry as the
log source.
references:
- https://github.com/Cracked5pider/Ekko
- https://github.com/janoglezcampos/DeathSleep
- https://github.com/thefLink/Hunt-Sleeping-Beacons
author: Valhguard
date: 2026/04/23
modified: 2026/04/23
tags:
- attack.defense_evasion
- attack.t1027
logsource:
product: windows
category: memory_protection
detection:
selection:
EventType|contains: 'ProtectVirtualMemory'
ProtectionBefore|contains: 'EXECUTE'
ProtectionAfter|contains: 'READWRITE'
MemoryType: 'Private'
filter_clr:
CallerModule|endswith:
- '\\clr.dll'
- '\\coreclr.dll'
- '\\clrjit.dll'
- '\\mscorjit.dll'
filter_browser_jit:
CallerModule|endswith:
- '\\v8.dll'
- '\\jscript9.dll'
- '\\chakra.dll'
- '\\libxul.dll'
filter_java:
SourceImage|endswith:
- '\\java.exe'
- '\\javaw.exe'
filter_security_tools:
SourceImage|endswith:
- '\\MsMpEng.exe'
- '\\SenseIR.exe'
condition: selection and not 1 of filter_*
falsepositives:
- JIT compilation (.NET CLR, V8, SpiderMonkey, Java HotSpot)
- Security tools modifying process memory during scanning or remediation
- Application update mechanisms with self-modifying code
level: medium
The filter set covers the most common sources of legitimate executable-to-writable permission changes: JIT engines (.NET CLR, V8, SpiderMonkey), Java HotSpot, and security tools that modify process memory during scanning. Environments with custom JIT workloads or DRM-protected applications that self-modify code will need additional tuning. The temporal correlation (same base address flipping RX to RW and back within a regular interval) is a stronger signal than any single event, but expressing that correlation in Sigma requires a backend that supports stateful rules or aggregation.
Sleep obfuscation has moved through three generations in four years. The first encrypted the beacon inline and left a stub as a signature. The second, Ekko and Foliage, eliminated the stub by outsourcing the encryption to OS timer and APC callbacks via NtContinue, so no beacon code remains unencrypted during sleep. The third combines sleep encryption with call stack spoofing, start-address masking, heap encryption, and module stomping to produce a beacon that leaves almost no content-based evidence in memory.
The pattern is the same one from the call stack spoofing post: the technique evolves by eliminating one more layer of observable evidence, and the detection evolves by finding the next layer the technique cannot eliminate. For sleep obfuscation, that next layer is the behavioral footprint. The timer objects still point to NtContinue. The memory permissions still flip. The private allocation still exists. The entropy still spikes. These are structural artifacts of the mechanism itself, not content that can be encrypted away.
If you take one thing from this: do not rely solely on content-based memory scanning. A YARA rule against a plaintext beacon is the right first check, but any operator running Ekko or Foliage defeats it trivially. Layer it with behavioral scanning (Hunt-Sleeping-Beacons for timer callback analysis), ETW-TI monitoring (for the VirtualProtect flip pattern), and pe-sieve or Moneta (for private executable region detection). The beacon’s code can sleep. The artifacts of its sleep cannot.

How three generations of call stack spoofers defeat the x64 unwinder, and where the detection still holds.

Both techniques make a process lie about itself, and both are on nearly every engagement. But they fail differently under scrutiny: one leaves a clean mechanical tell the spoof cannot erase, the other leaves nothing to compare against. Where the truth actually lives.

Patch ETW and the CLR goes dark, load from a byte array and you dodge AMSI, Sysmon catches the DLL load anyway. Two of those are wrong on modern .NET and the third is structurally impossible. Where the telemetry for in-memory .NET really is, and which signals survive an operator inside the process.

ETW is the eyes of your EDR. What happens when an attacker covers them? We dissect the specific Red Team techniques used to blind Windows Event Tracing, including session hijacking and EtwEventWrite patching, and show Blue Teams how to spot the silence.

Virtualization is not just infrastructure; it is a weapon. Red Teams leverage portable QEMU instances to bypass host-based EDRs and evade behavioral analysis. We analyze how attackers deploy these “invisible machines” without administrative privileges and provide the KQL hunting queries defenders need to detect unauthorized virtualization on their networks.

Loading kernel drivers is a “Holy Grail” operation for attackers, granting Ring 0 privileges for persistence or EDR blinding. We analyze the forensic difference between the loud method (sc.exe) and the stealthy method (devcon.exe) to help Blue Teams build resilient detections.
Subscribe now to keep reading and get access to the full archive.