阅读时间: 3-4 分钟
前置知识: C++ 基础、Windows API 基础
学习目标: 理解进程注入原理,掌握远程线程注入技术


📺 配套视频教程

本文配套视频教程已发布在 B 站,建议结合视频学习效果更佳:

💡 提示: 点击视频右下角可全屏观看,建议配合文章食用!

视频链接: https://www.bilibili.com/video/BV1wGnozPEVA/


课程引入:为什么要学习进程注入?

在 Windows 系统安全、游戏安全、恶意软件分析等领域,进程注入(Process Injection)是一个绕不开的核心技术。

你可能见过这些场景:

  • 🎮 游戏外挂修改游戏逻辑,实现透视、自瞄
  • 🛡️ 杀毒软件注入监控模块,实时检测恶意行为
  • 🔧 调试器(如 Visual Studio)注入调试引擎,实现断点、单步执行
  • 💉 恶意软件注入正常进程,隐藏自己逃避检测

这些都是进程注入技术的实际应用。

本课程的学习目标:

  1. 理解进程注入的原理与实现
  2. 掌握远程线程注入的完整流程
  3. 为后续的攻防对抗课程打下基础

重要声明: 本课程仅用于防御性安全研究与教学,所有技术均应用于提升安全防护能力,严禁用于非法用途。


什么是进程注入

定义

进程注入(Process Injection)是一种将自己的代码插入到其他正在运行的进程中,并让目标进程执行这些代码的技术。

简单来说,就是:

借用别人的进程来运行自己的代码

生活类比:玩具工厂的故事

想象一家正常运营的玩具工厂,每天按照正常流程生产各种玩具。

正常情况:

  • 工厂接收订单 → 按图纸生产 → 出厂检验 → 发货

进程注入的场景:

现在有个人想生产假冒产品,但又不想被发现。于是他:

  1. 潜入工厂(获取进程访问权限)

    • 伪装成工人,混进工厂
    • 对应代码:OpenProcess
  2. 占据生产线空位(分配内存)

    • 在车间找个空闲的工作台
    • 对应代码:VirtualAllocEx
  3. 偷运假冒图纸(写入代码/数据)

    • 把假冒产品的设计图纸带进来
    • 对应代码:WriteProcessMemory
  4. 启动生产(创建线程执行)

    • 启动生产线,开始生产假冒产品
    • 对应代码:CreateRemoteThread

这样,工厂表面上还是在正常生产玩具,但实际上也在偷偷生产假冒产品。**外界看到的是”正常工厂”,内部却在执行”恶意操作”**。

技术本质

从技术角度看,进程注入利用了 Windows 进程的以下特性:

  1. 进程间访问机制

    • Windows 允许具有足够权限的进程访问其他进程的内存
  2. 内存空间独立性

    • 每个进程有独立的虚拟地址空间
    • 但具有权限的进程可以操作其他进程的内存
  3. 线程执行机制

    • 进程通过线程执行代码
    • 可以在目标进程中创建新线程来执行注入的代码

进程注入的典型应用场景

合法用途

  1. 调试与逆向工程

    • 调试器(如 Visual Studio、WinDbg)通过注入调试引擎实现断点、单步执行
    • 性能分析工具(如 Intel VTune)注入性能监控代码
  2. 安全监控与防护

    • 杀毒软件注入监控模块,实时检测恶意行为
    • DLP(数据防泄漏)软件注入监控敏感数据操作
  3. 辅助功能与增强工具

    • 输入法程序注入到应用,提供输入支持
    • 截图工具、翻译工具注入目标程序获取界面内容

攻击用途(仅供防御学习)

  1. 游戏外挂

    • 注入作弊代码,实现透视、自瞄、无敌等功能
  2. 恶意软件

    • 木马、后门程序注入正常进程隐藏自己
    • 窃取敏感数据(如银行账号、密码)
  3. 绕过安全检测

    • 通过注入到白名单进程,绕过安全软件检测

重要声明: 本课程仅用于防御性安全研究与教学,所有技术均应用于提升安全防护能力,严禁用于非法用途。


进程注入的四个核心步骤

无论使用哪种注入技术,核心流程都可以归纳为四个步骤:

1
2
3
4
5
6
7
1. 获取访问权限(OpenProcess)

2. 分配内存空间(VirtualAllocEx)

