DEV Community

aliangbo
aliangbo

Posted on

Sleep Mask in Red Team vs. Blue Team Engagements (Part 1): A Deep Dive into Ekko's Timer-Based Obfuscation

Series Note: This is the first article in the "Sleep Mask in Red Team vs. Blue Team Engagements" series, focusing on the classic open-source implementation, Ekko. Subsequent articles will cover additional Sleep Obfuscation variants, along with detection and countermeasure techniques.

Abstract: In red team vs. blue team engagements, a C2 framework's Beacon retains a complete payload image in its process memory while it sleeps and waits for tasking, making it highly susceptible to detection by defenders' memory scans. The Sleep Mask (sleep obfuscation) technique emerged to address this problem: its core idea is to encrypt or obfuscate the in-memory payload before the Beacon sleeps and to restore it once the Beacon wakes. Using the classic open-source implementation Ekko as a starting point, this article provides an in-depth analysis of Sleep Obfuscation based on the Windows Timer Queue and ROP chains.


1. Background: Why Is Sleep Mask Needed?

1.1 The C2 Beacon Lifecycle

A typical C2 (Command & Control) Beacon operates in the following cycle:

[Start] → [Connect to C2] → [Get Tasking] → [Execute Task] → [Sleep/Wait] → [Connect to C2] → ...
Enter fullscreen mode Exit fullscreen mode

During the "sleep/wait" phase, the Beacon usually calls system APIs such as Sleep() or WaitForSingleObject() to pause execution. However, the following remain fully intact in its process memory:

  • The Beacon's code section (.text) and data section (.data)
  • Loaded module images (PE headers, import tables, etc.)
  • Sensitive data written during task execution (keys, credentials, shellcode, etc.)

1.2 The Threat of Memory Scanning

Defenders (blue teams, EDR products, and security researchers) can scan the Beacon's process memory while it sleeps, using methods such as:

Scanning Method Description
Signature Scanning Matching YARA rules against process memory to find known Beacon signatures
PE Structure Detection Detecting anomalous PE images in memory (e.g., executable images with no corresponding file on disk)
Entropy Analysis High-entropy memory regions may indicate encrypted or packed content
Behavioral Correlation Correlating thread behavior (periodic wake-ups) with memory characteristics to reach a detection verdict

1.3 Limitations of Traditional Approaches

Approach Limitation
Manually calling VirtualProtect + manual encryption At the instant the encryption/decryption code executes, a plaintext Beacon image still exists in memory, leaving a race window that memory scanners can exploit
Using NtProtectVirtualMemory to directly modify page protection The call is conspicuous and is easily intercepted by API hooking
Writing the Beacon to a file and then deleting it Disk I/O leaves forensic artifacts

Core dilemma: The Beacon must run its encryption/decryption logic using its own code, so that code is necessarily plaintext at the moment encryption or decryption runs. If the Beacon's memory image could be rendered completely unreadable throughout the entire sleep window between encryption and decryption, memory scanning could be countered to the greatest possible extent.


2. The Core Idea of Sleep Mask

The ideal Sleep Mask execution flow is as follows:

Beacon running normally
       │
       ▼
┌──────────────────────────────────────┐
│ 1. Set page protection to RW         │  (VirtualProtect → PAGE_READWRITE)
│ 2. Encrypt the Beacon image          │  (RC4 / XOR / other symmetric ciphers)
│ 3. Perform the sleep                 │  (Sleep / WaitForSingleObject)
│    Memory now contains ciphertext    │
│ 4. Decrypt the Beacon image          │  (Reverse operation with the same key)
│ 5. Restore page protection to RX     │  (VirtualProtect → PAGE_EXECUTE_READWRITE)
└──────────────────────────────────────┘
       │
       ▼
Beacon continues execution
Enter fullscreen mode Exit fullscreen mode

Key challenge: Steps 1-2 and 4-5 must themselves be executed by the Beacon's plaintext code. How can the Beacon be "safely" woken up and its image decrypted after encryption?

This is precisely where Ekko's elegance lies.


3. Ekko: Timer Queue-Based Sleep Obfuscation

3.1 Technique Origins

The Ekko technique was originally discovered by Peter Winter-Smith and deployed in MDSec's Nighthawk C2 framework; it was later publicly analyzed by Austin Hudson (@SecIdiot) in his research. Ekko is the open-source PoC implementation of the technique, written by C5pider.

3.2 Core Design Philosophy

