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
- Callback — Ekko sets this to
NtContinue, not a normal callback function. NtContinue expects a CONTEXT* as its first parameter, which aligns with thelpParameterposition. - Parameter — Ekko passes a pointer to a pre-configured CONTEXT structure. NtContinue reads this to set the thread's register state.
- DueTime — Ekko staggers timers at 100ms intervals (100, 200, 300, ...) to ensure they fire in the correct sequence.
- Period — Set to 0 for one-shot timers. Each timer fires exactly once.
3. The WT_EXECUTEINTIMERTHREAD Flag
The Flags parameter is where Ekko's technique becomes possible. The critical flag is WT_EXECUTEINTIMERTHREAD:
| Flag | Value | Behavior |
|---|---|---|
WT_EXECUTEDEFAULT | 0x00 | Callback runs on a thread pool worker thread |
WT_EXECUTEINTIMERTHREAD | 0x20 | Callback runs directly on the timer queue's dedicated thread |
WT_EXECUTELONGFUNCTION | 0x10 | Hints that the callback may run for a long time |
WT_EXECUTEONLYONCE | 0x08 | Timer 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
Timers queued
Timer 1: RW
Timer 2: Encrypt
Timer 3: Sleep
Timer 3 returns
Timer 4: Decrypt
Timer 5: RX
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
- Main thread creates the event (non-signaled)
- Main thread queues all 6 timers
- Main thread calls
WaitForSingleObject(hEvent, INFINITE)and blocks - Timers 1-5 execute the encrypt-sleep-decrypt cycle on the timer thread
- Timer 6 calls
SetEvent(hEvent) - 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:
| Mechanism | Callback Thread | Ordering Guarantee | Suitability for Ekko |
|---|---|---|---|
| Timer Queue | Dedicated timer thread (with WT_EXECUTEINTIMERTHREAD) | Sequential with staggered DueTimes | Ideal — single thread, ordered execution |
| APC Queue | Target thread (when alertable) | FIFO order guaranteed | Good — used by FOLIAGE instead |
| Thread Pool | Random pool thread | No ordering guarantee | Poor — context manipulation would be chaotic |
| Waitable Timer | Requires manual thread management | Manual sequencing needed | Possible 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?
Q2: What is the purpose of the initial RtlCaptureContext timer (Timer 0)?
Q3: How does Ekko ensure timers fire in the correct sequence?