3. 写入代码/数据(WriteProcessMemory)

4. 执行代码(CreateRemoteThread 或其他方式)

接下来,我们用远程线程注入(最经典的注入方式)来详细讲解这四个步骤。


准备工作:明确目标与资源

确定注入目标

在开始注入之前,我们需要明确两个关键要素:

1. 目标进程

  • 进程 ID(PID)或进程名
  • 本例使用 PID 1234 作为演示

2. 注入内容

  • DLL 文件路径:C:\injection.dll
  • DLL 功能:弹出”注入成功”提示框

注入成功的验证标准

  • ✅ 目标进程成功加载 injection.dll
  • ✅ 弹出 MessageBox 提示”注入成功”
  • ✅ 目标进程未崩溃,功能正常

步骤一:获取目标进程的访问权限

原理讲解

在 Windows 中,进程之间是相互隔离的。要操作其他进程,必须先获得进程句柄(Process Handle),它类似于”访问通行证”。

进程句柄的作用:

  • 标识目标进程
  • 携带访问权限信息
  • 后续所有操作都需要通过这个句柄

核心 API:OpenProcess

1
2
3
4
5
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 请求的访问权限
BOOL bInheritHandle, // 句柄是否可继承
DWORD dwProcessId // 目标进程 ID
);

参数说明:

  • dwDesiredAccess:请求的权限

    • PROCESS_ALL_ACCESS:完全访问权限(包括读写内存、创建线程)
    • PROCESS_VM_WRITE:写入内存权限
    • PROCESS_VM_OPERATION:内存操作权限
    • PROCESS_CREATE_THREAD:创建线程权限
  • bInheritHandle:通常设为 FALSE

  • dwProcessId:目标进程的 PID

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 目标进程 PID
DWORD targetPid = 1234;

// 打开目标进程,请求完全访问权限
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // 完全访问权限
FALSE, // 句柄不可继承
targetPid // 目标进程 ID
);

if (hProcess == NULL)
{
std::cout << "打开进程失败,错误码:" << GetLastError() << std::endl;
return -1;
}

std::cout << "成功获取进程句柄:" << hProcess << std::endl;

权限要求

管理员权限:

  • 访问系统进程或高权限进程时,需要以管理员身份运行注入程序

权限不足的典型错误:

  • 错误码 5(ERROR_ACCESS_DENIED):访问被拒绝
  • 原因:当前进程权限不足,无法访问目标进程

步骤二:在目标进程中分配内存空间

原理讲解

获得进程句柄后,我们需要在目标进程的虚拟地址空间中分配一块内存,用于存放 DLL 路径字符串。

为什么需要分配内存?

  • 目标进程有独立的虚拟地址空间
  • 我们的 DLL 路径字符串在注入程序的内存中
  • 需要在目标进程中分配空间,把路径复制过去

类比:

  • 就像在工厂(目标进程)里找个空地方(分配内存)
  • 用来存放设备(DLL 路径)

核心 API:VirtualAllocEx

1
2
3
4
5
6
7
LPVOID VirtualAllocEx(
HANDLE hProcess, // 目标进程句柄
LPVOID lpAddress, // 分配地址(通常为 NULL,让系统自动选择)
SIZE_T dwSize, // 分配大小(字节)
DWORD flAllocationType,// 分配类型
DWORD flProtect // 内存保护属性
);

参数说明:

  • hProcess:目标进程句柄(步骤一获得)
  • lpAddress:指定分配地址,通常为 NULL(让系统自动选择)
  • dwSize:分配大小(DLL 路径长度)
  • flAllocationType
    • MEM_COMMIT | MEM_RESERVE:提交并预留内存
  • flProtect
    • PAGE_READWRITE:可读可写

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// DLL 完整路径
const char* dllPath = "C:\\injection.dll";

// 计算路径长度(包括结尾的 '\0')
size_t pathLength = strlen(dllPath) + 1;

// 在目标进程中分配内存
LPVOID pRemoteMemory = VirtualAllocEx(
hProcess, // 目标进程句柄
NULL, // 让系统自动选择地址
pathLength, // 分配大小(DLL 路径长度)
MEM_COMMIT | MEM_RESERVE, // 提交并预留
PAGE_READWRITE // 可读可写
);

