动物杂交开发人员模式逆向工程

在真实的GameCube上使用代码

去年夏天,我开始对GameCube的Animal Crossing进行逆向工程。 我想探索为该游戏创建mod的可能性。 另外,我想记录一下为对ROM和逆向工程感兴趣的人们创建教程的过程。 在本文中,我将讨论游戏中剩余的开发人员调试功能,并分享我如何发现可用于解锁功能的作弊连击。

new_Debug_mode


研究了其余的调试符号后,我注意到包含单词“ debug”的函数和变量的名称,并决定看看游戏中是否还有任何调试功能会很有趣。 如果我设法激活调试或开发功能,这将在创建mod的过程中为我提供帮助。

我注意到的第一个功能是new_Debug_mode 。 它由entry功能调用,该功能在Nintendo徽标屏幕完成后立即启动。 她所做的只是放置0x1C94字节结构并保存一个指向它的指针。

在宿主结构中以偏移量0xD4entry中调用它之后0xD4紧接着在调用mainproc之前mainproc值设置mainproc 0。


为了查看当值不为零时会发生什么,我在80407C8C修补了li r0, 0 80407C8C指令,将其替换为li r0, 1 80407C8Cli r0, 0指令的原始字节为38 00 00 00 ,其中赋值位于指令的末尾,因此我只需将字节替换为38 00 00 01得到li r0, 1 。 作为构建指令的更可靠方法,可以使用kstool类的kstool

$ kstool ppc32be "li 0, 1"
li 0, 1 = [ 38 00 00 01 ]


在Dolphin模拟器中,可以通过在游戏属性中转到“补丁”选项卡并按如下方式输入来应用此补丁:


分配值1之后,屏幕底部会出现一个有趣的图形:



它看起来像一个性能指标:屏幕底部的小条会增加或减少。 (后来,当我查看绘制此图的函数的名称时,我发现它们实际上显示了CPU和内存使用率指标。)

很棒,但不是特别有用。 分配值1后,我的城市停止加载,因此此处无法进行其他操作。

祖鲁模式


我再次开始寻找有关调试功能的其他参考,并且几次遇到称为“ zuru模式”的东西。 具有调试功能的代码块分支经常检查zurumode_flag变量。

game_move_first函数

仅当zurumode_flag不等于0时, zurumode_flag调用上面显示的game_move_first函数中的zzz_LotsOfDebug (我自己zzz_LotsOfDebug的名称)。

寻找与此值关联的函数,我发现了这些:

  • zurumode_init
  • zurumode_callback
  • zurumode_update
  • zurumode_cleanup

乍一看,它们的用途是神秘的,它们在osAppNMIBuffer变量的偏移量中osAppNMIBuffer

乍看之下,这些功能的工作方式如下:

zurumode_init


  • zurumode_flag设置为0
  • 检查osAppNMIBuffer几个位
  • padmgr结构中保存指向zurumode_callback函数的指针
  • 调用zurumode_update

zurumode_update


  • 检查osAppNMIBuffer几个位
  • 根据这些位的值, zurumode_flag更新
  • 将格式字符串打印到OS控制台。

通常在为代码提供上下文时很有用,但是该行中有很多不可打印的字符。 唯一可识别的文本是“ zurumode_flag”和“%d”。

zuru模式格式字符串

假设它可能是带有多字节字符编码的日语文本,我将该字符串通过了编码识别工具,然后发现该字符串是使用Shift-JIS编码的。 在翻译中,该行仅表示“ zurumode_flag的值已从%d更改为%d”。 这不会给我们带来太多新信息,但是现在我们知道使用了Shift-JIS:在二进制文件和行表中,此编码中包含更多行。

zurumode_callback


  • 调用zerumode_check_keycheck
  • 检查osAppNMIBuffer几个位
  • zurumode_flag值在zurumode_flag
  • 调用zurumode_update

zerumode_check_keycheck直到我们由于拼写不同而见面……这是什么?

zerumode_check_keycheck

一个巨大的复杂函数,对具有未命名值的位进行更多工作。

此时,我决定退后一步,研究其他调试功能和变量,因为我不确定zuru模式的重要性。 另外,我不明白“密钥检查”在这里的含义。 这可能是加密密钥吗?

返回调试