Ekko's central insight: By leveraging the Windows Timer Queue mechanism, the system's timer thread can automatically execute a pre-orchestrated ROP chain while the encrypted Beacon "sleeps," performing decryption and protection restoration on its behalf — thereby working around the unavailability of the Beacon's own code.

Specifically:

  1. While the Beacon is still in plaintext form, a set of CONTEXT structures (the ROP chain) is pre-built, each describing a single function call
  2. These ROP chains are registered as timer callbacks via CreateTimerQueueTimer
  3. The Beacon image is then encrypted, and the Beacon enters its sleep phase
  4. During the sleep, the Windows thread pool's timer thread fires these callbacks in sequence at their scheduled times
  5. The callbacks restore the CONTEXT via the NtContinue system call, thereby executing VirtualProtect, SystemFunction032 (RC4), WaitForSingleObject, and other functions
  6. Finally, the Beacon image is decrypted, its page protection is restored, and the main thread is woken up

3.3 Key APIs and Data Structures

3.3.1 Timer Queue API

// Create a timer queue
HANDLE CreateTimerQueue(void);

// Create a timer in the queue; when it expires, the callback runs on a thread pool thread
BOOL CreateTimerQueueTimer(
    PHANDLE             phNewTimer,
    HANDLE              TimerQueue,
    WAITORTIMERCALLBACK Callback,    // Callback function pointer
    PVOID               Parameter,   // Parameter passed to the callback
    DWORD               DueTime,     // Delay before the first firing (milliseconds)
    DWORD               Period,      // Period (0 = one-shot)
    ULONG               Flags        // WT_EXECUTEINTIMERTHREAD, etc.
);
Enter fullscreen mode Exit fullscreen mode

When Callback is RtlCaptureContext or NtContinue, Parameter is interpreted as a CONTEXT* (output) or a CONTEXT* (input), respectively.

3.3.2 NtContinue

NTSTATUS NtContinue(
    HANDLE    ThreadContext,   // Pointer to a CONTEXT structure
    BOOLEAN   RaiseAlert
);
Enter fullscreen mode Exit fullscreen mode

NtContinue is a low-level system call used to restore a thread's context. Ekko uses it to "execute" a pre-built CONTEXT structure — which is equivalent to performing an arbitrary function call on the target thread.

3.3.3 SystemFunction032 (RC4)

// Exported by Advapi32.dll; this is actually an RC4 encryption/decryption function
NTSTATUS SystemFunction032(
    USTRING* Data,    // Data to encrypt/decrypt
    USTRING* Key      // Key
);
Enter fullscreen mode Exit fullscreen mode

SystemFunction032 is an RC4 implementation exported internally by Windows. Because RC4 is a symmetric cipher, encryption and decryption are the same operation — applying the same RC4 operation twice to the same data restores the original. Ekko exploits this property, calling the same function in both the encryption and decryption phases.

The USTRING structure is defined as follows:

typedef struct {
    DWORD   Length;
    DWORD   MaximumLength;
    PVOID   Buffer;
} USTRING;
Enter fullscreen mode Exit fullscreen mode

3.4 Execution Flow in Detail

Below is Ekko's complete execution flow, divided into the preparation phase and the obfuscation phase:

Preparation Phase (Beacon in Plaintext)

┌──────────────────────────────────────────────────────────────┐
│ 1. Obtain the current process image base (ImageBase)         │
│    and image size (ImageSize)                                │
│ 2. Create the Event and the TimerQueue                       │
│ 3. Resolve the addresses of key functions:                   │
│    - NtContinue  (Ntdll)                                     │
│    - SystemFunction032 (Advapi32)                            │
│    - VirtualProtect (Kernel32)                               │
│    - WaitForSingleObject / SetEvent                          │
│ 4. Prepare the RC4 key (16 bytes)                            │
│ 5. Capture the current thread context with                   │
│    RtlCaptureContext                                         │
│    (via a 0 ms timer callback on the Timer Queue)            │
└──────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Building the ROP Chain (6 CONTEXT Structures)

Using the captured thread context as a template, Ekko builds 6 CONTEXT structures, each representing one function call:

