夜行侦探 EVE burst error 游戏文件分析(二)

cutter / dosbox / ghidra / imhex / eve / pc98

今天我们还是继续 eve,顺带一提,我又切换回了 ghidra, 因为 ghidra 再一次向我证明了,它虽然 UI 很差,但是功能绝对够用。

目录

本系列基本完结,以下是目录供快速翻阅:

  1. 启动、GDT 文件分析一:19 个函数
  2. GDT 文件分析二:ghidra 偏移量、读取、初次绘画
  3. GDT 文件分析三:多次读取、偏移量、调色板
  4. GDT 文件分析四:完整解开及遗留问题
  5. 游戏文本解码
  6. 汉化方案制定
  7. 游戏文本编码
  8. 汉字日文编码、字库、以及第一个汉化文件
  9. 配置一个汉化 demo
  10. 手机游玩、程序文字汉化

从 cutter 到 ghidra

每次用 cutter 的理由都很简单,UI 更好,更简洁。而 ghidra 简直就是个弹窗地狱, 查询的条件和结果都不能放在一起。所以每次我被 ghidra 恶心到的时候, 就想着“cutter 又更新了,看看会不会好一点”,然后用着用着,就又切回 ghidra, 这可能和「女生喜欢看帅的男生做任何事」的情结类似, 我也总会给颜值高的应用多一些机会,但是最终还是会选那个脚踏实地的。

这次切换,其实还是偏移量的问题,比如,在日志中的1427:0166 这行:

1427:00000137  call 00000166 ($+2c)                                    E8 2C 00              EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000001 EDI:000000A0 EBP:00000F78 ESP:00000F70 DS:1427 ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:1 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:00000166  cmp  byte cs:[00DE],00          cs:[00DE]=8C14          2E 80 3E DE 00 00     EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000001 EDI:000000A0 EBP:00000F78 ESP:00000F6E DS:1427 ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:1 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:0000016C  jne  0000016F ($+1)             (down)                  75 01                 EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000001 EDI:000000A0 EBP:00000F78 ESP:00000F6E DS:1427 ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:0000016F  test word cs:[00A4],4000        cs:[00A4]=CF00          2E F7 06 A4 00 00 40  EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000001 EDI:000000A0 EBP:00000F78 ESP:00000F6E DS:1427 ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:00000176  jne  0000018C ($+14)            (down)                  75 14                 EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000001 EDI:000000A0 EBP:00000F78 ESP:00000F6E DS:1427 ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:0000018C  call 000038AF ($+3720)                                  E8 20 37              EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000001 EDI:000000A0 EBP:00000F78 ESP:00000F6E DS:1427 ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:000038AF  lds  si,cs:[00CC]               cs:[00CC]=0034          2E C5 36 CC 00        EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000001 EDI:000000A0 EBP:00000F78 ESP:00000F6C DS:1427 ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:000038B4  mov  ax,cs:[001F]               cs:[001F]=22B2          2E A1 1F 00           EAX:00000028 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000034 EDI:000000A0 EBP:00000F78 ESP:00000F6C DS:421A ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:000038B8  call 000038F4 ($+39)                                    E8 39 00              EAX:000022B2 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000034 EDI:000000A0 EBP:00000F78 ESP:00000F6C DS:421A ES:1427 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010

在 cutter 中是这样:

0000:41e6      2e803ede0000           cmp     byte cs:[0xde], 0 ; compare two operands
0000:41ec      7501                   jne     0x41ef ; jump short if not equal/not zero (zf=0)
0000:41ee      c3                     ret ; return from subroutine. pop 4 bytes from esp and jump there.
0000:41ef      2ef706a4000040         test    word cs:[0xa4], 0x4000 ; set eflags after comparing two registers (AF, CF, OF, PF, SF, ZF)
0000:41f6      7514                   jne     0x420c ; jump short if not equal/not zero (zf=0)
0000:41f8      e8f33c                 call    fcn.00007eee ; fcn.00007eee ; calls a subroutine, push eip into the stack (esp)
0000:41fb      e88700                 call    fcn.00004285 ; fcn.00004285 ; calls a subroutine, push eip into the stack (esp)

我们可以看到,偏移量有很大的问题,我想大部分人可能和我一样, 没办法做到脑内十六进制偏移量转换,所以这个偏移量对我们来说就没什么参考意义了。

我们再看看 ghidra:

   1427:0166 2e  80        CMP      byte ptr CS :[LAB_101f_00dd+1 ],0x0
             3e  de 
             00  00
   1427:016c 75  01        JNZ      LAB_1427_016f
   1427:016e c3           RET
   1427:016f 2e  f7        TEST     word ptr CS :[LAB_101f_00a4 ],0x4000
             06  a4 
             00  00  40
   1427:0176 75  14        JNZ      LAB_1427_018c
   1427:0178 e8  f3  3c     CALL     FUN_1427_3e6e                         undefined FUN_1427_3e6e()
   1427:017b e8  87  00     CALL     FUN_1427_0205                         undefined FUN_1427_0205()

和日志中是完全一致的,不过这是调教后的结果,调教前是 1408:0166, 我给基础地址增加了一个偏移量,加一个偏移量没有很难,打开 memory map:

