前言

去年(2018)年中,有汉化组的成员表示希望汉化这款游戏,托我研究一下技术上是否可行。但由于现实中的事情,加上拖延症,以及有其它汉化坑,迟迟没有开坑这个游戏。经过漫长的拖延,终于在今年年初抽出时间开始研究。 这是我第一次接触PSP游戏的破解,我对这个平台并不了解。不过好在模拟器PPSSPP的调试功能还算完备,对逆向分析游戏十分有帮助。

初步分析

首先,我们需要将游戏的镜像文件解包。PSP游戏镜像是ISO格式,7zip、WinRAR等软件都能直接解压,在Windows 10上还能挂载到文件系统中直接访问。

我们来观察一下解包出来的目录结构(部分文件未展示): banner

PSP游戏遵循程序和数据分别储存的原则:程序二进制代码的路径是PSP_GAME/SYSDIR/EBOOT.BIN,这是一个加密的ELF文件,关于如何让解密这个ELF后面会提及;数据的路径是PSP_GAME/USRDIR/,一般来说需要汉化的文本、图片等内容都在数据目录下,当然,少数情况下,EBOOT.BIN中也可能会有一些文本。 下面着重看一下USRDIR这个目录。在USRDIR/data下有几个子目录,根据它们的命名,可以猜测当中包含的文件的用途,分别是:视频(movie)、共享游戏(sharing)、音频(sound),很明显,我们需要汉化的文本,并不存在于这些目录下。那么,我们可以猜测,文本应该是储存在data目录下的bin文件中的,但是这些bin(二进制)文件到底是什么呢?

为了研究这些bin文件,我们用16进制编辑器打开这些文件进行观察。 banner

可以看到,这些文件最开始的8个字节都是字符串"SECTPACK",我们可以顾名思义地作出假设,这些文件都是PACK,即文件包。我们继续往下看。 banner

从0x310字节开始,有一系列的文件路径一样的字符串,几乎可以肯定之前的猜测,这些bin文件就是文件包,而这些字符串就是打包到文件包中的文件。

那么从现在开始,我们把这些bin文件称为SectPack。

仔细观察可以发现,0x10~0x310之间有0x300字节的意义不明的内容,这到底是什么呢?

根据以往的经验,对于文件包格式,比较常见的结构是把文件数据偏移、文件数据长度、文件名偏移等信息作为一个列表保存在文件包头部。那么很自然的,这个列表的大小,会随着文件包中包含的文件数量增加而增大。因此,SectPack开头的0x300字节不可能是保存这些信息的列表,因为所有包的文件数量完全一致的情况并不多见。实际上,通过统计各文件包中的文件路径数量也可以验证这个判断。

并且,我们可以注意到,在每一条文件路径之间还有一些意义不明的字节。

很显然,依靠简单地找规律的做法,是没办法破解SectPack文件的。

进一步分析

为了进一步分析,我们利用模拟器进行调试。 首先,勾选调试菜单中的“载入后停止”选项,并打开反汇编窗口: banner

然后,加载游戏,此时游戏就会停止在程序入口处: banner

PPSSPP会对SDK中的一些函数进行自动标记,这是一个非常方便的功能。

我们要分析 SectPack 文件,不妨先思考一下,从这些文件包中读取文件,需要经历哪些过程。

首先,一个最基本的事实:这些 SectPack 都是 ISO 文件系统中的文件。因此必定会调用标准库中的打开文件函数;

然后,在打开文件后,必然需要读取,读取文件也是标准库中包含的函数之一;

接着,需要调整文件指针来读取指定位置中的内容,就需要 fseek 之类的函数;

另外,要打开/读取 SectPack 中的文件,假设包中的文件是通过路径来索引的(从文件头部附近确实也看到了一些文件路径), 那么就需要进行字符串比较,也就是 strcmp。

大致整理一下思路,我们可以着手进行分析的函数分别是SDK中的sceIoOpenAsync、sceIoReadAsync、sceIoLseekAsync,以及 strcmp ,这些函数,模拟器已经标注出来了:

banner banner

关于以上几个io函数的原型,我们可以参考 pspsdk 中的文档。

