阅读时间: 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. 获取访问权限(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:通常设为 FALSE

  • dwProcessId:目标进程的 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.dllntdll.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:线程安全属性,通常为 NULL
  • dwStackSize:栈大小,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;
}

测试步骤

  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. 如果你是防御者,你会如何检测这种注入?

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

By UD2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注