DMALibrary Shellcode注入教程:如何寻找Code Cave并用JMP跳转调用任意函数
DMALibrary 是一款面向游戏破解场景的开源 DMA 内存库,内置开箱即用的 Code Cave 查找器与 Shellcode 注入能力。本教程带你看懂它的 c_shellcode 类:如何三步定位目标进程中带读、写、执行权限的空闲内存(Code Cave),再用一条 14 字节的 JMP 跳转指令挂钩任意函数入口,实现 Shellcode 执行与任意函数调用 🎯
📌 Code Cave 是什么:一句话看懂 Shellcode 注入的核心
Shellcode 要在目标进程里跑起来,首先得有一块「能读、能写、能执行(R/W/X),并且恰好是空的」内存。这块内存就叫 Code Cave。
DMALibrary 的注入链路可以概括为三步:
- 找位置:在目标模块的节区末尾页对齐处,定位满足权限与大小的空闲空间;
- 写代码:把你的 Shellcode 写入这块空间;
- 搭跳板:用 JMP 跳转把某个函数的入口"劫持"到你的 Shellcode,触发执行。
下面按源码逐层拆解。
🛠️ 快速上手:让 DMALibrary 跑起来的 3 个步骤
步骤 1:获取项目
git clone https://gitcode.com/gh_mirrors/dm/DMALibrary
步骤 2:准备依赖文件
根据 README.md,运行目录需要放置 DMA 供应商提供的三个 DLL:FTD3XX.dll、leechcore.dll、vmm.dll;编译期还需要 libs/ 目录下的 leechcore.lib 与 vmm.lib(出于安全考虑未随仓库提供,需自行编译获取)。
步骤 3:初始化 DMA 句柄
一切内存操作的前提是初始化,可参考示例工程 Example.cpp:
mem.Init("explorer.exe", true, true);
初始化完成后,GetShellcode() 即可拿到 Shellcode 模块实例,见 Memory.h。
🧩 c_shellcode 类:Shellcode 注入的 3 个核心 API
c_shellcode 类声明位于 Shellcode.h,只有三个公开方法,各司其职:
| API | 作用 | 源码位置 |
|---|---|---|
find_codecave(大小, 进程名, 模块名) | 在指定模块中找一块足够大的 Code Cave,成功返回地址,失败返回 0 | Shellcode.h#L20 |
find_all_codecave(大小, 进程名) | 遍历进程内所有模块(黑名单除外),返回全部可用 Cave 地址 | Shellcode.h#L28 |
call_function(跳板, 目标函数, 进程名) | 用 14 字节 JMP 钩住目标函数入口,跳转执行后自动还原 | Shellcode.h#L36 |
🔍 Code Cave 查找原理:三段式定位法
核心实现见 find_codecave,它通过"节区特征过滤 → 页内余量计算 → 全零校验"三步保证找到的空间真的可用。
① 节区特征过滤
通过 VMM 接口读取目标模块的所有 PE 节区,只保留同时具备 IMAGE_SCN_MEM_READ | WRITE | EXECUTE 三种标志的节区,见 Shellcode.cpp#L38——没有执行权限的空间写进去也跑不起来。
② 页内余量计算
Cave 的起点取在节区虚拟大小结束后的偏移 0x10 处:
codecave = 模块基址 + 节区.VirtualAddress + 节区.Misc.VirtualSize + 0x10;
筛选条件要求节区未占满最后一页的余量大于 Shellcode 大小((VirtualSize & 0xfff) < (0x1000 - function_size)),见 Shellcode.cpp#L38-L40。
③ 全零校验
地址算出来还不够,库会把这段内存读回来逐字节检查,只要发现任何一个非 0x00 字节,就判定"空间不够用"并返回失败,见 Shellcode.cpp#L59-L66。这一步避免了覆盖有效数据导致的崩溃。
另外,find_all_codecave 会遍历进程的全部模块,但会跳过一份写死的系统模块黑名单(kernel32.dll、kernelbase.dll、ntoskrnl.exe 等),见 Shellcode.cpp#L5——往这些模块里注入是高危操作,务必保持默认。
🔗 JMP 跳转:14 个字节调用任意函数
想让目标进程"替你跑代码",最经典的手法就是劫持函数入口。call_function 的实现(Shellcode.cpp#L89-L121)构造了一条 14 字节的间接跳转指令:
BYTE jmp_bytes[14] = {
0xff, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp [RIP+0x00000000]
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // 8 字节目标地址
};
FF 25 是 jmp [RIP+偏移] 指令,后 8 字节填入你指定的跳板地址(通常是 Code Cave)。整体流程是一个安全的"借道还路":
- 读原字节:先把目标函数入口的 14 字节保存下来;
- 写入跳转:把 JMP 指令写入函数入口(Shellcode.cpp#L105);
- 短暂等待:
Sleep(100),给目标进程留出执行窗口; - 还原现场:写回原始字节,恢复函数(Shellcode.cpp#L114)。
参数含义注意区分:hook 是跳转落点(你的 Shellcode 地址),function 是被劫持的函数入口。
⚡ 完整流程演练:找 Cave → 写码 → 挂钩
三个 API 串起来只有几行:
auto shell = mem.GetShellcode();
uint64_t cave = shell.find_codecave(0x100, "explorer.exe", "explorer.exe");
mem.Write(cave, myShellcode, 0x100);
shell.call_function((void*)cave, (void*)(base + 0x1234), "explorer.exe");
先找 0x100 字节的 Cave 并写入 Shellcode,再把 base + 0x1234 处的函数入口劫持到 Cave——当目标进程调用该函数时,你的代码就被执行了。若需要枚举全部可用位置,可直接调用 find_all_codecave 拿到地址列表。
🧯 常见坑与排错技巧
- 返回 0 表示失败:
find_codecave在进程信息获取失败、节区不足、空间非空三种情况下都会返回 0,注意看LOG输出的具体原因; - 大小要对齐预期:余量判断以页(0x1000)为单位,Shellcode 超过节区末页余量必然失败,可适当调小
function_size或换模块; - 黑名单别乱动:默认排除
kernel32.dll等系统模块,强行注入这些模块极易蓝屏; - 100ms 窗口是经验值:若你的 Shellcode 执行较慢,可仿照 call_function 自行调整延时;
- 还原失败看日志:日志中出现
Failed to write memory说明现场还原失败,函数仍停留在钩子状态,需重新写入原始字节。
📂 文件索引
- Shellcode 头文件:Shellcode.h
- Shellcode 实现:Shellcode.cpp
- 内存读写与进程信息:Memory.h
- 示例工程入口:Example.cpp
- 依赖与许可证说明:README.md
掌握"找位置 → 写代码 → 搭跳板"这套组合拳后,你就可以基于 DMALibrary 自由拓展自己的注入逻辑了。祝实验顺利 🚀
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



