我的头像小萧的开源知识站首页标签系列

导出内存并逆向调试分析

撰写日期 2022-09-29 阅读大约用时 11 分钟

本人不是很有自信能写好这个教程,加上本人时间紧张没法保证能稳定更新,如果有疏漏或者疑问请在评论区指出,非常感谢! 首先非常感谢指导我学习 NDS 游戏破解的 Enler 大佬,如果没有他我的童年回忆的汉化和这个破解教程就不能实现了,非常感谢!

本教程的破解源代码(流星洛克人一代汉化的汉化破解源代码)将会在汉化完成后整理(为了尊重汉化组劳动成果,仅包含我之前乱翻的测试文本)并传到 Github 上,届时大家可以自行查看学习。

如果你是初次接触老游戏机游戏的破解,我个人先建议你去尝试一下简单的游戏破解教程: https://www.bilibili.com/read/cv18346849/

前言

想必各位都应该已经能够在网络上找到各种各样针对游戏素材、游戏自带字体的修改和打包了。但是如果遇到非常早期的游戏(比如早期 NDS 并没有使用 NitroSDK 的内置字体功能的游戏(就是说你呢流星洛克人)),并不存在真正意义上的字体文件且字库容量非常匮乏,那么我们只能通过编写程序来修改游戏打印/渲染文字的行为,让它能够打印我们的中文汉字了。

但是很可惜这样的教程过去曾经有,但是都随着时间不是网站到期关闭就是埋没了。而现在随着技术发展,游戏破解已经不再像过去那样复杂且繁琐了(相对上的)。所以我根据 Enler 大佬的指导,尝试总结出了现在相对方便的游戏代码破解/编写方式。

代码上的破解注重对程序执行流程的推测和判断,毕竟每个游戏绘制文字的方式都不一样,本教程只能算是抛砖引玉,真正要编写代码的话可能不能完全按照教程的走,所以要与自己破解的游戏相结合,再整合本教程里你能利用的来进行你的游戏破解,效果会更好一些。

本教程以我所做的流星洛克人一代破解为基础,大部分技术应该是通用的,其它的游戏因为没有接触过所以本人也不太好说明,所以请大家多多谅解。

需要的东西

在开始破解前,我们需要一些前期准备,毕竟本教程并非能让零基础的小白直接上手,所以可以的话请先了解以下知识或者资料后再回来看本文,会更容易理解:

然后是一些用来逆向分析/破解的工具,因为版权和其它原因只告诉大家可以从哪里找到,不提供下载链接,请见谅。

那么工具就准备这么多,接下来最后准备好你要破解的游戏原始文件(ROM),就可以开始继续下一步了!我们教程就以流星洛克人一代天马版日版为破解对象,为其扩展字库吧!

第 1 步:导出游戏内存

我们通过 No$GBA 打开游戏,先进入游戏并大概运行到文字开始打印的位置,比如说:

如果你的 No$GBA 并不是长这样的,检查一下你下载的是否是调试器版本

在这里点击我们的主窗口(有很多代码/内存/栈/寄存器的那个窗口),此时游戏会暂停,接下来我们要跳转到我们的内存起始位置,并导出全部的内存原始数据。

点击一下左下角部分的内存窗口(把鼠标焦点集中在这个窗口):

点击一下之后此时你应该可以看到有一小块数据被你选中了(蓝色高亮)

然后点击左上角的 Search 搜索菜单,选择跳转光标到地址(Goto, move cursor to address nnnn)或直接按下 Ctrl + G 快捷键打开跳转的输入窗口。

打开跳转的地址的输入框

根据 GBATEK 的信息,我们可以知道 NDS 的主内存所属的地址范围是 0x02000000 - 0x02400000(总计 4MB(0x400000)的主内存)。那么我们输入 0x02000000,点击 OK 或者回车,跳转到主内存的开头位置。

此时应该可以看到光标所在的位置应该是 0x02000000(左侧的地址),且左下角的状态文本也是 02000000

接下来是导出内存,点击左上角的 Utility 实用菜单,选择 导出字节到 .bin 文件(Binarydump to .bin File),在弹出的窗口里输入我们需要导出的字节数量的十六进制值 0x400000(也就是 4MB = 4194304 Byte = 0x400000 Byte)

弹出的输入需要导出多少字节数据的窗口

接下来选择要保存的位置,确定后会卡顿一小会时间(切勿触碰模拟器窗口(容易未响应)),等你要导出的文件出现之后,我们就算完成内存导出了:

确认大小是否是 4MB,如果不是的话回去看看是不是数据导出大小那里输入错误了

第 2 步:导入到 IDA Pro

打开我们的 IDA Pro 32 位版本,在打开的欢迎页面里选择 逆向一个新的文件 New (Disassemble a new file)

如果你是直接进入了主窗口页面的话,点击左上角的 File 文件菜单,选择 Open 也可以打开新文件

当然这个文件夹图标也可以打开新文件