# Name Purpose Code
1 RopProtRW Set the Beacon image's page protection to PAGE_READWRITE VirtualProtect(ImageBase, ImageSize, PAGE_READWRITE, &OldProtect)
2 RopMemEnc Encrypt the Beacon image with RC4 SystemFunction032(&Img, &Key)
3 RopDelay Perform the actual sleep WaitForSingleObject(NtCurrentProcess(), SleepTime)
4 RopMemDec Decrypt the Beacon image with RC4 SystemFunction032(&Img, &Key)
5 RopProtRX Restore page protection to PAGE_EXECUTE_READWRITE VirtualProtect(ImageBase, ImageSize, PAGE_EXECUTE_READWRITE, &OldProtect)
6 RopSetEvt Signal the event to wake the main thread SetEvent(hEvent)

Key field assignments for each CONTEXT:

// Using RopProtRW as an example
RopProtRW.Rsp  -= 8;               // Move the stack pointer down to emulate the return-address slot a ret instruction would land on
RopProtRW.Rip   = VirtualProtect;  // Set the instruction pointer to the target function
RopProtRW.Rcx   = ImageBase;       // Arg 1: image base
RopProtRW.Rdx   = ImageSize;       // Arg 2: image size
RopProtRW.R8    = PAGE_READWRITE;  // Arg 3: new protection
RopProtRW.R9    = &OldProtect;     // Arg 4: receives the old protection
Enter fullscreen mode Exit fullscreen mode

Note: The purpose of Rsp -= 8 is to prepare a valid return-address slot for the ret instruction when NtContinue returns. Since Period is 0 (one-shot), the timer thread exits normally after the callback completes, so the return address does not need to point to valid code.

Obfuscation Phase (Timer-Triggered)

The 6 timers are registered on the Timer Queue with increasing DueTime values:

Timeline (ms)   0     100    200    300    400    500    600
                │      │      │      │      │      │      │
                │  ProtRW MemEnc Delay MemDec ProtRX SetEvt
                │  (to RW) (enc)  (sleep)(dec)  (to RWX)(wake)
                │
                ▼
            Main thread waits on the Event
Enter fullscreen mode Exit fullscreen mode

Key detail: All timer callbacks use the WT_EXECUTEINTIMERTHREAD flag, ensuring these callbacks are invoked on the timer thread instead of the main thread. This means:

  • The main thread can safely call WaitForSingleObject(hEvent, INFINITE) and go to sleep
  • Encryption, decryption, and page protection changes are performed on its behalf by the timer thread
  • During the sleep (step 3), both the main thread's stack frames and its code section are ciphertext, so memory scans cannot identify Beacon signatures

4. Technical Principle Diagrams

4.1 Memory State Transitions

Time        Memory State         Description
────────────────────────────────────────────────────────
T0          [Plaintext Beacon]   Preparation: build the ROP chain
T1 (100ms)  [Plaintext Beacon]   VirtualProtect → RW (must be writable so the ciphertext can be written back)
T2 (200ms)  [Ciphertext Beacon]  RC4 encryption complete; no identifiable signatures in memory
T3 (300ms)  [Ciphertext Beacon]  Main thread sleeping; the timer thread is also waiting
T4 (400ms)  [Ciphertext Beacon]  RC4 decryption; plaintext restored
T5 (500ms)  [Plaintext Beacon]   VirtualProtect → RWX (executability restored)
T6 (600ms)  [Plaintext Beacon]   SetEvent wakes the main thread
Enter fullscreen mode Exit fullscreen mode

4.2 Thread Interaction Model

┌──────────────────────┐     ┌───────────────────────────────┐
│     Main Thread      │     │  Timer Queue Timer Thread     │
│                      │     │                               │
│  Build CONTEXTs      │     │                               │
│  Register 6 timers   │     │                               │
│                      │     │                               │
│  Wait(hEvent, INF) ──┼────►│  T+100ms: VirtualProtect(RW)  │
│  [main thread sleeps]│     │  T+200ms: RC4 encrypt         │
│  [memory ciphertext] │     │  T+300ms: WaitForSingleObject │
│  [invisible]         │     │  T+400ms: RC4 decrypt         │
│                      │     │  T+500ms: VirtualProtect(RWX) │
│                      │◄─────┼  T+600ms: SetEvent           │
│  Woken up; resumes   │     │                               │
└──────────────────────┘     └───────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

5. Key Code Analysis

5.1 Obtaining Image Information

ImageBase = GetModuleHandleA( NULL );
ImageSize = ( ( PIMAGE_NT_HEADERS ) ( ImageBase + 
    ( ( PIMAGE_DOS_HEADER ) ImageBase )->e_lfanew ) 
    )->OptionalHeader.SizeOfImage;