if (pRemoteMemory == NULL)
{
std::cout << "分配内存失败,错误码:" << GetLastError() << std::endl;
CloseHandle(hProcess);
return -1;
}

std::cout << "成功分配远程内存,地址:0x" << pRemoteMemory << std::endl;

内存保护属性说明

属性 含义 用途
PAGE_READWRITE 可读可写 存放数据(如 DLL 路径)
PAGE_EXECUTE_READWRITE 可读可写可执行 存放代码(如 Shellcode)
PAGE_READONLY 只读 存放常量数据

本例中,我们只需要存放 DLL 路径字符串,因此使用 PAGE_READWRITE 即可。


步骤三:写入 DLL 路径到目标进程

原理讲解

内存分配完成后,我们需要把 DLL 路径字符串从注入程序的内存复制到目标进程的内存中。

跨进程内存写入:

  • 源地址:注入程序的内存(dllPath 变量)
  • 目标地址:目标进程的内存(pRemoteMemory
  • 操作方式:使用 WriteProcessMemory

核心 API:WriteProcessMemory

1
2
3
4
5
6
7
BOOL WriteProcessMemory(
HANDLE hProcess, // 目标进程句柄
LPVOID lpBaseAddress, // 目标地址(远程内存地址)
LPCVOID lpBuffer, // 源数据(本地内存地址)
SIZE_T nSize, // 写入大小
SIZE_T *lpNumberOfBytesWritten // 实际写入字节数(可为 NULL)
);

参数说明:

  • hProcess:目标进程句柄
  • lpBaseAddress:目标地址(步骤二分配的远程内存地址)
  • lpBuffer:源数据(DLL 路径字符串)
  • nSize:写入大小(路径长度)
  • lpNumberOfBytesWritten:实际写入字节数,可为 NULL

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 将 DLL 路径写入目标进程
BOOL writeSuccess = WriteProcessMemory(
hProcess, // 目标进程句柄
pRemoteMemory, // 远程内存地址(目标)
dllPath, // DLL 路径字符串(源)
pathLength, // 写入大小
NULL // 不关心实际写入字节数
);

if (!writeSuccess)
{
std::cout << "写入内存失败,错误码:" << GetLastError() << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE); // 释放分配的内存
CloseHandle(hProcess);
return -1;
}

std::cout << "成功写入 DLL 路径到远程进程" << std::endl;

验证写入是否成功

可选:使用 ReadProcessMemory 读取刚才写入的内容,验证是否正确:

1
2
3
char readBuffer[MAX_PATH] = {0};
ReadProcessMemory(hProcess, pRemoteMemory, readBuffer, pathLength, NULL);
std::cout << "远程内存内容:" << readBuffer << std::endl;

步骤四:获取 LoadLibrary 函数地址

原理讲解

现在,DLL 路径已经写入目标进程的内存中。接下来需要让目标进程加载这个 DLL

核心问题:

  • 如何让目标进程执行”加载 DLL”的操作?

解决方案:

  • 使用 Windows API LoadLibraryA
  • 这个函数的作用就是加载指定路径的 DLL

LoadLibraryA 函数原型:

1
HMODULE LoadLibraryA(LPCSTR lpLibFileName);
  • 参数:DLL 文件路径(字符串指针)
  • 返回值:DLL 模块句柄

关键技巧:

  • LoadLibraryA 位于 kernel32.dll
  • kernel32.dll 在所有进程中加载地址相同
  • 因此,在注入程序中获取的 LoadLibraryA 地址,在目标进程中同样有效

核心 API:GetModuleHandle 和 GetProcAddress

1
2
3
4
5
// 1. 获取 kernel32.dll 模块句柄
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");

// 2. 获取 LoadLibraryA 函数地址
FARPROC pLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");

