概述:CreateRemoteThread 的简单使用 Demo

在阅读之前,建议大家带着几个问题

  1. CreateRemoteThread 是如何实现的
  2. 自己实现需要哪几个主要的步骤
  3. 权限问题是如何验证的(这个问题涉及比较多,仅提出来供参考)

说明

打开一个 GUI 进程,并在进程中调用 MessageBox 弹出消息框。

关键说明:由于注入只能传递一个参数,因此不能直接在目标进程中调用 MessageBox. 因此至少需要创建一个只有一个入参的函数来传递 MessageBox 的相关调用。

步骤说明:

  1. 根据传递的进程名获取进程 PID
  2. 根据 PID 打开目标进程
  3. 申请内存写入调用函数的地址
  4. 申请内存写入调用函数的参数
  5. 创建远程线程调用写入的函数

代码

调用 CreateRemoteThread

#pragma once
#include <windows.h>  
#include <TlHelp32.h>  
#include "stdio.h"  
//线程参数结构体定义  
typedef struct _RemoteParam {
	char szMsg[12];    //MessageBox 函数中显示的字符提示  
	DWORD dwMessageBox;//MessageBox 函数的入口地址  
} RemoteParam, * PRemoteParam;
//定义 MessageBox 类型的函数指针  
typedef int(__stdcall* PFN_MESSAGEBOX)(HWND, LPCTSTR, LPCTSTR, DWORD);
 
//线程函数定义  
DWORD __stdcall threadProc(LPVOID lParam)
{
	RemoteParam* pRP = (RemoteParam*)lParam;
	PFN_MESSAGEBOX pfnMessageBox;
	pfnMessageBox = (PFN_MESSAGEBOX)pRP->dwMessageBox;
	pfnMessageBox(NULL, pRP->szMsg, pRP->szMsg, 0);
	return 0;
}
//提升进程访问权限  
bool enableDebugPriv()
{
	HANDLE hToken;
	LUID sedebugnameValue;
	TOKEN_PRIVILEGES tkp;
 
	if (!OpenProcessToken(GetCurrentProcess(),
		TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
		return false;
	}
	if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &sedebugnameValue)) {
		CloseHandle(hToken);
		return false;
	}
	tkp.PrivilegeCount = 1;
	tkp.Privileges[0].Luid = sedebugnameValue;
	tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL)) {
		CloseHandle(hToken);
		return false;
	}
	return true;
}
 
