Galgame汉化中的逆向(四):IDA静态分析psv游戏
Galgame汉化中的逆向(四):IDA静态分析psv游戏
0x0 前言
前三篇都在谈pc汉化中的基本方法,这篇来说说主机汉化的操作。由于主机的特殊性,用户权限比较低,一般很难做到动态调试(大多是只有比较完善的模拟器才能动态调试),甚至连编写自制软件都不能用类似于gdb的调试,往往都是插入log来看输出。而且因为主机游戏往往没有加壳和混淆什么的(本身主机系统加密防破解就很变态了,游戏就没必要再搞防护了),因此一般都是进行静态分析。静态分析最重要的是定位,通常是通过特征字节来定位(如游戏opcode,文件magic等),或者根据代码量、大的switch结构、相关字符串(别抱太大希望,字符串很难找到refer)。这篇来谈谈如何用ida静态分析主机游戏,在魔改算法不容易看出来的情况下,如何将ida的伪代码翻译成可执行的代码。本次以psv游戏arm汇编为例。
0x1 文件结构分析
此游戏有一堆*.ARC
*.BIN
文件,直接用GARBRO
打开,可以看到文件名但是无法打开每个文件,观察可知是用了叫做gss的游戏引擎。简单分析可知 *.bin
为索引文件。以SCRDATA.BIN
SCRDATA.ARC
为例,文件头如下所示。
这里看着SCRDATA.BIN
SCRDATA.ARC
规律很明显,两者结合(根据SCRDATA.BIN
中的索引来打印SCRDATA.ARC
的每项头信息)简单打印一下,可以得到如下的数据:
b’LSDARC V.100’ 325
0 AUTOEXEC 0 0x0 0x1a6 0x800
1 DEBUG 0 0x800 0x143 0x800
2 INIT 1 NH 0x1000 0x1612 0x1000
3 PRG_BG 1 NH 0x2000 0x14c6 0x1000
4 PRG_EFFECT 1 NH 0x3000 0xc0d 0x1000
5 PRG_NAME 1 ND 0x4000 0x689 0x800
6 PRG_ROUTE 1 NH 0x4800 0x2e18 0x2800
7 PRG_STAND 1 NH 0x7000 0x31e0 0x2800
8 RELOAD 1 ND 0x9800 0x349 0x800
9 SELECT2 0 0xa000 0x102 0x800
10 COM001 1 WD 0xa800 0x3f1 0x800
11 COM002 1 NH 0xb000 0x130d 0x1800
12 COM003 0 0xc800 0xa23 0x1000
13 COM004 1 NH 0xd800 0x3e80 0x3800
14 COM005 1 NH 0x11000 0x1fb0 0x2000
15 COM006 1 NH 0x13000 0x4973 0x4000
16 COM007 1 NH 0x17000 0x1003 0x1000
17 COM008 1 NH 0x18000 0x3e5a 0x3800
18 COM009 1 NH 0x1b800 0x194f 0x1800
19 COM010 1 NH 0x1d000 0x2ad2 0x2800…
根据对照很容易分析出SCRDATA.BIN
数据结构,文件magic为12字节,之后4字节为index数量。然后就是index_entry了。
magic[12] //“LSDARC V.100”
count 4
index[]
|IsPacked 4 //导入不用打包,IsPacked调成0即可
|Offset 4
|UnpackedSize 4
|Size 4 //0x800的倍数
|Name 00
同理可得SCRDATA.ARC
的数据结构。
//unpacked
53 43 52 20 32 2E 30 30 //SCR 2.00//packed
fileblock[]
|magic[4] //4C 53 44 1A, LSD\x1A
|enc_method 1 //enc_method, B W S
|pack_method 1 //pack_method, D R H W
|unpacked_size 4 //may be different than the value in BIN index, often 4 byte asign
0x2 解包算法定位
上面文件数据结构分析,我们发现有是否压缩的flag和压缩后和解压的体积,再加上来查看*.ARC
文件,内部比较紧凑,因此判定是压缩或者加密了。我们观察得到pack_method
字段有”D,R,H,W”这四个很醒目的标识符,这里可以作为突破口。必须用ida6.8安装VitaLoaderIDA载入eboot.bin
, search immediate
搜索”H”等标志位即可找到。
同时我们注意到GARBRO
源码中已经有了框架分析了一部分,但是最难的一个函数UnpackH
仍是throw new NotImplementedException();
我们需要自己分析一下补全。
同时,我们在定位处上下翻翻,还能看到huffman
和runleng
的字符串,因此压缩算法很可能是基于这两种算法的魔改。
0x3 解包算法伪代码分析
下面以UnpackH
函数为例(其他函数UnpackD
, UnpackW
等也同理),来谈谈如何分析。伪代码层面上的分析一般有下列方法:
猜每个变量的含义并改名(字符串缓冲区,当前位置指针等比较容易识别)
修改到合适数据类型,
有些是数组和结构的,添加结构体看着舒服一些。
还有一些情况,地址显示的是负号,我们需要手动
invert signed
。合并相同变量(注意不能乱合并,尤其是临时赋值的)
用上面方法初步分析整理,可得到如下的伪代码。
1 | // UnpackH |
0x4 解包算法伪代码复原为可执行代码
下面,我们就需要将伪代码来转换成可执行代码了,由于GARBRO
有了一部分解包代码,我们就在此基础上完善了,而且c#代码也和c很相似,就把ida的伪代码转换成c#代码。这个操作其实难度不是很大,但是需要非常细心,错一处最后结果都可能有问题,而且很不好调试。一般情况下看着解包后的数据有规律且可读,通常情况下就可以认为是正确了,但是遇到特殊情况需要用金手指插件去dump内存,然后去逐字节比对。至于为什么不直接dump解包,因为dump过程非常繁琐,游戏不是把所有资源都加载的,主机上的hook非常麻烦。因此还是很有必要来分析静态解包方法的。
将偏移地址转换为数组
以下为伪代码和c#代码,如将0x811c6780
这个地址命名为buf1
,所有这个基地址的偏移也可用数组表示。
如*(_DWORD *)(8 * v29 + 0x811C8784) != (v27 & *(_DWORD *)(8 * cur_char2 + 0x8107F828))
转换为BitConverter.ToUInt32(buf, (int)off2 + 8 * v29) != (v27 & dword_8107F828[2 * cur_char2])
注意指针加减*((dword *)addr + 1)
, 其实是addr+4
的地址里的dword值。
同时要特别注意高级语言的数据类型,数据类型不对可能会把后面数组里的内容覆盖了。
1 | buf = (_BYTE *)&unk_811C6780; |
1 | var buf = new Byte[0x10000]; |
dump静态全局变量数组
我们注意到伪代码里有调用dword_8107F828
,去ida里看看相应的地址,把截至到0的数据dump出来作为全局变量到c#里。
1 | dword_8107F828: |
1 | static readonly uint[] dword_8107F828 = { |
goto相关的转换
由于c#的goto不能跳入其他循环,遇到这种情况我们就需要把goto进入的代码都粘贴过来了。
1 | LABEL_22: |
1 | LABEL_26_2: |
将所有的子函数也都按照上述方法复原
就是寻找所有的子函数调用,并将其逐个还原。有时候可能伪代码显示有点问题,要看看汇编。
1 | v27 = sub_8105E56C(v25, HIDWORD(v25), (v33 & 0x1F) - (v33 & 0x18)); |
1 | uint v27 = sub_8105E56C((uint)v25, (uint)(v25 >> 32), (uint)((v33 & 0x1F) - (v33 & 0x18))); |
提取同类项与整体结构优化
在运行结果正确的技术上,这时候可以进行整体的考虑了。即站在更高层来分析那些代码逻辑是相同的,把每部分相同功能的变量与代码提到外层,使得整体看着简洁。同时优化位运算等操作的可读性。此处UnpackH
这个函数的整体还原代码如下:
1 | int UnpackH(byte[] buf_packed, byte[] output, uint unpacked_size) |
0X5 后记
根据这个游戏,我把GARBRO
的gss
引擎解包完善了,详见我的github同时我也pull request了,不过作者好像好久没维护了。
还原算法后测试一下,发现剧本提取没问题,文字也都出来了,同理图片封包格式也一样。
至于封包,我们可以走一个捷径,就是这个游戏有个IsPacked
这个flag。修改这个flag,我们可以不压缩和加密,直接把原来文件封包,就是要注意一下0x800等字节对齐与补零,索引重建等。
之后为了汉化文本超长,需要分析一下opcode。这个游戏根据猜测把opcode打印一下即可看出规律,分析出每条指令是干什么的。这游戏比较简单,放一下opcode打印代码和图了,不再详细分析了。当然还有字库问题,这个相对来说处理起来不难,这里不再赘述了。
1 | def print_opcode(scrpath): |
另外此游戏psv和psp文件结构非常相似,一些内容也可以用psp模拟器测试提高效率。至于psp的mips汇编和如何动态调试,这些我打算之后出教程。
这些都分析完后,重新导入文本和图片,搞定!