22、汇编指令的修改与 NOP:NOP 指令的作用、使用 NOP 填充来禁用功能、修改跳转指令(JMP, JNZ, JE 等)
好,咱们今天聊点硬核的。修改汇编指令,说白了就是直接对游戏程序的二进制代码动刀子。你想想看,游戏里那些烦人的检测、限制、扣血逻辑,归根到底都是一条条指令在 CPU 里跑。我们只要把其中关键的一条改掉,整个功能就废了。
我个人习惯把这种技术叫做「代码层面的外科手术」。不动数据,只改逻辑。今天重点讲两个最常用的手术刀:NOP 填充和跳转指令修改。
核心思想: 找到关键判断指令 → 修改或抹除它 → 功能失效。
22.1 NOP 指令:CPU 的「空转」指令
NOP 的全称是 No Operation。顾名思义,它什么都不做。CPU 读到 NOP 就浪费一个时钟周期,然后继续执行下一条。嗯,这里要注意:NOP 本身不改变任何寄存器、内存或标志位。
它的机器码是 0x90(x86 架构下)。一个字节,干净利落。
那它有什么用?
- 填充空间:当你需要删除一条或多条指令时,用 NOP 填满原来的位置,保持代码对齐。
- 禁用功能:把条件判断、函数调用等指令替换成 NOP,让它们失效。
- 延迟/时序调整:极少数情况下用于微调执行时序(游戏逆向中不常用)。
我的经验: 我在逆向某款 FPS 游戏时,发现它的「武器耐久度减少」逻辑写在一个子函数里。我直接把 CALL 指令的 5 个字节全部替换成 NOP。结果武器永远不掉耐久。就这么简单。
22.2 使用 NOP 填充来禁用功能
这是最基础也是最常用的手法。场景是这样的:你找到了一条指令,比如 DEC [eax+04](减少某个值),你不想让它执行。怎么办?
把这条指令的机器码全部改成 0x90。
举个例子:
原始指令:
地址 机器码 汇编
00401000 FF 48 04 DEC DWORD PTR [EAX+04]
修改后:
00401000 90 90 90 NOP
00401003 90 NOP
原来 3 个字节的指令,我们用 3 个 NOP 填满。CPU 执行到这里,直接滑过去,什么都不发生。
注意: 替换的 NOP 数量必须和原指令长度完全一致。多一个或少一个都会导致指令对齐错乱,程序大概率崩溃。我曾经因为少填了一个 NOP,让整个游戏在启动时直接蓝屏……嗯,从那以后我再也不敢数错字节了。
实际在 Cheat Engine 里操作时,你只需要选中指令,右键 → 「Replace with NOPs」。工具会自动计算长度并填充。但手动修改时一定要数清楚字节数。
22.3 修改跳转指令:改变程序流向
NOP 是「抹除」,跳转修改是「篡改」。跳转指令控制着程序的「岔路口」——是走左边(条件成立)还是走右边(条件不成立)。
常见的跳转指令:
| 指令 | 含义 | 跳转条件 |
|---|---|---|
| JMP | 无条件跳转 | 永远跳转 |
| JE / JZ | 相等/零时跳转 | ZF=1 |
| JNE / JNZ | 不相等/非零时跳转 | ZF=0 |
| JG / JNLE | 大于时跳转 | SF=OF 且 ZF=0 |
| JL / JNGE | 小于时跳转 | SF≠OF |
| JGE / JNL | 大于等于时跳转 | SF=OF |
| JLE / JNG | 小于等于时跳转 | SF≠OF 或 ZF=1 |
你想想看,如果游戏里有一段代码:
CMP [HP], 0 ; 比较 HP 是否为 0
JE GAME_OVER ; 如果 HP=0,跳转到游戏结束
我们只要把 JE 改成 JMP,或者改成 JNE,逻辑就完全反过来了。
22.4 实战:修改跳转实现「不死」
我记得有一次逆向一款横版动作游戏。角色受到伤害时,会调用一个函数处理扣血和死亡判定。关键代码大概是这样:
00402000 CMP [ESI+14], 0 ; 检查是否还有生命值
00402004 JE 00402030 ; 如果生命值为0,跳转到死亡处理
00402006 ... ; 正常扣血逻辑
我想让角色永远不死。怎么做?
方案一:把 JE 00402030 改成 JMP 00402030。这样只要受到伤害,不管 HP 是多少,直接跳转到死亡处理?不对,那角色就永远死了。我们要的是不死,不是秒死。
方案二:把 JE 00402030 改成 JNE 00402030。这样只有 HP 不为 0 时才跳转到死亡处理?逻辑反了,更不对。
方案三:把 JE 00402030 改成 NOP NOP(2 个字节)。这样条件判断被抹除,无论 HP 是否为 0,都不会跳转到死亡处理。角色永远不死。
你看,有时候 NOP 比改跳转更直接。
避坑指南: 我曾经在修改一个 JMP 指令时,直接把目标地址改成了另一个函数的入口。结果因为栈帧不匹配,游戏直接崩溃。后来我学乖了——改跳转前,先确认目标地址处的代码上下文,确保调用约定一致。
22.5 跳转指令的机器码结构
理解机器码能让你在 Cheat Engine 里更精准地修改。x86 下的跳转指令通常有两种形式:
- 短跳转(2 字节):
EB+ 偏移量(范围 -128 ~ 127)。例如EB 10表示向前跳转 16 个字节。 - 近跳转(5 字节):
E9+ 32 位偏移量。例如E9 00 10 00 00表示向前跳转 0x1000 个字节。 - 条件跳转(2 字节):
70~7F+ 偏移量。例如74 20是 JE,75 20是 JNE。
在 Cheat Engine 里,你不需要手动计算偏移量。直接右键 → 「Assemble」→ 输入新的指令,工具会自动重算机器码。
22.6 知识体系图
下面这张图帮你理清本章的核心逻辑:
22.7 常见修改模式总结
在实际逆向中,我总结了几种最常用的修改模式:
- 禁用检查:找到
CMP ... / JE ...,把JE改成JMP或 NOP 掉。 - 强制通过:找到
JNE ...(不相等时跳转),改成JMP,让条件永远成立。 - 跳过函数:找到
CALL xxx,用 5 个 NOP 替换,让函数不执行。 - 反转逻辑:把
JE改成JNE,或者反过来。条件完全颠倒。
重要提醒: 修改跳转指令时,一定要看清楚跳转的目标地址。有些游戏会做代码完整性校验(比如 CRC 校验),你改了指令后游戏检测到异常会直接退出。遇到这种情况,需要配合绕过校验的技术,那是后面章节的内容了。
好了,这一章的内容就到这里。NOP 和跳转修改是逆向工程里最基础也最强大的两个工具。你想想看,只要找到一条关键指令,改一个字节,整个游戏逻辑就变了。这就是逆向的魅力所在。