小木曾雪菜 发表于 2020-11-27 18:18

Galgame汉化中的逆向(四)_IDA静态分析psv游戏

本帖最后由 小木曾雪菜 于 2020-11-27 18:19 编辑


# Galgame汉化中的逆向(四) IDA静态分析psv游戏

本帖在论坛和[我的博客](https://blog.schnee.moe/posts/GalgameReverse4/)同时发布
上期链接:[ Galgame汉化中的逆向(五):Switch平台下的Unity后端il2cpp分析](https://www.52pojie.cn/thread-1236497-1-1.html)

## 0x0 前言

前三篇都在谈pc汉化中的基本方法,这篇来说说主机汉化的操作。由于主机的特殊性,用户权限比较低,一般很难做到动态调试(大多是只有比较完善的模拟器才能动态调试),甚至连编写自制软件都不能用类似于gdb的调试,往往都是插入log来看输出。而且因为主机游戏往往没有加壳和混淆什么的(本身主机系统加密防破解就很变态了,游戏就没必要再搞防护了),因此一般都是进行静态分析。静态分析最重要的是定位,通常是通过特征字节来定位(如游戏opcode,文件magic等),或者根据代码量、大的switch结构、相关字符串(别抱太大希望,字符串很难找到refer)。这篇来谈谈如何用ida静态分析主机游戏,在魔改算法不容易看出来的情况下,如何将ida的伪代码翻译成可执行的代码。本次以psv游戏arm汇编为例。



!(https://img.vim-cn.com/03/a1a3f4bed1d72aac0af24d1b9323290f513a2f.png)



## 0x1 文件结构分析



此游戏有一堆`*.ARC` `*.BIN` 文件,直接用`GARBRO`打开,可以看到文件名但是无法打开每个文件,观察可知是用了叫做(https://github.com/morkt/GARbro/blob/master/ArcFormats/Gss/ArcARC.cs)的游戏引擎。简单分析可知 `*.bin`为索引文件。以`SCRDATA.BIN` `SCRDATA.ARC`为例,文件头如下所示。



!(https://img.vim-cn.com/82/d6d07d94a805e9fc4177b469ec3de576a92d15.png)



这里看着`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 //"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 //4C 53 44 1A, LSD\x1A

>|enc_method1 //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安装(https://github.com/Electry/VitaLoaderIDA)载入`eboot.bin`,`search immediate`搜索"H"等标志位即可找到。



同时我们注意到`GARBRO`源码中已经有了框架分析了一部分,但是最难的一个函数`UnpackH`仍是`throw new NotImplementedException();`我们需要自己分析一下补全。



!(https://img.vim-cn.com/10/f2244c9491be19960707fa47534b2bcee2e4b9.png)



同时,我们在定位处上下翻翻,还能看到`huffman`和`runleng`的字符串,因此压缩算法很可能是基于这两种算法的魔改。



!(https://img.vim-cn.com/95/d4d0489dfee070d893c54107faab016c9353bd.png)



## 0x3 解包算法伪代码分析



下面以`UnpackH`函数为例(其他函数`UnpackD`, `UnpackW`等也同理),来谈谈如何分析。伪代码层面上的分析一般有下列方法:



- 猜每个变量的含义并改名(字符串缓冲区,当前位置指针等比较容易识别)



- 修改到合适数据类型,



有些是数组和结构的,添加结构体看着舒服一些。



还有一些情况,地址显示的是负号,我们需要手动`invert signed`。



- 合并相同变量(注意不能乱合并,尤其是临时赋值的)



用上面方法初步分析整理,可得到如下的伪代码。



```c

// 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; // @2

signed __int64 v33; // @18

int next; // @1

int v35; // @18

_BYTE *cur_output; // @1

int first_char; // @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;

      v14 = cur_addr;

      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;

            *(_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 = v8;

          buf = cur_char1;

          v32 = cur_addr + 3;

          next = v10 + 3;

      }

      }

      else

      {

      v32 = cur_addr + 2;

      next = v10 + 2;

      v12 = cur_addr;

      buf = v8;

      buf = 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 == 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;

    v25 = (signed int)(((buf_packed | (buf_packed << 8)) & 0xFF00FFFF | (buf_packed << 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)`



注意指针加减`*((dword *)addr + 1)`, 其实是`addr+4`的地址里的dword值。



同时要特别注意高级语言的数据类型,数据类型不对可能会把后面数组里的内容覆盖了。



```c

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;

```



```c#

var buf = new Byte;

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;

int idx1 = 0, v28=0;

```



### dump静态全局变量数组



我们注意到伪代码里有调用`dword_8107F828`,去ida里看看相应的地址,把截至到0的数据dump出来作为全局变量到c#里。



!(https://img.vim-cn.com/57/f37544bc58b3da3fd1763f60a3f94e2522467f.png)



```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

```



```c#

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进入的代码都粘贴过来了。



```c

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;

    }

```



```c#

LABEL_26_2:

while (true)

{

    v29 = buf;

    if (v29 != buf)

      break;

    LABEL_26: //goto can not jump here

    if (++cur_char2 == 0x18)

    {

      return 0;

      //outchar = buf; //this seems a hack...

      //goto LABEL_29;                  

    }

}

while (BitConverter.ToUInt32(buf, (int)off2 + 8 * v29) !=

       (v27 & dword_8107F828))

{

    v29++;

    if (v29 == buf)

    {

      if (++cur_char2 == 0x18)

      {

            return 0;

      }

      goto LABEL_26_2;

    }

}

```



### 将所有的子函数也都按照上述方法复原



就是寻找所有的子函数调用,并将其逐个还原。有时候可能伪代码显示有点问题,要看看汇编。



```c

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, ; dword_810604D4

seg000:8105E57A               STR             R4,

seg000:8105E57C               STRD.W          R2, R3,

seg000:8105E580               LDR             R2,

seg000:8105E582               CBNZ            R2, loc_8105E5CE

seg000:8105E584               LDR             R2,

seg000:8105E586               CMP             R2, #0x3F

seg000:8105E588               BHI             loc_8105E5CE

seg000:8105E58A               LDR             R2,

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,

seg000:8105E594               CMP             R2, #0x1F

seg000:8105E596               LDR             R3,

seg000:8105E598               LDR.W         R12,

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,

seg000:8105E5B0               ORRS            R0, R1

seg000:8105E5B2               STR             R0,

seg000:8105E5B4               LDRD.W          R0, R1,

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,

seg000:8105E5BE               SUBS.W          R1, R2, #0x20

seg000:8105E5C2               LSR.W         R0, R12, R1

seg000:8105E5C6               STR             R0,

seg000:8105E5C8               LDRD.W          R0, R1,

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,

seg000:8105E5D4               LDR             R2,

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

```



```c#

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`这个函数的整体还原代码如下:



```c#

int UnpackH(byte[] buf_packed, byte[] output, uint unpacked_size)

{

    var buf = new Byte;

    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;

    int idx1 = 0, v28=0;

    do

    {

      pre_pos = next_pos;

      var cur_char1 = buf_packed;

      var next_addr = cur_addr + 1;

      next_pos++;

      if (cur_char1 != 0)

      {

            byte l1 = buf_packed;

            byte h1 = buf_packed;

            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 << 16));

                        next_addr = cur_addr + 4;

                        next_pos = pre_pos + 4;

                  }

                  buf = cur_char1;

                  BitConverter.GetBytes((uint)(d)).CopyTo(buf, off3 + 8 * i1);

                  buf = (byte)idx1;

                  i1++;

                }

                else //2byte

                {

                  buf = (byte)idx1;

                  buf = cur_char1;

                  next_addr = cur_addr + 3;

                  next_pos = pre_pos + 3;

                }

            }

            else // 1byte

            {

                buf = (byte)idx1;

                buf = 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 = (byte)idx2;

      if (i1 != 0)

      {

            int buf3_addr = 0x2804; //buf3_addr = (_BYTE *)&dword_811C8F84;

            do

            {

                if (buf == 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 = (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);

                BitConverter.GetBytes((int)(2 * v28 + 0x811C6780)).CopyTo(buf, off2 - 4); //must pay attention to the type convert and length

                if (cur_char2 == buf)// buf1+1

                  break;

                cur_char2++;

                if (cur_char2 == 0xD)

                  goto LABEL_22;

            }

            outchar = buf;

            goto LABEL_29;

      }

      LABEL_22:

      if (cur_char2 == 0x18)

            break;

      LABEL_26_2:

      while (true)

      {

            v29 = buf;

            if (v29 != buf)

                break;

            LABEL_26: //goto can not jump here

            if (++cur_char2 == 0x18)

            {

                return 0;

                //outchar = buf; //this seems a hack...

                //goto LABEL_29;                  

            }

      }

      while (BitConverter.ToUInt32(buf, (int)off2 + 8 * v29) !=

               (v27 & dword_8107F828))

      {

            v29++;

            if (v29 == buf)

            {

                if (++cur_char2 == 0x18)

                {

                  return 0;

                }

                goto LABEL_26_2;

            }

      }

      outchar = buf;

      LABEL_29:

      output = (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`引擎解包完善了,详见我的(https://github.com/YuriSizuku/GARbro)同时我也(https://github.com/morkt/GARbro/pull/435)了,不过作者好像好久没维护了。



还原算法后测试一下,发现剧本提取没问题,文字也都出来了,同理图片封包格式也一样。



!(https://img.vim-cn.com/8f/30d48d1f1b9fb6ddbf64b2e9febf7cda4ba6c4.png)



至于封包,我们可以走一个捷径,就是这个游戏有个`IsPacked` 这个flag。修改这个flag,我们可以不压缩和加密,直接把原来文件封包,就是要注意一下0x800等字节对齐与补零,索引重建等。



之后为了汉化文本超长,需要分析一下opcode。这个游戏根据猜测把opcode打印一下即可看出规律,分析出每条指令是干什么的。这游戏比较简单,放一下opcode打印代码和图了,不再详细分析了。当然还有字库问题,这个相对来说处理起来不难,这里不再赘述了。



```python

def print_opcode(scrpath):

    with open(scrpath, 'rb') as fp:

      data = fp.read()

    magic = data

    unk1, unk2 = struct.unpack("<II", data)

    print(magic, hex(unk1), hex(unk2))

    i = 0x10

    while i+2 < len(data):

      optype = data

      oplen = data

      opcodestr = ""

      texts = []

      j = i + 3

      while j < i + oplen:

            if data == 0x3 and data != 0x3:

                start = j+1

                while data != 0: j+=1

                texts.append(data.decode('sjis'))

            else:

                opcodestr += "{:02X} ".format(data)

            j += 1

      print(hex(i), hex(int.from_bytes(optype, 'big')), opcodestr, " #".join(texts))

      i += oplen

```



!(https://img.vim-cn.com/5b/428e9fbfac43afe59b14e080e3c486bf56f6af.png)



另外此游戏psv和psp文件结构非常相似,一些内容也可以用psp模拟器测试提高效率。至于psp的mips汇编和如何动态调试,这些我打算之后出教程。



这些都分析完后,重新导入文本和图片,搞定!



!(https://img.vim-cn.com/c1/f467852123e1dc3f60d2ef8b6d0661b0aad88f.jpg)



!(https://img.vim-cn.com/f5/f55b5e3929d1b5e675a2095ed8e795d65e3e0a.png)

小木曾雪菜 发表于 2020-12-5 20:56

namefin 发表于 2020-12-4 20:45
哇塞,之前玩小黄油遇到非汉化版还超抓狂,想着汉化不就是替换字幕么怎么还没人做,这才了解到,这么难,看 ...

是呀,汉化游戏要远比影视字幕难。要考虑很多东西,解包,封包,文本opcode解析,字库改造与扩容,尤其是冷门的游戏,没有现成的工具,都要从头开始分析

tiemoboy 发表于 2020-11-27 19:29

专业,膜拜,学习了

easthq 发表于 2020-11-27 18:23

牛啊,涉及了好多知识,我原来以为汉化只是简单的字幕文件替换,没想到这么复杂

yl17 发表于 2020-11-27 18:31

牛啊大佬

小木曾雪菜 发表于 2020-11-27 18:38

easthq 发表于 2020-11-27 18:23
牛啊,涉及了好多知识,我原来以为汉化只是简单的字幕文件替换,没想到这么复杂

汉化游戏和汉化影视字幕不同,里面涉及到很多东西。往往最难搞的就是字库了,尤其是遇到冷门主机游戏,没法动态调式,只能一点点的静态分析

芽衣 发表于 2020-11-27 19:11

黄油就靠你了{:301_978:}

看来柚子社不怕没人汉化,草

gunxsword 发表于 2020-11-27 20:55

看不懂啊,顶起来就对了!!!

guangzisam 发表于 2020-11-27 21:04

我的IDA只能算菜鸟。目前还学不懂

九条可怜 发表于 2020-11-28 00:00

学会了,一点,哈哈哈{:1_919:}

talon___ 发表于 2020-11-28 00:47

楼主厉害啊,膜拜
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: Galgame汉化中的逆向(四)_IDA静态分析psv游戏