Enter fullscreen mode Exit fullscreen mode

Ekko obtains the image base and size by parsing the PE header of the current process's main module. GetModuleHandleA(NULL) returns the base address of the current process's executable.

5.2 Context Capture

CreateTimerQueueTimer( &hNewTimer, hTimerQueue, RtlCaptureContext, 
    &CtxThread, 0, 0, WT_EXECUTEINTIMERTHREAD );
WaitForSingleObject( hEvent, 0x32 );  // Wait 50 ms
Enter fullscreen mode Exit fullscreen mode

Ekko cleverly uses RtlCaptureContext as the Timer Queue callback function. Its prototype is VOID RtlCaptureContext(PCONTEXT), which happens to match the WAITORTIMERCALLBACK signature (its calling convention is compatible with VOID CALLBACK(PVOID, BOOLEAN)). The first parameter Parameter (i.e., &CtxThread) is interpreted by RtlCaptureContext as a PCONTEXT, thereby capturing the timer thread's context.

The 50 ms timeout in WaitForSingleObject(hEvent, 0x32) here ensures the timer callback has enough time to complete the context capture.

5.3 ROP Chain Orchestration

All 6 CONTEXT structures are copied from the CtxThread template and then have their key fields modified:

memcpy( &RopProtRW, &CtxThread, sizeof( CONTEXT ) );
// ... for each CONTEXT, set Rip (target function) and Rcx/Rdx/R8/R9 (arguments)
Enter fullscreen mode Exit fullscreen mode

This "template copy" approach ensures that all context state other than Rip and the argument registers (segment registers, floating-point registers, etc.) remains consistent, reducing the risk of exceptions when NtContinue executes.

5.4 Timer Registration and Execution

CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopProtRW, 100, 0, WT_EXECUTEINTIMERTHREAD );
CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopMemEnc, 200, 0, WT_EXECUTEINTIMERTHREAD );
// ... registered in order, with DueTime increasing by 100 ms
Enter fullscreen mode Exit fullscreen mode

All six timers use NtContinue as their callback function, with the corresponding CONTEXT structure pointer as the parameter. When a timer expires, the timer thread calls NtContinue(CONTEXT*); the system restores that context and jumps to the function specified by Rip.

5.5 Complete Example Code

Below is the complete Ekko implementation, covering all the key elements described above:

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

// ============================================================
// Data structure definitions
// ============================================================

// String structure required by SystemFunction032
typedef struct {
    DWORD   Length;
    DWORD   MaximumLength;
    PVOID   Buffer;
} USTRING;

// Common handle macro
#define NtCurrentProcess()  ((HANDLE)(LONG_PTR)-1)

