Difficulty: Beginner

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 VectorWhat the Scanner Looks ForWhy It Works
Signature MatchingKnown byte sequences from public implants (Cobalt Strike, Meterpreter, Sliver)Public tools have documented signatures; YARA rules catch them reliably
Unbacked Executable MemoryPrivate memory regions with PAGE_EXECUTE_* that don’t map to any file on diskLegitimate code comes from DLLs with backing files; implants exist only in memory
RWX PagesMemory with simultaneous read, write, and execute permissionsNormal code sections are RX; RWX strongly suggests self-modifying or injected code
Behavioral HeuristicsPatterns like alloc → write → protect → execute sequences, abnormal thread start addressesThe mechanics of injection follow predictable patterns
PE Header ArtifactsMZ/PE headers in non-file-backed memory, reflectively loaded DLLsPE 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

Check-In
~100ms active
SLEEP
~59.9s idle
Check-In
~100ms active
SLEEP
~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

  1. Create a timer queue with CreateTimerQueueTimer
  2. Queue callback 1: VirtualProtect → change implant memory to RW (remove execute)
  3. Queue callback 2: SystemFunction032 (RC4) → encrypt the implant’s memory in-place
  4. Queue callback 3: WaitForSingleObject → sleep for the beacon interval
  5. Queue callback 4: SystemFunction032 → decrypt the implant’s memory
  6. Queue callback 5: VirtualProtect → restore RX permissions

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.

LimitationImpact
All-or-nothing decryptionWhen 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 changesFlipping an entire image between RW and RX is itself a detectable pattern; ETW and minifilter callbacks can observe VirtualProtect calls
No protection at sleep 0If 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 anomaliesEven with clean call stacks, the sudden appearance and disappearance of large encrypted memory regions can be correlated with thread state changes
Timing correlationEDR 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

Traditional
Encrypt ALL during sleep
Decrypt ALL on wake
vs
FunctionPeekaboo
Each function encrypted at rest
Decrypt ONE on call, re-encrypt on return

The key principles are:

Per-Function Self-Masking

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:

ApproachCompiler-Level (FunctionPeekaboo)Source-Level (Manual)
Code modificationNone — original source unchangedEvery function must be manually wrapped
CoverageAutomatic for all attributed functionsEasy to miss functions; error-prone
MaintenanceZero per-function overhead; add attribute and recompileMust maintain wrapper code alongside business logic
CorrectnessCompiler guarantees all code paths are instrumentedEdge cases (early returns, exceptions) can skip re-encryption
PerformanceStubs are minimal machine code, no abstraction overheadWrapper 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

ComponentPurposeLocation
X86RetModPassLLVM MachineFunctionPass that injects prologue/epilogue stubs into registered functionsLLVM X86 backend (PreEmit phase)
Prologue Stub0x46-byte code block at function entry that calls the handler to decrypt the function bodyPrepended to each function
Epilogue StubCode block at every return point that calls the handler to re-encrypt the function bodyReplaces each RET instruction
Handler~380-byte routine that performs the actual XOR encryption/decryption and memory permission changesShared across all functions
.funcmeta SectionCustom PE section containing metadata about each registered function (address, size, key)PE file custom section
.stub SectionCustom PE section containing the initialization entry pointPE file custom section
modifyEP.pyPost-processing script that adjusts the PE entry point to the .stub sectionBuild pipeline
TEB UserReservedThread Environment Block fields used to store per-thread state (current function pointer, flags)Runtime, via GS segment

8. Comparison with Sleep Obfuscation

PropertySleep Obfuscation (Ekko/Zilean/FOLIAGE)FunctionPeekaboo
GranularityEntire imagePer-function
When encryptedOnly during sleepAlways (except currently executing function)
Coverage during activity0% (all decrypted while awake)~98% (only active function decrypted)
Sleep 0 protectionNoneFull protection
Implementation levelRuntime (source code or library)Compiler (LLVM backend)
Source code changesRequired (integrate sleep mask)Add attribute only
CET compatibleNo (Ekko/Zilean use ROP-like chains)Yes (legitimate call/ret flow)
Can be combinedYes — use both togetherYes — 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

ModuleTopicBuilds On
1 (this)Memory Scanning & Sleep Obfuscation
2LLVM Compiler ArchitectureUnderstanding the compilation pipeline
3PE Internals & Custom SectionsWhere metadata and stubs live in the binary
4Function Registration & X86RetModPassHow functions are marked and instrumented
5Prologue & Epilogue StubsThe injected code at function boundaries
6The Handler & XOR EngineThe core encryption/decryption logic
7Initialization & Runtime FlowHow the system bootstraps and runs
8Detection, CET & NighthawkReal-world deployment, defenses, and production use

Knowledge Check

Q1: What is the fundamental limitation of traditional sleep obfuscation?

A) It cannot encrypt memory at all
B) It only works on 32-bit systems
C) During active execution, 100% of code is decrypted and scannable
D) It requires kernel-mode drivers

Q2: Approximately what percentage of code remains encrypted with FunctionPeekaboo during active execution?

A) ~50%
B) ~98%
C) ~75%
D) 100%

Q3: At what level is FunctionPeekaboo implemented?

A) LLVM compiler backend modification
B) Windows kernel driver
C) Source-level wrapper functions
D) Hypervisor-based interception