如何逆向NDS游戏(下):逆向工程与代码分析

本文翻译自《Reverse Engineering a DS Game》。

经过两部分的铺垫,终于来到了使用GhidraDeSmuME分别进行静态/动态代码分析的实战部分。

使用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的地方,其中之一是地址0x22F798Cbl指令,在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由地址0x22E0938beq指令引用。
  • 反编译的函数代码位于反编译器窗口,显示函数的反编译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以打开多个内存查看器窗口。

内存查看器窗口

在上图中,内存查看器显示了地址0x20000000x20000F0的内存值。字节数据以表格形式显示,地址的第一个数字为列,0x10的倍数为行。例如,0x2000010行包含地址0x20000100x200001F的值,而地址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访问反汇编器,将打开ARM9ARM7反汇编器。由于我们要查看的代码是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 ReturnStep 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)和背景。如果你对这些感兴趣,可以自行探索。

逆向工程策略

有了汇编知识、GhidraDeSmuME,我们就具备了对游戏进行逆向工程的所有工具。本教程的最后部分将讨论一些逆向工程策略,以查找游戏功能在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来指示),因此可以忽略它。函数的顶部附近有我们要找的指令,地址为0x2315278mov r5, r2

追踪橙橙果的治疗量到函数的顶部

r5中的0x64来自r2,但在这里和函数的开始之间没有更多对r2的引用。记得寄存器r0-r3是用于将参数传递给函数的,因此r2FUN_0231526c的第三个参数,0x64来自调用FUN_0231526c的代码。下一步是找到该函数的调用者。在FUN_0231526c的底部,找到0x23152DC处的ldmia指令,标志着函数的结束。使用DeSmuME0x23152DC设置断点,然后继续程序执行以命中断点。注意,在命中断点之前,你会再次命中玩家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的地方,向上看几行。在0x231C0A40x231C0AC行有对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恢复量所在的位置。

橙橙果恢复量的位置

为了测试我们找到的恢复量地址,重新加载存档状态,在内存查看器中将地址0x22C45EC0x64更改为0x0A(0x0A=十进制的10),然后再次吃橙橙果看看它恢复了多少。删除之前的断点以避免再次触发,因为他们已经没用了。

更改后的橙橙果恢复量

成功了!我们发现橙橙果的恢复量存储在0x22C45EC。

橙橙果的恢复量在哪个overlay文件里?前文提到,我们加载的overlay文件在内存中的地址如下:

  • Overlay 10: 0x22BCA80
  • Overlay 29: 0x22DC240
  • Overlay 31: 0x2382820

查看这三个地址,overlay 2931的地址在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。

虽然我们可以在汇编代码中追踪r0r3的值,但设置一个断点并查看游戏运行时的值可能更容易。在ble指令0x23152BC处添加一个断点,然后重新加载保存的状态并吃掉橙橙果。

命中0x23152BC处的断点

r00x65,是橙橙果的恢复量加上玩家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如何是工作的,你可能会遵循以下步骤:

  1. 使用RAM搜索查找变化,定位内存中的玩家HP。
  2. 找到一个敌人攻击你,然后在玩家HP的地址添加一个写入断点,找出是哪段代码造成了攻击伤害。
  3. 从伤害处理代码开始,反向追踪汇编代码,直到找到游戏决定敌人攻击应该造成伤害的位置。通过使用断点对比伤害攻击和无伤害攻击的代码路径,可能会发现一个用于确定攻击效果的攻击ID检查。
  4. 反向追踪汇编代码和/或使用观察点,找出代码设置敌人攻击ID的位置。这很可能是由敌人AI做出的决策,这是进入敌人AI代码的切入点。
  5. 正向追踪汇编代码,了解敌人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游戏的代码,看看能找到什么。请注意,逆向工程过程并不总是简单明了的,它需要足够的创造力和耐心,但这是一个可以通过练习和坚持提高的技能。祝你好运!

如果你想就本教程与我(译注:原作者)联系,你可以在《宝可梦》神秘迷宫逆向工程服务器如SkyTemplepret上找到我(Some Body),或者可以在Reddit上找到我,用户名是u/AnonymousRandPerson

Fin.

0条搜索结果。