在弹出的新窗口里选择我们的处理器类型(Processor Type)为 ARM Little-endian [ARM] (因为我们的 NDS 机器的 CPU 就是 ARM 架构的处理器),其余选项不用变更(应该会和下图一致),然后点击 OK 确定。

如果你没有找到 ARM Little-endian 类型,请确认你下载的 IDA Pro 是否支持这个处理器

接下来弹出的窗口要我们配置好加载的文件在内存中的实际位置,我们只加载了内存部分所以我们勾选 Create RAM section(创建内存区域),取消勾选 Create ROM section(创建只读内存(卡带)区域),将 RAM start address(内存初始位置)和下面 Input file(输入文件)的 Loading Address(加载地址)设置成我们刚刚导出内存时的那个初始位置,RAM Size(内存大小)和我们导出来的文件大小一致。然后点击 OK 确认。

输入完信息后应该长这样

接下来 IDA 会准备开始分析文件,此时还会提示你两个重要的信息,后面会用到:

ARM 和 THUMB 模式指令集切换提示

ARM 和 THUMB 模式指令集切换提示

这个处理器包含两种指令集编码:ARM 指令集和 THUMB 指令集。

IDA 允许给每一个特定的指令使用特定的指令集编码。为此 IDA 会使用一个虚拟寄存器 T。如果这个寄存器的值是 0,那么会使用 ARM 指令集来解析接下来的代码,否则(非零)使用 THUMB 指令集来解析接下来的代码。你可以通过使用 “变更序列寄存器值(change segment register value)” 来变更这个寄存器的数值。(快捷键是 Alt + G)

加载二进制文件提示

你刚刚加载了一个纯二进制文件。

IDA 无法自动判定程序的进入点,因为纯二进制文件没有特定的标准格式

请移动到你认为是程序进入点的地址,然后按下 C 键来开始自动分析。

阅读完成后点确定,接下来 IDA 开始分析程序,等待下方的日志输出了 The initial autoanalysis has been finished 后,初次分析就正式完成了。

第 3 步:找到文字打印的代码并在 IDA 里分析

在进行这一步之前,先确保你已经了解了 ARM 汇编和相应的调试知识,否则这一段会非常晦涩难懂。

那么先简单了解一下游戏都是怎么打印文字的吧:

在内存和显存匮乏的老旧游戏机上,为了节省内存,我们会使用图块(Tile)+索引(Map)+调色板(Palette)的方式降低显存的用量,大概的流程(这里指背景模式 BG0)是:

  1. 先设置一个调色板,之后图块的颜色由此调色板决定,根据 NDS 渲染模式可以是 16 色或 256 色。
  2. 然后往图块的显存区域里写入要显示的内容,通常一个图块是 8×8 的像素图,根据调色板颜色的数量和显示模式可以是 16 色或 256 色,前者需要 8×8÷2 字节存储图块,后者则是 8×8 字节。
  3. 接下来在图块索引的显存区域里写入这个图块所在显存位置的编号,此时图块才真正的被显示出来了。

No$GBA 里的调色板查看器,整个游戏机只有四组调色板可供设置,且上下屏各有两组(分别是 Engine A(下屏)和 Engine B(上屏))

显存里其中一个图层的内容,这里正好是我们文字框的所在图层

知道了这个原理之后,我们来大概猜测一下这个游戏里的文字应该是这样打印的:

  1. 先分配好文字的调色板
  2. 然后分配好文字所在的图块的内存区域以及索引
  3. 读取字库,把字形写入到图块里

为了证明我们的猜想是正确的,我们可以尝试捕捉(也就是条件断点)到游戏写入显存的请求,这样就可以明白是否是这个方式打印了。

为了捕获这样的写入指令,我们需要编写一个条件断点,在写入到显存指定位置时暂停游戏。

条件编写的文档可以在 No$GBA 里的 Help 帮助菜单的 Debugging 选项里找到:

断点的帮助文档

接下来我们来观察文字框的所在图层的文字,点击 左上角的 Window 窗口菜单,找到 BG Maps > BG0/1/2/3 Map 查看显存中的图层内容。

每个游戏使用的图层(或者使用方式)可能会不一样,所以要一个个查看,我这里是定位在了 BG3 图层里:

我们把光标放在第一个字的位置,此时右侧会显示这个图块的状态,我们需要留意它的 Map Address(索引地址)和 Tile Address(图块地址):

考虑到这个字体是 12×12 而非规矩的 8×8 字体(刚好塞进一个图块里),一般不会频繁更改索引,所以我们优先考虑写入图块的情况。以本例来看,这个图块的图块显存地址是 0x0600C800,那么我们来捕获这部分的显存修改即可,其条件判断应该这样写:

[0x0600C800..0x0600C800+0x20]!

0x20 是一个图块的在 16 调色板模式下的大小,如果是 256 模式的话可能会是 0x40,当然这个大小不是特别重要,我们只要可以捕获到写入显存的操作就可以了。

