Cheat Engine 官方 Tutorial x64 完整教程(中文翻译)

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:帮助菜单打开教程

图 01|后续替换为:帮助菜单打开教程。

然后附加到 Cheat Engine Tutorial 进程;x64 原文使用的进程名是 Tutorial-x86_64.exe。如果不熟悉如何附加,可参阅如何附加进程。

第 1 关:欢迎

教程启动后会显示欢迎界面。读完帮助文字后,点击“下一步”。后续关卡的密码可以保存下来,方便因注入导致程序崩溃后恢复,或下次从相应关卡开始。

查看原文英文图 02:欢迎页及教程提示

图 02|后续替换为:欢迎页及教程提示。

第 2 关:精确数值扫描

第 2 关练习精确数值扫描。

查看原文英文图 03:第二关练习界面

图 03|后续替换为:第二关练习界面。

需要查找的是生命值,这里是一个整数。将扫描类型设为精确数值(Exact Value),数值类型先选 4 Bytes,再输入当前生命值。整数也可能使用 1、2、4 或 8 字节存储;此处从四字节开始。准备好后点击首次扫描。

查看原文英文图 04:精确数值首次扫描设置

图 04|后续替换为:精确数值首次扫描设置。

结果列表会显示一批地址。

查看原文英文图 05:首次扫描结果列表

图 05|后续替换为:首次扫描结果列表。

点击 Tutorial 中的“打我”(Hit me),使生命值变化;再次输入当前数值并点击再次扫描。列表中的红色数值表示该值发生了变化。

查看原文英文图 06:修改数值后的候选变化

图 06|后续替换为:修改数值后的候选变化。

必要时重复点击 Hit me 并重新扫描,直到结果少到便于检查。

查看原文英文图 07:重复筛选后的结果

图 07|后续替换为:重复筛选后的结果。

双击结果,将地址加入地址表。双击地址表中的值来修改它,并通过前面的启用框冻结该记录。

查看原文英文图 08:地址表的数值与冻结状态

图 08|后续替换为:地址表的数值与冻结状态。

Next 按钮此时应该可以使用;如果尚未启用,再点击一次 Hit me,然后点击 Next 进入下一关。

第 3 关:未知初始值

第 3 关练习未知初始值扫描。

查看原文英文图 09:第三关练习界面

图 09|后续替换为:第三关练习界面。

按照提示,在开始新一轮扫描之前点击“新扫描”(New Scan),清除上一轮结果。

查看原文英文图 10:新扫描按钮

图 10|后续替换为:新扫描按钮。

可以先点击 Hit me,观察数值如何减少,以帮助判断应扫描的类型。

查看原文英文图 11:第三关数值变化界面

图 11|后续替换为:第三关数值变化界面。

原文观察到它减少的是整数,因此选择 4 Bytes 和 Unknown initial value,然后点击首次扫描。

查看原文英文图 12:未知初始值首次扫描设置

图 12|后续替换为:未知初始值首次扫描设置。

再点击 Hit me,随后选择 Decreased value 并点击再次扫描。

查看原文英文图 13:减少的数值再次扫描

图 13|后续替换为:减少的数值再次扫描。

留意结果数量:本练习的候选相对较少,而其他程序可能有数百万条。继续让值减少,并以 Decreased value 扫描,直到候选数量便于检查。

查看原文英文图 14:再次筛选后的候选

图 14|后续替换为:再次筛选后的候选。

选一个地址修改,观察是否出现预期效果。原文建议修改前记录或复制原值,以便发现选错地址时恢复。将正确值设为 5000 后 Next 应该启用;修改后点击 Hit me,进度条可能填满,但这不是必须的。

查看原文英文图 15:第三关修改记录及结果

图 15|后续替换为:第三关修改记录及结果。

点击 Next 继续;如果按钮尚未启用,再点击一次 Hit me。

第 4 关:浮点数

本关练习扫描浮点数。

查看原文英文图 16:第四关练习界面

图 16|后续替换为:第四关练习界面。

点击 New Scan,选择 Float 和 Exact Value,输入当前生命值并首次扫描。像前面一样改变数值、继续扫描,再将生命值地址加入地址表。

查看原文英文图 17:Float 数值扫描

图 17|后续替换为:Float 数值扫描。