//根据进程名称得到进程 ID, 如果有多个运行实例的话,返回第一个枚举到的进程的 ID  
DWORD processNameToId(LPCTSTR lpszProcessName)
{
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	PROCESSENTRY32 pe;
	pe.dwSize = sizeof(PROCESSENTRY32);
	if (!Process32First(hSnapshot, &pe)) {
		MessageBox(NULL,
			"The frist entry of the process list has not been copyied to the buffer",
			"Notice", MB_ICONINFORMATION | MB_OK);
		return 0;
	}
	while (Process32Next(hSnapshot, &pe)) {
		if (!strcmp(lpszProcessName, pe.szExeFile)) {
			return pe.th32ProcessID;
		}
	}
 
	return 0;
}
int main(int argc, char* argv[])
{
 
	//定义线程体的大小  
	const DWORD dwThreadSize = 4096;
	DWORD dwWriteBytes;
	//提升进程访问权限  
	enableDebugPriv();
 
	char* szExeName = (char*)"calc.exe";
 
	DWORD dwProcessId = processNameToId(szExeName);
	if (dwProcessId == 0) {
		MessageBox(NULL, "The target process have not been found !",
			"Notice", MB_ICONINFORMATION | MB_OK);
		return -1;
	}
	//根据进程 ID 得到进程句柄  
	HANDLE hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
 
	if (!hTargetProcess) {
		MessageBox(NULL, "Open target process failed !",
			"Notice", MB_ICONINFORMATION | MB_OK);
		return 0;
	}
 
	//在宿主进程中为线程体开辟一块存储区域  
	//在这里需要注意 MEM_COMMIT | MEM_RESERVE 内存非配类型以及 PAGE_EXECUTE_READWRITE 内存保护类型  
	//其具体含义请参考 MSDN 中关于 VirtualAllocEx 函数的说明。  
	void* pRemoteThread = VirtualAllocEx(hTargetProcess, 0,
		dwThreadSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (!pRemoteThread) {
		MessageBox(NULL, "Alloc memory in target process failed !",
			"notice", MB_ICONINFORMATION | MB_OK);
		return 0;
	}
 
	//将线程体拷贝到宿主进程中  
	if (!WriteProcessMemory(hTargetProcess,
		pRemoteThread, &threadProc, dwThreadSize, 0)) {
		MessageBox(NULL, "Write data to target process failed !",
			"Notice", MB_ICONINFORMATION | MB_OK);
		return 0;
	}
	//定义线程参数结构体变量  
	RemoteParam remoteData;
	ZeroMemory(&remoteData, sizeof(RemoteParam));
 
	//填充结构体变量中的成员  
	HINSTANCE hUser32 = LoadLibrary("User32.dll");
	remoteData.dwMessageBox = (DWORD)GetProcAddress(hUser32, "MessageBoxA");
	strcat_s(remoteData.szMsg, "Hello\0");
 
	//为线程参数在宿主进程中开辟存储区域  
	RemoteParam* pRemoteParam = (RemoteParam*)VirtualAllocEx(
		hTargetProcess, 0, sizeof(RemoteParam), MEM_COMMIT, PAGE_READWRITE);
 
	if (!pRemoteParam) {
		MessageBox(NULL, "Alloc memory failed !",
			"Notice", MB_ICONINFORMATION | MB_OK);
		return 0;
	}
	//将线程参数拷贝到宿主进程地址空间中  
	if (!WriteProcessMemory(hTargetProcess,
		pRemoteParam, &remoteData, sizeof(remoteData), 0)) {
		MessageBox(NULL, "Write data to target process failed !",
			"Notice", MB_ICONINFORMATION | MB_OK);
		return 0;
	}
 
	//在宿主进程中创建线程  
	HANDLE hRemoteThread = CreateRemoteThread(
		hTargetProcess, NULL, 0, (DWORD(__stdcall*)(void*))pRemoteThread,
		pRemoteParam, 0, &dwWriteBytes);
	if (!hRemoteThread) {
		MessageBox(NULL, "Create remote thread failed !", "Notice", MB_ICONINFORMATION | MB_OK);
		return 0;
	}
	CloseHandle(hRemoteThread);
	FreeLibrary(hUser32);
	return 0;
}

调用 ZwCreateThreadEx

////////////////////////////////
//
// FileName : KernelFuncInject.cpp
// Creator : PeterZheng
// Date : 2019/01/10 21:32
// Comment : Use Kernel Function To Inject
//
////////////////////////////////
 
#pragma once
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <strsafe.h>
#include <Windows.h>
#include <TlHelp32.h>
 
#ifdef _WIN64
typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	ULONG CreateThreadFlags,
	SIZE_T ZeroBits,
	SIZE_T StackSize,
	SIZE_T MaximumStackSize,
	LPVOID pUnkown);
#else
typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	BOOL CreateSuspended,
	DWORD dwStackSize,
	DWORD dw1,
	DWORD dw2,
	LPVOID pUnkown);
#endif
 
using namespace std;
 
// 提权函数
BOOL EnableDebugPriv(LPCSTR name)
{
	HANDLE hToken;
	LUID luid;
	TOKEN_PRIVILEGES tp;
	// 打开进程令牌
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hToken))
	{
		printf("[!]Get Process Token Error!\n");
		return false;
	}
	// 获取权限 Luid
	if (!LookupPrivilegeValue(NULL, name, &luid))
	{
		printf("[!]Get Privilege Error!\n");
		return false;
	}
	tp.PrivilegeCount = 1;
	tp.Privileges[0].Luid = luid;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	// 修改进程权限
	if (!AdjustTokenPrivileges(hToken, false, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
	{
		printf("[!]Adjust Privilege Error!\n");
		return false;
	}
	return true;
}
 
// 根据进程名字获取进程 Id
BOOL GetProcessIdByName(CHAR *szProcessName, DWORD& dwPid)
{
	HANDLE hSnapProcess = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hSnapProcess == NULL)
	{
		printf("[*] Create Process Snap Error!\n");
		return FALSE;
	}
	PROCESSENTRY32 pe32 = { 0 };
	::RtlZeroMemory(&pe32, sizeof(pe32));
	pe32.dwSize = sizeof(pe32);
	BOOL bRet = ::Process32First(hSnapProcess, &pe32);
	while (bRet)
	{
		if (_stricmp(pe32.szExeFile, szProcessName) == 0)
		{
			dwPid = pe32.th32ProcessID;
			return TRUE;
		}
		bRet = ::Process32Next(hSnapProcess, &pe32);
	}
	return FALSE;
}
 