大约在这个时候,我注意到我在IDA中加载调试符号的方式存在问题。 游戏磁盘上的foresta.map文件包含许多地址以及函数和变量的名称。 最初,我没有看到每个部分的地址都从头开始,因此我编写了一个简单的脚本,为文件的每一行添加了一个名称条目。

我编写了新的IDA脚本来修复程序不同部分的加载符号表: .text.rodata.data.bss.text部分包含所有功能,因此我做到了,这样,当我设置名称时,脚本将自动识别每个地址处的功能。

现在,在数据部分中,他为每个二进制对象创建了一个段(例如, m_debug.o ,应该将其编译为m_debug代码),并为每个数据段设置空间和名称。

这给了我更多的信息,但是我必须为每个数据手动设置数据类型,因为我将每个数据对象都定义为一个简单的字节数组。 (回想一下,我理解最好假设4个字节的片段包含32位整数,因为存在很多,并且包含许多对构建交叉引用很重要的函数和数据的地址。)

研究m_debug_mode.o的新.bss段,我发现了几个变量,形式为quest_draw_statusevent_status 。 这很有趣,因为我希望在调试模式下显示有用的信息,而不仅仅是性能图。 幸运的是,从这些数据记录中可以看到对debug_print_flg的大量代码的交叉引用。

在Dolphin模拟器中使用调试器,我在检查debug_print_flg的函数的位置(在8039816C )设置了一个断点,以了解此检查的工作方式。 但是该程序从未传递到此断点。

让我们game_debug_draw_last一下发生这种情况的原因: game_debug_draw_last调用此函数。 猜猜在有条件调用之前检查了什么值? zurumode_flag ! 到底是怎么回事?

zurumode_flag检查

我在此检查( 80404E18 )上设置了一个断点,它立即起作用。 zurumode_flag的值为零,因此在正常执行中,程序将错过对该函数的调用。 我插入了一条NOP分支指令(用一条什么都不做的指令代替)来检查调用该函数时发生的情况。

在Dolphin调试器中,可以通过暂停游戏,右键单击说明并选择“ Insert nop”来完成此操作:

海豚调试器

没事 然后,我检查了函数内部发生的情况,发现了另一个分支构造,该构造避开了803981a8发生的所有有趣的事情。 我也插入了NOP,屏幕的右上角出现了字母“ D”。

调试模式字母D

8039816C此函数(我将其称为zzz_DebugDrawPrint )中,仍然有很多有趣的代码,但是没有被调用。 如果以图表的形式查看此函数,则可以看到有一系列分支运算符会跳过整个函数中的代码块:

zzz_DebugDrawPrint中的分支

插入NOP而不是其他几个分支结构后,我开始在屏幕上看到各种有趣的东西:

打印更多调试内容

下一个问题是如何在不更改代码的情况下激活此调试功能。

另外,在某些分支构造中, zurumode_flag在此调试绘制函数中再次出现。 我添加了另一个补丁,以便在zurumode_update zurumode_flag始终zurumode_update标志分配值2,因为当它不与0比较时,它专门与值2比较。

重新启动游戏后,我在屏幕的右上角看到了一条消息“ msg。 不。”

消息号显示

数字687是最近显示的消息的记录标识符。 我使用分析开始时编写的表查看器程序对其进行了检查,但是您也可以使用具有完整GUI字符串表编辑器对其进行检查,该GUI是我为黑客ROM而编写的。 这是帖子在编辑器中的外观:

弦表编辑器中的消息687

在这一点上,很明显,不再研究zuru模式了-它与游戏的调试功能直接相关。

再次回到祖鲁模式


zurumode_init初始化几件事:

  • 0xC(padmgr_class)分配了zurumode_callback地址的值
  • 0x10(padmgr_class)分配了padmgr_class本身的地址值
  • 0x4(zuruKeyCheck)分配了从0x3C(osAppNMIBuffer)加载的字中的最后一位的值。

我弄清楚padmgr什么,“ gamepad manager”的缩写。 这意味着可能存在可以在游戏板上输入以激活zuru模式的按键(按钮)的特殊组合,或者可以用于发送信号以激活信号的某种调试设备或开发者控制台的功能。

zurumode_init仅在游戏的第一次启动时执行(按下重置按钮zurumode_init不起作用)。

