|
|
Day 9 -- 敌我识别:当你和怪物共享同一段代码
Cheat Engine 从入门到住院 · Day 9
Day 5 我们学 NOP 的时候埋过一个伏笔:NOP 掉扣血代码后,不仅你不掉血,敌人也不掉血了。
这不是 bug,这是程序设计的常见模式——玩家和敌人用的是同一段扣血代码。程序不会为你和每只怪物分别写一个,那太浪费了。它只写一个通用的函数,通过参数区分对谁操作。
所以问题来了:怎么修改代码,让它只对你免疫,对敌人照常生效?
━━━━━━━━━━━━━━━━━━━━
本文你将学到
共享代码的产生原因
如何区分"谁在被扣血"
通过结构体比较进行敌我识别
条件注入脚本的编写
阅读时间:15 分钟 | 实操时间:35 分钟 | 难度:进阶
━━━━━━━━━━━━━━━━━━━━
CE Tutorial Step 9:区分玩家和敌人
进入 Step 9。这一步有你的角色和多个敌人,每个都有血量。
任务:让你的角色不掉血(或者加血),但敌人正常掉血。
如果你直接 NOP 掉扣血代码,所有人都不掉血,过不了关。
━━━━━━━━━━━━━━━━━━━━
为什么代码是共享的
在面向对象编程中,玩家和敌人通常是同一个类的不同实例:
- class Entity:
- def __init__(self, name, health):
- self.name = name
- self.health = health
- def take_damage(self, amount):
- self.health -= amount # 只有这一行代码
- player = Entity("Player", 100)
- enemy1 = Entity("Goblin", 50)
- enemy2 = Entity("Dragon", 200)
复制代码 只有一份代码,但通过参数知道操作的是哪个实例。
在汇编层面,就是一个指向结构体的指针(通常存在某个寄存器里)。不同的实例有不同的结构体地址。
━━━━━━━━━━━━━━━━━━━━
核心思路:比较结构体地址
每次扣血代码执行时,某个寄存器里存着"当前被操作对象"的结构体地址。
当这个地址指向你的角色 → 不扣血(或加血)
当这个地址指向敌人 → 正常扣血
所以关键问题变成了:怎么知道你的角色的结构体地址是什么?
━━━━━━━━━━━━━━━━━━━━
实操步骤
第一步:分别找到你和敌人的血量地址
用精确搜索分别找到:
你的角色的血量地址(比如)
至少一个敌人的血量地址(比如)
第二步:找到扣血代码
对你的血量地址做 Find out what writes to this address。
点击被攻击的操作,找到扣血指令,比如:
这里就是结构体指针,是血量在结构体中的偏移量。
第三步:确认共享代码
对敌人的血量地址也做 Find out what writes to this address。
攻击敌人,你会发现出现的指令地址和你的一模一样——同一条。
这就证实了:扣血代码是共享的,区别仅在于执行时的值不同。
第四步:记录你的结构体地址
当 CE 记录到你被攻击时的指令信息,在详情中可以看到当时的值,比如。
也就是说:
你的结构体地址 =你的血量 == 地址
第五步:编写条件注入脚本
打开反汇编视图,对扣血指令按 Ctrl+A,使用 Full Injection 模板,然后修改:
- [ENABLE]
- alloc(newmem, 2048)
- label(returnhere)
- label(originalcode)
- label(exit)
- // 存储玩家的结构体地址
- alloc(playerbase, 8)
- newmem:
- // 比较当前 rbx 是不是玩家
- push rax
- mov rax, [playerbase]
- cmp rbx, rax
- pop rax
- je isplayer // 如果是玩家,跳到特殊处理
- jmp originalcode // 如果是敌人,执行原始代码
- isplayer:
- add [rbx+18], 2 // 玩家:加血
- jmp exit
- originalcode:
- sub [rbx+18], eax // 敌人:正常扣血
- exit:
- jmp returnhere
- // 记录玩家结构体地址
- playerbase:
- dq 0x0A001000 // 填入你的结构体地址
- "Tutorial-x86_64.exe"+XXXXX:
- jmp newmem
- nop
- returnhere:
- [DISABLE]
- dealloc(newmem)
- dealloc(playerbase)
- "Tutorial-x86_64.exe"+XXXXX:
- sub [rbx+18], eax
复制代码
核心逻辑就是一个(比较)+(条件跳转):
- 如果 rbx == 玩家地址:
- 加血
- 否则:
- 正常扣血
复制代码
第六步:执行并验证
执行脚本后,攻击敌人——敌人正常掉血。你被攻击——血量反而增加。
完美的敌我识别。
━━━━━━━━━━━━━━━━━━━━
更优雅的方案:用 CE 的结构体分析
上面的方法有一个问题:玩家的结构体地址是硬编码的。如果游戏重启,地址变了,脚本就失效了。
更好的方法是通过结构体中的某个特征来区分。比如:
玩家的名字是 "Player",敌人的名字是 "Enemy"
玩家的 teamID 是 1,敌人的 teamID 是 2
CE 的 Structure Dissect 工具可以帮你分析结构体的布局,找到可以用来区分的字段。
用结构体特征做判断的脚本:
- newmem:
- // 假设结构体偏移 0x04 处存的是 teamID
- cmp dword ptr [rbx+04], 1 // teamID == 1 ?
- je isplayer
- jmp originalcode
- isplayer:
- add [rbx+18], 2
- jmp exit
- originalcode:
- sub [rbx+18], eax
- exit:
- jmp returnhere
复制代码
这种方案不依赖硬编码地址,更加健壮。
━━━━━━━━━━━━━━━━━━━━
Structure Dissect 快速入门
Structure Dissect 是 CE 的结构体分析工具,可以查看一个内存地址附近的数据布局。
基本用法:
在 CE 中打开 Memory View
菜单 Tools → Dissect Data/Structures
输入结构体地址(比如你的角色的基地址)
CE 会显示从这个地址开始的连续内存数据,并尝试猜测每个偏移量处的数据类型
你可以在这个视图中看到:
- 偏移 +00: 某个值(可能是指向虚表的指针)
- 偏移 +04: 1(这可能是 teamID)
- 偏移 +08: 某个浮点数(可能是 X 坐标)
- 偏移 +0C: 某个浮点数(可能是 Y 坐标)
- ...
- 偏移 +18: 100(血量)
复制代码
对比你的角色和敌人的结构体,找出不同的字段,就能用来做敌我识别。
━━━━━━━━━━━━━━━━━━━━
常见问题
Q:怎么确定哪个寄存器是结构体指针?
A:看扣血指令。中,就是结构体指针。方括号里的寄存器就是。有时候是、、等。
Q:玩家和敌人的结构体偏移量不一样怎么办?
A:那它们可能不是同一个类的实例,扣血代码也不会是共享的。这种情况下你只需要修改影响玩家的那个代码即可。
Q:游戏里有多种敌人,需要分别处理吗?
A:通常不需要。你只需要识别出"是不是玩家",不是玩家的一律走原始代码就行。除非你想对不同敌人做不同的修改。
━━━━━━━━━━━━━━━━━━━━
小结
今天我们解决了 CE 修改中最经典的难题——共享代码的敌我识别:
游戏通常让玩家和敌人共用同一段代码
区分方式是比较结构体指针或结构体中的特征字段
用条件跳转实现"如果是玩家则特殊处理,否则正常执行"
Structure Dissect 工具可以帮助分析结构体布局
恭喜!你已经通关了 CE Tutorial 的全部 9 个步骤。但 CE 的能力远不止这些。
接下来三天,我们将离开 Tutorial 的舒适区,学习三个实战利器:指针扫描(Day 10)、Auto Assembler 进阶(Day 11)、Lua 脚本(Day 12)。
从 Tutorial 靶场走向真实世界,准备好了吗? |
|