// ============================================================
// EkkoObf - Sleep Obfuscation core function
// ============================================================
VOID EkkoObf( DWORD SleepTime )
{
    // --- Thread context: the template captured via RtlCaptureContext ---
    CONTEXT CtxThread   = { 0 };

    // --- 6 ROP chain contexts, each representing one function call ---
    CONTEXT RopProtRW   = { 0 };   // VirtualProtect    → PAGE_READWRITE
    CONTEXT RopMemEnc   = { 0 };   // SystemFunction032 → RC4 encrypt
    CONTEXT RopDelay    = { 0 };   // WaitForSingleObject → sleep
    CONTEXT RopMemDec   = { 0 };   // SystemFunction032 → RC4 decrypt
    CONTEXT RopProtRX   = { 0 };   // VirtualProtect    → PAGE_EXECUTE_READWRITE
    CONTEXT RopSetEvt   = { 0 };   // SetEvent → wake the main thread

    HANDLE  hTimerQueue = NULL;
    HANDLE  hNewTimer   = NULL;
    HANDLE  hEvent      = NULL;
    PVOID   ImageBase   = NULL;
    DWORD   ImageSize   = 0;
    DWORD   OldProtect  = 0;

    // RC4 key (16 bytes; should be randomly generated in real deployments)
    CHAR    KeyBuf[16] = {
        0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
        0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55
    };
    USTRING Key = { 0 };
    USTRING Img = { 0 };

    PVOID   NtContinue  = NULL;
    PVOID   SysFunc032  = NULL;

    // ==========================================================
    // Step 1: Initialize handles and resolve function addresses
    // ==========================================================
    hEvent      = CreateEventW( 0, 0, 0, 0 );
    hTimerQueue = CreateTimerQueue();

    // NtContinue: restores a CONTEXT structure; equivalent to an arbitrary function call
    NtContinue  = GetProcAddress(
        GetModuleHandleA( "Ntdll" ), "NtContinue" );

    // SystemFunction032: the RC4 encrypt/decrypt function exported by Advapi32
    SysFunc032  = GetProcAddress(
        LoadLibraryA( "Advapi32" ), "SystemFunction032" );

    // ==========================================================
    // Step 2: Obtain the base and size of the current process image
    // ==========================================================
    ImageBase = GetModuleHandleA( NULL );
    ImageSize = ( ( PIMAGE_NT_HEADERS )( ImageBase +
        ( ( PIMAGE_DOS_HEADER )ImageBase )->e_lfanew )
        )->OptionalHeader.SizeOfImage;

    // Fill the USTRING structures describing the memory region to encrypt/decrypt
    Key.Buffer  = KeyBuf;
    Key.Length  = Key.MaximumLength = 16;

    Img.Buffer  = ImageBase;
    Img.Length  = Img.MaximumLength = ImageSize;

    // ==========================================================
    // Step 3: Capture the timer thread context (as the ROP chain template)
    // ==========================================================
    // Use RtlCaptureContext as the Timer Queue callback;
    // DueTime=0 means it fires immediately; the callback runs on the timer thread
    if ( CreateTimerQueueTimer(
            &hNewTimer, hTimerQueue,
            RtlCaptureContext,       // Callback
            &CtxThread,              // Parameter = output CONTEXT
            0, 0,                    // DueTime=0, Period=0
            WT_EXECUTEINTIMERTHREAD ) )
    {
        // Wait 50 ms to ensure the context capture has completed
        WaitForSingleObject( hEvent, 0x32 );

        // ======================================================
        // Step 4: Build the 6 ROP chains from the captured context template
        // ======================================================
        memcpy( &RopProtRW, &CtxThread, sizeof( CONTEXT ) );
        memcpy( &RopMemEnc, &CtxThread, sizeof( CONTEXT ) );
        memcpy( &RopDelay,  &CtxThread, sizeof( CONTEXT ) );
        memcpy( &RopMemDec, &CtxThread, sizeof( CONTEXT ) );
        memcpy( &RopProtRX, &CtxThread, sizeof( CONTEXT ) );
        memcpy( &RopSetEvt, &CtxThread, sizeof( CONTEXT ) );

        // ----- ROP 1: VirtualProtect(ImageBase, ImageSize, PAGE_READWRITE, &OldProtect) -----
        RopProtRW.Rsp  -= 8;
        RopProtRW.Rip   = VirtualProtect;
        RopProtRW.Rcx   = ImageBase;
        RopProtRW.Rdx   = ImageSize;
        RopProtRW.R8    = PAGE_READWRITE;
        RopProtRW.R9    = &OldProtect;

        // ----- ROP 2: SystemFunction032(&Img, &Key) — RC4 encrypt -----
        RopMemEnc.Rsp  -= 8;
        RopMemEnc.Rip   = SysFunc032;
        RopMemEnc.Rcx   = &Img;
        RopMemEnc.Rdx   = &Key;

        // ----- ROP 3: WaitForSingleObject(NtCurrentProcess(), SleepTime) — sleep -----
        RopDelay.Rsp   -= 8;
        RopDelay.Rip    = WaitForSingleObject;
        RopDelay.Rcx    = NtCurrentProcess();
        RopDelay.Rdx    = SleepTime;

        // ----- ROP 4: SystemFunction032(&Img, &Key) — RC4 decrypt -----
        RopMemDec.Rsp  -= 8;
        RopMemDec.Rip   = SysFunc032;
        RopMemDec.Rcx   = &Img;
        RopMemDec.Rdx   = &Key;

        // ----- ROP 5: VirtualProtect(ImageBase, ImageSize, PAGE_EXECUTE_READWRITE, &OldProtect) -----
        RopProtRX.Rsp  -= 8;
        RopProtRX.Rip   = VirtualProtect;
        RopProtRX.Rcx   = ImageBase;
        RopProtRX.Rdx   = ImageSize;
        RopProtRX.R8    = PAGE_EXECUTE_READWRITE;
        RopProtRX.R9    = &OldProtect;

        // ----- ROP 6: SetEvent(hEvent) — wake the main thread -----
        RopSetEvt.Rsp  -= 8;
        RopSetEvt.Rip   = SetEvent;
        RopSetEvt.Rcx   = hEvent;

        // ======================================================
        // Step 5: Register the 6 timers; they fire in increasing DueTime order
        // ======================================================
        // Every timer callback is NtContinue, with the matching CONTEXT pointer as its parameter
        // DueTime step is 100 ms; Period=0 means one-shot
        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopProtRW, 100, 0, WT_EXECUTEINTIMERTHREAD );
        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopMemEnc, 200, 0, WT_EXECUTEINTIMERTHREAD );
        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopDelay,  300, 0, WT_EXECUTEINTIMERTHREAD );
        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopMemDec, 400, 0, WT_EXECUTEINTIMERTHREAD );
        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopProtRX, 500, 0, WT_EXECUTEINTIMERTHREAD );
        CreateTimerQueueTimer( &hNewTimer, hTimerQueue, NtContinue, &RopSetEvt, 600, 0, WT_EXECUTEINTIMERTHREAD );

        // ======================================================
        // Step 6: The main thread sleeps, waiting for the ROP chain to complete
        // ======================================================
        // The process image is now encrypted; no identifiable Beacon signatures remain in memory
        WaitForSingleObject( hEvent, INFINITE );
    }

    // Clean up the timer queue
    DeleteTimerQueue( hTimerQueue );
}

