Cheat Engine 官方 Tutorial x64 完整教程(中文翻译)
本教程翻译自 Cheat Engine Wiki 的 x64 Tutorial 指南,按官方练习程序的第 1 至第 9 关介绍扫描数值、调试指令、指针、代码注入和共享代码。适合在 CE 自带 Tutorial 中跟随操作的读者。
原文所示地址、偏移、寄存器和脚本来自旧版运行实例,XXXXXX、offset、Original bytes 是原文的占位内容;它们不是当前安装的实测结果。请在自己的 Tutorial 中重新寻找并验证。本文按原教程标出 51 处配图位置,可打开原文英文图对照;后续按图号替换汉化截图。
开始之前
打开 Cheat Engine,在主菜单的“帮助”(Help)中选择“Cheat Engine 教程”(Cheat Engine Tutorial)。
图 01|后续替换为:帮助菜单打开教程。
然后附加到 Cheat Engine Tutorial 进程;x64 原文使用的进程名是 Tutorial-x86_64.exe。如果不熟悉如何附加,可参阅如何附加进程。
第 1 关:欢迎
教程启动后会显示欢迎界面。读完帮助文字后,点击“下一步”。后续关卡的密码可以保存下来,方便因注入导致程序崩溃后恢复,或下次从相应关卡开始。
图 02|后续替换为:欢迎页及教程提示。
第 2 关:精确数值扫描
第 2 关练习精确数值扫描。
图 03|后续替换为:第二关练习界面。
需要查找的是生命值,这里是一个整数。将扫描类型设为精确数值(Exact Value),数值类型先选 4 Bytes,再输入当前生命值。整数也可能使用 1、2、4 或 8 字节存储;此处从四字节开始。准备好后点击首次扫描。
图 04|后续替换为:精确数值首次扫描设置。
结果列表会显示一批地址。
图 05|后续替换为:首次扫描结果列表。
点击 Tutorial 中的“打我”(Hit me),使生命值变化;再次输入当前数值并点击再次扫描。列表中的红色数值表示该值发生了变化。
图 06|后续替换为:修改数值后的候选变化。
必要时重复点击 Hit me 并重新扫描,直到结果少到便于检查。
图 07|后续替换为:重复筛选后的结果。
双击结果,将地址加入地址表。双击地址表中的值来修改它,并通过前面的启用框冻结该记录。
图 08|后续替换为:地址表的数值与冻结状态。
Next 按钮此时应该可以使用;如果尚未启用,再点击一次 Hit me,然后点击 Next 进入下一关。
第 3 关:未知初始值
第 3 关练习未知初始值扫描。
图 09|后续替换为:第三关练习界面。
按照提示,在开始新一轮扫描之前点击“新扫描”(New Scan),清除上一轮结果。
图 10|后续替换为:新扫描按钮。
可以先点击 Hit me,观察数值如何减少,以帮助判断应扫描的类型。
图 11|后续替换为:第三关数值变化界面。
原文观察到它减少的是整数,因此选择 4 Bytes 和 Unknown initial value,然后点击首次扫描。
图 12|后续替换为:未知初始值首次扫描设置。
再点击 Hit me,随后选择 Decreased value 并点击再次扫描。
图 13|后续替换为:减少的数值再次扫描。
留意结果数量:本练习的候选相对较少,而其他程序可能有数百万条。继续让值减少,并以 Decreased value 扫描,直到候选数量便于检查。
图 14|后续替换为:再次筛选后的候选。
选一个地址修改,观察是否出现预期效果。原文建议修改前记录或复制原值,以便发现选错地址时恢复。将正确值设为 5000 后 Next 应该启用;修改后点击 Hit me,进度条可能填满,但这不是必须的。
图 15|后续替换为:第三关修改记录及结果。
点击 Next 继续;如果按钮尚未启用,再点击一次 Hit me。
第 4 关:浮点数
本关练习扫描浮点数。
图 16|后续替换为:第四关练习界面。
点击 New Scan,选择 Float 和 Exact Value,输入当前生命值并首次扫描。像前面一样改变数值、继续扫描,再将生命值地址加入地址表。
图 17|后续替换为:Float 数值扫描。
再次点击 New Scan,选择 Double 和 Exact Value,输入当前弹药数值并首次扫描。同样继续筛选并将结果加入地址表。
图 18|后续替换为:Double 数值扫描。
将两个值设为 5000,Next 应该启用,点击它进入下一关。
第 5 关:查找代码
先找到数值并将地址加入地址表。此时可以保存地址表和关卡密码,以防调试器配置不当造成中断。如果需要配置调试器,可参阅调试器选项。
图 19|后续替换为:第五关练习界面。
在地址表中右击该地址,选择“查找是什么访问了这个地址”(Find out what accesses this address)。CE 询问是否附加调试器时选择确认。
图 20|后续替换为:地址右键菜单访问此地址。
图 21|后续替换为:调试器附加确认。
调试器窗口打开后,点击 Tutorial 的“改变数值”(Change value)按钮,窗口中会出现访问代码。要找的是写入指令,其形式可能类似:
mov [**],**
add [**],**
sub [**],**
*** [**],**
图 22|后续替换为:调试器访问指令列表。
选中写入指令,可点击“显示反汇编器”(Show disassembler)在内存中查看代码,然后点击“替换”(Replace)。不要忘记点击“停止”(Stop)。Replace 会用 NOP 替换该行;CE 会要求为高级选项列表中的条目命名,输入名称后确认。
图 23|后续替换为:替换指令并命名。
再次点击 Tutorial 的 Change value,Next 应该启用。高级选项中被替换的条目会以红色显示。点击 CE 状态栏左下角的 Advanced options 可以查看列表。
图 24|后续替换为:高级选项红色条目。
要恢复代码,在列表中右击条目,选择 Restore with original code。
图 25|后续替换为:高级选项恢复原代码菜单。
恢复后文字变回黑色。
图 26|后续替换为:恢复后高级选项列表。
第 6 关:指针
先查找数值,将地址加入地址表,再右击并选择“查找是什么访问了这个地址”。
图 27|后续替换为:第六关练习界面。
图 28|后续替换为:查找访问地址的结果。
点击 Change value 触发访问。选择用于寻找指针基址的代码时,尽量选一条不会覆盖基址寄存器本身的指令。原文关注方括号内的 RDX:
mov [rdx],eax
它将 EAX 的内容写入 RDX 所指向的内存。原文示例中要寻找修改地址 012348D0 的代码,该地址位于 RDX 中。
图 29|后续替换为:写入指令及寄存器信息。
此处偏移为 0;如果指令是 mov [rdx+12C],eax,偏移则为十六进制 12C。接下来查找哪个指针保存对象基址:选择 8 Bytes、Exact Value,勾选 Hex,输入观察到的 RDX 基址,点击首次扫描。原文的零偏移例子中,这个基址恰好也是目标地址;若存在非零偏移,要区分寄存器基址和最终字段地址。结果中绿色显示的是静态地址。
图 30|后续替换为:八字节十六进制指针扫描结果。
将找到的指针加入地址表,双击记录的地址并复制;勾选 Pointer,把复制的地址粘贴为指针基址。原文的表达式形如:
["Tutorial-x86_64.exe"+XXXXXX]+0
按实际发现的偏移填写后确认。关于界面操作可参考如何添加地址到地址表。
图 31|后续替换为:指针地址记录编辑窗口。
将值冻结为 5000,点击 Change pointer;如果 Next 没有启用,尝试结果列表中另一条绿色、其值发生变化的地址,重复配置并确认是否指向正确数值。完成后点击 Next。
第 7 关:代码注入
与第 5 关一样定位相关代码,但这次点击 Show disassembler,而不是 Replace。
图 32|后续替换为:第七关练习界面。
图 33|后续替换为:查找到的写入指令。
反汇编窗口会打开在对应指令的位置。
图 34|后续替换为:内存浏览器反汇编窗口。
选中指令后按 Ctrl+A 打开 Auto Assembler,在菜单 Template 中选择 Full injection,生成起始脚本。
查看原文英文图 35:Full injection 模板菜单
图 35|后续替换为:Full injection 模板菜单。
查看原文英文图 36:Auto Assembler 注入模板
图 36|后续替换为:Auto Assembler 注入模板。
原文的目标是增加数值 2,并移除原来的减少操作。增加可用 INC 或 ADD,例如下面的原文片段:
...
newmem:
add dword ptr [rsi+780],2
code:
//sub dword ptr [rsi+00000780],01
jmp return
address:
jmp newmem
nop
nop
return:
...
把脚本加入地址表并启用,然后点击 Hit me,Next 应该启用。原文中的偏移和 NOP 数量是当时指令布局下的示例;当前版本必须依据实际完整指令和生成的模板检查,停用时恢复原字节。有关添加脚本的界面操作见如何将脚本加入地址表。
第 8 关:多级指针
本关练习查找多级指针。
图 37|后续替换为:第八关练习界面。
手动逐级查找
方法与第 6 关相同,但需要继续查找是什么访问了每次找到的基址,重复这一过程直到找到静态基址。下面是原文第一次调试器输出:
10002D8D1 - B9 A00F0000 - mov ecx,00000FA0
10002D8D6 - E8 3522FEFF - call Tutorial-x86_64.exe+XXXXXX
10002D8DB - 89 46 18 - mov [rsi+18],eax <<<<<<
10002D8DE - 89 C2 - mov edx,eax
10002D8E0 - 48 8D 4D F8 - lea rcx,[rbp-08]
RAX=00000000000007F7
RBX=000000000125CD60
RCX=0000000000000FA0
RDX=00000000828087F3
RSI=0000000001287960 <<<<<<
RDI=0000000100258308
RSP=000000000102F070
RBP=000000000102F0B0
RIP=000000010002D8DE
R8=0000000100161BA0
R9=00000000008E06A0
R10=0000000000000002
R11=0000000000000206
R12=00000000012607C0
R13=0000000100161BA0
R14=0000000100258300
R15=0000000100257A18
原作者第一次扫描基址时找到一个静态基址,但认为它是错误候选。这里想找形如 process.exe+offset 的基址;也可尝试 module.dll+offset 形式,但原文指出该练习中这些候选可能是错误指针。实际程序里也常出现错误候选,需要验证。
接下来是访问保存 0000000001287960 的地址时的调试器输出:
10002D88B - E8 90961200 - call Tutorial-x86_64.exe+XXXXXX
10002D890 - E9 65000000 - jmp Tutorial-x86_64.exe+XXXXXX
10002D895 - 48 83 3E 00 - cmp qword ptr [rsi],00 <<<<<<
10002D899 - 74 5F - je Tutorial-x86_64.exe+XXXXXX
10002D89B - 48 8B 36 - mov rsi,[rsi]
RAX=0000000000013117
RBX=000000000125CD60
RCX=000000000125CD60
RDX=0000000000003CE3
RSI=0000000002D6D540 <<<<<<
RDI=0000000100258308
RSP=000000000102F070
RBP=000000000102F0B0
RIP=000000010002D899
R8=0000000100161BA0
R9=00000000008E06A0
R10=0000000000000002
R11=0000000000000206
R12=00000000012607C0
R13=0000000100161BA0
R14=0000000100258300
R15=0000000100257A18
访问保存 0000000002D6D540 的地址时:
10002D845 - E8 D6961200 - call Tutorial-x86_64.exe+XXXXXX
10002D84A - E9 AB000000 - jmp Tutorial-x86_64.exe+XXXXXX
10002D84F - 48 83 7E 18 00 - cmp qword ptr [rsi+18],00 <<<<<<
10002D854 - 0F84 A0000000 - je Tutorial-x86_64.exe+XXXXXX
10002D85A - 48 8B 76 18 - mov rsi,[rsi+18]
RAX=00000000000166D2
RBX=000000000125CD60
RCX=000000000125CD60
RDX=000000000000302E
RSI=0000000002D6CE40 <<<<<<
RDI=0000000100258308
RSP=000000000102F070
RBP=000000000102F0B0
RIP=000000010002D854
R8=0000000100161BA0
R9=00000000008E06A0
R10=0000000000000002
R11=0000000000000206
R12=00000000012607C0
R13=0000000100161BA0
R14=0000000100258300
R15=0000000100257A18
访问保存 0000000002D6CE40 的地址时:
10002D800 - E8 1B971200 - call Tutorial-x86_64.exe+XXXXXX
10002D805 - E9 F0000000 - jmp Tutorial-x86_64.exe+XXXXXX
10002D80A - 48 83 7E 10 00 - cmp qword ptr [rsi+10],00 <<<<<<
10002D80F - 0F84 E5000000 - je Tutorial-x86_64.exe+XXXXXX
10002D815 - 48 8B 76 10 - mov rsi,[rsi+10]
RAX=000000000000B567
RBX=000000000125CD60
RCX=000000000125CD60
RDX=00000000000050A1
RSI=000000000123F1C0 <<<<<<
RDI=0000000100258308
RSP=000000000102F070
RBP=000000000102F0B0
RIP=000000010002D80F
R8=0000000100161BA0
R9=00000000008E06A0
R10=0000000000000002
R11=0000000000000206
R12=00000000012607C0
R13=0000000100161BA0
R14=0000000100258300
R15=0000000100257A18
搜索最后的基址 000000000123F1C0,原文中应能找到静态地址。其他程序可能需要继续追踪。原文据此构造的指针链为:
[[[["Tutorial-x86_64.exe"+XXXXXX]+10]+18]+0]+18
图 38|后续替换为:多级指针地址配置。
指针扫描
也可以先找到目标值的地址并保存一份指针映射(pointer map),重新启动目标,再找一次地址并保存另一份指针映射,比较两份结果。更多说明见指针扫描。
验证指针
找到指针后,将值冻结为 5000 并点击 Change pointer。如果基址正确,大约两秒后 Next 应该启用;点击 Next 继续。
第 9 关:共享代码
本关有不止一种解法。先找到其中一个数值地址并加入地址表;如果找不到,尝试其他数值类型,并记得开始新的扫描。像第 7 关那样查找访问该地址的代码,以找到写入角色生命值的函数。可以保存密码以便尝试不同方案。
图 39|后续替换为:第九关练习界面。
为了区分两组角色,先理解可能的结构。原文用以下伪代码说明角色和玩家的关系:
//// Actor, base for all actors
class Actor(object){
string Name = 'Actor';
Coord Coords = new Coord(0, 0, 0);
float Health = 100.0;
...
}
//// Player
class Player(Actor){ //// Player inherits form Actor
string Name = 'Player';
int Team = 1;
...
}
Team 也可能是一个结构;如果像 Coords 一样声明为对象,就需要寻找指向队伍结构的指针。原文给出以下两种区分办法。
方法一:查找玩家结构中的队伍 ID
找到降低生命值的函数后,在反汇编窗口右击指令,选择“查找此指令访问了哪些地址”。
图 40|后续替换为:指令访问地址菜单。
分别点击四个数值对应的攻击按钮,调试器列表应出现四个地址。
图 41|后续替换为:四个访问地址结果。
将它们加入地址表。
图 42|后续替换为:加入地址表的四个记录。
打开 Dissect data/structures。按提示处理弹窗后会看到结构分析窗口;原作者为移动各列而加宽了窗口。
图 43|后续替换为:数据结构分析窗口。
在原文示例中,偏移 0x10 被猜为指针,在 64 位进程占 8 字节,但其值看起来不像指针。因此将它改成 4 Bytes,并在偏移 0x14 增加一个 4 Bytes 元素。原文据此判断队伍变量位于结构偏移 0x14。
图 44|后续替换为:结构字段偏移 10 与 14 设置。
接下来建立注入脚本,检查结构中的队伍变量,判断角色所属队伍。以下代码是原文图片的文字转录;offset 和 Original bytes 原图中就是需要替换的占位内容,不能直接执行。实际改写时,还需检查当前版本的指令边界、跳转编码长度和完整原始字节,并确认停用后安全恢复;仅替换占位文字并不能保证脚本可用。
图 45|后续替换为:队伍判断注入脚本。
{ Game : Tutorial-x86_64.exe
Version:
Date : 2017-03-19
Author : TheyCallMeTim13
This script does blah blah blah
}
define(address,"Tutorial-x86_64.exe"+offset)
define(bytes, Original bytes )
[ENABLE]
assert(address,bytes)
alloc(newmem,$1000,"Tutorial-x86_64.exe"+offset)
label(code)
label(exit)
label(team1)
label(team2)
label(team1Value)
label(team2Value)
label(return)
newmem:
pushfq //// Save the flags register
cmp dword ptr [rbx+14],1 //// Check if actor is on team 1
je team1
cmp dword ptr [rbx+14],2 //// Check if actor is on team 2
je team2
exit:
popfq //// Restore the flags register
code:
movss [rbx+08],xmm0
jmp return
team1:
movss xmm0,[team1Value]
jmp exit
team2:
movss xmm0,[team2Value]
jmp exit
team1Value:
dd (float)5000
team2Value:
dd 0
address:
jmp newmem
return:
[DISABLE]
address:
db bytes
// movss [rbx+08],xmm0
dealloc(newmem)
启用后,当程序写入角色生命值并跳转到注入代码时,脚本先 PUSH 保存 RFLAGS;检查是否为队伍 1,若是则将新值设为浮点数 5000;检查是否为队伍 2,若是则设为十六进制 0。浮点零、整数零和十六进制零的位表示都为零。最后 POP 恢复 RFLAGS;如果已经 PUSH,这一步不可省略。
启用脚本后,点击 Restart game and autoplay,界面应变为完成状态。
图 46|后续替换为:自动运行后的完成界面。
点击 Next 完成教程,随后会看到教程完成的提示。
方法二:查找寄存器差异
找到降低生命值的函数后,在反汇编窗口右击指令,选择“查找此指令访问了哪些地址”。
图 47|后续替换为:指令访问地址菜单(寄存器方案)。
分别点击四个数值对应的攻击按钮,列表中应出现四个地址。
图 48|后续替换为:四个访问地址结果(寄存器方案)。
逐个选中地址,按 Ctrl+R 查看寄存器,排列窗口方便比较。
图 49|后续替换为:四个对象寄存器对照。
原文示例中,对方角色的 RSI 为 1,因此使用下面的原图代码。此判断只代表原文示例运行上下文,不是所有版本的通用规律。
图 50|后续替换为:寄存器差异判断注入脚本。
{ Game : Tutorial-x86_64.exe
Version:
Date : 2017-03-19
Author : TheyCallMeTim13
This script does blah blah blah
}
define(address,"Tutorial-x86_64.exe"+offset)
define(bytes, Original bytes )
[ENABLE]
assert(address,bytes)
alloc(newmem,$1000,"Tutorial-x86_64.exe"+offset)
label(code)
label(exit)
label(ohk)
label(team1Value)
label(team2Value)
label(return)
newmem:
pushfq //// Save the flags register
cmp rsi,1 //// Check if RSI is 1
je ohk
//// If RSI is not 1 then we assume that it's an ally
movss xmm0,[team1Value]
exit:
popfq //// Restore the flags register
code:
movss [rbx+08],xmm0
jmp return
ohk:
movss xmm0,[team2Value]
jmp exit
team1Value:
dd (float)5000
team2Value:
dd 0
address:
jmp newmem
return:
[DISABLE]
address:
db bytes
// movss [rbx+08],xmm0
dealloc(newmem)
脚本跳转后先保存 RFLAGS,检查 RSI 是否为 1:若是,设定新值为十六进制 0;否则原文假定是己方角色,将新值设为浮点数 5000。最后恢复 RFLAGS。启用后点击 Restart game and autoplay,界面应变为完成状态。
图 51|后续替换为:自动运行后的完成界面(寄存器方案)。
点击 Next 完成教程,随后会看到完成提示。
原文与相关资料
译自 Cheat Engine Wiki:Cheat Engine Tutorial Guide (x64),原页面标注 Public Domain;页内代码及地址保留其示例性质。另见 Tutorials 和原文列出的 Syntax Highlighter。