5、代码注入基础:代码注入的概念、使用 Cheat Engine 的自动汇编(Auto Assemble)、简单的代码注入示例(修改游戏逻辑)
好,我们进入第五章。代码注入。
说实话,这是整个逆向工程里最让人兴奋的部分之一。前面几章我们一直在「看」——看内存、看数值、看代码。但从这一章开始,我们开始「改」。改游戏逻辑,改程序行为,让程序按照我们的想法跑。
我个人觉得,代码注入就像是在别人的代码里「打补丁」。你不动原来的房子结构,只是在墙上开个洞,装个自己的窗户。听起来很酷?嗯,确实很酷。
5.1 什么是代码注入?
代码注入,说白了就是:把我们的代码塞进目标进程里,让它执行。
你想想看,游戏本身是一堆指令在跑。我们找到某个关键指令,比如「扣血」的指令,然后把它改成「加血」——这就是代码注入。
但这里有个问题:我们怎么把代码塞进去?
Cheat Engine 给了我们一个非常方便的工具——自动汇编(Auto Assemble)。
核心概念
- 代码注入点:我们要修改的那条原始指令的位置
- 跳转(JMP):从原始代码跳到我们的代码
- 代码洞穴(Code Cave):我们存放自定义代码的空闲内存区域
- 返回(JMP back):执行完我们的代码后,跳回原始流程
我在项目中遇到过不少新手,一上来就想直接改指令。结果改完游戏直接崩了。为什么?因为没搞清楚代码注入的基本流程。
5.2 自动汇编(Auto Assemble)入门
Cheat Engine 的自动汇编,其实就是一个脚本引擎。它让我们用类似汇编的语法,描述「在哪里注入」「注入什么代码」「什么时候执行」。
打开 Cheat Engine,附加游戏进程后,点击「Memory View」→「Tools」→「Auto Assemble」。你会看到一个编辑器窗口。
嗯,这里要注意:自动汇编脚本的格式是固定的。我给大家一个最基础的模板:
[ENABLE]
// 启用脚本时执行的代码
alloc(MyCode, 512)
label(ReturnHere)
label(OriginalCode)
label(Exit)
MyCode:
// 这里写你的自定义代码
// ...
jmp ReturnHere
OriginalCode:
// 这里保存被覆盖的原始指令
// ...
ReturnHere:
// 跳回点
// 注入点:将原始指令替换为跳转到 MyCode
// 假设原始地址是 00401000
00401000:
jmp MyCode
nop
[DISABLE]
// 禁用脚本时恢复原始代码
00401000:
// 恢复原始指令
// ...
dealloc(MyCode)
这个模板我用了好多年。每次做注入,我都是复制这个模板,然后改改地址和代码内容。
小技巧:alloc 命令会在目标进程里分配一块内存,大小自己定。我一般给 512 字节,够用了。如果代码复杂,可以给 1024 或更大。
5.3 实战:修改游戏逻辑——让扣血变加血
光说不练假把式。我们拿一个经典例子来演示:修改扣血逻辑。
假设我们找到了扣血的指令,在地址 00402000,指令是:
sub [eax+04], edx // 从玩家血量中减去 edx 的值
我们要把它改成加血。怎么做?
思路很简单:把 sub 改成 add。但直接改指令不够灵活。我们用代码注入的方式:
[ENABLE]
alloc(MyCode, 512)
label(ReturnHere)
MyCode:
// 原始指令是 sub [eax+04], edx
// 我们改成 add [eax+04], edx
add [eax+04], edx
jmp ReturnHere
// 注入点
00402000:
jmp MyCode
nop
ReturnHere:
[DISABLE]
00402000:
sub [eax+04], edx
dealloc(MyCode)
就这么简单?嗯,就这么简单。但实际项目中,往往没这么顺利。
我曾经遇到过一个游戏,扣血指令是 fsub(浮点数减法)。我改成 fadd 后,游戏直接崩溃。后来发现,那个指令后面还有几条指令依赖浮点栈的状态。我改了一条,后面的全乱了。
所以,避坑指南:
- 一定要看清楚指令的上下文
- 注入点选择要谨慎,别选在循环或跳转指令上
- 注入的代码长度不能超过原始指令的长度(不够的话用 NOP 填充)
5.4 更复杂的注入:条件判断
有时候我们不想简单地改指令,而是想加条件。比如:「只有按下某个键时才加血,否则正常扣血」。
这时候,代码注入就真正体现出威力了。
[ENABLE]
alloc(MyCode, 512)
label(ReturnHere)
label(NormalSubtract)
MyCode:
// 检查某个按键状态(假设地址 0x00AABBCC 存储按键标志)
cmp [00AABBCC], 01
je NormalSubtract
// 如果按键按下,改为加血
add [eax+04], edx
jmp ReturnHere
NormalSubtract:
// 正常扣血
sub [eax+04], edx
jmp ReturnHere
00402000:
jmp MyCode
nop
ReturnHere:
[DISABLE]
00402000:
sub [eax+04], edx
dealloc(MyCode)
你看,我们不仅改了逻辑,还加了条件判断。这就是代码注入的灵活之处。
警告:条件判断中引用的地址(比如按键标志)必须是游戏进程中可读的。如果地址无效,游戏会直接崩溃。我建议先用 Cheat Engine 的内存浏览功能确认地址可用。
5.5 知识体系图
下面这张图,是我自己总结的代码注入知识体系。每次做注入前,我都会过一遍这个流程:
5.6 常见问题与避坑
做代码注入,有几个坑我踩过,分享给大家:
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 游戏崩溃 | 注入点选择不当,破坏了关键指令 | 选择不影响程序流程的指令,如 mov、add、sub |
| 注入无效 | 地址错误或代码长度不匹配 | 用 Cheat Engine 确认地址,用 NOP 填充多余空间 |
| 脚本无法禁用 | [DISABLE] 部分没有正确恢复原始指令 | 在 [ENABLE] 前备份原始指令,[DISABLE] 中恢复 |
| 多线程问题 | 注入代码不是线程安全的 | 使用锁或原子操作,避免竞争条件 |
我曾经在一个网络游戏上做注入,脚本一启用游戏就断线。后来发现是游戏有反作弊检测,检测到代码段被修改就主动断开连接。嗯,这种情况就需要更高级的注入技术了——比如 hook 或者 inline patching。不过那是后面章节的内容。
我的个人习惯:每次写注入脚本,我都会先写 [DISABLE] 部分,再写 [ENABLE] 部分。这样能确保脚本可以安全地启用和禁用。你想想看,如果脚本写了一半游戏崩了,至少还能恢复原状。
好了,代码注入的基础就讲到这里。记住:注入不是乱改,而是有策略地修改。选好注入点,写好脚本,测试通过,再应用到实际场景中。
下一章,我们会深入探讨更高级的注入技术——包括如何绕过反作弊、如何做多级注入、以及如何用代码注入实现更复杂的功能。但今天的内容,已经足够你开始动手了。
去试试吧。找个单机游戏,找到扣血指令,用自动汇编把它改成加血。相信我,当你看到游戏角色被打反而回血的那一刻,你会觉得——嗯,这一切都值了。