阅读时间: 3-4 分钟
前置知识: C++ 基础、Windows API 基础
学习目标: 理解进程注入原理,掌握远程线程注入技术
📺 配套视频教程
本文配套视频教程已发布在 B 站,建议结合视频学习效果更佳:
💡 提示: 点击视频右下角可全屏观看,建议配合文章食用!
视频链接: https://www.bilibili.com/video/BV1wGnozPEVA/
课程引入:为什么要学习进程注入?
在 Windows 系统安全、游戏安全、恶意软件分析等领域,进程注入(Process Injection)是一个绕不开的核心技术。
你可能见过这些场景:
- 🎮 游戏外挂修改游戏逻辑,实现透视、自瞄
- 🛡️ 杀毒软件注入监控模块,实时检测恶意行为
- 🔧 调试器(如 Visual Studio)注入调试引擎,实现断点、单步执行
- 💉 恶意软件注入正常进程,隐藏自己逃避检测
这些都是进程注入技术的实际应用。
本课程的学习目标:
- 理解进程注入的原理与实现
- 掌握远程线程注入的完整流程
- 为后续的攻防对抗课程打下基础
重要声明: 本课程仅用于防御性安全研究与教学,所有技术均应用于提升安全防护能力,严禁用于非法用途。
什么是进程注入
定义
进程注入(Process Injection)是一种将自己的代码插入到其他正在运行的进程中,并让目标进程执行这些代码的技术。
简单来说,就是:
借用别人的进程来运行自己的代码
生活类比:玩具工厂的故事
想象一家正常运营的玩具工厂,每天按照正常流程生产各种玩具。
正常情况:
- 工厂接收订单 → 按图纸生产 → 出厂检验 → 发货
进程注入的场景:
现在有个人想生产假冒产品,但又不想被发现。于是他:
潜入工厂(获取进程访问权限)
- 伪装成工人,混进工厂
- 对应代码:
OpenProcess
占据生产线空位(分配内存)
- 在车间找个空闲的工作台
- 对应代码:
VirtualAllocEx
偷运假冒图纸(写入代码/数据)
- 把假冒产品的设计图纸带进来
- 对应代码:
WriteProcessMemory
启动生产(创建线程执行)
- 启动生产线,开始生产假冒产品
- 对应代码:
CreateRemoteThread
这样,工厂表面上还是在正常生产玩具,但实际上也在偷偷生产假冒产品。外界看到的是”正常工厂”,内部却在执行”恶意操作”。
技术本质
从技术角度看,进程注入利用了 Windows 进程的以下特性:
进程间访问机制
- Windows 允许具有足够权限的进程访问其他进程的内存
内存空间独立性
- 每个进程有独立的虚拟地址空间
- 但具有权限的进程可以操作其他进程的内存
线程执行机制
- 进程通过线程执行代码
- 可以在目标进程中创建新线程来执行注入的代码
进程注入的典型应用场景
合法用途
调试与逆向工程
- 调试器(如 Visual Studio、WinDbg)通过注入调试引擎实现断点、单步执行
- 性能分析工具(如 Intel VTune)注入性能监控代码
安全监控与防护
- 杀毒软件注入监控模块,实时检测恶意行为
- DLP(数据防泄漏)软件注入监控敏感数据操作
辅助功能与增强工具
- 输入法程序注入到应用,提供输入支持
- 截图工具、翻译工具注入目标程序获取界面内容
攻击用途(仅供防御学习)
游戏外挂
- 注入作弊代码,实现透视、自瞄、无敌等功能
恶意软件
- 木马、后门程序注入正常进程隐藏自己
- 窃取敏感数据(如银行账号、密码)
绕过安全检测
- 通过注入到白名单进程,绕过安全软件检测
重要声明: 本课程仅用于防御性安全研究与教学,所有技术均应用于提升安全防护能力,严禁用于非法用途。
进程注入的四个核心步骤
无论使用哪种注入技术,核心流程都可以归纳为四个步骤:
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
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 请求的访问权限
BOOL bInheritHandle, // 句柄是否可继承
DWORD dwProcessId // 目标进程 ID
);参数说明:
dwDesiredAccess:请求的权限PROCESS_ALL_ACCESS:完全访问权限(包括读写内存、创建线程)PROCESS_VM_WRITE:写入内存权限PROCESS_VM_OPERATION:内存操作权限PROCESS_CREATE_THREAD:创建线程权限
bInheritHandle:通常设为FALSEdwProcessId:目标进程的 PID
代码实现
// 目标进程 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
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:可读可写
代码实现
// DLL 完整路径
const char* dllPath = "C:\injection.dll";
// 计算路径长度(包括结尾的 '')
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
BOOL WriteProcessMemory(
HANDLE hProcess, // 目标进程句柄
LPVOID lpBaseAddress, // 目标地址(远程内存地址)
LPCVOID lpBuffer, // 源数据(本地内存地址)
SIZE_T nSize, // 写入大小
SIZE_T *lpNumberOfBytesWritten // 实际写入字节数(可为 NULL)
);参数说明:
hProcess:目标进程句柄lpBaseAddress:目标地址(步骤二分配的远程内存地址)lpBuffer:源数据(DLL 路径字符串)nSize:写入大小(路径长度)lpNumberOfBytesWritten:实际写入字节数,可为NULL
代码实现
// 将 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 读取刚才写入的内容,验证是否正确:
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 函数原型:
HMODULE LoadLibraryA(LPCSTR lpLibFileName);- 参数:DLL 文件路径(字符串指针)
- 返回值:DLL 模块句柄
关键技巧:
LoadLibraryA位于kernel32.dll中kernel32.dll在所有进程中加载地址相同- 因此,在注入程序中获取的
LoadLibraryA地址,在目标进程中同样有效
核心 API:GetModuleHandle 和 GetProcAddress
// 1. 获取 kernel32.dll 模块句柄
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
// 2. 获取 LoadLibraryA 函数地址
FARPROC pLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");GetModuleHandleA:
- 获取指定模块的句柄
- 参数:模块名(如
"kernel32.dll") - 返回值:模块基地址
GetProcAddress:
- 获取指定函数的地址
- 参数:模块句柄、函数名
- 返回值:函数地址
代码实现
// 获取 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.dll、ntdll.dll等系统 DLL 在所有进程中的加载地址相同- 原因:系统优化,减少内存占用
- 因此,在注入程序中获取的函数地址,在目标进程中同样有效
步骤五:创建远程线程执行注入
原理讲解
现在我们已经准备好了所有资源:
- ✅ 目标进程句柄(步骤一)
- ✅ 远程内存地址,存放 DLL 路径(步骤二、三)
- ✅
LoadLibraryA函数地址(步骤四)
最后一步:
- 在目标进程中创建一个新线程
- 让这个线程执行
LoadLibraryA(dllPath) - 从而加载我们的 DLL
线程的本质:
- 线程是进程的执行单元
- 线程从指定的函数地址开始执行
- 我们指定的起始地址是
LoadLibraryA - 线程的参数是 DLL 路径(远程内存地址)
核心 API:CreateRemoteThread
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:线程安全属性,通常为NULLdwStackSize:栈大小,0表示使用默认大小lpStartAddress:线程起始地址(LoadLibraryA地址)lpParameter:线程参数(DLL 路径的远程内存地址)dwCreationFlags:创建标志,0表示立即执行lpThreadId:线程 ID,可为NULL
代码实现
// 创建远程线程
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. CreateRemoteThread 被调用
↓
2. 目标进程创建新线程
↓
3. 新线程从 LoadLibraryA 地址开始执行
↓
4. LoadLibraryA(pRemoteMemory) 被调用
↓
5. 加载 C:injection.dll
↓
6. 执行 DLL 的 DllMain 函数
↓
7. DllMain 中弹出 MessageBox("注入成功")
↓
8. 注入完成
资源清理:善后工作
注入完成后,需要清理分配的资源,避免内存泄漏。
清理步骤
// 1. 关闭远程线程句柄
CloseHandle(hRemoteThread);
// 2. 释放远程内存
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
// 3. 关闭进程句柄
CloseHandle(hProcess);
std::cout << "资源清理完成" << std::endl;注意事项
- ✅ 必须清理:避免句柄泄漏,耗尽系统资源
- ✅ 清理顺序:先关闭线程句柄,再释放内存,最后关闭进程句柄
- ⚠️ DLL 仍然加载:即使释放了 DLL 路径的内存,DLL 已经加载到目标进程,不会被卸载
完整代码示例
/**
* @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;
}运行与验证
准备 DLL
创建一个简单的 DLL(injection.dll),在 DllMain 中弹出提示:
#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;
}测试步骤
- 启动目标进程(如
notepad.exe),记录 PID - 运行注入程序,输入目标 PID
- 观察结果:目标进程弹出 “DLL 注入成功” 提示框
效果验证
成功运行注入程序后,可以看到以下效果:

验证要点:
- ✅ 目标进程成功加载了注入的 DLL
- ✅ 弹出 MessageBox 显示”DLL 注入成功!”
- ✅ 目标进程继续正常运行,未崩溃
- ✅ 可以使用 Process Explorer 等工具查看目标进程已加载的模块列表,确认 DLL 已被加载
总结
核心知识点
进程注入的本质
- 跨进程执行代码的技术
- 借用目标进程的执行环境
远程线程注入的五个步骤
- OpenProcess → VirtualAllocEx → WriteProcessMemory → GetProcAddress → CreateRemoteThread
关键 API
OpenProcess:获取进程访问权限VirtualAllocEx:分配远程内存WriteProcessMemory:跨进程写入数据GetProcAddress:获取函数地址CreateRemoteThread:创建远程线程
核心原理
kernel32.dll在所有进程中地址相同- 线程从指定地址开始执行,参数通过寄存器传递
互动讨论
欢迎在评论区讨论以下问题:
- 你在实际应用中遇到过哪些进程注入的场景(合法或非法)?
- 除了 DLL 注入,还能注入什么类型的代码?
- 如果你是防御者,你会如何检测这种注入?
⚠️ 本课程内容仅供防御性安全研究与教学使用,请勿用于非法用途。