在地址8040efa4处设置了一个断点,并为其指定了0x4(zuruKeyCheck)的值0x4(zuruKeyCheck) ,我们可以看到,在不按任何键加载的情况下,该值设置为0。如果将其替换为1,则会发生有趣的事情:

zuru模式的标题屏幕

字母“ D”再次出现在右上角(这次是绿色,而不是黄色),并且还会显示一些装配信息:

[CopyDate: 02/08/01 00:16:48 ]
[Date: 02-07-31 12:52:00]
[Creator:SRD@SRD036J]


一个始终在开始时始终将0x4(zuruKeyCheck)为1的补丁看起来像这样:

8040ef9c 38c00001

这似乎是初始化zuru模式的正确方法。 此后,可能需要采取各种措施来实现某些调试信息的显示。 开始游戏,散步并与村民交谈时,我们将看不到上述任何消息(角落处的字母“ D”除外)。

最可能的可疑对象是zurumode_updatezurumode_callback

zurumode_update


zurumode_update首先在zurumode_initzurumode_init ,然后由zurumode_callback不断调用。

它将再次检查最后一位0x3C(osAppNMIBuffer) ,然后根据此值更新zurumode_flag

如果该位为零,则标志设置为零。

如果不是,则执行以下语句,其完整值为0x3c(osAppNMIBuffer)r5

extrwi r3, r5, 1, 28

它从r5提取第28位并将其存储在r3

然后将1添加到结果中,即最终结果始终为1或2。

然后将zurumode_flag与先前的结果进行比较,具体取决于第28位和最后一位中有多少位设置为0x3c(osAppNMIBuffer) :0、1或2。

该值被写入zurumode_flag 。 如果不做任何更改,该函数将退出并返回当前标志值。 如果更改了该值,则将执行更为复杂的代码块链。

一条消息以日语显示:相同的“ zurumode_flag值从%d更改为%d”,如上所述。

然后,根据标志是否已变为零,使用不同的参数调用一系列函数。 这部分的汇编代码是单调的,因此我将显示其伪代码:

 if (flag_changed_to_zero) { JC_JUTAssertion_changeDevice(2) JC_JUTDbPrint_setVisible(JC_JUTDbPrint_getManager(), 0) } else if (BIT(nmiBuffer, 25) || BIT(nmiBuffer, 31)) { JC_JUTAssertion_changeDevice(3) JC_JUTDbPrint_setVisible(JC_JUTDbPrint_getManager(), 1) } 

注意,如果标志为零,则将参数0传递给JC_JUTDbPrint_setVisible。

如果该标志不等于零,并且位25或位31设置为0x3C(osAppNMIBuffer) ,则setVisible传递给参数1。

这是激活zuru模式的第一个关键:最后一位0x3C(osAppNMIBuffer)必须设置为1以显示调试信息并将zurumode_flag设置zurumode_flag非零值。

zurumode_callback


zurumode_callback位于8040ee74 ,可能由与游戏手柄相关的函数调用。 在Dolphin调试器中插入断点后,调用堆栈向我们显示实际上是从padmgr_HandleRetraceMsg调用padmgr_HandleRetraceMsg

她的第一个动作是执行zerucheck_key_check 。 该函数很复杂,但似乎通常设计为读取和更新zuruKeyCheck的值。 在继续进行keycheck函数之前,我决定检查在回调函数的其余部分中如何使用此值。

然后,它再次检查0x3c(osAppNMIBuffer)某些位。 如果设置了位26,或者设置了位25,并且padmgr_isConnectedController(1)返回非零值,则0x3c(osAppNMIBuffer)的最后一位0x3c(osAppNMIBuffer)为1!

如果没有设置这些位或设置了位25,但是padmgr_isConnectedController(1)返回0,则该函数检查地址0x4(zuruKeyCheck)处的字节是否等于零。 如果相等,则它将重置原始值的最后一位并将其写回到0x3c(osAppNMIBuffer) 。 如果不是,则仍将最后一位设置为1。

