Module 1: Memory Scanning & Sleep Obfuscation
The cat-and-mouse game between memory scanners and implant developers — and why sleep obfuscation alone isn’t enough.
Module Objective
Understand how EDR memory scanners detect implants, how traditional sleep obfuscation techniques (Ekko, Zilean, FOLIAGE) attempt to evade them, their fundamental limitations, and why MDSec’s FunctionPeekaboo introduces a paradigm shift from “encrypt everything during sleep” to “encrypt each function individually at rest.”
1. How Memory Scanners Find Implants
Once shellcode or an implant is loaded into memory, Endpoint Detection and Response (EDR) products periodically scan process memory looking for indicators of compromise. These scans target several categories of evidence:
| Detection Vector | What the Scanner Looks For | Why It Works |
|---|---|---|
| Signature Matching | Known byte sequences from public implants (Cobalt Strike, Meterpreter, Sliver) | Public tools have documented signatures; YARA rules catch them reliably |
| Unbacked Executable Memory | Private memory regions with PAGE_EXECUTE_* that don’t map to any file on disk | Legitimate code comes from DLLs with backing files; implants exist only in memory |
| RWX Pages | Memory with simultaneous read, write, and execute permissions | Normal code sections are RX; RWX strongly suggests self-modifying or injected code |
| Behavioral Heuristics | Patterns like alloc → write → protect → execute sequences, abnormal thread start addresses | The mechanics of injection follow predictable patterns |
| PE Header Artifacts | MZ/PE headers in non-file-backed memory, reflectively loaded DLLs | PE headers in private memory indicate manual loading outside the normal loader |
The critical insight is that scanners look at memory content at a point in time. If the implant’s code is in cleartext in executable memory when the scan happens, it gets caught. This observation drives all sleep obfuscation techniques.
2. The Sleep Window Problem
Command-and-control implants spend the vast majority of their runtime sleeping — waiting between check-ins to the C2 server. A typical beacon might check in every 60 seconds, meaning ~59.9 seconds of each cycle is idle sleep time. During this sleep window, the implant’s code sits in memory doing nothing, but it’s still fully readable by any scanner that comes along.
Implant Lifecycle Timeline
~100ms active
~59.9s idle
~100ms active
~59.9s idle
During those ~100ms of activity, the scanner is unlikely to catch the implant because the window is so brief. But during the ~59.9 seconds of sleep, the code is a sitting duck. This asymmetry — code is vulnerable during 99.8% of its runtime — is the fundamental problem that sleep obfuscation addresses.
3. Traditional Sleep Obfuscation Techniques
Several techniques have been developed to encrypt the implant’s memory during the sleep window. The core idea is the same in all of them: before sleeping, encrypt all implant code and data in memory; after waking, decrypt it and resume execution.
3.1 Ekko (by C5pider)
Ekko uses timer callbacks (via CreateTimerQueueTimer) to schedule the encryption and decryption operations. The flow is:
Ekko Execution Flow
- Create a timer queue with
CreateTimerQueueTimer - Queue callback 1:
VirtualProtect→ change implant memory toRW(remove execute) - Queue callback 2:
SystemFunction032(RC4) → encrypt the implant’s memory in-place - Queue callback 3:
WaitForSingleObject→ sleep for the beacon interval - Queue callback 4:
SystemFunction032→ decrypt the implant’s memory - Queue callback 5:
VirtualProtect→ restoreRXpermissions
Because the callbacks execute in a legitimate Windows thread pool thread, the call stack during sleep looks clean — it traces back to ntdll!TppWorkerThread rather than to the implant.
3.2 Zilean
Zilean extends the Ekko concept by using RtlRegisterWait instead of timer callbacks. This API registers a callback to execute when a specified wait handle is signaled or a timeout occurs, providing another mechanism to schedule the encrypt-sleep-decrypt chain without using the timer queue.
3.3 FOLIAGE (by Austin Hudson)
FOLIAGE combines APC queuing with NtContinue context manipulation for cleaner execution flow. It also handles the call stack more carefully, making the sleep state look even more like a legitimate suspended thread.
What They All Share
All three techniques follow the same fundamental pattern: (1) change memory permissions to writable, (2) encrypt everything, (3) sleep, (4) decrypt everything, (5) restore execute permissions. The differences are in the mechanism used to schedule these steps (timer callbacks vs wait registration vs APC queuing with context manipulation), not in the strategy itself.
4. Limitations of Whole-Image Encryption
While sleep obfuscation dramatically improves stealth during the sleep window, it has several fundamental weaknesses:
Critical Limitation: The Active Window
During the brief period when the implant is awake and executing (processing commands, exfiltrating data, communicating with C2), the entire codebase is decrypted and sitting in executable memory. A well-timed scan during this window catches everything. The attacker has no control over when EDR scans occur.
| Limitation | Impact |
|---|---|
| All-or-nothing decryption | When the implant wakes up, 100% of its code is decrypted, even if only a tiny fraction is needed for the current task |
| Predictable memory permission changes | Flipping an entire image between RW and RX is itself a detectable pattern; ETW and minifilter callbacks can observe VirtualProtect calls |
| No protection at sleep 0 | If the beacon interval is set to 0 (continuous check-in), the code is never encrypted because there is no sleep window. Some operational scenarios require sleep 0 |
| Thread state anomalies | Even with clean call stacks, the sudden appearance and disappearance of large encrypted memory regions can be correlated with thread state changes |
| Timing correlation | EDR can correlate periodic permission changes with C2 beaconing intervals, revealing the implant even without reading its content |
5. The FunctionPeekaboo Paradigm Shift
FunctionPeekaboo by MDSec (@saab_sec) fundamentally rethinks the problem. Instead of encrypting the entire implant image during sleep, it operates at the individual function level:
Traditional vs FunctionPeekaboo
Encrypt ALL during sleep
Decrypt ALL on wake
Each function encrypted at rest
Decrypt ONE on call, re-encrypt on return
The key principles are:
Per-Function Self-Masking
- Functions are encrypted by default — every registered function’s body is XOR-encrypted at rest
- Decrypt on call — when a function is called, only that specific function is decrypted
- Re-encrypt on return — when the function returns, it is immediately re-encrypted
- At most one function in cleartext — at any point, only the currently executing function is decrypted; all others remain masked
- ~98% coverage — even with sleep set to 0 (no sleep at all), approximately 98% of the implant’s code remains encrypted at any given moment
This is implemented at the compiler level — specifically as a modification to the LLVM X86 backend. The compiler automatically injects encryption/decryption stubs into every registered function, meaning the source code of the implant does not need to be modified at all.
6. Why Compiler-Level Instrumentation?
FunctionPeekaboo modifies the LLVM compiler rather than the implant’s source code. This approach has several advantages:
| Approach | Compiler-Level (FunctionPeekaboo) | Source-Level (Manual) |
|---|---|---|
| Code modification | None — original source unchanged | Every function must be manually wrapped |
| Coverage | Automatic for all attributed functions | Easy to miss functions; error-prone |
| Maintenance | Zero per-function overhead; add attribute and recompile | Must maintain wrapper code alongside business logic |
| Correctness | Compiler guarantees all code paths are instrumented | Edge cases (early returns, exceptions) can skip re-encryption |
| Performance | Stubs are minimal machine code, no abstraction overhead | Wrapper functions add call overhead and may prevent inlining |
7. High-Level Architecture
Before diving into the implementation details (covered in later modules), here is the high-level architecture of FunctionPeekaboo:
Component Overview
| Component | Purpose | Location |
|---|---|---|
| X86RetModPass | LLVM MachineFunctionPass that injects prologue/epilogue stubs into registered functions | LLVM X86 backend (PreEmit phase) |
| Prologue Stub | 0x46-byte code block at function entry that calls the handler to decrypt the function body | Prepended to each function |
| Epilogue Stub | Code block at every return point that calls the handler to re-encrypt the function body | Replaces each RET instruction |
| Handler | ~380-byte routine that performs the actual XOR encryption/decryption and memory permission changes | Shared across all functions |
| .funcmeta Section | Custom PE section containing metadata about each registered function (address, size, key) | PE file custom section |
| .stub Section | Custom PE section containing the initialization entry point | PE file custom section |
| modifyEP.py | Post-processing script that adjusts the PE entry point to the .stub section | Build pipeline |
| TEB UserReserved | Thread Environment Block fields used to store per-thread state (current function pointer, flags) | Runtime, via GS segment |
8. Comparison with Sleep Obfuscation
| Property | Sleep Obfuscation (Ekko/Zilean/FOLIAGE) | FunctionPeekaboo |
|---|---|---|
| Granularity | Entire image | Per-function |
| When encrypted | Only during sleep | Always (except currently executing function) |
| Coverage during activity | 0% (all decrypted while awake) | ~98% (only active function decrypted) |
| Sleep 0 protection | None | Full protection |
| Implementation level | Runtime (source code or library) | Compiler (LLVM backend) |
| Source code changes | Required (integrate sleep mask) | Add attribute only |
| CET compatible | No (Ekko/Zilean use ROP-like chains) | Yes (legitimate call/ret flow) |
| Can be combined | Yes — use both together | Yes — use both together |
Complementary, Not Competing
FunctionPeekaboo and sleep obfuscation are complementary techniques. You can use sleep obfuscation to encrypt the entire image during sleep (catching the ~2% that FunctionPeekaboo leaves decrypted), and FunctionPeekaboo to maintain ~98% encryption during active execution. Together, they provide near-complete memory protection across the entire implant lifecycle.
9. Course Roadmap
What Comes Next
| Module | Topic | Builds On |
|---|---|---|
| 1 (this) | Memory Scanning & Sleep Obfuscation | — |
| 2 | LLVM Compiler Architecture | Understanding the compilation pipeline |
| 3 | PE Internals & Custom Sections | Where metadata and stubs live in the binary |
| 4 | Function Registration & X86RetModPass | How functions are marked and instrumented |
| 5 | Prologue & Epilogue Stubs | The injected code at function boundaries |
| 6 | The Handler & XOR Engine | The core encryption/decryption logic |
| 7 | Initialization & Runtime Flow | How the system bootstraps and runs |
| 8 | Detection, CET & Nighthawk | Real-world deployment, defenses, and production use |
Knowledge Check
Q1: What is the fundamental limitation of traditional sleep obfuscation?
Q2: Approximately what percentage of code remains encrypted with FunctionPeekaboo during active execution?
Q3: At what level is FunctionPeekaboo implemented?