接下来我们添加这个断点,点击 Debug 调试菜单,选择 定义断点/条件断点(Define Break/Condition)(你也可以按下快捷键 Ctrl + B 来打开)

提示输入条件断点的窗口

输入我们的断点,然后回车,如果主窗口的左下角变成了断点列表且显示着我们刚刚添加的断点,就添加成功了。

对了,要回到之前的内存视图的话,点击一下断点列表然后按下 Tab 键即可切换回去,再按一次也可以切换回来。

添加成功后,左下角会变成断点列表,显示了我们刚刚添加的断点

Huh? 有什么地方敲错了吧?

接下来我们回到游戏,点击只有游戏画面的窗口继续游戏,然后点击下一个对话,如果运气好的话,游戏会突然暂停,且主窗口会弹出来,这便是成功捕获了。

如果没有出现的话也不要灰心,这个情况时有发生,多尝试几下,或者从其它打印方式入手来写断点,都是可以的。

断点成功触发时的情景举例,实际情况可能略有不同

那么接下来我们要做的就是找到程序的调用点了,观察右下角的栈内存窗口,我们能发现一个 Return from Lxx_#XXXXXXX 字样,这个是模拟器能捕获到的将上一个跳转的地址入栈的信息,我们可以通过它来判断一个函数的入口点,从这里可以看到这个子程序的入口点是 0x202E6A8。

但是光有这个还不够,还记得 IDA 给我们的第一个提示吗?我们的处理器有两套指令集,所以我们还要搞清楚这个子程序是用的 ARM 指令集还是 THUMB 指令集,那么我们可以一步一步执行程序,回到接近这个子程序的位置。

在单步执行前,我们可能要删除一下刚刚设置的断点,避免单步执行因为断点而中断,带来不必要的麻烦。回到断点列表,点击我们的断点然后按 Delete 键删除即可(如果键盘不包含这个键的话,右键断点后的菜单中也有删除的选项)。

点击 Run 运行菜单,点击 Run to Sub-return 运行到上一个子程序执行完当前子程序的位置:

你也可以按 F8 来触发它

不出意外的话,此时应该会在上一个函数的附近了:

此时我们观察此时的 CPSR 寄存器(Current Program Status Register:当前程序状态寄存器),可以发现此时的 Thumb 位(右侧标注了 t 的复选框)为勾选状态,表明这个子程序是使用的 THUMB 指令集的,如果没有勾选那就是 ARM 指令集。

No$GBA 很贴心,帮我们把 CPSR 寄存器的每一位都单独提出来方便查看了

确认了指令集是 THUMB 之后,我们可以切换到 IDA 里,选择 Jump 跳转菜单,选择 Jump to address(跳转到地址)(你也可以按下 G 键来打开)打开跳转地址窗口,输入我们刚刚找到的子程序的位置(0x0202E6A8)然后确认。

跳转成功后此时的 IDA 视图应该是这样的:

当前光标所在位置应该是我们刚刚输入的地址

我们 IDA 默认使用 ARM 指令集,那么遇到 THUMB 指令集的话要怎么切换呢?

还记得刚刚 IDA 给我们的两个提示吗?接下来就根据第二个提示,来修改 T 寄存器的值吧:

按下 Alt + G 打开序列寄存器窗口,把 T 寄存器的值改为非零值然后确认

此时视图应该会在我们的程序入口标注 CODE16,代表从此处开始分析器将会使用 THUMB 指令集来分析:

最后让光标对准我们的子程序入口,按下 P 键定义一个子程序入口点(C 键也可以,不过不会将其作为一个子程序解析),此时 IDA 将会开始分析,并把刚刚这段数据转换成响应的汇编代码

解析后的汇编代码的样子

你也可以和 No$GBA 里的汇编视图比对一下是否一致

如果你发现生成的代码不太一样(比如说忘记切换指令集什么的)可以通过对错误的代码区域按下 U 键将其置为未定义数据块,就可以重新开始解析代码了。

最后,把光标放在解析出来的代码块,按下 F5,对这段汇编代码生成对应的伪 C 代码:

如果能够出现这样的伪 C 代码,说明成功了

如果你非常顺利地做到了这里,那么恭喜你!你已经基本明白了调试和逆向分析的非常非常简单的流程了(其实我自己也不会特别复杂的),那么只要你对 C 语言有所熟悉,配合伪代码能帮助你更好地理解程序的执行走向。

当然,配合经验之谈,其实我们逆向的这段代码并非真正的打印文字的部分。这里仅仅是把内存中的某部分复制到了显存里。

当然不要灰心,这样的情况我也遇到过不少,逆向工程本来就是一种不断挖掘探索的过程,虽然枯燥但是非常有趣。毕竟说不定你还可以发现游戏的废案资料呢?

那么这一大部分的逆向分析教程到这里就先告一段落了,后面我会根据情况多写新的教程,请大家敬请期待吧!