在伪代码中,它看起来像这样:

 x = osAppNMIBuffer[0x3c] if (BIT(x, 26) || (BIT(x, 25) && isConnectedController(1)) || zuruKeyCheck[4] != 0) { osAppNMIBuffer[0x3c] = x | 1 // set last bit } else { osAppNMIBuffer[0x3c] = x & ~1 // clear last bit } 

此后,如果未设置位26,则函数继续调用zurumode_update ,然后退出。

如果该位置1,则如果0x4(zuruKeyCheck)不等于零,则它将加载格式字符串,并在其中显示以下内容:“ ZURU%d /%d”。

汇总小计


这是发生了什么:

padmgr_HandleRetraceMsg调用zurumode_callback 。 我假设此“句柄回溯消息”意味着它仅扫描控制器的击键。 每次扫描都会导致一系列不同的回调。

执行zurumode_callback它将检查当前的击键(按钮)。 看来她正在检查特定按钮或按钮组合。

NMI缓冲区中的最后一位将根据其当前值中的特定位以及zuruKeyCheck字节之一( 0x4(zuruKeyCheck) )的值进行更新。

然后zurumode_update并检查该位。 如果为0,则zuru模式标志设置为0。如果为1,则模式标志更改为1或2,具体取决于是否设置了位28。

有三种激活zuru模式的方法:

  1. 位26设置为0x3C(osAppNMIBuffer)
  2. 位25设置为0x3C(osAppNMIBuffer) ,并且控制器连接到端口2
  3. 0x4(zuruKeyCheck)不为零

osAppNMIBuffer


osAppNMIBuffer含义感兴趣,我开始寻找“ NMI”,并在Nintendo上下文中找到了指向“不可屏蔽中断”的链接。 事实证明,此变量的名称在Nintendo 64的开发人员文档中被完全提及:

osAppNMIBuffer是一个64字节的缓冲区,在冷重启后会清除。 如果系统由于NMI而重启,则该缓冲区的状态不会更改。

实际上,这只是在“软”重启(使用复位按钮)期间保存的一小块内存。 当控制台在网络上时,游戏可以使用此缓冲区存储任何数据。 原始的《动物穿越》是在Nintendo 64上发布的,因此逻辑上应该在代码中出现类似的内容。

如果转到二进制boot.dol文件(上面显示的所有内容都在foresta.rel ),则其main功能具有指向osAppNMIBuffer的大量链接。 快速查看显示了一系列检查,这些检查可以导致使用OR操作0x3c(osAppNMIBuffer)不同位0x3c(osAppNMIBuffer)值。

以下OR操作数值可能很有趣:

  • 位31:0x01
  • 位30:0x02
  • 位29:0x04
  • 位28:0x08
  • 位27:0x10
  • 位26:0x20

我们记得,第25、26和28位特别有趣:第25和26位决定zuru模式是否开启,第28位决定标志位(1或2)。
第31位也很有趣,但是看起来它根据其他值而变化。

位26

首先:在地址800062e0有一条指令ori r0, r0, 0x20 0x3c ,其缓冲区值为0x3c 。 它将位26置位,该位始终打开zuru模式。

设置位26

对于要设置的位,从DVDGetCurrentDiskID返回的第八个字节必须为0x99 。 该标识符位于游戏磁盘映像的最开始,并以80000000加载到内存中。 在游戏的常规零售版中,ID如下所示:

47 41 46 45 30 31 00 00 GAFE01..

将标识符的最后一个字节替换为0x990x99 ,我们在开始游戏时会得到以下图片:

游戏版本ID 0x99

并且在操作系统控制台中显示以下内容:

06:43:404 HW\EXI_DeviceIPL.cpp:339 N[OSREPORT]: ZURUMODE2 ENABLE
08:00:288 HW\EXI_DeviceIPL.cpp:339 N[OSREPORT]: osAppNMIBuffer[15]=0x00000078


可以删除所有其他修补程序,然后字母D再次出现在屏幕的右上角,但是不再激活调试消息。

25位

位25与控制器端口2检查一起使用,是什么导致它打开?

25位和28位

事实证明,他应该使用与位28相同的检查:版本必须大于或等于0x90 。 如果设置了位26(ID为0x99 ),那么这两个位也将被设置,并且zuru模式仍将被激活。

但是,如果版本的范围是0x900x98 ,则zuru模式不会立即打开。 回顾在zurumode_callback执行的检查-仅当设置了位25 并且 padmgr_isConnectedController(1)返回非零值时,该模式才会启用。

控制器连接到端口2后(参数isConnectedController索引为零),将激活zuru模式。 字母D和有关程序集的信息出现在初始屏幕上,我们...可以使用第二个控制器的按钮来控制调试的显示!

一些按钮执行的操作不仅会改变显示效果,而且还会例如提高游戏速度。

zerucheck_key_check


最后一个谜仍然是0x4(zuruKeyCheck) 。 事实证明,此值是使用上面显示的巨大复杂功能更新的:

zerumode_check_keycheck

使用Dolphin模拟器调试器,我能够确定此功能检查的值是一组与第二个控制器上的按钮按下相对应的位。

对按钮单击的跟踪以16位值存储在0x2(zuruKeyCheck) 。 未连接控制器时,值为0x7638

将下载2个字节,其中包含控制器按钮按下的标志,然后在zerucheck_key_check的开头进行zerucheck_key_check 。 在调用回调函数时,新值将通过寄存器r4传递r4 padmgr_HandleRetraceMsg函数。

关键检查结束

zerucheck_key_check末尾附近,还有另一个更新0x4(zuruKeyCheck)地方0x4(zuruKeyCheck)它没有出现在交叉引用列表中,因为它使用它作为基址r3,并且我们r3只能在调用此函数之前通过查看分配给它的值找到该值

在该地址,该8040ed88值被r4写入0x4(zuruKeyCheck)。就在此之前,但是它是从同一位置写入的,然后是从1进行的XOR。此操作的任务是在0和1之间切换字节的值(实际上是最后一位)。(如果值为0,
则从1 进行XOR 的结果将为1 。如果值为1,则结果将为0。有关XOR的信息,请参见真值表。

密钥检查结束

之前,当我研究内存中的值时,我没有注意到这种现象,但是我将尝试在调试器中破坏此指令以了解正在发生的情况。原始值加载到8040ed7c

如果不触摸控制器按钮,则不会在初始屏幕上到达此断点。要进入此代码块,该值r5必须0xb在断点(8040ed74之前的分支指令之前等于。在通往此块的许多不同路径中,只有一个在其前面的address处分配一个r50xb8040ed68

将r5设置为0xb

请注意,为了到达分配该r5的块0xB,该值r0必须紧接在它之前0x1000在整个功能块开始之前,我们可以看到实现此块所需的所有限制:

  • 8040ed74:值r5必须相等0xB
  • 8040ed60:值r0必须相等0x1000
  • 8040ebe8:值r5必须相等0xA
  • 8040ebe4:值r5必须小于0x5B
  • 8040eba4:值r5必须更大0x7
  • 8040eb94:值r6必须为1
  • 8040eb5c:值r0不能为0
  • 8040eb74:端口2按钮值应更改

跟踪代码路径

在这里,我们到达了加载旧按钮值并保存新值的地步。然后是对新值和旧值应用的几个操作:XOR操作标记两个值之间已更改的所有位。然后,AND操作会屏蔽新输入,以将当前未设置为状态0的所有位设置为1。结果是新值的一组新位(按钮按下)。如果不为空,那么我们走在正确的轨道上。为了要紧,应该从第四16位跟踪按钮来改变。在XOR / AND操作后插入断点后,我发现START按钮触发了此状态。下一个问题是如何使它最初相等

old_vals = old_vals XOR new_vals
old_vals = old_vals AND new_vals


r0

r00x1000

r50xAr5并且在我们不进入包含的代码块时r60x0(zuruKeyCheck)关键测试函数的开头加载并在接近结尾时进行更新0x4(zuruKeyCheck)

在此之前,有几个地方r5分配0xA

  • 8040ed50
  • 8040ed00
  • 8040ed38

8040ed38

  • 8040ed34:值r0必须相等0x4000(按下按钮B)
  • 8040ebe0:值r5必须相等0x5b
  • 8040eba4:值r5必须更大0x7
  • 然后一切都会像以前一样...

r5 应该从 0x5b

8040ed00

  • 8040ecfc:值r0必须相等0xC000(按下A和B)
  • 8040ebf8:值r5必须> = 9
  • 8040ebf0:值r5必须小于10
  • 8040ebe4:值r5必须小于0x5b
  • 8040eba4r5应该更多0x7
  • 然后一切都会像以前一样...

r5 应该从9开始

8040ed50

  • 8040ed4c:值r0必须相等0x8000(按下按钮A)
  • 8040ec04:值r5必须小于0x5d
  • 8040ebe4:值r5必须更大0x5b
  • 8040eba4:值r5必须更大0x7
  • 然后一切都会像以前一样...

r5应该开始0x5c

于按键之间似乎存在某种状态,此后您需要从按钮输入一定的连击序列,最后按START键。看来A和/或B应该在START之前就走了。

如果您跟踪将r5设置为9 的代码的路径,则会出现一个模式:r5-它是一个递增的值,当r0找到合适的值时可以增加,也可以为零。最奇怪的情况是它不是从0x0的范围内的值0xB当用几个按钮处理步骤时,例如同时按下A和B时,会出现,试图进入此组合的人在跟踪游戏手柄时通常不能完全同时按下两个按钮,因此您必须处理被按下的按钮第一个。

我们将继续探索不同的代码路径:

  • r5在地址处按RIGHT时,取值为9 8040ece8
  • r5按下地址上的右键C时,取值为8 8040eccc
  • r5当在地址处按左按钮C时,取值为7 8040ecb0
  • r5在地址处按LEFT键时,取值为6 8040ec98
  • r5当在地址处按DOWN时,取值5(而r6取值1)8040ec7c
  • r5按下地址上的上按钮C时,值为4 8040ec64
  • r5按下地址上的下按钮C时,值为3 8040ec48
  • r5在地址上按UP时,取值为2 8040ec30
  • r5r6在地址处按Z时,取值1(并取值1)8040ec1c

当前顺序为:

Z,UP,C-DOWN,C-UP,DOWN,LEFT,C-LEFT,C-RIGHT,RIGHT,A + B,START

在检查Z之前,还要检查另外一种情况:尽管应该按下一个新按钮Z,当前标志必须相等0x2030:必须同时按下左右保险杠(它们的值为0x100x20)。另外,上/下/左/右是D-pad按钮,不是模拟摇杆。

作弊代码


完全组合看起来像这样:

  1. 握住保险杠L + R,然后按Z
  2. 向上
  3. C-DOWN
  4. C-up
  5. D向下
  6. D向左
  7. C向左
  8. D-右
  9. A + B
  10. 开始

有效!将控制器连接到第二个端口并输入代码,然后显示调试信息。之后,您可以开始按下第二个(甚至第三个)控制器上的按钮以执行各种操作。

该组合无需更改游戏版本号即可使用。它甚至可以在没有任何作弊工具或游戏机模组的情况下,用于游戏的常规零售副本中。重新输入连击将禁用zuru模式。

在真实的GameCube上使用代码

zurumode_callback如果在磁盘ID已经相等的情况下输入组合0x99(可能是为了调试作弊代码本身),则消息“ ZURU%d /%d” 用于显示此组合的状态。第一个数字是您在序列中的当前位置,即r5。第二个取值为1,当按住序列中的某些按钮时,它们可以对应于r6分配值1 时的情况。

大多数消息在屏幕上都没有解释它们的作用,因此,要了解其用途,您需要找到处理它们的功能。例如,屏幕顶部的一排长长的蓝色和红色星星是占位符,用于显示各种任务的状态。激活任务后,那里会显示一些数字,报告任务的状态。

按下Z所显示的黑屏是用于显示调试消息的控制台,特别是针对低级方面(例如内存分配,堆错误和其他不良异常)的控制台。根据行为,fault_callback_scroll可以假定它用于在系统重新启动之前显示这些错误。它不会引发任何这些错误,但是会导致它们打印多个NOP的垃圾字符。我认为将来对显示您自己的调试消息将非常有用:

JUTConsole垃圾字符

完成所有这些操作后,我发现其他人已经知道通过修补
版本ID进入调试模式0x99https : //tcrf.net/Animal_Crossing#Debug_Mode(在链接上也有很多不错的注释,它们指示各种消息,并讨论了端口3中的控制器可以完成的其他操作。)但是,据我所知,还没有人发布欺骗组合。

仅此而已。 我还想探索其他开发人员功能,例如卡的调试屏幕和NES仿真器选择屏幕,以及如何在不使用补丁的情况下激活它们。

地图选择画面


另外,我将发表有关对话系统,事件和任务的逆向工程的文章,以创建mod为目标。

Source: https://habr.com/ru/post/zh-CN413967/


All Articles