本人不是很有自信能写好这个教程,加上本人时间紧张没法保证能稳定更新,如果有疏漏或者疑问请在评论区指出,非常感谢! 首先非常感谢指导我学习 NDS 游戏破解的 Enler 大佬,如果没有他我的童年回忆的汉化和这个破解教程就不能实现了,非常感谢!
本教程的破解源代码(流星洛克人一代汉化的汉化破解源代码)将会在汉化完成后整理(为了尊重汉化组劳动成果,仅包含我之前乱翻的测试文本)并传到 Github 上,届时大家可以自行查看学习。
如果你是初次接触老游戏机游戏的破解,我个人先建议你去尝试一下简单的游戏破解教程: https://www.bilibili.com/read/cv18346849/
想必各位都应该已经能够在网络上找到各种各样针对游戏素材、游戏自带字体的修改和打包了。但是如果遇到非常早期的游戏(比如早期 NDS 并没有使用 NitroSDK 的内置字体功能的游戏(就是说你呢流星洛克人)),并不存在真正意义上的字体文件且字库容量非常匮乏,那么我们只能通过编写程序来修改游戏打印/渲染文字的行为,让它能够打印我们的中文汉字了。
但是很可惜这样的教程过去曾经有,但是都随着时间不是网站到期关闭就是埋没了。而现在随着技术发展,游戏破解已经不再像过去那样复杂且繁琐了(相对上的)。所以我根据 Enler 大佬的指导,尝试总结出了现在相对方便的游戏代码破解/编写方式。
代码上的破解注重对程序执行流程的推测和判断,毕竟每个游戏绘制文字的方式都不一样,本教程只能算是抛砖引玉,真正要编写代码的话可能不能完全按照教程的走,所以要与自己破解的游戏相结合,再整合本教程里你能利用的来进行你的游戏破解,效果会更好一些。
本教程以我所做的流星洛克人一代破解为基础,大部分技术应该是通用的,其它的游戏因为没有接触过所以本人也不太好说明,所以请大家多多谅解。
在开始破解前,我们需要一些前期准备,毕竟本教程并非能让零基础的小白直接上手,所以可以的话请先了解以下知识或者资料后再回来看本文,会更容易理解:
然后是一些用来逆向分析/破解的工具,因为版权和其它原因只告诉大家可以从哪里找到,不提供下载链接,请见谅。
那么工具就准备这么多,接下来最后准备好你要破解的游戏原始文件(ROM),就可以开始继续下一步了!我们教程就以流星洛克人一代天马版日版为破解对象,为其扩展字库吧!
我们通过 No$GBA 打开游戏,先进入游戏并大概运行到文字开始打印的位置,比如说:
在这里点击我们的主窗口(有很多代码/内存/栈/寄存器的那个窗口),此时游戏会暂停,接下来我们要跳转到我们的内存起始位置,并导出全部的内存原始数据。
点击一下左下角部分的内存窗口(把鼠标焦点集中在这个窗口):
然后点击左上角的 Search 搜索菜单,选择跳转光标到地址(Goto, move cursor to address nnnn)或直接按下 Ctrl + G 快捷键打开跳转的输入窗口。
根据 GBATEK 的信息,我们可以知道 NDS 的主内存所属的地址范围是 0x02000000 - 0x02400000(总计 4MB(0x400000)的主内存)。那么我们输入 0x02000000,点击 OK 或者回车,跳转到主内存的开头位置。
接下来是导出内存,点击左上角的 Utility 实用菜单,选择 导出字节到 .bin 文件(Binarydump to .bin File),在弹出的窗口里输入我们需要导出的字节数量的十六进制值 0x400000(也就是 4MB = 4194304 Byte = 0x400000 Byte)
接下来选择要保存的位置,确定后会卡顿一小会时间(切勿触碰模拟器窗口(容易未响应)),等你要导出的文件出现之后,我们就算完成内存导出了:
打开我们的 IDA Pro 32 位版本,在打开的欢迎页面里选择 逆向一个新的文件 New (Disassemble a new file)
在弹出的新窗口里选择我们的处理器类型(Processor Type)为 ARM Little-endian [ARM] (因为我们的 NDS 机器的 CPU 就是 ARM 架构的处理器),其余选项不用变更(应该会和下图一致),然后点击 OK 确定。
接下来弹出的窗口要我们配置好加载的文件在内存中的实际位置,我们只加载了内存部分所以我们勾选 Create RAM section(创建内存区域),取消勾选 Create ROM section(创建只读内存(卡带)区域),将 RAM start address(内存初始位置)和下面 Input file(输入文件)的 Loading Address(加载地址)设置成我们刚刚导出内存时的那个初始位置,RAM Size(内存大小)和我们导出来的文件大小一致。然后点击 OK 确认。
接下来 IDA 会准备开始分析文件,此时还会提示你两个重要的信息,后面会用到:
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 后,初次分析就正式完成了。
在进行这一步之前,先确保你已经了解了 ARM 汇编和相应的调试知识,否则这一段会非常晦涩难懂。
那么先简单了解一下游戏都是怎么打印文字的吧:
在内存和显存匮乏的老旧游戏机上,为了节省内存,我们会使用图块(Tile)+索引(Map)+调色板(Palette)的方式降低显存的用量,大概的流程(这里指背景模式 BG0)是:
知道了这个原理之后,我们来大概猜测一下这个游戏里的文字应该是这样打印的:
为了证明我们的猜想是正确的,我们可以尝试捕捉(也就是条件断点)到游戏写入显存的请求,这样就可以明白是否是这个方式打印了。
为了捕获这样的写入指令,我们需要编写一个条件断点,在写入到显存指定位置时暂停游戏。
条件编写的文档可以在 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 键即可切换回去,再按一次也可以切换回来。
接下来我们回到游戏,点击只有游戏画面的窗口继续游戏,然后点击下一个对话,如果运气好的话,游戏会突然暂停,且主窗口会弹出来,这便是成功捕获了。
如果没有出现的话也不要灰心,这个情况时有发生,多尝试几下,或者从其它打印方式入手来写断点,都是可以的。
那么接下来我们要做的就是找到程序的调用点了,观察右下角的栈内存窗口,我们能发现一个 Return from Lxx_#XXXXXXX 字样,这个是模拟器能捕获到的将上一个跳转的地址入栈的信息,我们可以通过它来判断一个函数的入口点,从这里可以看到这个子程序的入口点是 0x202E6A8。
但是光有这个还不够,还记得 IDA 给我们的第一个提示吗?我们的处理器有两套指令集,所以我们还要搞清楚这个子程序是用的 ARM 指令集还是 THUMB 指令集,那么我们可以一步一步执行程序,回到接近这个子程序的位置。
在单步执行前,我们可能要删除一下刚刚设置的断点,避免单步执行因为断点而中断,带来不必要的麻烦。回到断点列表,点击我们的断点然后按 Delete 键删除即可(如果键盘不包含这个键的话,右键断点后的菜单中也有删除的选项)。
点击 Run 运行菜单,点击 Run to Sub-return 运行到上一个子程序执行完当前子程序的位置:
不出意外的话,此时应该会在上一个函数的附近了:
此时我们观察此时的 CPSR 寄存器(Current Program Status Register:当前程序状态寄存器),可以发现此时的 Thumb 位(右侧标注了 t
的复选框)为勾选状态,表明这个子程序是使用的 THUMB 指令集的,如果没有勾选那就是 ARM 指令集。
确认了指令集是 THUMB 之后,我们可以切换到 IDA 里,选择 Jump 跳转菜单,选择 Jump to address(跳转到地址)(你也可以按下 G 键来打开)打开跳转地址窗口,输入我们刚刚找到的子程序的位置(0x0202E6A8)然后确认。
跳转成功后此时的 IDA 视图应该是这样的:
我们 IDA 默认使用 ARM 指令集,那么遇到 THUMB 指令集的话要怎么切换呢?
还记得刚刚 IDA 给我们的两个提示吗?接下来就根据第二个提示,来修改 T 寄存器的值吧:
此时视图应该会在我们的程序入口标注 CODE16
,代表从此处开始分析器将会使用 THUMB 指令集来分析:
最后让光标对准我们的子程序入口,按下 P 键定义一个子程序入口点(C 键也可以,不过不会将其作为一个子程序解析),此时 IDA 将会开始分析,并把刚刚这段数据转换成响应的汇编代码
如果你发现生成的代码不太一样(比如说忘记切换指令集什么的)可以通过对错误的代码区域按下 U 键将其置为未定义数据块,就可以重新开始解析代码了。
最后,把光标放在解析出来的代码块,按下 F5,对这段汇编代码生成对应的伪 C 代码:
如果你非常顺利地做到了这里,那么恭喜你!你已经基本明白了调试和逆向分析的非常非常简单的流程了(其实我自己也不会特别复杂的),那么只要你对 C 语言有所熟悉,配合伪代码能帮助你更好地理解程序的执行走向。
当然,配合经验之谈,其实我们逆向的这段代码并非真正的打印文字的部分。这里仅仅是把内存中的某部分复制到了显存里。
当然不要灰心,这样的情况我也遇到过不少,逆向工程本来就是一种不断挖掘探索的过程,虽然枯燥但是非常有趣。毕竟说不定你还可以发现游戏的废案资料呢?
那么这一大部分的逆向分析教程到这里就先告一段落了,后面我会根据情况多写新的教程,请大家敬请期待吧!