GetModuleHandleA:

  • 获取指定模块的句柄
  • 参数:模块名(如 "kernel32.dll"
  • 返回值:模块基地址

GetProcAddress:

  • 获取指定函数的地址
  • 参数:模块句柄、函数名
  • 返回值:函数地址

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取 kernel32.dll 模块句柄
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (hKernel32 == NULL)
{
std::cout << "获取 kernel32.dll 失败" << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}

// 获取 LoadLibraryA 函数地址
FARPROC pLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");
if (pLoadLibraryA == NULL)
{
std::cout << "获取 LoadLibraryA 地址失败" << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}

std::cout << "LoadLibraryA 地址:0x" << pLoadLibraryA << std::endl;

为什么 kernel32.dll 地址相同?

Windows 系统特性:

  • kernel32.dllntdll.dll 等系统 DLL 在所有进程中的加载地址相同
  • 原因:系统优化,减少内存占用
  • 因此,在注入程序中获取的函数地址,在目标进程中同样有效

步骤五:创建远程线程执行注入

原理讲解

现在我们已经准备好了所有资源:

  • ✅ 目标进程句柄(步骤一)
  • ✅ 远程内存地址,存放 DLL 路径(步骤二、三)
  • LoadLibraryA 函数地址(步骤四)

最后一步:

  • 在目标进程中创建一个新线程
  • 让这个线程执行 LoadLibraryA(dllPath)
  • 从而加载我们的 DLL

线程的本质:

  • 线程是进程的执行单元
  • 线程从指定的函数地址开始执行
  • 我们指定的起始地址是 LoadLibraryA
  • 线程的参数是 DLL 路径(远程内存地址)

核心 API:CreateRemoteThread

1
2
3
4
5
6
7
8
9
HANDLE CreateRemoteThread(
HANDLE hProcess, // 目标进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全属性(通常为 NULL)
SIZE_T dwStackSize, // 栈大小(0 表示默认)
LPTHREAD_START_ROUTINE lpStartAddress,// 线程起始地址(函数地址)
LPVOID lpParameter, // 线程参数(传递给函数)
DWORD dwCreationFlags,// 创建标志(0 表示立即执行)
LPDWORD lpThreadId // 线程 ID(可为 NULL)
);

参数说明:

  • hProcess:目标进程句柄
  • lpThreadAttributes:线程安全属性,通常为 NULL
  • dwStackSize:栈大小,0 表示使用默认大小
  • lpStartAddress:线程起始地址(LoadLibraryA 地址)
  • lpParameter:线程参数(DLL 路径的远程内存地址)
  • dwCreationFlags:创建标志,0 表示立即执行
  • lpThreadId:线程 ID,可为 NULL

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 创建远程线程
HANDLE hRemoteThread = CreateRemoteThread(
hProcess, // 目标进程句柄
NULL, // 默认安全属性
0, // 默认栈大小
(LPTHREAD_START_ROUTINE)pLoadLibraryA, // 线程起始地址(LoadLibraryA)
pRemoteMemory, // 线程参数(DLL 路径)
0, // 立即执行
NULL // 不关心线程 ID
);

if (hRemoteThread == NULL)
{
std::cout << "创建远程线程失败,错误码:" << GetLastError() << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}

std::cout << "成功创建远程线程,句柄:" << hRemoteThread << std::endl;

// 等待远程线程执行完毕
WaitForSingleObject(hRemoteThread, INFINITE);

std::cout << "远程线程执行完毕,DLL 注入成功!" << std::endl;

执行流程分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. CreateRemoteThread 被调用

2. 目标进程创建新线程

3. 新线程从 LoadLibraryA 地址开始执行

4. LoadLibraryA(pRemoteMemory) 被调用

5. 加载 C:\injection.dll

6. 执行 DLL 的 DllMain 函数

7. DllMain 中弹出 MessageBox("注入成功")

8. 注入完成

资源清理:善后工作

注入完成后,需要清理分配的资源,避免内存泄漏。

清理步骤

1
2
3
4
5
6
7
8
9
10
// 1. 关闭远程线程句柄
CloseHandle(hRemoteThread);

// 2. 释放远程内存
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);

// 3. 关闭进程句柄
CloseHandle(hProcess);

std::cout << "资源清理完成" << std::endl;

注意事项

  • 必须清理:避免句柄泄漏,耗尽系统资源
  • 清理顺序:先关闭线程句柄,再释放内存,最后关闭进程句柄
  • ⚠️ DLL 仍然加载:即使释放了 DLL 路径的内存,DLL 已经加载到目标进程,不会被卸载

完整代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* @file Injection.cpp
* @brief 远程线程注入演示程序
* @details 本程序演示了Windows平台下的远程线程注入技术
* 用于防御性安全研究和教学目的
* @author UD2
* @date 2025-09-25
*/

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