// ============================================================
// Entry point — demonstrates a continuous loop of sleep obfuscation
// ============================================================
int main()
{
    puts( "[*] Ekko Sleep Obfuscation" );

    do
        EkkoObf( 4 * 1000 );   // Sleep for 4 seconds per iteration
    while ( TRUE );

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Code Recap:

Step Key Operation APIs Involved
Initialization Create the Event and TimerQueue; resolve function addresses CreateEventW, CreateTimerQueue, GetProcAddress
Image Locating Parse the PE header to obtain the base and size GetModuleHandleA, PE header parsing
Context Capture Capture a thread context using a Timer Queue callback CreateTimerQueueTimer + RtlCaptureContext
ROP Orchestration Copy 6 CONTEXTs and set the target function and arguments for each memcpy, manual Rip/Rcx/Rdx/R8/R9 assignment
Timer Registration Register 6 NtContinue callbacks at 100 ms intervals CreateTimerQueueTimer × 6
Sleep/Wait The main thread blocks; memory stays encrypted WaitForSingleObject(hEvent, INFINITE)

6. Security Analysis

6.1 Countermeasures Against Memory Scanning

Ekko's core adversarial value:

  • Memory is ciphertext throughout the sleep window: Between T2 and T4, the Beacon's PE image is fully RC4-encrypted, so traditional YARA scanning and PE structure detection cannot identify it
  • No extra disk I/O: The entire encryption/decryption process happens in memory, producing no additional file operations
  • Abusing legitimate system mechanisms: Timer Queues are a legitimate Windows feature, and both NtContinue and SystemFunction032 are exported by the operating system

6.2 Detection Approaches

Defenders can detect this class of Sleep Mask techniques from the following angles:

  1. Timer Queue anomaly monitoring: Monitor CreateTimerQueueTimer calls, paying special attention to NtContinue callbacks whose parameter points to a CONTEXT structure
  2. Memory protection flip detection: Monitor rapid RXRWRX flips in process memory page protection
  3. Thread behavior analysis: Detect anomalous NtContinue calls originating from the timer thread
  4. Memory entropy monitoring: Periodically sample memory entropy during idle periods; a sudden rise may indicate encryption activity
  5. API call sequence correlation: Treat the call sequence VirtualProtect + SystemFunction032 + WaitForSingleObject as a detection rule

6.3 iDefender's Real-World Detection and Blocking

The detection approaches above are not merely theoretical. In real-world testing, iDefender can effectively detect and block Ekko-style Sleep Mask techniques. Below is a screenshot of the Ekko PoC being detected in real time while it was running:

Summary

iDefender's detection does not rely on code signatures. No matter how Ekko's implementation is transformed — swapping encryption algorithms, randomizing timer intervals, obfuscating the order of API calls — the very act of triggering the obfuscation exposes behavioral indicators: anomalous timer callback orchestration, NtContinue restoring an attacker-controlled context, and sudden flips of memory page protection. These behavioral patterns are detected at the moment of execution, rather than discovered after the fact through static scanning.

For more technical details and product capabilities of iDefender - The Behavior-Driven Intelligent Defender, please visit: iDefender Official Website

References

Top comments (0)