int main(int argc, char* argv[])
{
	if (argc != 3)
	{
		printf("[*] Format Error!  \nYou Should FOLLOW THIS FORMAT: <APCInject EXENAME DLLNAME> \n");
		return 0;
	}
	LPSTR szExeName = (LPSTR)::VirtualAlloc(NULL, 100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	LPSTR szDllPath = (LPSTR)::VirtualAlloc(NULL, 100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	::RtlZeroMemory(szExeName, 100);
	::RtlZeroMemory(szDllPath, 100);
	::StringCchCopy(szExeName, 100, argv[1]);
	::StringCchCopy(szDllPath, 100, argv[2]);
	DWORD dwPid = 0;
	// 系统进程必须先提权才能打开,否则在 OpenProcess 步骤会失败
	EnableDebugPriv(SE_DEBUG_NAME);
	BOOL bRet = GetProcessIdByName(szExeName, dwPid);
	if (!bRet)
	{
		printf("[*] Get Process Id Error!\n");
		return 0;
	}
	HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
	if (hProcess == NULL)
	{
		printf("[*] Open Process Error!\n");
		return 0;
	}
	DWORD dwDllPathLen = strlen(szDllPath) + 1;
	LPVOID lpBaseAddress = ::VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	if (lpBaseAddress == NULL)
	{
		printf("[*] VirtualAllocEx Error!\n");
		return 0;
	}
	SIZE_T dwWriten = 0;
	// 把 DLL 路径字符串写入目标进程
	::WriteProcessMemory(hProcess, lpBaseAddress, szDllPath, dwDllPathLen, &dwWriten);
	if (dwWriten != dwDllPathLen)
	{
		printf("[*] Write Process Memory Error!\n");
		return 0;
	}
	// 获取 LoadLibrary 函数地址
	LPVOID pLoadLibraryFunc = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
	if (pLoadLibraryFunc == NULL)
	{
		printf("[*] Get Func Address Error!\n");
		return 0;
	}
	HMODULE hNtdll = ::LoadLibrary("ntdll.dll");
	if (hNtdll == NULL)
	{
		printf("[*] Load NtDLL Error!\n");
		return 0;
	}
	typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdll, "ZwCreateThreadEx");
	if (ZwCreateThreadEx == NULL)
	{
		printf("[*] Get NTDLL Func Address Error!\n");
		return 0;
	}
	DWORD dwStatus = 0;
	HANDLE hRemoteThread = NULL;
	dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pLoadLibraryFunc, lpBaseAddress, 0, 0, 0, 0, NULL);
	if (hRemoteThread == NULL)
	{
		printf("[*] Create Remote Thread Error!\n");
		return 0;
	}
 
	// DLL 路径分割,方便输出
	LPCSTR szPathSign = "\\";
	LPSTR p = NULL;
	LPSTR next_token = NULL;
	p = strtok_s(szDllPath, szPathSign, &next_token);
	while (p)
	{
		StringCchCopy(szDllPath, 100, p);
		p = strtok_s(NULL, szPathSign, &next_token);
	}
	printf("[*] High Privilege Inject Info [%s ==> %s] Success\n", szDllPath, szExeName);
 
	::CloseHandle(hProcess);
	::FreeLibrary(hNtdll);
	::VirtualFree(szExeName, 0, MEM_RELEASE);
	::VirtualFree(szDllPath, 0, MEM_RELEASE);
	::ExitProcess(0);
	return 0;
}

ZwCreateThreadEx 分析

ZwCreateThreadEx 属于 native 函数,微软官方并没有给出相关文档,本小节分析内容参考本文引用文章。

NTSTATUS ZwCreateThreadEx(
    OUT PHANDLE ThreadHandle,  //输出参数,新创建的线程的句柄。
    IN ACCESS_MASK DesiredAccess,  //所需的访问权限标志,例如 PROCESS_ALL_ACCESS 代表全部权限
    IN PVOID ObjectAttributes,  //对象的属性,通常为 NULL。
    IN HANDLE ProcessHandle,  //所创建线程将要在其内运行的进程的句柄
    IN PTHREAD_START_ROUTINE StartRoutine,  //新线程的开始地址
    IN PVOID Argument,  //要传递给新线程的参数
    IN ULONG CreateFlags,  //要传递给新线程的参数
    
    //ZeroBits, StackSize, MaximumStackSize: 这些参数一般设置为 0,表示使用默认的堆栈大小
    IN ULONG_PTR ZeroBits,  
    IN SIZE_T StackSize,
    IN SIZE_T MaximumStackSize,
    
    IN PPS_ATTRIBUTE_LIST AttributeList  //用于传递更高级的线程属性,通常设置为 NULL
);