再次点击 New Scan,选择 Double 和 Exact Value,输入当前弹药数值并首次扫描。同样继续筛选并将结果加入地址表。

查看原文英文图 18:Double 数值扫描

图 18|后续替换为:Double 数值扫描。

将两个值设为 5000,Next 应该启用,点击它进入下一关。

第 5 关:查找代码

先找到数值并将地址加入地址表。此时可以保存地址表和关卡密码,以防调试器配置不当造成中断。如果需要配置调试器,可参阅调试器选项。

查看原文英文图 19:第五关练习界面

图 19|后续替换为:第五关练习界面。

在地址表中右击该地址,选择“查找是什么访问了这个地址”(Find out what accesses this address)。CE 询问是否附加调试器时选择确认。

查看原文英文图 20:地址右键菜单访问此地址

图 20|后续替换为:地址右键菜单访问此地址。

查看原文英文图 21:调试器附加确认

图 21|后续替换为:调试器附加确认。

调试器窗口打开后,点击 Tutorial 的“改变数值”(Change value)按钮,窗口中会出现访问代码。要找的是写入指令,其形式可能类似:

mov [**],**
add [**],**
sub [**],**
*** [**],**

查看原文英文图 22:调试器访问指令列表

图 22|后续替换为:调试器访问指令列表。

选中写入指令,可点击“显示反汇编器”(Show disassembler)在内存中查看代码,然后点击“替换”(Replace)。不要忘记点击“停止”(Stop)。Replace 会用 NOP 替换该行;CE 会要求为高级选项列表中的条目命名,输入名称后确认。

查看原文英文图 23:替换指令并命名

图 23|后续替换为:替换指令并命名。

再次点击 Tutorial 的 Change value,Next 应该启用。高级选项中被替换的条目会以红色显示。点击 CE 状态栏左下角的 Advanced options 可以查看列表。

查看原文英文图 24:高级选项红色条目

图 24|后续替换为:高级选项红色条目。

要恢复代码,在列表中右击条目,选择 Restore with original code。

查看原文英文图 25:高级选项恢复原代码菜单

图 25|后续替换为:高级选项恢复原代码菜单。

恢复后文字变回黑色。

查看原文英文图 26:恢复后高级选项列表

图 26|后续替换为:恢复后高级选项列表。

第 6 关:指针

先查找数值,将地址加入地址表,再右击并选择“查找是什么访问了这个地址”。

查看原文英文图 27:第六关练习界面

图 27|后续替换为:第六关练习界面。

查看原文英文图 28:查找访问地址的结果

图 28|后续替换为:查找访问地址的结果。

点击 Change value 触发访问。选择用于寻找指针基址的代码时,尽量选一条不会覆盖基址寄存器本身的指令。原文关注方括号内的 RDX:

mov [rdx],eax

它将 EAX 的内容写入 RDX 所指向的内存。原文示例中要寻找修改地址 012348D0 的代码,该地址位于 RDX 中。

查看原文英文图 29:写入指令及寄存器信息

图 29|后续替换为:写入指令及寄存器信息。

此处偏移为 0;如果指令是 mov [rdx+12C],eax,偏移则为十六进制 12C。接下来查找哪个指针保存对象基址:选择 8 Bytes、Exact Value,勾选 Hex,输入观察到的 RDX 基址,点击首次扫描。原文的零偏移例子中,这个基址恰好也是目标地址;若存在非零偏移,要区分寄存器基址和最终字段地址。结果中绿色显示的是静态地址。

查看原文英文图 30:八字节十六进制指针扫描结果

图 30|后续替换为:八字节十六进制指针扫描结果。

将找到的指针加入地址表,双击记录的地址并复制;勾选 Pointer,把复制的地址粘贴为指针基址。原文的表达式形如:

["Tutorial-x86_64.exe"+XXXXXX]+0

按实际发现的偏移填写后确认。关于界面操作可参考如何添加地址到地址表。

查看原文英文图 31:指针地址记录编辑窗口

图 31|后续替换为:指针地址记录编辑窗口。

将值冻结为 5000,点击 Change pointer;如果 Next 没有启用,尝试结果列表中另一条绿色、其值发生变化的地址,重复配置并确认是否指向正确数值。完成后点击 Next。

