本文翻译自《Reverse Engineering a DS Game 》。
经过两部分的铺垫,终于来到了使用 Ghidra 和 DeSmuME 分别进行静态 / 动态代码分析的实战部分。
使用 Ghidra Ghidra 是一款功能强大的逆向工程工具,具有丰富的特性。本节将介绍一些 Ghidra 的基本用法,帮助你入门阅读汇编代码。
Ghidra 界面如下所示:
Ghidra 界面,包含两个窗口。 Ghidra 工作区主要有两个。
中央窗口是listing (列表),这里显示的是汇编代码。 右侧窗口是decompiler (反编译),它分析当前选中函数的汇编代码并将其反编译为 C 语言代码。 Ghidra 界面中还有其他窗口,但本教程不会涉及它们。你可以关闭那些窗口以为列表窗口腾出更多空间。
在本演示中,我们将使用地址 0x22E0354 处的函数。要导航到此地址,按 ‘g’ 并输入 “22E0354”。向下滚动一点查看整个函数,结果如下所示:
FUN_022e0354 in Ghidra 让我们来分解屏幕上的内容。
FUN_022e0354 in Ghidra 函数名称 是显而易见的。可以向汇编代码添加注释。板注释 (plate comment)是占据多行的注释。Ghidra 会自动添加一个板注释来标记函数,当然你也可以自己添加(我们稍后会讨论这个)。 函数引用 部分列出了汇编代码中调用当前函数的所有位置。列表格式为 “函数名称:指令地址”。在本例中,Ghidra 找到了 4 个调用FUN_022e0354
的地方,其中之一是地址 0x22F798C 的bl
指令,在FUN_022f7910
中。汇编指令 就是原始的汇编代码,还包括表示分支目的地和硬编码数据值的标签 。十六进制数据 包含 ROM 文件中与汇编指令对应的原始十六进制值。在FUN_022e0354
的开始,ldr r0,[PTR_DAT_022e0958]
指令来源于 ROM 中的十六进制值28 00 9f e5
。地址 包含文件中每条指令的地址或偏移量。FUN_022e0354
开始的ldr r0, [PTR_DAT_022e0958]
指令位于 ROM 的地址 0x22E0928 处。分支 部分包含表示函数内分支的箭头;箭头的起点是分支指令,终点是分支将pc
指向的地址。标签引用 列出了跳转到每个标签的位置。这里列出了每个跳转指令到给定标签的地址。例如,LAB_022e0948
由地址 0x22E0938 的beq
指令引用。反编译的函数代码 位于反编译器窗口,显示函数的反编译 C 代码。反编译器 反编译器是一种工具,试图将汇编代码反编译为 C 代码。由于 C 语言比汇编语言更高级,有时阅读反编译的 C 代码比汇编代码更快,以理解函数的工作原理。
有了反编译器,你可能会问:如果汇编代码可以被反编译为 C,为什么还要学习阅读汇编代码呢?
由于 C 代码是生成的,而不是手动编写的,有时比原始汇编代码更难阅读。 反编译器并不完美,有时它可能无法反编译某函数。 在调试正在运行的游戏时,逐行执行的是汇编代码,而不是 C 代码。 根据你逆向工程的目标不同,通常需要使用汇编而不是反编译的 C 代码。例如,如果你想通过更改代码来为游戏制作补丁,必须通过汇编来完成,除非游戏的逆向工程社区已经创建了将编译的 C 代码注入到 ROM 中的工具,然而这种情况非常少见。 把反编译器当作工具箱中的另一个工具,而不仅仅依赖它。根据当前的任务需要,你很可能需要在原始汇编代码和反编译的 C 代码之间来回切换阅读。
导航 Ghidra 中有多种导航代码的方法。
如前所示,按下 ‘g’(Go To…)可以跳转到 ROM 中的特定地址。 双击函数、标签或函数 / 标签的引用,将跳转到 ROM 中的相应位置。 在 Ghidra 窗口顶部的工具栏上,有按钮用于前进和后退。这些按钮会跟踪你通过 Go To…跳转到不同地址时的历史记录,或者在单击函数和标签时的历史记录。你可以将鼠标悬停在这些按钮上查看它们的键盘快捷键(可能因操作系统不同而有所差异)。 高亮 Ghidra 允许你高亮某些相应元素,这在瞪眼法检查代码时非常有用。
单击汇编代码行将高亮相应的反编译 C 代码行。同样,单击 C 代码行将高亮相应的汇编代码。 中键单击 Ghidra 中的一个符号,将高亮该符号的所有出现位置。例如,你可以中键单击汇编中的r1
,高亮该符号的所有其他使用位置。 右键单击反编译器中的变量,选择 Highlight > Forward Slice or Highlight > Backward Slice,就会高亮数据流动进 / 出选中变量的其他所有变量。 注释 你可以通过右键单击汇编指令并在上下文菜单中转到 Comments,向汇编代码添加注释。你可以选择将注释放置在多个位置,包括行尾(EOL)、行前、行后和板注释。默认键绑定 ‘;’ 将打开 EOL 注释界面,你还可以在 Edit > Tool Options > Key Bindings 中为其他类型的注释分配键绑定。
以下是一个行尾注释的示例。
行尾注释 建议多使用注释。汇编中没有描述性变量名,如果不为自己留下笔记,很容易迷失方向。
标签名称 一旦你弄清楚函数、分支标签、堆栈值或数据值的作用,就可以通过右键单击标签并点击 Edit label(快捷键 ‘L’)来重命名。你也可以在反编译器中编辑变量名。
重命名函数的标签 有时,Ghidra 的反汇编器会自动将寄存器标记为参数。这很少有帮助,因为寄存器在函数中会被多次重复使用。要删除这些标签,请转到 Edit > Tool Options…,在打开的菜单中选择 Options > Listing Fields > Operands Field,然后取消选中 Markup Register Variable References(标记寄存器变量引用)。
禁用寄存器上的标签 Ghidra 总结 目前为止,我们已经探索了足以开始阅读游戏的汇编代码的 Ghidra 功能。Ghidra 还有许多其他对逆向工程非常有用的功能,你可以自己探索,看看哪些功能对你有用。
Ghidra 非常适合分析游戏未运行时的代码。虽然这本身已经非常有用,但通过分析游戏运行时的代码来补充它同样有价值。下一节将重点介绍 DeSmuME 的调试功能。
使用 DeSmuME 进行调试 在逆向工程中,检查游戏运行时的状态通常很有用,这一过程称为动态代码分析 。主要有查看内存值、逐步执行汇编代码、检查寄存器等。本节将简要介绍 DeSmuME 提供的一些调试功能。
内存查看器 内存查看器可以查看和编辑主内存中的值。可以通过 Tools > View Memory 访问内存查看器,这会弹出一个窗口,如下图所示。请注意,你可以多次选择 View Memory 以打开多个内存查看器窗口。
内存查看器窗口 在上图中,内存查看器显示了地址 0x2000000 到 0x20000F0 的内存值。字节数据以表格形式显示,地址的第一个数字为列,0x10 的倍数为行。例如,0x2000010 行包含地址 0x2000010 到 0x200001F 的值,而地址 0x2000014 包含值 0xD0。内存查看器默认按单个字节显示,你也可以将视图更改为半字(2 字节)或字(4 字节),以显示更大的值,例如 4 字节的指针。
在内存表的右侧是数据的字符串表示。浏览用于表示游戏内文本在内存里的部分的时候非常有用,或者用于快速寻找数据的规律。
你可以点击来选中表中的字节值。然后窗口底部就会(以十进制而不是十六进制)显示该值的有符号 / 无符号整形数值。当选中了一个值时,你也可以更改该值,且会立即反映在游戏中。注意,如果游戏代码里每帧都设置该值,则某些值可能会立即恢复为先前的值。
你可以通过在顶部的地址 中输入地址然后按转到 来跳转到内存中的任何地址。窗口将记住你跳转到的最近地址。
为了演示内存查看器的功能,请跳转到地址 0x21CCB00,该地址包含玩家和伙伴宝可梦的数据。该地址周围的某些值不断变化,这表示游戏中的值在变化。此时,这几个快速变化的值控制着屏幕上宝可梦的动画。如果你在游戏中移动,你会注意到其他变量发生变化,这些变量表示宝可梦在屏幕和迷宫地面上的位置。
接下来,跳转到地址 0x21BA538。此区域包含与玩家和伙伴宝可梦相关的更多值。将地址为 0x21BA538 处的值更改为 “08”,你会注意到游戏中的玩家生命值(HP)会变成刚刚输入的值。
编辑内存值 除内存查看器外还有 RAM 监视器,可以通过 Tools > RAM Watch…访问。你可以在此窗口中固定特定的内存地址,以便监视其值。
RAM 监视器窗口 RAM 搜索 RAM 搜索窗口可以在内存中搜索特定值。可以通过 Tools > RAM Search…访问,弹出窗口如下。
RAM 搜索窗口 RAM 搜索是查找与游戏内相关值的地址的重要工具。举个栗子,我们可以搜索 RAM 看看玩家的 HP 存储在哪。
要搜索某个值,首先输入要搜索的值,然后配置搜索选项,最后点击 Search 按钮。为了搜索玩家当前的 HP,输入我们之前设置好的 “8”。
搜索值 8 是一个较小的值,因此在内存中经常出现。虽然我们可以滚动搜索结果挨个尝试,看看哪个地址代表玩家的 HP,但有更好的方法。
在不关闭 RAM 搜索的情况下,回到游戏并走动一会儿,直到你的 HP 恢复到 9。接下来,将比较运算符更改为 Different By,在旁边输入 1,然后再次点击 Search。这将会在先前搜索结果里筛选出增加了 1 的所有值。由于上次搜索是值 8,这次将筛选出两次搜索之间从 8 更改为 9 的所有值。
搜索变化的值 现在搜索结果缩小到了少数几个值,你可以尝试更改这些地址中的每一个值,找到哪个值控制玩家的 HP。最后你会发现正确的地址是 0x21BA538。
为了设置上述示例,我们手动将玩家的 HP 更改为 8,这需要事先知道玩家 HP 的地址。如果你不知道该地址,可以通过游戏内手段降低 HP,例如找到敌人并让它攻击你。
反汇编器 在 DeSmuME 中,反汇编器是一个工具,它提供了几项有用的调试功能,可以设置断点以暂停游戏执行,逐步执行汇编代码,查看寄存器值。可以通过 Tools > Disassembler 访问反汇编器,将打开 ARM9 和 ARM7 反汇编器。由于我们要查看的代码是 ARM9 CPU 的代码,请使用 ARM9 反汇编器。(译注:ARM 7 的窗口点击 Close 直接关掉就行)
ARM9 反汇编器窗口 断点 类似于高级语言中的调试器,breakpoint (断点)在程序执行到特定行时暂停执行。断点用途广泛,例如用来确定游戏内执行某个动作时是否到达了这一行代码,或者在特定函数中停止程序执行以进行调试。
作为演示,让我们在函数FUN_022ec7e8
的开头设置一个断点。在 Add Breakpoint 按钮下方输入值 “22EC7E8”,然后点击 Add Breakpoint 按钮。现在尝试在游戏中移动角色。程序将试图执行FUN_022ec7e8
并命中断点,暂停游戏执行。绿色线条表示即将执行的指令。
命中断点 图中左侧是汇编代码视图,表示地址和十六进制数据,类似于 Ghidra。你可以通过在 Go to: 中输入地址并点击 GO 来跳转到特定指令。
汇编视图下方是 Step(步进)和 Cont(继续)按钮,它们的功能类似于标准 IDE 调试器中的功能。步进使程序前进一条指令,如果更改按钮旁边的数字,则前进多条指令。我建议在逐步执行代码时打开 Auto-update,否则每次前进到新指令时都需要点击 Refresh 按钮来更新视图。Cont 将继续程序执行。有时,点击 Cont 之后游戏将保持暂停状态;如果发生这种情况,请使用游戏窗口中的暂停 / 播放按钮(译注:在 File 右边的图形按钮而没有文字)取消暂停。
汇编视图右侧是寄存器和他们的值,你可以查看并编辑它们。在编辑寄存器值时,请确保禁用了 Auto-update,以避免寄存器值在尝试编辑时立即恢复。完成编辑后,点击更新寄存器以应用寄存器更改。
窗口右侧是活动断点。注意,如果你点击 Delete breakpoint,当前显示列表顶部的断点将被删除。你可以使用上下箭头按钮滚动列表,将要操作的断点移动到删除位置。
在我(译注:原作者)的经验里 Run To Return 和 Step over(步过)按钮表现不一致,请自行判断使用。
监视点 监视点 (watchpoint)会在内存中的特定地址被读取或写入时暂停程序执行。这对于找到代码中操纵内存中特定值的部分非常有用,也可以用来追踪尚未弄清楚用途的内存值。在 DeSmuME 中,监视点包括读断点 (read breakpoint)和写断点 (write breakpoint)两种。
可以在内存查看器中设置监视点。为了演示监视点,我们来监视玩家的 HP 值。打开内存查看器,右侧找到监视点编辑器。在文本框中输入 21BA538,然后点击 Add Write Breakpoint。
添加写断点 设置写断点后,移动角色直到 HP 自然恢复。当 HP 恢复时,监视点会暂停程序。进入反汇编器并点击 Refresh 或开启 Auto-update,然后转到当前pc
所指向的地址,应该是 0x2311264。向上滚动一些(确保光标不在汇编视图内),找到下一个将要执行的指令的绿色高亮标记,地址是 0x231125C。注意,pc
可能比当前执行的指令稍微靠前,这是 CPU 的工作方式导致的。(译注:查阅资料可知,ARM9 有五级流水线)
玩家 HP 的写断点命中后的反汇编视图 注意,游戏暂停的指令是访问被监视地址的指令之后 的指令,这意味着写入 HP 值的指令位于 0x2311258。此地址包含指令strh r1, [r7, #10]
。你可以看到r7
中的地址是 0x21BA528,加上 10(根据strh
指令)得到 0x21BA538,即玩家的 HP 地址。r1
中包含玩家 HP 刚被设置的新值,这个值也可以在内存查看器中的地址 0x21BA538 看到。
如果你在 Ghidra 中查看地址 0x2311258 的指令,你会发现该指令位于FUN_02311088
内,表明该函数处理被动的 HP 恢复。
状态保存 状态保存(Save states)是大多数模拟器的标配功能,可以随时保存和加载游戏状态。除了通常的游戏用途,调试程序时也可以用状态保存做更细致的控制。
DeSmuME 有十个状态保存槽。要保存或加载状态,可以使用 File > Save State and File > Load State,或它们各自的快捷键。如果你在反汇编器中逐步执行汇编代码,状态保存在调试过程中也很有用,因为你可以多次以相同的游戏状态逐步执行代码。请注意,在逐步执行代码时加载状态可能不会立即刷新游戏内的图形,直到程序继续执行,但这不会影响你逐步执行代码。
DeSmuME 总结 我们已经了解了 DeSmuME 用于检查和调试实时游戏的一些通用工具。该模拟器还有许多本教程不会涉及的更为专业化的工具,例如查看当前加载的调色板、图块(tile)和背景。如果你对这些感兴趣,可以自行探索。
逆向工程策略 有了汇编知识、Ghidra 和 DeSmuME,我们就具备了对游戏进行逆向工程的所有工具。本教程的最后部分将讨论一些逆向工程策略,以查找游戏功能在 ROM 和内存中的位置。
本教程的 DeSmuME 部分讨论了使用RAM 搜索 和监视点 的一些策略,如果你还没有查看,可以回去看看。
通过汇编代码反向追踪值 One way to find the location of a specific value is to trace related values backwards through the assembly to find where they came from. This might lead to the value you are looking for.
找到特定值位置的一种方法是通过汇编代码反向追踪它们的来源。该方法可能会帮你找到要查找的值。
出于演示目的,我们来找找代码中治疗道具恢复角色 HP 的地方在哪。在游戏中探索迷宫,直到你找到地面上的一个橙橙果(Oran Berry)。你可能不会在当前楼层找到它;如果你探索完当前楼层但没有发现,请寻找楼梯进入下一层继续寻找。
玩家右侧地上的橙橙果 找到橙橙果就走过去把它捡起来。该果实能恢复 100HP(最多恢复到你的 HP 上限),而我们在本演示中的目标是将其更改为只恢复 10HP。为此,我们需要知道 100 这个值在游戏代码中的存储位置。
你需要将 HP 调低以观察果实恢复了多少 HP,因此请打开内存查看器,将你的 HP(地址 0x21BA538)设置为 01。按 X 键(译注:手柄映射而非键盘按键)打开菜单,进入物品栏,查看你背包中的橙橙果。保存游戏状态,以便你可以返回这个时间点,因为一会得反复吃果实来调试代码。状态保存后,选择该果实,选择食用,最后选择玩家角色,果实将恢复所有玩家的 HP。
正常情况下,橙橙果恢复最多 100HP(上限为玩家的 HP 上限) 现在我们已经看到了橙橙果的正常效果。重新加载状态保存,并使用内存查看器在地址 0x21BA538(玩家的 HP)添加一个写断点。再次食用果实,命中写断点并暂停游戏。进入反汇编器,转到pc
指向的地址,向上滚动找到游戏暂停的指令。我们要找的是接下来的strh r0, [r2, #10]
,在 0x231529C。
吃掉橙橙果后命中玩家 HP 写断点 刚刚r0
被存到玩家的 HP 中了,因此查看反汇编器中的r0
,发现值为 0x65(十进制的 101),正好等于橙橙果恢复的 100HP 再加上玩家的 1HP。稍后会有代码将玩家的 HP 限制在其 HP 上限,但未限制的 HP 也可以正常使用。
之前的两行汇编代码也值得注意。
1 2 ldrsh r0 , [r2 , #10 ]add r0 , r0 , r5
游戏加载了玩家的 HP([r2, #10]
,与 strh
指令相同),然后又加上了r5
的值。r5
的值是 0x64(100),与 橙橙果的治疗量 0x64(100)一致。
现在我们来看看 0x64 是在哪里赋值给 r5
的。转到 Ghidra 中的地址 0x231529C,找到反汇编器中的相同指令。鼠标中键点击add r0,r0,r5
指令中的r5
来高亮其用法,然后向上滚动函数。在地址 0x2315288 处有另一个对r5
的引用:ldmiaeq sp!, {r3 r4 r5 pc}
,但这是函数的早回(early return)(通过从堆栈中弹出到pc
来指示),因此可以忽略它。函数的顶部附近有我们要找的指令,地址为 0x2315278 的mov r5, r2
。
追踪橙橙果的治疗量到函数的顶部 r5
中的 0x64 来自r2
,但在这里和函数的开始之间没有更多对r2
的引用。记得寄存器r0
-r3
是用于将参数传递给函数的,因此r2
是FUN_0231526c
的第三个参数,0x64 来自调用FUN_0231526c
的代码。下一步是找到该函数的调用者。在FUN_0231526c
的底部,找到 0x23152DC 处的ldmia
指令,标志着函数的结束。使用 DeSmuME 在 0x23152DC 设置断点,然后继续程序执行以命中断点。注意,在命中断点之前,你会再次命中玩家 HP 的写断点,当时玩家的 HP 被限制在 HP 上限。
命中函数末尾的断点后,再执行一条指令,查看程序从函数返回后的pc
值并向上滚动,找到当前指令。
函数FUN_0231526c
之外 在当前指令的上方,你可以看到地址 0x2315460 处的bl 0231526c
,确认这是调用FUN_0231526c
的地方。转到 Ghidra 中的地址 0x2315460。
在 Ghidra 中地址 0x2315460 调用FUN_0231526c
我们正在寻找r2
被赋值为 0x64 的位置,所以查看函数调用之前的几行。我们可以看到地址 0x231545C 处的mov r2, r5
紧接在函数调用之前,意味着 0x64 来自于r5
。鼠标中键点击r5
并查找写入它的指令。
在 0x2315378 处的指令是add r5, r5, r0, asr #0x8
,它写入了r5
。让我们看看在吃橙橙果时是否会到达这条指令。首先,删除现有的断点和观察点,因为我们不再需要它们了。在 0x2315378 处设置断点,重新加载存档状态并吃橙橙果。吃树果时不会触发断点,这意味着这行代码与我们无关。继续查看函数中其他可能给r5
赋值的地方。
下一个相关的指令在函数开始附近,地址为 0x23152F4 处的mov r5,r2
。在此处和函数开始之间没有对r2
的赋值,所以它再次成为传递给函数的参数。我们需要追踪这个值到调用FUN_023152e4
的地方,并从那里跟踪r2
,看它是在哪里被赋值为 0x64 的。
这次,我们使用不同的方法来查找调用函数。删除之前的断点,在 0x23152F4 处设置断点,然后重新加载存档并再次吃橙橙果。这次会触发断点,因此前往反汇编器中的地址 0x23152F4。
地址 0x23152F4 处的断点 由于这是函数的开头,还没有调用其他函数,因此寄存器lr
的值包含了调用函数的返回地址,即地址 0x231C0C8。前往 Ghidra 中的地址 0x231C0C8。
Ghidra 中的地址 0x231C0C8 我们可以看到 0x231C0C4 处对FUN_023152e4
的函数调用。回到寻找r2
被赋值为 0x64 的地方,向上看几行。在 0x231C0A4 和 0x231C0AC 行有对r2
的引用。
1 2 3 ldr r1 , [PTR_DAT_0231c730]... ldrsh r2 , [r1 , #0x0 ]
可以看到一个地址被加载到r1
,该地址来自数据值PTR_DAT_0231c730
,然后该地址中的值又被加载到r2
。在这些指令的右边,Ghidra 的分析显示PTR_DAT_0231c730
指向地址 0x22C45EC。
记住这个地址,回到 DeSmuME 并在内存查看器中转到地址 0x22C45EC。你会在该地址找到 0x64 的值,确认这是橙橙果的 HP 恢复量所在的位置。
橙橙果恢复量的位置 为了测试我们找到的恢复量地址,重新加载存档状态,在内存查看器中将地址 0x22C45EC 的 0x64 更改为 0x0A(0x0A= 十进制的 10),然后再次吃橙橙果看看它恢复了多少。删除之前的断点以避免再次触发,因为他们已经没用了。
更改后的橙橙果恢复量 成功了!我们发现橙橙果的恢复量存储在 0x22C45EC。
橙橙果的恢复量在哪个 overlay 文件里?前文提到,我们加载的 overlay 文件在内存中的地址如下:
Overlay 10: 0x22BCA80 Overlay 29: 0x22DC240 Overlay 31: 0x2382820 查看这三个地址,overlay 29 和 31 的地址在 0x22BCA80 之后,所以该值位于 overlay 文件 10 中。通过将该值的地址减去 overlay 文件的地址,你可以计算出该值在 overlay 文件中的偏移量(与内存中的偏移量不同)。0x22C45EC - 0x22BCA80 = 0x7B6C。你可以通过在十六进制编辑器中打开overlay_0010.bin
并转到字节 0x7B6C 来验证这一点。
实际上,如果你在overlay_0010.bin
中将偏移量 0x7B6C 处的值更改,然后将 ROM 文件重新打包成一个单独的.nds 文件(使用与解包时相同的工具),你将创建一个最小化的 ROM hack,削弱橙橙果的治疗效果。创建 ROM hack 超出了本教程的范围,这里所展示的是如何创建此类 hack 的过程的一部分。
直接阅读汇编代码 显然,直接去读汇编代码是了解游戏逻辑的一种方式。
让我们探索与上一节中相同的功能(玩家通过浆果恢复 HP),并寻找将玩家的 HP 限制在其 HP 上限的代码。返回 Ghidra 中的地址 0x231529C。
通过浆果恢复玩家 HP 的汇编代码 我们看到 0x231529C 处的指令strh r0, [r2, #0x10]
将玩家的 HP 设置为恢复量加上玩家的当前 HP。在地址 0x23152BC,你可以看到一个条件ble
语句,检查r0
是否小于r3
。或者,你可以查看反编译代码来找到相同的条件语句。如果条件不满足,则会将另一个值存储到玩家的 HP 中(0x23152CC 处的strh r1, [r2, #0x10]
)。
由于使用偏移量访问玩家的 HP,这表明玩家的 HP 位于一个结构体中,可能还包含其他与玩家相关的值。玩家的 HP 在该结构体中的偏移量为 0x10。
虽然我们可以在汇编代码中追踪r0
和r3
的值,但设置一个断点并查看游戏运行时的值可能更容易。在ble
指令 0x23152BC 处添加一个断点,然后重新加载保存的状态并吃掉橙橙果。
命中 0x23152BC 处的断点 r0
为 0x65,是橙橙果的恢复量加上玩家 HP 的和(100 + 1 = 101 = 0x65)。r3
等于玩家的 HP 上限,截图中为 0x20,即 32,但根据你扮演的不同宝可梦,这个值可能略有不同。这意味着代码检查恢复的 HP 是否大于 HP 上限,并在必要时将玩家的 HP 限制在最大值。如果你继续执行接下来的几条指令,你会看到r1
中的值(也等于玩家的 HP 上限)被算作玩家的 HP。
为了进一步确认,我们可以更改 0x23152CC 处的strh
指令,使其不再限制玩家的 HP。在反汇编器中,你可以看到这条指令的十六进制值为E1C211B0
。转到内存查看器中的地址 0x23152CC,你会发现那里有值B011C2E1
,这是小端格式的E1C211B0
。重新加载状态保存,然后将这四个字节更改为00
,这将使指令变成一个空操作(即什么也不做的指令)。(译注:准确来说是会变成andeq r0, r0, r0
,从指令集设计的角度考虑很有意思。)
将 0x23152CC 处的指令更改为空操作 现在让我们试着吃一颗浆果。
移除恢复 HP 的上限(某种程度上) 游戏现在显示恢复了 100HP,你可能已经注意到面板中当前 HP 一度高于 HP 上限,但随即又被重置回最大值。显然,代码的其他地方还有另一个安全检查,确保玩家的 HP 不会超过最大值。然而,游戏显示了 100HP 的恢复,这证明我们正在查看的代码确实是在检查并限制 HP 恢复。如果你愿意,可以对玩家的 HP 添加另一个写断点,并通过类似的过程找到并禁用其他检查。
检查已知值附近的值 一旦你在内存中找到了一个值,很可能附近的值也与此相关,例如某个结构体或数组的一部分。我们可以使用内存查看器查看已知值附近的其他 RAM 值,可以用瞪眼法或瞎改法发现更多的值。这种方法没什么特定目标,而是旨在快速发现一系列值,为以后搜索特定值奠定基础。
本例将使用本教程中找到的玩家 HP 地址。打开内存查看器并转到地址 0x21BA538。既然玩家的 HP 在这里,合理推测其他与玩家角色相关的值也在附近。
查看内存中玩家 HP 附近的值 与 HP 直接相关的值是玩家的 HP 上限。查看游戏中的面板以找到你的 HP 上限(在当前 HP 的右侧),然后尝试在内存查看器中找到它。
你不需要找太远。在当前 HP 的前两个字节处,有一个与你的 HP 上限相匹配的数字,地址为 0x21BA53A。尝试更改此值,你会看到 HP 上限在面板上发生变化,证明 0x21BA53A 就是玩家的 HP 上限。
另一个靠近的数字是玩家的等级(在面板上标记为 “Lv”),刚开始游戏时为 5。在内存查看器中寻找值 5,一旦找到可能的值,尝试更改它,看看玩家的等级是否在面板上发生变化。正确的地址在下面的提示中。
剧透:玩家等级的地址 0x21BA532
有个类似的方法是在游戏中执行某个动作并观察内存中的值如何变化,包括移动、攻击等。为了演示此方法,在游戏中按住 Y(或 Start)键以进入可以面向不同方向而不移动的模式。转动几次方向,同时观察内存查看器,你会看到地址 0x23BA574 处的值在变化。如果没有其他可见值发生变化,这表明 0x23BA574 是玩家面对的方向(编码为枚举)。
简单地遍历内存地址,挨个更改它们,并查看是否对游戏中的任何内容产生影响,这也是一种试错法。例如,如果你重复这个过程,最终就会抵达地址 0x21BA5E5。将此值置 1,你会看到玩家角色进入睡眠状态,这表明该值用于记录睡眠状态条件。
发现控制玩家是否入睡的地址 没有人说得清用这些方法需要投入多少才能找到相关信息,因此你自己决定何时止损换用其他策略。
策略链 在逆向工程过程中,上述策略通常组合使用。基本思路是从一个已知值开始,找到另一个将你引向所需功能的值。重复此过程,逐步发现更多有助于找到最终目标的值。
例如,如果你在寻找一个更抽象的概念,比如在游戏中AI 如何是工作的 ,你可能会遵循以下步骤:
使用 RAM 搜索查找变化,定位内存中的玩家 HP。 找到一个敌人攻击你,然后在玩家 HP 的地址添加一个写入断点,找出是哪段代码造成了攻击伤害。 从伤害处理代码开始,反向追踪汇编代码,直到找到游戏决定敌人攻击应该造成伤害的位置。通过使用断点对比伤害攻击和无伤害攻击的代码路径,可能会发现一个用于确定攻击效果的攻击 ID 检查。 反向追踪汇编代码和 / 或使用观察点,找出代码设置敌人攻击 ID 的位置。这很可能是由敌人 AI 做出的决策,这是进入敌人 AI 代码的切入点。 正向追踪汇编代码,了解敌人 AI 的工作原理。 使用现有资源 但凡你正在逆向工程的游戏有点人气,那么很可能已经有其他人已经进行过相关研究。逆向工程文档可能包括已知数据、函数的地址、结构体布局和代码架构等信息,这可以为你节省发现它们的时间,并作为进一步探索游戏信息的起点。
请注意,游戏的破解和逆向工程资源通常是零散且没人整理的,信息分布在 Google Docs/Sheets、GitHub 仓库、Discord 服务器、Reddit 帖子、论坛、维基等多个平台。可以从Data Crystal 开始寻找游戏逆向工程资源,它包含了相当多的游戏的逆向工程文档以及外部资源的链接。Google(或你喜欢的替代搜索引擎)也是个选择,搜索 “<游戏名> hacking” 之类的词可能会带来结果。留意任何活跃的破解和逆向工程社区,比如 Discord 服务器或 subreddit。
一些逆向工程社区更进一步,维护了一个进行中或已完成的手动反编译 (decomp)或反汇编 的项目,使用源代码或结构化的汇编代码构建了一个与实际游戏的 ROM 文件匹配的游戏二进制文件。由于这些项目需要构建匹配的二进制文件,所以它们通常包含大量标记好的游戏信息。例如,《空之探险队》有一个正在进行的反编译项目这里 。手动反编译是逆向工程领域中一个高度技术化的子领域,超出了本教程的范围,但如果你正在逆向工程的游戏有这样的项目存在,值得留意。
《空之探险队》资源 对于《空之探险队》来说,你可能首先会通过搜索找到 ROM 编辑工具SkyTemple ,以及大部分《空之探险队》破解讨论发生的SkyTemple Discord 。
《空之探险队》的破解社区创建了一个集中的文档仓库,称为pmdsky-debug ,用于记录函数、结构体和其他技术数据,并能够将文档导入 Ghidra 和其他逆向工程工具。
正如前文提到的,《空之探险队》有一个正在进行的反编译 项目。
各种技术文档也可以在Project Pokémon 找到,比如文件格式、压缩算法和解包 ROM 中的所有文件的分解。
结论 到现在为止,你已经搭建了逆向工程环境,学习了汇编基础和一些逆向工程工具,并走过了一些发现游戏内功能的策略。从这里开始,你已经准备好深入你最喜欢的 NDS 游戏的代码,看看能找到什么。请注意,逆向工程过程并不总是简单明了的,它需要足够的创造力和耐心,但这是一个可以通过练习和坚持提高的技能。祝你好运!
如果你想就本教程与我(译注:原作者)联系,你可以在《宝可梦》神秘迷宫逆向工程服务器如SkyTemple 和pret 上找到我(Some Body),或者可以在 Reddit 上找到我,用户名是u/AnonymousRandPerson 。
Fin.