去年夏天,我开始对GameCube的Animal Crossing进行逆向工程。 我想探索为该游戏创建mod的可能性。 另外,我想记录一下为对ROM和逆向工程感兴趣的人们创建教程的过程。 在本文中,我将讨论游戏中剩余的开发人员调试功能,并分享我如何发现可用于解锁功能的作弊连击。
new_Debug_mode
研究了其余的调试符号后,我注意到包含单词“ debug”的函数和变量的名称,并决定看看游戏中是否还有任何调试功能会很有趣。 如果我设法激活调试或开发功能,这将在创建mod的过程中为我提供帮助。
我注意到的第一个功能是
new_Debug_mode
。 它由
entry
功能调用,该功能在Nintendo徽标屏幕完成后立即启动。 她所做的只是放置
0x1C94
字节结构并保存一个指向它的指针。
在宿主结构中以偏移量
0xD4
在
entry
中调用它之后
0xD4
紧接着在调用
mainproc
之前
mainproc
值设置
mainproc
0。
为了查看当值不为零时会发生什么,我在
80407C8C
修补了
li r0, 0
80407C8C
指令,将其替换为
li r0, 1
80407C8C
。
li 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
变量。
仅当
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”。
假设它可能是带有多字节字符编码的日语文本,我将该字符串通过了编码识别工具,然后发现该字符串是使用Shift-JIS编码的。 在翻译中,该行仅表示“ zurumode_flag的值已从%d更改为%d”。 这不会给我们带来太多新信息,但是现在我们知道使用了Shift-JIS:在二进制文件和行表中,此编码中包含更多行。
zurumode_callback
- 调用
zerumode_check_keycheck
- 检查
osAppNMIBuffer
几个位 zurumode_flag
值在zurumode_flag
- 调用
zurumode_update
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_status
和
event_status
。 这很有趣,因为我希望在调试模式下显示有用的信息,而不仅仅是性能图。 幸运的是,从这些数据记录中可以看到对
debug_print_flg
的大量代码的交叉引用。
在Dolphin模拟器中使用调试器,我在检查
debug_print_flg
的函数的位置(在
8039816C
)设置了一个断点,以了解此检查的工作方式。 但是该程序从未传递到此断点。
让我们
game_debug_draw_last
一下发生这种情况的原因:
game_debug_draw_last
调用此函数。 猜猜在有条件调用之前检查了什么值?
zurumode_flag
! 到底是怎么回事?
我在此检查(
80404E18
)上设置了一个断点,它立即起作用。
zurumode_flag
的值为零,因此在正常执行中,程序将错过对该函数的调用。 我插入了一条NOP分支指令(用一条什么都不做的指令代替)来检查调用该函数时发生的情况。
在Dolphin调试器中,可以通过暂停游戏,右键单击说明并选择“ Insert nop”来完成此操作:
没事 然后,我检查了函数内部发生的情况,发现了另一个分支构造,该构造避开了
803981a8
发生的所有有趣的事情。 我也插入了NOP,屏幕的右上角出现了字母“ D”。
在
8039816C
此函数(我将其称为
zzz_DebugDrawPrint
)中,仍然有很多有趣的代码,但是没有被调用。 如果以图表的形式查看此函数,则可以看到有一系列分支运算符会跳过整个函数中的代码块:
插入NOP而不是其他几个分支结构后,我开始在屏幕上看到各种有趣的东西:
下一个问题是如何在不更改代码的情况下激活此调试功能。
另外,在某些分支构造中,
zurumode_flag
在此调试绘制函数中再次出现。 我添加了另一个补丁,以便在
zurumode_update
zurumode_flag
始终
zurumode_update
标志分配值2,因为当它不与0比较时,它专门与值2比较。
重新启动游戏后,我在屏幕的右上角看到了一条消息“ msg。 不。”
数字687是最近显示的消息的记录标识符。 我使用分析开始时编写的表查看器程序对其进行了检查,但是您也可以使用
具有完整GUI的
字符串表编辑器对其进行检查,该
GUI是我为黑客ROM而编写的。 这是帖子在编辑器中的外观:
在这一点上,很明显,不再研究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,则会发生有趣的事情:
字母“ 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_update
和
zurumode_callback
。
zurumode_update
zurumode_update
首先在
zurumode_init
中
zurumode_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
此后,如果未设置位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模式的方法:- 位26设置为
0x3C(osAppNMIBuffer)
- 位25设置为
0x3C(osAppNMIBuffer)
,并且控制器连接到端口2 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模式。
对于要设置的位,从
DVDGetCurrentDiskID
返回的第八个字节必须为
0x99
。 该标识符位于游戏磁盘映像的最开始,并以
80000000
加载到内存中。 在游戏的常规零售版中,ID如下所示:
47 41 46 45 30 31 00 00 GAFE01..
将标识符的最后一个字节替换为
0x99
的
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检查一起使用,是什么导致它打开?
事实证明,他应该使用与位28相同的检查:版本必须大于或等于
0x90
。 如果设置了位26(ID为
0x99
),那么这两个位也将被设置,并且zuru模式仍将被激活。
但是,如果版本的范围是
0x90
到
0x98
,则zuru模式不会立即打开。 回顾在
zurumode_callback
执行的检查-仅当设置了位25
并且 padmgr_isConnectedController(1)
返回非零值时,该模式才会启用。
控制器连接到端口2后(参数
isConnectedController
索引为零),将激活zuru模式。 字母D和有关程序集的信息出现在初始屏幕上,我们...可以使用第二个控制器的按钮来控制调试的显示!
一些按钮执行的操作不仅会改变显示效果,而且还会例如提高游戏速度。
zerucheck_key_check
最后一个谜仍然是
0x4(zuruKeyCheck)
。 事实证明,此值是使用上面显示的巨大复杂功能更新的:
使用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处分配一个r5
值。0xb
8040ed68
请注意,为了到达分配该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
r0
0x1000
r5
0xA
。r5
并且在我们不进入包含的代码块时r6
从0x0(zuruKeyCheck)
关键测试函数的开头加载并在接近结尾时进行更新0x4(zuruKeyCheck)
。在此之前,有几个地方r5
分配了值0xA
:8040ed38
8040ed34
:值r0
必须相等0x4000
(按下按钮B)8040ebe0
:值r5
必须相等0x5b
8040eba4
:值r5
必须更大0x7
- 然后一切都会像以前一样...
r5
应该从 0x5b
8040ed00
8040ecfc
:值r0
必须相等0xC000
(按下A和B)8040ebf8
:值r5
必须> = 98040ebf0
:值r5
必须小于108040ebe4
:值r5
必须小于0x5b
8040eba4
:r5
应该更多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
。r5
r6
在地址处按Z时,取值1(并取值1)8040ec1c
。
当前顺序为:Z,UP,C-DOWN,C-UP,DOWN,LEFT,C-LEFT,C-RIGHT,RIGHT,A + B,START在检查Z之前,还要检查另外一种情况:尽管应该按下一个新按钮Z,当前标志必须相等0x2030
:必须同时按下左右保险杠(它们的值为0x10
和0x20
)。另外,上/下/左/右是D-pad按钮,不是模拟摇杆。作弊代码
完全组合看起来像这样:- 握住保险杠L + R,然后按Z
- 向上
- C-DOWN
- C-up
- D向下
- D向左
- C向左
- 右
- D-右
- A + B
- 开始
有效!将控制器连接到第二个端口并输入代码,然后显示调试信息。之后,您可以开始按下第二个(甚至第三个)控制器上的按钮以执行各种操作。该组合无需更改游戏版本号即可使用。它甚至可以在没有任何作弊工具或游戏机模组的情况下,用于游戏的常规零售副本中。重新输入连击将禁用zuru模式。zurumode_callback
如果在磁盘ID已经相等的情况下输入组合0x99
(可能是为了调试作弊代码本身),则消息“ ZURU%d /%d” 用于显示此组合的状态。第一个数字是您在序列中的当前位置,即r5
。第二个取值为1,当按住序列中的某些按钮时,它们可以对应于r6
分配值1 时的情况。大多数消息在屏幕上都没有解释它们的作用,因此,要了解其用途,您需要找到处理它们的功能。例如,屏幕顶部的一排长长的蓝色和红色星星是占位符,用于显示各种任务的状态。激活任务后,那里会显示一些数字,报告任务的状态。按下Z所显示的黑屏是用于显示调试消息的控制台,特别是针对低级方面(例如内存分配,堆错误和其他不良异常)的控制台。根据行为,fault_callback_scroll
可以假定它用于在系统重新启动之前显示这些错误。它不会引发任何这些错误,但是会导致它们打印多个NOP的垃圾字符。我认为将来对显示您自己的调试消息将非常有用:完成所有这些操作后,我发现其他人已经知道通过修补版本ID进入调试模式0x99
:https : //tcrf.net/Animal_Crossing#Debug_Mode。(在链接上也有很多不错的注释,它们指示各种消息,并讨论了端口3中的控制器可以完成的其他操作。)但是,据我所知,还没有人发布欺骗组合。仅此而已。
我还想探索其他开发人员功能,例如卡的调试屏幕和NES仿真器选择屏幕,以及如何在不使用补丁的情况下激活它们。另外,我将发表有关对话系统,事件和任务的逆向工程的文章,以创建mod为目标。