区分 32 位和 64 位操作系统:

#ifdef _WIN64
typedef DWORD(WINAPI* Fn_ZwCreateThreadEx)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	ULONG CreateThreadFlags,
	SIZE_T ZeroBits,
	SIZE_T StackSize,
	SIZE_T MaximumStackSize,
	LPVOID pUnkown);
#else
typedef DWORD(WINAPI* Fn_ZwCreateThreadEx)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	BOOL CreateSuspended,
	DWORD dwStackSize,
	DWORD dw1,
	DWORD dw2,
	LPVOID pUnkown);
#endif

CreateRemoteThread 函数调用分析

如下所示为上述代码在调用 CreateRemoteThread 之后的堆栈情况。

[0x0]   ntdll!NtCreateThreadEx   0x57f524   0x77809a17   
[0x1]   KERNELBASE!CreateRemoteThreadEx+0x1e7   0x57f528   0x774b2e38   
[0x2]   KERNEL32!CreateRemoteThreadStub+0x28   0x57f81c   0x5a2c7a   
[0x3]   DllInject!main+0x37a   0x57f844   0x5a36d3   
[0x4]   DllInject!invoke_main+0x33   0x57f998   0x5a3527   

通过 Windbg查看函数 CreateRemoteThead 在用户模式下的调用流程,观察这个调用情况可以确定在用户模式下,这个函数涉及到了三个 dll 模块(KERNEL32、KERNELBASE、ntdll)。而 CreateRemoteThead 这个 API 在 KERNEL32 模块中真正的函数名是 CreateRemoteThreadStub ,通过这个 KERNEL32 中的 CreateRemoteThreadStub API 将参数转发到 KERNELBASE 模块中的 CreateRemoteThreadEx 中,然后在 KERNELBASE 中调用 ntdll 模块中的 NtCreateThreadEx API,进入内核。待内核处理结束后获取返回值,进行返回值的处理并返回结果。

系统调用以及 SSDT(System Service Descriptor Table)文章推荐:强烈建议可以阅读这篇文章 Windows-Internals/System Architecture and Components/System Service Descriptor Table.md at main · Faran-17/Windows-Internals

函数说明

根据上述已经有的堆栈,结合 IDA 查看下函数调用。

CreateRemoteThreadStub

如下所示,为 CreateRemoteThreadStub 汇编和反汇编的代码,可以看到在 CreateRemoteThreadStub 中增加了一个参数,并且 dwCreationFlags 参数进行了校验,与标记 0x1004 进行与操作,规避了无效参数,含义如下所示,而也就是增加的参数,会存在注入的 dll 不启动的情况。

含义
0线程在创建后立即运行。
CREATE_SUSPENDED 0x00000004线程以挂起状态创建,在调用 ResumeThread 函数之前不会运行。
STACK_SIZE_PARAM_IS_A_RESERVATION 0x00010000dwStackSize 参数指定堆栈的初始保留大小。 如果未指定此标志, dwStackSize 将指定提交大小。

image-20240426154912568 image-20240426154930989

CreateRemoteThreadEx

CreateRemoteThread 函数位于 KernelBase.dll 中,最终调用了 NtCreateThreadEx 函数

如下所示为反汇编的 CreateRemoteThreadEx 函数。

image-20240426164918321

这里可以关注下 第七个 参数,始终为 1,这个参数表示创建的线程始终为挂起状态。参考:安全之路 —— 利用内核函数实现注入系统进程 - 倚剑问天 - 博客园

image-20240426165245346

函数原型:

NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateThreadEx(
    _Out_ PHANDLE ThreadHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_ HANDLE ProcessHandle,
    _In_ PVOID StartRoutine, // PUSER_THREAD_START_ROUTINE
    _In_opt_ PVOID Argument,
    _In_ ULONG CreateFlags, // THREAD_CREATE_FLAGS_*
    _In_ SIZE_T ZeroBits,
    _In_ SIZE_T StackSize,
    _In_ SIZE_T MaximumStackSize,
    _In_opt_ PPS_ATTRIBUTE_LIST AttributeList
);

NtCreateThreadEx