以下是从 pspsdk 网站中摘抄的函数说明:


SceUID sceIoOpenAsync(  const char * 	file,
                        int 	        flags,
                        SceMode     	mode 
                        )
Open or create a file for reading or writing (asynchronous)

Parameters
file	- Pointer to a string holding the name of the file to open
flags	- Libc styled flags that are or'ed together
mode	- File access mode (One or more SceIoMode).
Returns
A non-negative integer is a valid fd, anything else an error

int sceIoReadAsync(	SceUID  	fd,
                        void * 	        data,
                        SceSize 	size 
                        )
Read input (asynchronous)

Example:
bytes_read = sceIoRead(fd, data, 100);
Parameters
fd	- Opened file descriptor to read from
data	- Pointer to the buffer where the read data will be placed
size	- Size of the read in bytes
Returns
< 0 on error.

int sceIoLseekAsync	(	SceUID 	fd,
SceOff 	offset,
int 	whence 
)
Reposition read/write file descriptor offset (asynchronous)

Parameters
fd	- Opened file descriptor with which to seek
offset	- Relative offset from the start position given by whence
whence	- One of SceIoSeekMode.
Returns
< 0 on error. Actual value should be passed returned by the ::sceIoWaitAsync call.

首先简单说明一下,PSP采用的MIPS架构处理器,参数的传递使用的是 $a0 ~ $a3 寄存器, 分别对应的是 C/C++ 中函数声明的第1 ~ 第4个参数,当参数数目大于4个时, 多出的参数会使用栈进行传递。而函数的返回值使用 $v0 传递,函数返回地址保存在 $ra 寄存器中。

下面先对 sceIoOpenAsync 断点,继续运行,当断点触发时,跳转到 $a0 寄存器指向的内存地址, 可以看到, $a0 指向的就是将要打开的文件路径:

banner

这里打开的文件 common.bin 恰好是一个 SectPack 文件。 跳出(Step Out) sceIoOpenAsync ,可以看到 $v0 寄存器的值,即 sceIoOpenAsync 的返回值是 0x00000004 。 根据文档中的说明,这个返回值是一个文件指针,而无论是 Seek、Read、Write、Close等IO操作,都需要传入这个指针。 banner

继续运行,这次断点停在了 sceIoReadAsync 处,可以看到,这里传入的文件指针($a0)正是前面打开的 SectPack 的文件指针(0x00000004)。

banner

跳转到 $a1 指向的内存视图,这个参数是读取的内数据存放的地址。 跳出 sceIoReadAsync ,可以看到 common.bin 的前 0x10 字节已经被读到内存中。

banner

↑内存中的视图

↓common.bin 文件视图

banner


继续运行,断点再次停在了 sceIoReadAsync ,用同样的方法查看内存,可以看到这次读取的是 SectPack 中 0x10 开始的一段数据:

banner

到了这一步就可以使用另一个调试工具——内存断点,来分析这些数据的作用了。 点击调试窗口中的 Breakpoint 按钮即可打开断点设置:

banner

内存断点的设置如下,我们对这 0x300 字节的数据进行监视:

banner

内存断点设置完毕,继续运行至断点被触发,观察一下断点附近的程序:

banner

把 0x088C075C ~ 0https://www.pbteam.cn C 语言(伪代码)就是:

char * s = a1 + (v0[1] & 0xFF) | ((v0[2] & 0xFF) << 8) + 0x300; strcpy(a0, s);

也就是从 SectPack 头部中读取一个小端的16位整型数,这个值指向了一个字符串。执行完 strcpy 函数后我们可以看到读取此处指向的字符串就是一个储存在SectPack头部的文件路径:

banner

现在可以判断这 0x300 字节的作用,就是一个指向文件路径的偏移值的表。至于为什么这个表的长度是固定的?有一种可能性,就是所有的文件路径经过某种哈希算法进行了分组,哈希的结果在 0 ~ 0xFF 之间(表的大小是 0x300 字节,表中每一项大小 3 字节,因此最多 0x100 项),这个表保存的就是每个分组的第一个路径的偏移值。这也就能解释为什么表中有些值是 0 ,因为路径的哈希结果没有分布在这些项中。

未完待续……