第 7 关:代码注入

与第 5 关一样定位相关代码,但这次点击 Show disassembler,而不是 Replace。

查看原文英文图 32:第七关练习界面

图 32|后续替换为:第七关练习界面。

查看原文英文图 33:查找到的写入指令

图 33|后续替换为:查找到的写入指令。

反汇编窗口会打开在对应指令的位置。

查看原文英文图 34:内存浏览器反汇编窗口

图 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:第八关练习界面

图 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:多级指针地址配置

图 38|后续替换为:多级指针地址配置。

指针扫描

也可以先找到目标值的地址并保存一份指针映射(pointer map),重新启动目标,再找一次地址并保存另一份指针映射,比较两份结果。更多说明见指针扫描。

验证指针

找到指针后,将值冻结为 5000 并点击 Change pointer。如果基址正确,大约两秒后 Next 应该启用;点击 Next 继续。

第 9 关:共享代码

本关有不止一种解法。先找到其中一个数值地址并加入地址表;如果找不到,尝试其他数值类型,并记得开始新的扫描。像第 7 关那样查找访问该地址的代码,以找到写入角色生命值的函数。可以保存密码以便尝试不同方案。

查看原文英文图 39:第九关练习界面

图 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:指令访问地址菜单

图 40|后续替换为:指令访问地址菜单。

分别点击四个数值对应的攻击按钮,调试器列表应出现四个地址。

查看原文英文图 41:四个访问地址结果

图 41|后续替换为:四个访问地址结果。

将它们加入地址表。

查看原文英文图 42:加入地址表的四个记录

图 42|后续替换为:加入地址表的四个记录。

打开 Dissect data/structures。按提示处理弹窗后会看到结构分析窗口;原作者为移动各列而加宽了窗口。

查看原文英文图 43:数据结构分析窗口

图 43|后续替换为:数据结构分析窗口。

在原文示例中,偏移 0x10 被猜为指针,在 64 位进程占 8 字节,但其值看起来不像指针。因此将它改成 4 Bytes,并在偏移 0x14 增加一个 4 Bytes 元素。原文据此判断队伍变量位于结构偏移 0x14。

查看原文英文图 44:结构字段偏移 10 与 14 设置

图 44|后续替换为:结构字段偏移 10 与 14 设置。

接下来建立注入脚本,检查结构中的队伍变量,判断角色所属队伍。以下代码是原文图片的文字转录;offset 和 Original bytes 原图中就是需要替换的占位内容,不能直接执行。实际改写时,还需检查当前版本的指令边界、跳转编码长度和完整原始字节,并确认停用后安全恢复;仅替换占位文字并不能保证脚本可用。

查看原文英文图 45:队伍判断注入脚本

图 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:自动运行后的完成界面

图 46|后续替换为:自动运行后的完成界面。

点击 Next 完成教程,随后会看到教程完成的提示。

方法二:查找寄存器差异

找到降低生命值的函数后,在反汇编窗口右击指令,选择“查找此指令访问了哪些地址”。

查看原文英文图 47:指令访问地址菜单(寄存器方案)

图 47|后续替换为:指令访问地址菜单(寄存器方案)。

分别点击四个数值对应的攻击按钮,列表中应出现四个地址。

查看原文英文图 48:四个访问地址结果(寄存器方案)

图 48|后续替换为:四个访问地址结果(寄存器方案)。

逐个选中地址,按 Ctrl+R 查看寄存器,排列窗口方便比较。

查看原文英文图 49:四个对象寄存器对照

图 49|后续替换为:四个对象寄存器对照。

原文示例中,对方角色的 RSI 为 1,因此使用下面的原图代码。此判断只代表原文示例运行上下文,不是所有版本的通用规律。

查看原文英文图 50:寄存器差异判断注入脚本

图 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:自动运行后的完成界面(寄存器方案)

图 51|后续替换为:自动运行后的完成界面(寄存器方案)。

点击 Next 完成教程,随后会看到完成提示。

原文与相关资料

译自 Cheat Engine Wiki:Cheat Engine Tutorial Guide (x64),原页面标注 Public Domain;页内代码及地址保留其示例性质。另见 Tutorials 和原文列出的 Syntax Highlighter。