Galgame汉化中的逆向(四) IDA静态分析psv游戏
本帖在论坛和我的博客同时发布
上期链接: Galgame汉化中的逆向(五):Switch平台下的Unity后端il2cpp分析
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
等也同理),来谈谈如何分析。伪代码层面上的分析一般有下列方法:
用上面方法初步分析整理,可得到如下的伪代码。
// UnpackH
int __fastcall sub_81043752(_BYTE *buf_packed, _BYTE *output, int unpacked_size)
{
_BYTE *buf; // r10@1
_BYTE *cur_addr; // r6@1
int v7; // t1@1
int v8; // r2@1
int i1; // r8@1
int v10; // r1@2
signed int cur_char1; // r5@2
int v12; // r0@4
int v13; // r7@7
int v14; // lr@7
unsigned int v15; // r7@8
int v16; // r10@9
int v17; // r0@10
int v18; // r6@6
signed int v19; // r7@6
int i2; // r5@12
_BYTE *buf3_addr; // lr@13
int v22; // r4@15
int v23; // r0@18
int v24; // r12@19
__int64 v25; // r0@19
int cur_char2; // r9@19
unsigned int v27; // r5@19
int v28; // lr@20
int v29; // lr@23
char v30; // lr@28
_BYTE *v32; // [sp+0h] [bp-50h]@2
signed __int64 v33; // [sp+8h] [bp-48h]@18
int next; // [sp+10h] [bp-40h]@1
int v35; // [sp+14h] [bp-3Ch]@18
_BYTE *cur_output; // [sp+1Ch] [bp-34h]@1
int first_char; // [sp+28h] [bp-28h]@1
buf = (_BYTE *)&unk_811C6780;
cur_output = output;
memset(0x811C6780, 0, 0x2000); // buf1
memset(0x811C8784, 0, 0x800); // buf2
memset(0x811C8F84, 0, 0x800); // buf3
v7 = *buf_packed;
cur_addr = buf_packed + 2;
v8 = 0;
first_char = v7;
i1 = 0;
next = 2;
do
{
v10 = next;
cur_char1 = *cur_addr;
v32 = cur_addr + 1;
++next;
if ( *cur_addr )
{
if ( cur_char1 >= 8 )
{
v13 = cur_addr[1];
v14 = cur_addr[2];
if ( cur_char1 >= 0xD )
{
v16 = (unsigned __int8)v13 + (v14 << 8);
if ( cur_char1 < 0x10 )
{
*(_BYTE *)(8 * i1 + 0x811C8F88) = cur_char1;// buf3 + 4
*(_DWORD *)(8 * i1 + 0x811C8F84) = v16;// buf3
*(_BYTE *)(8 * i1 + 0x811C8F88) = v8;// bu3 + 4
v32 = cur_addr + 3;
next = v10 + 3;
i1 = (unsigned __int8)(i1 + 1);
}
else
{
v17 = cur_addr[3];
*(_BYTE *)(8 * i1 + 0x811C8F88) = cur_char1;// buf3+4
v32 = cur_addr + 4;
*(_DWORD *)(8 * i1 + 0x811C8F84) = v16 + (v17 << 16);// buf3
*(_BYTE *)(8 * i1 + 0x811C8F89) = v8;// buf3+5
next = v10 + 4;
i1 = (unsigned __int8)(i1 + 1);
}
}
else
{
v15 = v13 & 0xFFFF00FF | ((unsigned __int8)v14 << 8);
buf[2 * (unsigned __int16)v15] = v8;
buf[2 * (unsigned __int16)v15 + 1] = cur_char1;
v32 = cur_addr + 3;
next = v10 + 3;
}
}
else
{
v32 = cur_addr + 2;
next = v10 + 2;
v12 = cur_addr[1];
buf[2 * v12] = v8;
buf[2 * v12 + 1] = cur_char1;
}
}
buf = (_BYTE *)&unk_811C6780;
cur_addr = v32;
++v8;
}
while ( v8 != 0x100 );
v18 = 0;
v19 = 13;
do
{
i2 = 0;
*(_BYTE *)(v19 + 0x811C9784) = v18; // buf4, buf2+0x1000
if ( i1 )
{
buf3_addr = (_BYTE *)&dword_811C8F84;
do
{
if ( buf3_addr[4] == v19 )
{
v22 = *((_DWORD *)buf3_addr + 1);
*(_DWORD *)(8 * v18 + 0x811C8784) = *(_DWORD *)buf3_addr;// buf2
*(_DWORD *)(8 * v18 + 0x811C8788) = v22;// buf2+4
v18 = (unsigned __int8)(v18 + 1);
}
++i2;
buf3_addr += 8;
}
while ( i2 != i1 );
}
v19 = (unsigned __int16)(v19 + 1);
}
while ( v19 != 24 );
unk_811C979C = v18;
v35 = 0;
v33 = 0i64;
qword_811C97A8 = 0i64;
v23 = unpacked_size;
if ( !unpacked_size )
return v35;
while ( 1 )
{
v24 = next + ((unsigned int)(v33 & 0x1F) >> 3) + ((v33 >> 3) & 0xFFFFFFFC);
LOBYTE(v23) = buf_packed[v24 + 3];
v25 = (signed int)(((buf_packed[v24] | (buf_packed[v24 + 1] << 8)) & 0xFF00FFFF | (buf_packed[v24 + 2] << 16)) & 0xFFFFFF | (v23 << 24));
cur_char2 = first_char;
v27 = sub_8105E56C(v25, HIDWORD(v25), (v33 & 0x1F) - (v33 & 0x18));
if ( first_char != 0xD )
{
while ( 1 )
{
v28 = v27 & *(_DWORD *)(8 * cur_char2 + 0x8107F828);// const1
dword_811C8780 = 2 * v28 + 0x811C6780; // buf1
if ( cur_char2 == *(_BYTE *)(2 * v28 + 0x811C6781) )// buf1+1
break;
cur_char2 = (unsigned __int16)(cur_char2 + 1);
if ( (unsigned __int16)cur_char2 == 0xD )
goto LABEL_22;
}
v30 = *(_BYTE *)(2 * v28 + 0x811C6780); // buf1
goto LABEL_29;
}
LABEL_22:
if ( cur_char2 == 0x18 )
break;
while ( 1 )
{
v29 = *(_BYTE *)(cur_char2 + 0x811C9784); // buf4
if ( v29 != *(_BYTE *)(cur_char2 + 0x811C9785) )// buf4+1
break;
LABEL_26:
if ( ++cur_char2 == 0x18 )
goto LABEL_33;
}
while ( *(_DWORD *)(8 * v29 + 0x811C8784) != (v27 & *(_DWORD *)(8 * cur_char2 + 0x8107F828)) )// buf2, const1
{
v29 = (unsigned __int16)(v29 + 1);
if ( (unsigned __int16)v29 == *(_BYTE *)(cur_char2 + 0x811C9785) )// buf4+1
goto LABEL_26;
}
v30 = *(_BYTE *)(8 * v29 + 0x811C8789); // buf2+5
LABEL_29:
*cur_output = v30;
qword_811C97A8 = v33 + (unsigned __int16)cur_char2;
v23 = v35 + 1;
++cur_output;
v33 += (unsigned __int16)cur_char2;
v35 = v23;
if ( v23 == unpacked_size )
return v35;
}
LABEL_33:
sub_8104332C(); // None
return 0;
}
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值。
同时要特别注意高级语言的数据类型,数据类型不对可能会把后面数组里的内容覆盖了。
buf = (_BYTE *)&unk_811C6780;
cur_output = output;
memset(0x811C6780, 0, 0x2000); // buf1
memset(0x811C8784, 0, 0x800); // buf2
memset(0x811C8F84, 0, 0x800); // buf3
v7 = *buf_packed;
cur_addr = buf_packed + 2;
v8 = 0;
first_char = v7;
i1 = 0;
next = 2;
var buf = new Byte[0x10000];
Array.Clear(buf, 0, buf.Length);
const uint off2 = 0x2004;
const uint off3 = 0x2804;
const uint off4 = 0x3004; //0x811C9784
uint cur_addr = 2, pre_pos, next_pos = 2, cur_output_addr = 0;
byte outchar, i1 = 0, i2, v29, first_char = buf_packed[0];
int idx1 = 0, v28=0;
dump静态全局变量数组
我们注意到伪代码里有调用dword_8107F828
,去ida里看看相应的地址,把截至到0的数据dump出来作为全局变量到c#里。
dword_8107F828:
00000000 00000000 00000001 00000000 00000003 00000000 00000007 00000000
0000000F 00000000 0000001F 00000000 0000003F 00000000 0000007F 00000000
000000FF 00000000 000001FF 00000000 000003FF 00000000 000007FF 00000000
00000FFF 00000000 00001FFF 00000000 00003FFF 00000000 00007FFF 00000000
0000FFFF 00000000 0001FFFF 00000000 0003FFFF 00000000 0007FFFF 00000000
000FFFFF 00000000 001FFFFF 00000000 003FFFFF 00000000 007FFFFF 00000000
00FFFFFF 00000000 00000000 00000000 00000000 00000000 00000007 0000000F
static readonly uint[] dword_8107F828 = {
0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000003, 0x00000000, 0x00000007, 0x00000000,
0x0000000F, 0x00000000, 0x0000001F, 0x00000000, 0x0000003F, 0x00000000, 0x0000007F, 0x00000000,
0x000000FF, 0x00000000, 0x000001FF, 0x00000000, 0x000003FF, 0x00000000, 0x000007FF, 0x00000000,
0x00000FFF, 0x00000000, 0x00001FFF, 0x00000000, 0x00003FFF, 0x00000000, 0x00007FFF, 0x00000000,
0x0000FFFF, 0x00000000, 0x0001FFFF, 0x00000000, 0x0003FFFF, 0x00000000, 0x0007FFFF, 0x00000000,
0x000FFFFF, 0x00000000, 0x001FFFFF, 0x00000000, 0x003FFFFF, 0x00000000, 0x007FFFFF, 0x00000000,
0x00FFFFFF, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000007, 0x0000000F,
0x0000001F, 0x0000003F, 0x0000007F, 0x000000FF, 0x000001FF, 0x000003FF, 0x000007FF, 0x00000FFF,
0x00001FFF, 0x00003FFF, 0x00007FFF, 0x0000FFFF, 0x0001FFFF, 0x0003FFFF, 0x000FFFFF, 0x00000000,
0x06050403, 0x0A090807, 0x0E0D0C0B, 0x00000000, 0x00000001, 0x00000002, 0x00000004, 0x00000008,
0x00000010, 0x00000020, 0x00000040, 0x00000080, 0x00000100, 0x00000200, 0x00000400, 0x00000800,
0x00001000, 0x00002000, 0x00004000, 0x00000000, 0xFFFF0001, 0x00000000, 0x00000000, 0x00000001,
0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x00000001, 0x00000001,
};
goto相关的转换
由于c#的goto不能跳入其他循环,遇到这种情况我们就需要把goto进入的代码都粘贴过来了。
LABEL_22:
if ( cur_char2 == 0x18 )
break;
while ( 1 )
{
v29 = *(_BYTE *)(cur_char2 + 0x811C9784); // buf4
if ( v29 != *(_BYTE *)(cur_char2 + 0x811C9785) )// buf4+1
break;
LABEL_26:
if ( ++cur_char2 == 0x18 )
goto LABEL_33;
}
while ( *(_DWORD *)(8 * v29 + 0x811C8784) != (v27 & *(_DWORD *)(8 * cur_char2 + 0x8107F828)) )// buf2, const1
{
v29 = (unsigned __int16)(v29 + 1);
if ( (unsigned __int16)v29 == *(_BYTE *)(cur_char2 + 0x811C9785) )// buf4+1
goto LABEL_26;
}
LABEL_26_2:
while (true)
{
v29 = buf[off4 + cur_char2];
if (v29 != buf[off4 + 1 + cur_char2])
break;
LABEL_26: //goto can not jump here
if (++cur_char2 == 0x18)
{
return 0;
//outchar = buf[off2 + 5 + 8 * v29]; //this seems a hack...
//goto LABEL_29;
}
}
while (BitConverter.ToUInt32(buf, (int)off2 + 8 * v29) !=
(v27 & dword_8107F828[2 * cur_char2]))
{
v29++;
if (v29 == buf[off4 + 1 + cur_char2 ])
{
if (++cur_char2 == 0x18)
{
return 0;
}
goto LABEL_26_2;
}
}
将所有的子函数也都按照上述方法复原
就是寻找所有的子函数调用,并将其逐个还原。有时候可能伪代码显示有点问题,要看看汇编。
v27 = sub_8105E56C(v25, HIDWORD(v25), (v33 & 0x1F) - (v33 & 0x18));
seg000:8105E56C sub_8105E56C ; CODE XREF: sub_81043752+22Ap
seg000:8105E56C ; sub_8105E344+80p
seg000:8105E56C
seg000:8105E56C var_18 = -0x18
seg000:8105E56C var_10 = -0x10
seg000:8105E56C
seg000:8105E56C PUSH {R4,R5,LR}
seg000:8105E56E SUB SP, SP, #0xC
seg000:8105E570 MOV R5, #0x810604D0
seg000:8105E578 LDR R4, [R5] ; dword_810604D4
seg000:8105E57A STR R4, [SP,#0x18+var_10]
seg000:8105E57C STRD.W R2, R3, [SP]
seg000:8105E580 LDR R2, [SP,#0x18+var_18+4]
seg000:8105E582 CBNZ R2, loc_8105E5CE
seg000:8105E584 LDR R2, [SP,#0x18+var_18]
seg000:8105E586 CMP R2, #0x3F
seg000:8105E588 BHI loc_8105E5CE
seg000:8105E58A LDR R2, [SP,#0x18+var_18]
seg000:8105E58C CBNZ R2, loc_8105E590
seg000:8105E58E B loc_8105E5D2
seg000:8105E590 ; ---------------------------------------------------------------------------
seg000:8105E590
seg000:8105E590 loc_8105E590 ; CODE XREF: sub_8105E56C+20j
seg000:8105E590 STRD.W R0, R1, [SP]
seg000:8105E594 CMP R2, #0x1F
seg000:8105E596 LDR R3, [SP,#0x18+var_18]
seg000:8105E598 LDR.W R12, [SP,#0x18+var_18+4]
seg000:8105E59C BHI loc_8105E5BA
seg000:8105E59E RSBS.W R0, R2, #0x20
seg000:8105E5A2 LSRS.W R1, R3, R2
seg000:8105E5A6 LSL.W R0, R12, R0
seg000:8105E5AA LSR.W R2, R12, R2
seg000:8105E5AE STR R2, [SP,#0x18+var_18+4]
seg000:8105E5B0 ORRS R0, R1
seg000:8105E5B2 STR R0, [SP,#0x18+var_18]
seg000:8105E5B4 LDRD.W R0, R1, [SP]
seg000:8105E5B8 B loc_8105E5D2
seg000:8105E5BA ; ---------------------------------------------------------------------------
seg000:8105E5BA
seg000:8105E5BA loc_8105E5BA ; CODE XREF: sub_8105E56C+30j
seg000:8105E5BA MOVS R0, #0
seg000:8105E5BC STR R0, [SP,#0x18+var_18+4]
seg000:8105E5BE SUBS.W R1, R2, #0x20
seg000:8105E5C2 LSR.W R0, R12, R1
seg000:8105E5C6 STR R0, [SP,#0x18+var_18]
seg000:8105E5C8 LDRD.W R0, R1, [SP]
seg000:8105E5CC B loc_8105E5D2
seg000:8105E5CE ; ---------------------------------------------------------------------------
seg000:8105E5CE
seg000:8105E5CE loc_8105E5CE ; CODE XREF: sub_8105E56C+16j
seg000:8105E5CE ; sub_8105E56C+1Cj
seg000:8105E5CE MOVS R0, #0
seg000:8105E5D0 MOVS R1, #0
seg000:8105E5D2
seg000:8105E5D2 loc_8105E5D2 ; CODE XREF: sub_8105E56C+22j
seg000:8105E5D2 ; sub_8105E56C+4Cj ...
seg000:8105E5D2 LDR R3, [SP,#0x18+var_10]
seg000:8105E5D4 LDR R2, [R5]
seg000:8105E5D6 CMP R2, R3
seg000:8105E5D8 BNE loc_8105E5DE
seg000:8105E5DA ADD SP, SP, #0xC
seg000:8105E5DC POP {R4,R5,PC}
seg000:8105E5DE ; ---------------------------------------------------------------------------
seg000:8105E5DE
seg000:8105E5DE loc_8105E5DE ; CODE XREF: sub_8105E56C+6Cj
seg000:8105E5DE BLX __stack_chk_fail
seg000:8105E5DE ; End of function sub_8105E56C
uint v27 = sub_8105E56C((uint)v25, (uint)(v25 >> 32), (uint)((v33 & 0x1F) - (v33 & 0x18)));
uint sub_8105E56C(uint result, uint a2, uint a3)
{
if (a3 > 63)
{
result = 0;
}
else if (a3 != 0)
{
if (a3 > 31)
result = a2 >> (int)(a3 - 32);
else
result = (a2 << (int)(32 - a3)) | (result >> (int)a3);
//insert the low a3 bit of the result into a2, then asign to result
}
return result;
}
提取同类项与整体结构优化
在运行结果正确的技术上,这时候可以进行整体的考虑了。即站在更高层来分析那些代码逻辑是相同的,把每部分相同功能的变量与代码提到外层,使得整体看着简洁。同时优化位运算等操作的可读性。此处UnpackH
这个函数的整体还原代码如下:
int UnpackH(byte[] buf_packed, byte[] output, uint unpacked_size)
{
var buf = new Byte[0x10000];
Array.Clear(buf, 0, buf.Length);
const uint off2 = 0x2004;
const uint off3 = 0x2804;
const uint off4 = 0x3004; //0x811C9784
uint cur_addr = 2, pre_pos, next_pos = 2, cur_output_addr = 0;
byte outchar, i1 = 0, i2, v29, first_char = buf_packed[0];
int idx1 = 0, v28=0;
do
{
pre_pos = next_pos;
var cur_char1 = buf_packed[cur_addr];
var next_addr = cur_addr + 1;
next_pos++;
if (cur_char1 != 0)
{
byte l1 = buf_packed[cur_addr + 1];
byte h1 = buf_packed[cur_addr + 2];
uint t1 = (ushort)(l1 + (h1 << 8));
if (cur_char1 >= 8)
{
if (cur_char1 >= 0xD)
{
uint d;
if (cur_char1 < 0x10) // 2 byte
{
d = t1;
next_addr = cur_addr + 3;
next_pos = pre_pos + 3;
}
else //3 byte
{
d = (uint)(t1 + (buf_packed[cur_addr + 3] << 16));
next_addr = cur_addr + 4;
next_pos = pre_pos + 4;
}
buf[off3 + 4 + 8 * i1] = cur_char1;
BitConverter.GetBytes((uint)(d)).CopyTo(buf, off3 + 8 * i1);
buf[off3 + 5 + 8 * i1] = (byte)idx1;
i1++;
}
else //2byte
{
buf[2 * t1] = (byte)idx1;
buf[2 * t1 + 1] = cur_char1;
next_addr = cur_addr + 3;
next_pos = pre_pos + 3;
}
}
else // 1byte
{
buf[2 * l1] = (byte)idx1;
buf[2 * l1 + 1] = cur_char1;
next_addr = cur_addr + 2;
next_pos = pre_pos + 2;
}
}
cur_addr = next_addr;
++idx1;
} while (idx1 != 0x100);
byte idx2 = 0;
byte v19 = 0xD;
do
{
i2 = 0;
buf[off4 + v19] = (byte)idx2;
if (i1 != 0)
{
int buf3_addr = 0x2804; //buf3_addr = (_BYTE *)&dword_811C8F84;
do
{
if (buf[buf3_addr + 4] == v19)
{
Array.Copy(buf, buf3_addr, buf, off2 + 8 * idx2, 4);
Array.Copy(buf, buf3_addr+4, buf, off2 + 4 + 8 * idx2, 4);
idx2++;
}
++i2;
buf3_addr += 8;
}
while (i2 != i1);
}
v19++;
} while (v19 != 0x18);
buf[0x301c] = (byte)idx2; //unk_811C979C = idx2;
Array.Clear(buf, 0x3028, 8);//qword_811C97A8 = 0i64;
int v23 = (int)unpacked_size, v33=0, size_done = 0;
while (true)
{
int v24 = (int)(next_pos + ((uint)(v33 & 0x1F) >> 3) + ((v33 >> 3) & 0xFFFFFFFC)); //this place, out of range
ulong v25 = BitConverter.ToUInt32(buf_packed, v24);
byte cur_char2 = first_char;
uint v27 = sub_8105E56C((uint)v25, (uint)(v25 >> 32), (uint)((v33 & 0x1F) - (v33 & 0x18)));
if (first_char != 0xD)
{
while (true)
{
v28 = (int)(v27 & dword_8107F828[2 * cur_char2]);
BitConverter.GetBytes((int)(2 * v28 + 0x811C6780)).CopyTo(buf, off2 - 4); //must pay attention to the type convert and length
if (cur_char2 == buf[2 * v28 + 1])// buf1+1
break;
cur_char2++;
if (cur_char2 == 0xD)
goto LABEL_22;
}
outchar = buf[2 * v28];
goto LABEL_29;
}
LABEL_22:
if (cur_char2 == 0x18)
break;
LABEL_26_2:
while (true)
{
v29 = buf[off4 + cur_char2];
if (v29 != buf[off4 + 1 + cur_char2])
break;
LABEL_26: //goto can not jump here
if (++cur_char2 == 0x18)
{
return 0;
//outchar = buf[off2 + 5 + 8 * v29]; //this seems a hack...
//goto LABEL_29;
}
}
while (BitConverter.ToUInt32(buf, (int)off2 + 8 * v29) !=
(v27 & dword_8107F828[2 * cur_char2]))
{
v29++;
if (v29 == buf[off4 + 1 + cur_char2 ])
{
if (++cur_char2 == 0x18)
{
return 0;
}
goto LABEL_26_2;
}
}
outchar = buf[off2 + 5 + 8 * v29];
LABEL_29:
output[cur_output_addr] = (byte)outchar;
BitConverter.GetBytes((Int64)v33 + (ushort)cur_char2).CopyTo(buf, 0x3028);
v23 = size_done + 1;
++cur_output_addr;
v33 += cur_char2 ;
size_done = v23;
if (v23 >= unpacked_size)
return size_done;
}
return 0;
}
uint sub_8105E56C(uint result, uint a2, uint a3)
{
if (a3 > 63)
{
result = 0;
}
else if (a3 != 0)
{
if (a3 > 31)
result = a2 >> (int)(a3 - 32);
else
result = (a2 << (int)(32 - a3)) | (result >> (int)a3);
//insert the low a3 bit of the result into a2, then asign to result
}
return result;
}
0X5 后记
根据这个游戏,我把GARBRO
的gss
引擎解包完善了,详见我的github同时我也pull request了,不过作者好像好久没维护了。
还原算法后测试一下,发现剧本提取没问题,文字也都出来了,同理图片封包格式也一样。
至于封包,我们可以走一个捷径,就是这个游戏有个IsPacked
这个flag。修改这个flag,我们可以不压缩和加密,直接把原来文件封包,就是要注意一下0x800等字节对齐与补零,索引重建等。
之后为了汉化文本超长,需要分析一下opcode。这个游戏根据猜测把opcode打印一下即可看出规律,分析出每条指令是干什么的。这游戏比较简单,放一下opcode打印代码和图了,不再详细分析了。当然还有字库问题,这个相对来说处理起来不难,这里不再赘述了。
def print_opcode(scrpath):
with open(scrpath, 'rb') as fp:
data = fp.read()
magic = data[0:8]
unk1, unk2 = struct.unpack("<II", data[8:0x10])
print(magic, hex(unk1), hex(unk2))
i = 0x10
while i+2 < len(data):
optype = data[i:i+2]
oplen = data[i+2]
opcodestr = ""
texts = []
j = i + 3
while j < i + oplen:
if data[j] == 0x3 and data[j+1] != 0x3:
start = j+1
while data[j] != 0: j+=1
texts.append(data[start:j].decode('sjis'))
else:
opcodestr += "{:02X} ".format(data[j])
j += 1
print(hex(i), hex(int.from_bytes(optype, 'big')), opcodestr, " #".join(texts))
i += oplen
另外此游戏psv和psp文件结构非常相似,一些内容也可以用psp模拟器测试提高效率。至于psp的mips汇编和如何动态调试,这些我打算之后出教程。
这些都分析完后,重新导入文本和图片,搞定!