Difficulty: Beginner

Module 2: Timer Queues & Waitable Timers

Scheduling deferred execution through the Windows timer subsystem.

Module Objective

Understand how Windows timer queues work, the CreateTimerQueueTimer API and its callback mechanism, the WT_EXECUTEINTIMERTHREAD flag that Ekko depends on, and how timer callbacks enable executing arbitrary functions at scheduled intervals without the main thread's involvement.

1. What Is a Timer Queue?

A timer queue is a Windows kernel object that manages a collection of timers. Each timer in the queue is associated with a callback function that executes when the timer expires. The system manages the scheduling internally, firing callbacks on a dedicated thread pool thread (or, critically for Ekko, on the timer thread itself).

Timer queues were introduced in Windows 2000 as a lightweight alternative to manually creating waitable timer objects and managing dedicated timer threads. They provide an efficient mechanism for scheduling deferred work without the overhead of full thread management.

C// Create a new timer queue
HANDLE hTimerQueue = CreateTimerQueue();

// The timer queue is now ready to accept timers
// When done, clean up with:
// DeleteTimerQueue( hTimerQueue );

CreateTimerQueue returns a handle to a new timer queue object. Internally, Windows creates the supporting structures and thread pool infrastructure needed to service timers in this queue. Multiple timer queues can coexist in the same process, each managing independent sets of timers.

2. CreateTimerQueueTimer — The Core API

The function that Ekko relies on most heavily is CreateTimerQueueTimer. This API creates a timer within a timer queue and associates it with a callback function:

CBOOL CreateTimerQueueTimer(
    PHANDLE             phNewTimer,      // Receives the timer handle
    HANDLE              hTimerQueue,     // Timer queue to add timer to
    WAITORTIMERCALLBACK Callback,        // Function to call when timer fires
    PVOID               Parameter,       // Argument passed to callback
    DWORD               DueTime,         // Delay before first firing (ms)
    DWORD               Period,          // Repeat interval (0 = one-shot)
    ULONG               Flags            // Execution flags
);

The callback function must match the WAITORTIMERCALLBACK signature:

CVOID CALLBACK WaitOrTimerCallback(
    PVOID lpParameter,    // The Parameter from CreateTimerQueueTimer
    BOOLEAN TimerOrWaitFired  // TRUE if timer expired, FALSE if wait abandoned
);

Key Parameters for Ekko

3. The WT_EXECUTEINTIMERTHREAD Flag

The Flags parameter is where Ekko's technique becomes possible. The critical flag is WT_EXECUTEINTIMERTHREAD:

FlagValueBehavior
WT_EXECUTEDEFAULT0x00Callback runs on a thread pool worker thread
WT_EXECUTEINTIMERTHREAD0x20Callback runs directly on the timer queue's dedicated thread
WT_EXECUTELONGFUNCTION0x10Hints that the callback may run for a long time
WT_EXECUTEONLYONCE0x08Timer fires only once (same as Period=0)

Why WT_EXECUTEINTIMERTHREAD Is Essential

Ekko uses WT_EXECUTEINTIMERTHREAD because it guarantees that the callback executes on the same thread for every timer in the queue. This is critical because NtContinue replaces the entire thread context (all registers, including RSP and RIP). If callbacks ran on different thread pool threads, context manipulation would be unpredictable and potentially corrupt unrelated threads. By forcing all callbacks onto the single timer thread, Ekko ensures each NtContinue call affects only the dedicated timer thread, and sequential timers execute in a controlled, predictable order.

C// Ekko's timer creation pattern - all use WT_EXECUTEINTIMERTHREAD
CreateTimerQueueTimer( &hNewTimer, hTimerQueue,
    NtContinue,    // Callback: restore context from CONTEXT struct
    &RopProtRW,    // Parameter: pointer to CONTEXT for VirtualProtect(RW)
    100,           // DueTime: fire after 100ms
    0,             // Period: one-shot
    WT_EXECUTEINTIMERTHREAD );

CreateTimerQueueTimer( &hNewTimer, hTimerQueue,
    NtContinue,    // Same callback for every timer
    &RopMemEnc,    // Different CONTEXT: this one calls SystemFunction032
    200,           // 200ms - fires after the first timer
    0,
    WT_EXECUTEINTIMERTHREAD );

4. Timer Firing Order & Sequencing

Ekko depends on timers firing in a specific order. The technique uses staggered DueTime values (100ms, 200ms, 300ms, 400ms, 500ms, 600ms) to ensure sequential execution:

Timer Firing Timeline

t=0ms
Timers queued
t=100ms
Timer 1: RW
t=200ms
Timer 2: Encrypt
t=300ms
Timer 3: Sleep
t=300ms+Sleep
Timer 3 returns
t=400ms
Timer 4: Decrypt
t=500ms
Timer 5: RX
t=600ms
Timer 6: Signal

Each timer fires at its DueTime relative to when it was created. Since WT_EXECUTEINTIMERTHREAD forces all callbacks onto a single thread, and each callback uses NtContinue to replace the thread context, the timer thread executes one operation at a time in sequence. Timer 3 (WaitForSingleObject) blocks the timer thread for the sleep duration, effectively delaying timers 4-6 until after the sleep completes.

5. The Initial Context Capture

Before queuing the six operational timers, Ekko first uses a timer to capture the current thread context via RtlCaptureContext:

CCONTEXT CtxThread = { 0 };

// Timer 0: Capture the timer thread's context
CreateTimerQueueTimer(
    &hNewTimer, hTimerQueue,
    RtlCaptureContext,  // Callback: captures full thread context
    &CtxThread,         // Parameter: CONTEXT struct to fill
    0,                  // DueTime: fire immediately
    0,                  // One-shot
    WT_EXECUTEINTIMERTHREAD );