/**
* @brief 程序入口点
* @return 程序执行结果
*/
int main()
{
DWORD processId = 34004;//要注入的进程
char dllPath[] = "C:\\Injection.dll";//dll的路径

std::cout << "目标进程ID:" << processId << std::endl;
std::cout << "DLL路径:" << dllPath << std::endl;

std::cout << "找到目标进程,进程ID:" << processId << std::endl;

// 打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,processId);
if (hProcess == nullptr)
{
std::cout << "错误:无法打开目标进程,错误代码:" << GetLastError() << std::endl;
return 1;
}

// 在目标进程中分配内存
LPVOID pRemoteMemory = VirtualAllocEx(hProcess,
nullptr,
strlen(dllPath) + 1,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (pRemoteMemory == nullptr)
{
std::cout << "错误:无法在目标进程中分配内存,错误代码:" << GetLastError() << std::endl;
CloseHandle(hProcess);
return 1;
}

// 将DLL路径写入目标进程内存
if (!WriteProcessMemory(hProcess,
pRemoteMemory,
dllPath,
strlen(dllPath) + 1,
nullptr))
{
std::cout << "错误:无法写入目标进程内存,错误代码:" << GetLastError() << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

// 获取kernel32.dll模块句柄
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (hKernel32 == nullptr)
{
std::cout << "错误:无法获取kernel32.dll模块句柄" << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
// 获取LoadLibraryA函数地址
FARPROC pLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryA");
if (pLoadLibrary == nullptr)
{
std::cout << "错误:无法获取LoadLibraryA函数地址" << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

// 创建远程线程
HANDLE hRemoteThread = CreateRemoteThread(hProcess,
nullptr,
0,
(LPTHREAD_START_ROUTINE)pLoadLibrary,
pRemoteMemory,
0,
nullptr);
if (hRemoteThread == nullptr)
{
std::cout << "错误:无法创建远程线程,错误代码:" << GetLastError() << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}

// 等待远程线程执行完成
WaitForSingleObject(hRemoteThread, INFINITE);

// 清理资源
CloseHandle(hRemoteThread);
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);
std::cout << "DLL注入成功完成!" << std::endl;
system("pause");
return 0;
}

运行与验证

编译程序

1
cl /std:c++17 /EHsc remote_thread_injection.cpp

准备 DLL

创建一个简单的 DLL(injection.dll),在 DllMain 中弹出提示:

1
2
3
4
5
6
7
8
9
10
#include <windows.h>

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
MessageBoxA(NULL, "DLL 注入成功!", "提示", MB_OK);
}
return TRUE;
}

测试步骤

  1. 启动目标进程(如 notepad.exe),记录 PID
  2. 运行注入程序,输入目标 PID
  3. 观察结果:目标进程弹出 “DLL 注入成功” 提示框

效果验证

成功运行注入程序后,可以看到以下效果:

注入成功效果

验证要点:

  • ✅ 目标进程成功加载了注入的 DLL
  • ✅ 弹出 MessageBox 显示”DLL 注入成功!”
  • ✅ 目标进程继续正常运行,未崩溃
  • ✅ 可以使用 Process Explorer 等工具查看目标进程已加载的模块列表,确认 DLL 已被加载

总结

核心知识点

  1. 进程注入的本质

    • 跨进程执行代码的技术
    • 借用目标进程的执行环境
  2. 远程线程注入的五个步骤

    • OpenProcess → VirtualAllocEx → WriteProcessMemory → GetProcAddress → CreateRemoteThread
  3. 关键 API

    • OpenProcess:获取进程访问权限
    • VirtualAllocEx:分配远程内存
    • WriteProcessMemory:跨进程写入数据
    • GetProcAddress:获取函数地址
    • CreateRemoteThread:创建远程线程
  4. 核心原理

    • kernel32.dll 在所有进程中地址相同
    • 线程从指定地址开始执行,参数通过寄存器传递

互动讨论

欢迎在评论区讨论以下问题:

  1. 你在实际应用中遇到过哪些进程注入的场景(合法或非法)?
  2. 除了 DLL 注入,还能注入什么类型的代码?
  3. 如果你是防御者,你会如何检测这种注入?

⚠️ 本课程内容仅供防御性安全研究与教学使用,请勿用于非法用途。