这里先补充一个知识点,“NtCreateThreadZwCreateThread的函数地址是一样的,NtCreateThreadExZwCreateThreadEx的函数地址是一样的。区别就是 Nt 的为用户层函数,用户态调用时会使用 Nt 开头的,Zw 开头的会在内核调用。 可以利用ZwCreateThread` 实现系统进程注入。前文有[相关代码](#调用 ZwCreateThreadEx). image-20240426170149651 用户态一般不会直接调用这个接口

image-20240428101833404

汇编说明:

mov eax, 0C7h:将值 0c7h 移动至 eax 寄存器,表示执行编号为 0c7h 的系统调用

[[【汇编】SyscallWithASM|syscall]]:这是执行系统调用的指令。系统调用编号和参数应在此之前已被设置好(在这种情况下,通过将 0xC7rcx 的内容移动到 eaxr10

这段代码表示执行编号为 0xC7 的系统调用,如果 ds:7FFE0308h 的最低位被设置,那么在系统调用前会先跳转到另一段代码(位于 loc_1800A0525,使用 int 2Eh 中断进入内核)。如果该位未被设置,那么直接执行系统调用

可以结合反汇编代码查看,如下所示:

__int64 NtCreateThreadEx()
{
  __int64 result; // rax
 
  result = 194i64;
  if ( (MEMORY[0x7FFE0308] & 1) != 0 ) // 查看标志位
    __asm { int     2Eh; DOS 2+ internal - EXECUTE COMMAND }
  else
    __asm { syscall; Low latency system call }
  return result;
}

总结

以下是 CreateRemoteThread 函数的调用流程图

  1. 应用程序调用 CreateRemoteThread,这是一个由 kernel32.dll 提供的 Win32 API,用于在另一个进程的地址空间中创建新线程。
  2. CreateRemoteThread 内部调用 CreateRemoteThreadEx,这是一个由 KernelBase.dll 提供的更底层的 API,提供了更多的选项,比如可以指定安全描述符,可以控制新线程是否立即开始运行等
  3. CreateRemoteThreadEx 内部调用 NtCreateThreadEx,这是由 ntdll.dll 提供的 Native API,也是用户空间可以直接调用的最底层的 API。
  4. NtCreateThreadEx 函数设置好系统调用的参数后,执行 syscall 指令,切换到内核模式。
  5. 在内核模式下,根据 syscall 提供的系统调用编号,在 SSDT 表中查找对应的内核函数。
  6. 执行 SSDT 表中找到的函数,完成线程的创建。
flowchart TD
    A(<b>CreateRemoteThread</b>)  --> B
    B(Kernel32!CreateRemoteThreadSutb) --> C
    C(Kernel32!CreateRemoteThreadEx) --> D
    D(ntdll!NtcreateThreadEx) --> F
     F(<b>nt!NtcreateThreadEx</b>)  --> E
     E(syscall) --> G
     subgraph 内核区
     G(SSDT)
     end

如下所示为一个略微详细的启动分析:

{% plantuml %} @startuml ! theme materia skinparam defaultFontName WenQuanYi Micro Hei skin rose title CreateThreadEx 调用逻辑 : CreateRemoteThreadEx; : 检查 dwCreationFlags 是否有效; if (检查结果) then (无效) : 返回参数无效; end else (有效) : 格式化参数为 Ring0 做准备; : 检查返回值是否正确; if (检查结果) then (错误) : 返回错误码; end else (正确) : 检查是否存在扩展参数列表; if (检查结果) then (存在) : 格式化参数列表为 ring0 做准备; else : bFlag = 1; endif if (hProcess == -1) then (true) note left 调用 CreateThread 时,hProcess 为 -1 end note else (false) : 拷贝一份进程句柄为 ring0 做准备; : 通过进程句柄获取进程 PID; if (?获取成功 && 进程 ID 是不是当前进程 ID) then (true) : bFlag = 0; : 获取子系统类型; if (获取成功&&系统类型错误) then (true) else (false) label 获取 ID if (获取成功) then (true) if (bFlag != 0)then(true) : 获取上下文环境,确定启动线程是否挂起; else(false) endif : 调用 NtCreateThreadEx 进入内核; if (检查返回值) then (true) if(检查上下文)then(true) : 申请栈空间; : 激活栈空间; else(false) endif if(根据全局变量判断是否是远程线程)then(true) : 加载模块,获取函数地址; : 通过线程句柄获取进程 ID; if(判断线程是否需要挂起)then(true) else(false) : Resume 目标线程; endif else(false) endif else(false) endif else (false) endif endif else (false) endif endif : 释放资源; : 返回线程句柄; end endif endif @enduml {% endplantuml %}

普通线程和远程线程的区别

可以看到普通线程函数 CreateThread 也调用了 CreateRemoteThread 函数,只不过其线程句柄参数的值为 -1,而远程线程的句柄参数为 目标进程句柄

image-20240428103631108