// Wait briefly for the capture to complete
WaitForSingleObject( hEvent, 0x32 );  // 50ms timeout

Why Capture Context First?

RtlCaptureContext fills a CONTEXT structure with the current state of all CPU registers (RIP, RSP, general-purpose registers, flags, segments, etc.). Ekko captures this context from the timer thread so it has a valid baseline to clone. Each of the six operational CONTEXT structures is created by copying this baseline and then modifying only the registers needed for its specific API call (RIP for the target function, RCX/RDX/R8/R9 for arguments). This ensures that every context has valid segment registers, flags, and other state that Windows requires for proper execution.

6. Events for Synchronization

Ekko uses a Windows event object to synchronize between the main thread and the timer chain:

C// Create an auto-reset event, initially non-signaled
HANDLE hEvent = CreateEventW( 0, 0, 0, 0 );

// Main thread waits for the timer chain to complete
WaitForSingleObject( hEvent, INFINITE );

// The last timer in the chain signals the event:
// RopSetEvt.Rip = SetEvent;
// RopSetEvt.Rcx = hEvent;

The main thread calls WaitForSingleObject(hEvent, INFINITE) after queuing all timers. This blocks the main thread until the final timer (Timer 6) calls SetEvent(hEvent), which signals that the entire encrypt-sleep-decrypt cycle has completed. At that point, the main thread wakes up and the beacon resumes normal operation.

The Full Synchronization Flow

  1. Main thread creates the event (non-signaled)
  2. Main thread queues all 6 timers
  3. Main thread calls WaitForSingleObject(hEvent, INFINITE) and blocks
  4. Timers 1-5 execute the encrypt-sleep-decrypt cycle on the timer thread
  5. Timer 6 calls SetEvent(hEvent)
  6. Main thread's wait is satisfied, it resumes execution

7. Timer Queue vs. Other Callback Mechanisms

Windows provides several callback-based execution mechanisms. Understanding why Ekko chose timer queues over alternatives helps appreciate the design:

MechanismCallback ThreadOrdering GuaranteeSuitability for Ekko
Timer QueueDedicated timer thread (with WT_EXECUTEINTIMERTHREAD)Sequential with staggered DueTimesIdeal — single thread, ordered execution
APC QueueTarget thread (when alertable)FIFO order guaranteedGood — used by FOLIAGE instead
Thread PoolRandom pool threadNo ordering guaranteePoor — context manipulation would be chaotic
Waitable TimerRequires manual thread managementManual sequencing neededPossible but more code

Timer queues with WT_EXECUTEINTIMERTHREAD provide the best balance of simplicity and control for Ekko's use case. The alternative approach (APCs) is what FOLIAGE uses — it queues APCs to the current thread and enters an alertable wait, causing the APCs to drain in FIFO order.

8. Practical Example: Timer Queue Basics

Before diving into Ekko's full implementation, here is a simple example demonstrating the timer queue API in isolation:

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

VOID CALLBACK MyCallback(PVOID param, BOOLEAN timerFired) {
    int step = *(int*)param;
    printf("[Timer] Step %d fired on thread %lu\n",
           step, GetCurrentThreadId());
}

int main() {
    HANDLE hQueue = CreateTimerQueue();
    HANDLE hTimer = NULL;

    int steps[] = { 1, 2, 3 };

    // Queue three timers at 100ms intervals
    for (int i = 0; i < 3; i++) {
        CreateTimerQueueTimer(
            &hTimer, hQueue,
            MyCallback, &steps[i],
            (i + 1) * 100,  // 100ms, 200ms, 300ms
            0,               // One-shot
            WT_EXECUTEINTIMERTHREAD );
    }

    // Wait for all timers to complete
    Sleep(500);

    printf("[Main] All timers fired. Main thread = %lu\n",
           GetCurrentThreadId());

    DeleteTimerQueue(hQueue);
    return 0;
}

Expected Output

All three timer callbacks report the same thread ID (the timer thread), which is different from the main thread's ID. This confirms that WT_EXECUTEINTIMERTHREAD forces all callbacks onto a single, predictable thread — exactly the behavior Ekko needs.

9. Clean Up: DeleteTimerQueue

After the timer chain completes, Ekko cleans up the timer queue to avoid resource leaks:

C// After WaitForSingleObject returns (event signaled by timer 6):
DeleteTimerQueue( hTimerQueue );

// DeleteTimerQueue cancels all pending timers in the queue and
// releases the queue handle. Passing INVALID_HANDLE_VALUE as the
// CompletionEvent parameter would make it wait for all callbacks
// to finish, but Ekko knows they're done because of the event.

DeleteTimerQueue marks all timers in the queue as cancelled and releases the queue resources. Since Ekko's final timer signals the event before the main thread calls this function, all timer callbacks have already completed by the time cleanup happens.

Knowledge Check

Q1: Why does Ekko use the WT_EXECUTEINTIMERTHREAD flag?

A) It makes the timers fire faster
B) It ensures all callbacks run on the same dedicated thread, making NtContinue context manipulation predictable
C) It prevents other threads from being created
D) It is required by the Windows API specification

Q2: What is the purpose of the initial RtlCaptureContext timer (Timer 0)?

A) To capture a valid baseline CONTEXT from the timer thread that can be cloned and modified for each operation
B) To encrypt the current thread's registers
C) To detect if a debugger is attached
D) To save the main thread's context for later restoration

Q3: How does Ekko ensure timers fire in the correct sequence?

A) It uses mutex objects between each timer
B) It sets thread priority levels for each callback
C) It uses APC queues which are inherently ordered
D) It staggers DueTime values at 100ms intervals (100, 200, 300, ...)