找到小房子的图标:

就可以调整偏移量了:

调整完会有关于书签的警告,我还没有弄清这些警告的危险性,我的建议是导入程序文件后,不做任何操作,就直接调整偏移量。

读取文件

书接上文,回到第一部分的函数表。我们推测从 rogo.gdt 的 0x30 位置开始,就是图像内容了,程序在这里反复进行如下操作:

  1. 读取一个字节
  2. 根据读取内容确定读取子过程
  3. 执行读取子过程
  4. 回到第一步

子过程一共有 19 个,分布在 0~255 的区间内,很多,但是基本上无外乎直接填色,从其他位置复制,跳行,等等,我想大部分读者可能对此兴趣不大,我就不展开了。其中有一个特殊过程 0xff,这个过程什么也不做直接返回,换句话说,当步骤 1 读取到 0xff 时,循环终止。

但是如果我们观察一下日志,就会发现,其实文件内容只读了一点点:

1427:000038FB  lodsb                                                   AC                    EAX:000022B2 EBX:00000DC6 ECX:00000000 EDX:0000434E ESI:00000034 EDI:000000B4 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:000038FB  lodsb                                                   AC                    EAX:00002200 EBX:00003B34 ECX:00000000 EDX:0000434E ESI:00000036 EDI:00005064 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:0 IF:1 TF:0 VM:0 FLG:00007202 CR0:00000010
1427:000038FB  lodsb                                                   AC                    EAX:00002200 EBX:00003B34 ECX:00000000 EDX:0000434E ESI:00000038 EDI:00005D34 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:1 IF:1 TF:0 VM:0 FLG:00007206 CR0:00000010
1427:000038FB  lodsb                                                   AC                    EAX:000022F8 EBX:00003B0C ECX:00000000 EDX:0000434E ESI:00000041 EDI:00005FB4 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:1 PF:1 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:000038FB  lodsb                                                   AC                    EAX:0000222C EBX:00003B77 ECX:00000000 EDX:0000434E ESI:00000044 EDI:00006144 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:0 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:000038FB  lodsb                                                   AC                    EAX:0000220B EBX:00003B18 ECX:00000000 EDX:0000434E ESI:00000051 EDI:000064B4 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:1 PF:0 IF:1 TF:0 VM:0 FLG:00007216 CR0:00000010
1427:000038FB  lodsb                                                   AC                    EAX:00002200 EBX:00003B34 ECX:00000000 EDX:0000434E ESI:00000052 EDI:00006874 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:0 IF:1 TF:0 VM:0 FLG:00007202 CR0:00000010
1427:000038FC  mov  bl,al                                              8A D8                 EAX:000022FF EBX:00003B34 ECX:00000000 EDX:0000434E ESI:00000053 EDI:00006874 EBP:00000F78 ESP:00000F6A DS:421A ES:22B2 FS:0000 GS:0000 SS:218C CF:0 ZF:0 SF:0 OF:0 AF:0 PF:0 IF:1 TF:0 VM:0 FLG:00007202 CR0:00000010

第七次就终止了循环,显然还有我们忽略的东西,我们看下函数表这部分的外层:

void __cdecl16near FUN_1427_38af(void)
{
  undefined2 uVar1;
  uVar1 = (undefined2)_DAT_1000_00cc;
  FUN_1427_38f4();
  _DAT_1000_00cc = CONCAT22(_DAT_1000_00ce,uVar1);
  uVar1 = (undefined2)_DAT_1000_00d0;
  FUN_1427_38f4();
  _DAT_1000_00d0 = CONCAT22(_DAT_1000_00d2,uVar1);
  uVar1 = (undefined2)_DAT_1000_00d4;
  FUN_1427_38f4();
  _DAT_1000_00d4 = CONCAT22(_DAT_1000_00d6,uVar1);
  uVar1 = (undefined2)_DAT_1000_00d8;
  FUN_1427_38f4();
  _DAT_1000_00d8 = CONCAT22(_DAT_1000_00da,uVar1);
  return;
}

前面我们还原的函数表部分是 FUN_1427_38f4,上层调用这个函数调了四次,不用仔细分析想必很多人也猜出来了,FUN_1427_38f4 每次只读取了一个平面,我们的读取过程还需要调整。四个平面读取完,我们看看每个平面的大小:

plane end reached, buffer: 0, index: 26560,
plane end reached, buffer: 1, index: 26560,
plane end reached, buffer: 2, index: 26560,
plane end reached, buffer: 3, index: 26560,

如果宽度是 640,那么对于每个平面就是 80 个字节一行,这样的话图像大小就是 640x332,332 对应的 16 进制是 0x014c,这就对应到了文件头的 0x0c 位置:

这样看来,我们应该可以画一画了,什么,还不知道调色板,熟悉我的系列的读者都知道,调色板只是装饰,随意指定一下就好:

好像画了什么,但好像又什么都没画,不过篇幅已经不短了,我们先到这里吧

总结

  1. 我切换回了 ghidra,因为在 ghidra 中可以解决偏移量和日志不同的问题
  2. 程序通过一个函数表读取平面内容,表中有 19 种函数

接下来我们要解决的问题,是搞清楚为什么我们画出来了上面这个东西。