IGS Arcade 逆向系列(四)- ASIC27协议和TSGROM文件静态分析

Monday, September 8, 2025

嵌入式架构分析

IGS的反盗版技术上不难,但是非常诡异,可能是代码写的太烂了。

IGS E2000 本质上,是 PC + 游戏基板 的组合(研华设计)。既要考虑到 Anti-Copy (反盗版),又需要考虑到软件工程上的复用。ASIC相当于一个完全黑盒的计算模块,将游戏关键逻辑放在里面,既能提升性能,也可以防止破解。

diagram

主程序流程分析

游戏主程序会开辟一段 0x200034的栈空间,其中缓冲区占0x200000,而且这个栈开辟出来后不会恢复,这会导致IDA Pro无法反编译,不知道是不是故意的。

main

首先需要patch这个缓冲区大小,减小函数的栈帧,然后将函数 undefine,最后重新识别,就可以成功反编译。

edit_func

  1. Kernel mount_root 时,以及游戏程序启动时 会校验 BIOS 版本信息,开发者却说是获取CRC结果,结果我愣是找遍了所有CRC的位置,也没找到任何反盗版有关的CRC计算逻辑;
  2. BIOS 信息校验失败就校验PCI的驱动信息,如果失败,好像也没有任何操作,但其他地方也插入了许多一样的校验桩,校验失败就会阻止运行;
  3. 系统初始化:屏幕、音频、图形、文字、语言、ASIC、Timer、PLXPCI、游戏、音乐、控制器、摄像机、台账、控制、投币、混合器等等;
  4. 刷新4次ASIC,why???;
  5. 加载基础 action 文件(TSGROM格式),每次加载都刷新一次ASIC;
  6. 游戏版本校验,显示第一屏,加载字体,加载声音;
  7. 加载读卡器;
  8. 游戏Loop,4种状态(Game, Test, Setting, Demo),可通过ASIC控制

main_loop

游戏使用了SDL 1.2.7开发,SDL(Simple DirectMedia Layer)是一个跨平台的多媒体开发库,主要用于提供对音频、输入设备(键盘/鼠标/游戏手柄)和图形硬件的底层访问‌。但是这个性能比较低,只适合2D游戏。Percussion Master 2008 是2D游戏。Speed Driver 2 是 3D游戏,两者的差别可能很大。

开发者在游戏主程序每一处与 ASIC 交互的位置,都插了代码桩,暂时把他称作 RealTimeEvent,应该是统一事件处理程序,每次逻辑的变化、动画的变化,都需要刷新事件。用来实现各种复杂的控制功能,也附带了一些反盗版功能。不得不吐槽,这个代码质量真糟糕,每次都要做大量的计算,性能很差,和用纯 js + html 开发单页面应用一样。

状态检查桩的逻辑

  1. 更新时钟
  2. 计时器检查
  3. Action处理
  4. 音乐处理
  5. 音频处理
  6. 按键状态以及控制输入
  7. 台账
  8. 游戏币处理
  9. PLX PCI 状态处理
  10. SDL 事件处理
  11. 绘制动态五边形动画
  12. PCI 控制写入
  13. ASIC 27 命令写入
  14. PCI 数据读取
  15. 图形刷新

区域初始化

percussion master 2008 支持7个地区,3种语言;简体中文、繁体中文、英文。

location_table

ROIO BIOS 信息校验

内核会运行一个驱动 /dev/roio, 游戏程序通过此驱动对比内置的版本信息表,实现校验功能。内核本身和游戏程序均内置了表格,原理应该是开发者通过某些工具解析了BIOS信息,然后将物理偏移硬编码到程序和内核里面。

bios_table

内核和主程序的 BIOS 信息表结构有些区别,Kernel的4字节对齐,但使用原理都是一样的。

Game BIOS table

struct bios_item {
    unsigned int index;  // index
    unsigned char table_cmp_max_count;
    unsigned int value_addr; // base addr 0xC0000000
    unsigned char char_cmp_max_count;
    unsigned int name_addr;
}

Kernel BIOS table

struct bios_item {
    unsigned int index;  // index
    unsigned int table_cmp_max_count;
    unsigned int value_addr; // base addr 0xC0000000
    unsigned int char_cmp_max_count;
    unsigned int name_addr;
}

对比的逻辑也很简单,

第一步通过遍历程序内置的 BIOS Table,获取版本字符串地址、目标字符串物理地址、遍历轮数等。 第二步,通过 IOCTL Call /dev/roio, 对比System ROM 区域指定偏移和程序内置的对应字符串,只要有一个字符相等就通过,太蠢了。

该内核只允许运行在4种主板,但是游戏允许运行在更多的设备,因此需要校验主程序、内核、主板是否匹配。这是反盗版的机制,直接patch掉就行了。

经过统计,地址和版本字符串如下,所有地址都是0x0f0000起始。

# Kernel + Game
0x0F086E   i852-W83627HF
0x0FEC7C   i852-W83627HF
0x0FEC8A   6A69YILTC-00
0x0FECDE   Ph6A69YILT

# Game
0x0FE0C1   L4S5MG3
0x0FEC84   6A6IXE19C-00
0x0FECDF   I6A6IXE19
0x0FE0C1   L4S5MG/651+
0x0F006D   nVidia-nForce
0x0FECDE   Ph6A61BPA9
0x0FEC8A   6A61B_00C-00
0x0FECDE   Ph6A61B_00

接下来分析ROIO驱动,大部分代码都插了 Anti-Copy 暗桩,使用XOR,对性能影响最小,可以防止游戏A程序放到游戏B的系统运行。

  • 输入参数 mask 0x1FB8408E
  • 返回值 mask 0xC2E83AB8

ioctl_0xfc

ROIO 的 Magic Number 有三种:

  • 0xfc 获取目标地址的32位数值,小端
  • 0xfd 获取目标地址的32位数值,大端
  • 0xfe 获取目标地址的8位数值

最后再 xor 0xC2E83AB8

roio_ioctl_dispatcher

这里的data作为偏移,基址是0xc0000000,然后加上BIOS信息的值,是因为x86打开了paging,因此CPU访问内存需要走虚拟地址。Linux i386 对于虚拟地址偏移的设定如下

#define __PAGE_OFFSET (0xC0000000)
#define __pa(x)			((unsigned long) (x) - PAGE_OFFSET)
#define __va(x)			((void *)((unsigned long) (x) + PAGE_OFFSET))

通过IOMEM的map,也可以看到BIOS信息的地址位于 System ROM

# cat /proc/iomem
00000000-0009fbff : System RAM
0009fc00-0009ffff : reserved
000a0000-000bffff : Video RAM area
000c0000-000c7fff : Video ROM
000f0000-000fffff : System ROM
00100000-1feeffff : System RAM
  00100000-0050aab5 : Kernel code
  0050aab6-006f8f27 : Kernel data
1fef0000-1fefffff : reserved
1ff00000-1ff003ff : Intel Corp. 82801DB Ultra ATA Storage Controller
d0000000-dfffffff : PCI Bus #01
  d0000000-dfffffff : PCI device 10de:0221 (nVidia Corporation)
e0000000-e7ffffff : Intel Corp. 82852/855GM Host Bridge
e8000000-eaffffff : PCI Bus #01
  e8000000-e8ffffff : PCI device 10de:0221 (nVidia Corporation)
    e8000000-e8ffffff : nvidia
  e9000000-e9ffffff : PCI device 10de:0221 (nVidia Corporation)
eb000000-eb01ffff : PLX Technology, Inc. PCI <-> IOBus Bridge Hot Swap
eb020000-eb02007f : PLX Technology, Inc. PCI <-> IOBus Bridge Hot Swap
eb021000-eb0213ff : PLX Technology, Inc. PCI <-> IOBus Bridge Hot Swap
eb022000-eb022fff : Intel Corp. 82801BD PRO/100 VE (CNR) Ethernet Controller
  eb022000-eb022fff : e100
eb100000-eb1003ff : Intel Corp. 82801DB USB2
  eb100000-eb1003ff : ehci_hcd
eb101000-eb1011ff : Intel Corp. 82801DB AC'97 Audio Controller
  eb101000-eb1011ff : Intel 82801DB-ICH4
eb102000-eb1020ff : Intel Corp. 82801DB AC'97 Audio Controller
  eb102000-eb1020ff : Intel 82801DB-ICH4
fec00000-ffffffff : reserved

BIOS芯片封装是PLCC 32,使用RT809H成功dump。

PLCC32_BIOS

使用系统启动后,BIOS ROM的一些数据被解析到了内存里,不是1:1 copy,偏移地址是 0xF0000,。

bios_version

PCCard 随机数校验

我实在不理解这个代码的目的是什么,驱动代码里有SPY的关键词,可能是反嗅探用的暗桩?在启动程序、初始化游戏和print日志会触发,如果上面的BIOS Check 失败了,也会触发此校验。通过ioctl来请求/dev/pccard0,获取结果或者不获取结果。

pccard_random_value_check1

request 0 列表,用于对比结果。列表有4个成员,对应相关偏移。从四个里随机选一个,并带上随机数,随机值区间 [17, 768],本地计算后,发到驱动执行一下,然后发回来,实际上PCI没有真正参与。

0x64 基址:0xC8000000 设置 SPY_FLAG spy_fixec_func
0x6e 基址:0xD0000000 设置 SPY_FLAG spy_quit_func
0x96 基址:0xA8000000 设置 SPY_FLAG
0xa0 基址:0xB0000000 设置 SPY_FLAG

request 1 列表,长度17

0xfe,0xc8,0xfd,0xa0,0x96,0x6e,0x64,0xdd,0xde,0xdf,0xe0,0xe1,0xe2,0xe3,0xe4,0xe5,0xe6

看[1,255]的值是否能命中列表的值,尝试5-30次,如果命中,尝试次数-1,然后再来一次。如果没命中,就通过ioctl来请求随机值对应的偏移(参数[17,768]),幻数就是命中的那个值。我感觉可能是用来初始化驱动的,实在想不到其他作用了。为什么写的那么复杂?

pccard_random_value_check

在游戏主程序的某处,发现了残留的代码,异或的内容是 0xD4AA268A,在 percussion master 2008 未发现触发逻辑,应该是另一个游戏的暗桩。可以更确定,这个功能就是为了反盗版的。(虽然设计的很烂)

pccard_random_value_check2

ASIC 27 协议

A27 初始化

游戏主程序与I/O板的通信,经过 PLX PCI 9030 芯片,以共享内存的方式,进行数据交换。

游戏启动后,ASIC 27 初始化之前,会先加载PCI 9030驱动,并开辟一段缓冲区,专门用来存放 ASIC Buffer,里面有各种数据状态。开发者称作 CommandPort。

pci9030_init

接下来初始化 ASIC 27,首先更新Checksum,将位于buffer的按键灵敏度、按键输入、灯光状态、system mode、buffersize累加得到数值checksum,然后放在buffer的两个位置。后续的每次ASIC 27 请求,都会重新计算 checksum。

首先写入0x2024字节到ASIC 27,cmd: 0xfe,也就是直接拷贝缓冲区数据到到共享内存。 然后ASIC处理后,刷新的共享内存,并且会将sm从0x1c改为其他值,代表处理结束。

ASIC会将游戏的配置信息,同步OS用于更新游戏配置,将会更新以下目录

./pm2_data/storename.dat
./pm2_data/soundset.bin
./pm2_data/gameset.bin

将sm设为0,同时再更新一次 checksum,发送到A27

System Mode

经过分析,以下模式

  • 0x0: 默认模式
  • 0x1: ASIC测试数据读取
  • 0x2: 按键测试
  • 0x3: 蜂鸣器测试
  • 0x4: 灯光板测试
  • 0x5: 投币测试
  • 0x6: Trackball 测试
  • 0x7: SelMode,IGS Logo
  • 0x8: Teammark
  • 0xc: Coin Page
  • 0xf: option
  • 0x14: Photo
  • 0x10: Song Play
  • 0x1a: CCD
  • 0x1d: 调整音量

A27 System Mode Write 状态机

发送数据到ASIC之前的预处理,当 sm 为以下值,无处理逻辑,返回1

0x0,0x2,0x3,0x6,0x9,0xa,0xb,0x11,0x12,0x15,0x16,0x17,0x18,0x19,0x1b,0x1c,0x1e
  • 0x1: 测试数据写入
  • 0x4: 灯光测试
  • 0x5: 投币测试
  • 0x7: SelMode
  • 0x8: Teammark
  • 0xc: 代码已删除
  • 0xe: 代码已删除
  • 0xf: 代码已删除
  • 0x10: Song
  • 0x13: 代码已删除
  • 0x14: 代码已删除
  • 0x1a: 摄像机测试
  • 0x1d: 调整音量

其他值则触发 Assert

A27 System Mode Analysis 状态机

ASIC 返回的数据,由游戏主程序处理,当 sm 为以下值,无处理逻辑,返回1

0x0,0x6,0x9,0xa,0xa,0xb,0x11,0x12,0x15,0x16,0x17,0x18,0x19,0x1b,0x1c,0x1d,0x1e

System Mode 对应的处理

  • 0x1: ASIC测试数据读取
  • 0x2: 进入按键测试
  • 0x3: 进入蜂鸣器测试
  • 0x4: 进入灯光板测试
  • 0x5: 投币测试
  • 0x7: 加载 IGS LOGO
  • 0x8: 加载 Teammark 数据
  • 0xC: 代码已删除
  • 0xE: 代码已删除
  • 0xF: 代码已删除
  • 0x10: Song
  • 0x13: 代码已删除
  • 0x14: 代码已删除
  • 0x1a: CCD 信息

其他值则触发 Assert

按键状态机

                press
        ┌──────────────────────┐
        │                      │
   ┌────▼─────┐  release    ┌──┴─────┐
   │   Idle   │────────────►│Released│
   │   (0)    │             │   (3)  │
   └────▲─────┘             └──▲─────┘
        │                      │
        │ press                │ release
        │                      │
   ┌────┴─────┐ long press  ┌──┴─────┐
   │ Pressed  │────────────►│Holding │
   │   (1)    │             │  (2)   │ keep holding, counter++
   └──────────┘             └────────┘

Buffer 结构体分析

Buffer 的最大长度是 8192

Buffer 响应的 Header 格式如下

struct g_rBufferRead {
  int    _dwBufferSize; // 数据大小
  int    system_mode;   // 系统模式
  char   coin_inserted; // 投币了
  char   a27_error_flag;
  short  error_number;
  int    key_io_list[6];
  int8   key_channels;
  char   pc0;
  char   pc1;
  int16  area_code;
  int16  padding_1;
  char   in_rom_version_name[8];
  char   ext_rom_version_name[8];
  int16  inet_password_data;
  int16  a27_has_message; // 决定 a27_message 是否携带数据
  char   is_light_io_reset;
  char   pci_card_version;
  char   bCheckSum1;
  char   bCheckSum2;
  char   a27_message[40]; 
  char   asic27_buffer[unknown];
}

Buffer 请求的 Header 格式如下

struct g_rBufferWrite {
  int    _dwBufferSize; // 数据大小
  int    system_mode;   // 系统模式
  int    key_input;
  int16  trackball_data[4];
  char   bCheckSum1;
  char   bCheckSum2;
  char   lightdisable;
  char   key_sensitivity;
  int    lightstate;
  int    lightpattern;
  char   data[unknown];
}

A27 Response Checksum

ASIC 27 响应也会携带 Checksum,游戏主程序会验证。计算方式为以下数据的累加

a27_has_message + inet_password_data + rd_is_light_io_reset + error_number + asic27_error + coin_inserted + system_mode[0] + buffer_size

Buffer 混淆分析

Percussion Master 2008对比旧版本,增加了简易混淆,目的是反盗版,避免拷贝ROM直接运行。当从缓冲区拷贝到 ASIC 27 buffer时,数据就会被混淆处理。

混淆的前提条件是 System Mode 符合下列值才会触发,刚好这些 mode 的数据都没有被Write状态机预处理。

  • 0x7: SelMode,IGS Logo
  • 0x8: Teammark
  • 0xc: Coin Page
  • 0xd:
  • 0xe:
  • 0xf: Option
  • 0x13:
  • 0x14: Photo
  • 0x15:

混淆阶段,程序把 asic27_buffer 的数据复制到 dest。用 dest 作为源,分块处理,每块大小是 0x500 = 1280 字节。取数据块,用块头 4 字节 + mask_table 计算扰动值,根据扰动值对块数据做循环重排,最终写回缓冲区。

v3 = mask_table[v1[0]];
v3 ^= mask_table[v1[1]];
v3 ^= mask_table[v1[2]];
v3 ^= mask_table[v1[3]];

用 v3 算一个偏移量,如果剩余数据不足 0x500,则 v3 % (剩余长度-4) + 4;否则固定 % 0x4FC + 4,保证偏移范围在 [4, 0x4FF]。

先把 [v3, end] 拷贝到目标,再把 [4, v3) 拷贝过去,最终得到一个“旋转过”的块,前 4 字节(header)本身不按顺序复制,而是被跳过+重新拼接。

让 AI 写了一个 python 的代码实现。

import random

mask_table = [0x00, 0x00, 0x00, 0x00, 0x39, 0x4E, 0xC1, 0xE6, 0x02, 0x19, 0xB1, 0xB9, 0x63, 0xCB, 0xC7, 0x9E, 0xE4, 0xCD, 0x76, 0xE7, 0x23, 0x8D, 0xB3, 0x6B, 0x3F, 0xDA, 0x89, 0xF5, 0x4D, 0xCB, 0x56, 0xB5, 0xD3, 0xA9, 0xBC, 0x2E, 0xA0, 0xE0, 0x80, 0xD6, 0x92, 0x62, 0xDE, 0xC9, 0xFD, 0x24, 0x04, 0x06, 0x4B, 0x70, 0xB2, 0x21, 0x26, 0xD1, 0xB1, 0xAF, 0xA0, 0x29, 0x29, 0x9D, 0x0C, 0x5E, 0x59, 0x09, 0xA2, 0xC9, 0xF3, 0x67, 0x4F, 0xE6, 0xCD, 0x6E, 0xF3, 0x97, 0xF1, 0xF9, 0xD1, 0xE1, 0xCD, 0x26, 0x62, 0x0D, 0xF4, 0x7A, 0x72, 0x98, 0x3C, 0x9B, 0xE2, 0x43, 0xCE, 0x54, 0xF4, 0x44, 0xE9, 0xF5, 0x22, 0xC4, 0x3F, 0xD0, 0x38, 0x5F, 0x96, 0xAD, 0x05, 0xB7, 0x18, 0x47, 0xFE, 0x00, 0x14, 0xED, 0x5B, 0x75, 0x3B, 0xF2, 0x08, 0xA2, 0x44, 0x1E, 0xE5, 0x59, 0x68, 0x4A, 0x36, 0x9E, 0xF6, 0x87, 0x74, 0xAA, 0x70, 0x68, 0x6A, 0x1B, 0xED, 0x84, 0xE9, 0xB2, 0x35, 0xC5, 0x54, 0x83, 0xE8, 0x5B, 0x05, 0xD9, 0x77, 0x9A, 0xD6, 0x20, 0xD9, 0x48, 0xA9, 0x59, 0x18, 0x40, 0xB1, 0x5A, 0x81, 0xC1, 0x96, 0x7B, 0xC7, 0x1F, 0xD5, 0x5A, 0xB1, 0x01, 0x9E, 0xA8, 0x67, 0x52, 0xF4, 0x7A, 0x39, 0x51, 0x80, 0x18, 0xC9, 0x61, 0xEE, 0x01, 0xEC, 0x19, 0x2F, 0x25, 0xBC, 0x74, 0x85, 0x6A, 0x99, 0x92, 0x6A, 0x28, 0x13, 0xF6, 0x9A, 0xED, 0x02, 0x26, 0xF4, 0x69, 0x9F, 0x1E, 0xED, 0xC3, 0x18, 0x0E, 0xBD, 0x32, 0x1F, 0x47, 0x4F, 0x55, 0x8B, 0x91, 0x75, 0xEC, 0x66, 0xC8, 0x83, 0xED, 0x2E, 0x1B, 0x0F, 0xB0, 0x65, 0xEC, 0x87, 0xD3, 0xE0, 0xE2, 0x2B, 0x16, 0xCB, 0x0A, 0x0F, 0x70, 0x64, 0x52, 0xBA, 0x38, 0x6B, 0x5C, 0xEA, 0xFD, 0xA9, 0xB1, 0x8D, 0x8F, 0x26, 0x4B, 0xD9, 0xD3, 0x40, 0x4A, 0x66, 0x33, 0xBB, 0x01, 0xCE, 0x3C, 0x3C, 0x56, 0x14, 0xAE, 0xFD, 0x05, 0x7A, 0x8F, 0x4D, 0x4D, 0x79, 0x29, 0xCC, 0x81, 0xCD, 0x07, 0x43, 0x68, 0x57, 0x0C, 0xDA, 0xDE, 0x79, 0x1D, 0xE0, 0x01, 0x8D, 0x91, 0x17, 0x55, 0x4F, 0xF8, 0x25, 0x60, 0xCE, 0x11, 0x34, 0x3F, 0x3F, 0x03, 0xA3, 0xEF, 0xFA, 0xF5, 0x13, 0xE5, 0xEA, 0x75, 0x6A, 0xD7, 0xE1, 0x65, 0x94, 0x90, 0x42, 0xC9, 0x1D, 0x7F, 0x66, 0xDB, 0x68, 0xB8, 0x18, 0x18, 0x8B, 0x22, 0x49, 0x70, 0x71, 0x88, 0x2D, 0xD9, 0x96, 0x29, 0x4B, 0xAC, 0x7F, 0x58, 0x50, 0x57, 0x0F, 0xDC, 0x4D, 0xB9, 0x53, 0x81, 0x65, 0xD9, 0xB7, 0x85, 0x10, 0xF0, 0xCE, 0x4B, 0x2B, 0xAA, 0x7F, 0x7C, 0x75, 0xBA, 0xB2, 0x01, 0x64, 0x13, 0x07, 0x0A, 0x5E, 0x3F, 0xEF, 0xFA, 0x00, 0x8B, 0x31, 0x89, 0x6A, 0xE9, 0x17, 0x81, 0xC1, 0x4D, 0xEE, 0x31, 0x8C, 0xF0, 0x3A, 0xFD, 0x77, 0x90, 0xDF, 0x7C, 0x83, 0xDF, 0xF9, 0x99, 0xE4, 0xC0, 0xE5, 0x82, 0x22, 0xBD, 0x46, 0xBC, 0xF8, 0x23, 0xE1, 0xDD, 0x48, 0xF3, 0xE1, 0xB0, 0x66, 0x13, 0x93, 0x85, 0xB8, 0xEC, 0x9B, 0xCE, 0x0C, 0xEA, 0xDD, 0x14, 0x42, 0xDF, 0x45, 0x50, 0xAE, 0xC0, 0x60, 0xB2, 0xB7, 0x16, 0xB1, 0xAD, 0x2A, 0x2E, 0x1D, 0xC8, 0xE8, 0xE9, 0xAF, 0x0F, 0x44, 0x5D, 0xC5, 0x80, 0xA6, 0xB2, 0x01, 0xCF, 0xDB, 0x96, 0x49, 0x52, 0xC2, 0xBA, 0x97, 0x36, 0xB0, 0x33, 0x59, 0x88, 0x1D, 0x5A, 0x22, 0xAD, 0xA5, 0x9C, 0xD7, 0x5B, 0x59, 0xCA, 0x83, 0x7D, 0x7B, 0xFA, 0x84, 0x22, 0x65, 0x64, 0x7C, 0xDF, 0xF3, 0xA6, 0x41, 0x49, 0x14, 0x81, 0xED, 0x3B, 0x0C, 0x0A, 0xDF, 0xF6, 0x35, 0x79, 0x98, 0xDC, 0x6A, 0x5D, 0x0E, 0x94, 0x8B, 0x87, 0x5D, 0x0A, 0xEC, 0xFA, 0xC1, 0x6C, 0xE5, 0x01, 0xFD, 0x1E, 0x54, 0x29, 0xB7, 0xC6, 0x26, 0x33, 0x49, 0x60, 0x92, 0x44, 0xD2, 0x0C, 0x1E, 0x84, 0x03, 0x2B, 0x67, 0x82, 0xC3, 0x75, 0x7E, 0x2E, 0x2B, 0xC6, 0x96, 0x6E, 0x8A, 0x5D, 0x27, 0x7A, 0x62, 0x8C, 0xFE, 0x00, 0xCA, 0xFB, 0xFA, 0xD0, 0x9A, 0xB4, 0x60, 0xD1, 0x52, 0xC8, 0xB8, 0x7A, 0x83, 0xA9, 0xAE, 0x2A, 0x14, 0xFE, 0x33, 0xB1, 0x0F, 0xA2, 0x89, 0x25, 0xC1, 0xD5, 0x3A, 0xDE, 0xED, 0x09, 0xE1, 0x49, 0x4A, 0xD7, 0x9F, 0x49, 0xF1, 0x28, 0x88, 0xD1, 0x50, 0x2C, 0x24, 0x4C, 0x09, 0x36, 0x3F, 0x15, 0xD3, 0x1D, 0xA8, 0x1F, 0xE8, 0xAD, 0xC5, 0x5F, 0x95, 0x04, 0xFE, 0x2C, 0x6E, 0xB6, 0x0E, 0xF6, 0x47, 0x4A, 0xF6, 0xAC, 0x5C, 0xBA, 0xD9, 0x35, 0xEA, 0x27, 0x41, 0xF8, 0x84, 0xF2, 0xF8, 0x74, 0x2F, 0xE4, 0xEF, 0x69, 0xC6, 0xC7, 0x4B, 0xEC, 0xD7, 0xEB, 0x83, 0x47, 0xE3, 0x82, 0x74, 0x06, 0xD2, 0x64, 0x1D, 0xEB, 0xCD, 0x7C, 0x74, 0xFC, 0xF2, 0xC9, 0x3F, 0x90, 0x14, 0xDE, 0x1B, 0x25, 0xF8, 0x52, 0xE8, 0x9D, 0xB9, 0x11, 0x0A, 0xEC, 0xA5, 0x59, 0xEA, 0x5C, 0x7E, 0x7D, 0x33, 0x79, 0xEA, 0x26, 0xF6, 0x06, 0x23, 0x4D, 0x67, 0x26, 0x88, 0x12, 0xFE, 0x13, 0x9A, 0xE9, 0x66, 0x5A, 0x4F, 0x67, 0xB1, 0xBD, 0xA2, 0x89, 0x02, 0x40, 0x01, 0x7E, 0xF2, 0x4D, 0x0E, 0x98, 0x2C, 0x40, 0x8F, 0x8F, 0x90, 0x1B, 0x9F, 0x4D, 0x84, 0xB3, 0x9A, 0x03, 0x6E, 0x71, 0x24, 0x03, 0xFC, 0xD3, 0x23, 0x14, 0x3C, 0xA8, 0x90, 0x11, 0x54, 0x07, 0xDA, 0x3A, 0xDB, 0x19, 0x94, 0xC2, 0x6E, 0x7A, 0x92, 0x9F, 0x0C, 0x0C, 0x0F, 0x7D, 0xFA, 0xA4, 0x3A, 0x9B, 0xA0, 0xBB, 0xC4, 0x5C, 0xDA, 0xCE, 0x74, 0x78, 0x88, 0x8E, 0x83, 0xD8, 0xEE, 0x21, 0x31, 0x9E, 0x75, 0xC0, 0x2E, 0x2B, 0xE9, 0x17, 0x31, 0x46, 0x39, 0xD8, 0x85, 0xBC, 0xA9, 0xF8, 0x57, 0xCA, 0xA3, 0xE0, 0x59, 0xC5, 0xF2, 0x0D, 0x52, 0x73, 0x95, 0x40, 0x7C, 0xAF, 0xB2, 0xAF, 0x14, 0x99, 0xD1, 0x62, 0xCE, 0xB3, 0xAD, 0x17, 0x5E, 0x95, 0x26, 0x8F, 0xF0, 0x2A, 0x92, 0xBF, 0xF1, 0xA1, 0x77, 0xE0, 0xF4, 0x6D, 0x62, 0xCF, 0xCE, 0x15, 0x74, 0xFD, 0x7A, 0xA5, 0xD0, 0x90, 0x75, 0x4B, 0xFE, 0xE0, 0x63, 0x5A, 0xBA, 0x8B, 0x09, 0x8B, 0xE6, 0x12, 0x71, 0xB7, 0xD4, 0xD9, 0x29, 0x1E, 0xFD, 0xEB, 0x93, 0x14, 0x0D, 0xD4, 0xA7, 0x5F, 0x04, 0x85, 0x7D, 0xDA, 0x26, 0xE4, 0x63, 0x94, 0xEC, 0x49, 0x0D, 0x21, 0xF1, 0x42, 0x20, 0x18, 0x66, 0x9F, 0xF6, 0x64, 0x5F, 0x57, 0xCE, 0x33, 0x43, 0xB2, 0x38, 0xFA, 0xF0, 0x5C, 0x1D, 0x4F, 0x65, 0xE8, 0x85, 0x1E, 0xC6, 0x9B, 0xDF, 0x85, 0x9B, 0x9D, 0xAD, 0x17, 0x81, 0x7C, 0xD5, 0x5C, 0xA8, 0xF8, 0x81, 0x40, 0x13, 0x38, 0xF0, 0x00, 0x5B, 0x73, 0xD3, 0xF0, 0x2D, 0x38, 0x00, 0xD7, 0x87, 0x47, 0x82, 0x81, 0xAF, 0xA5, 0xC8, 0x2D, 0x0C, 0xCC, 0x52, 0x2C, 0x5A, 0x09, 0x07, 0x38, 0xAB, 0x4D, 0x01, 0x4B, 0x11, 0x8C, 0xAF, 0x63, 0x25, 0x00, 0x82, 0x25, 0xA2, 0x77, 0x71, 0x07, 0x7B, 0x71, 0x95, 0x14, 0xD1, 0x23, 0x3D, 0x6C, 0x4E, 0xD7, 0x0C, 0x61, 0x7D, 0xFA, 0xC6, 0xCB, 0x6F, 0x6C, 0x97, 0x65, 0x57, 0x23, 0xEB, 0x7E, 0xCF, 0x89, 0x37, 0x69, 0x52, 0x19, 0x7F, 0xED, 0x1F, 0x96, 0xAD, 0xC6, 0x3C, 0x04, 0x31, 0x42, 0x31, 0xCD, 0xBB, 0xB5, 0xD9, 0x5D, 0xF2, 0xE5, 0xF4, 0x77, 0x21, 0xAF, 0xE8, 0x3E, 0xA5, 0x20, 0x2B, 0xFC, 0xE1, 0xDC, 0x5A, 0x2F, 0xEA, 0x5B, 0x85, 0x96, 0xBA, 0x97, 0xE1, 0x48, 0xA1, 0xC0]

BLOCK_SIZE = 0x500

def obfuscate_block(block: bytes) -> bytes:
    """混淆单个 0x500 大小的数据块"""
    if len(block) < 4:
        return block

    block_header = block[:4]

    obfs_value = mask_table[block_header[0]]
    for i in range(1, 4):
        obfs_value ^= mask_table[block_header[i]]

    # 计算偏移量(范围 4 ~ 0x4FF)
    obfs_value = obfs_value % 0x4FC + 4

    # 数据重排:
    #   [obfs_value:end] + [4:obfs_value]
    part1 = block[obfs_value:]       # 从 obfs_value 开始到结尾
    part2 = block[4:obfs_value]      # 从 4 到 obfs_value
    new_block = part1 + part2
    return new_block


def deobfuscate_block(block: bytes, header: bytes) -> bytes:
    """反混淆单个 0x500 数据块,需要原始 header"""
    if len(block) < 4:
        return block

    # 重新计算扰动值(必须用原始 header)
    obfs_value = mask_table[header[0]]
    for i in range(1, 4):
        obfs_value ^= mask_table[header[i]]
    obfs_value = obfs_value % 0x4FC + 4

    # block 的排列规则是:
    #   new_block = block[obfs_value:] + block[4:obfs_value]
    # 我们要反过来拼回原始
    part1_len = len(block) - (obfs_value - 4)  # 对应 obfs_value ~ end
    part1 = block[:part1_len]
    part2 = block[part1_len:]

    # 恢复成 [0:4] + [4:obfs_value] + [obfs_value:end]
    original = header + part2 + part1
    return original


def obfuscate(data: bytes) -> bytes:
    out = bytearray()
    for i in range(0, len(data), BLOCK_SIZE):
        block = data[i:i+BLOCK_SIZE]
        out.extend(obfuscate_block(block))
    return bytes(out)


def deobfuscate(data: bytes, headers: list[bytes]) -> bytes:
    out = bytearray()
    for idx, i in enumerate(range(0, len(data), BLOCK_SIZE)):
        block = data[i:i+BLOCK_SIZE]
        header = headers[idx]
        out.extend(deobfuscate_block(block, header))
    return bytes(out)


if __name__ == "__main__":
    data = bytearray()
    headers = []
    for blk in range(3):
        header = bytes([blk, blk+1, blk+2, blk+3])
        headers.append(header)
        body = bytes([blk]* (BLOCK_SIZE - 4))
        data.extend(header + body)

    print("原始数据前 32 字节:", data[:32])

    obfs = obfuscate(data)
    print("混淆后前 32 字节:", obfs[:32])

    deobfs = deobfuscate(obfs, headers)
    print("反混淆前 32 字节:", deobfs[:32])
    print("反混淆是否正确:", deobfs == data)

TSGROM 解析

TSGROM是游戏多媒体资源文件,有脚本、贴图。类似 unity 的 assets。

PM2008 的 TSGROM 版本支持不低于 00.0000.0004,和PM1一致。代码写的简单粗暴,全是while(1)。

tsgrom_parse

也有一些 rom 没有携带版本信息,不知道有什么作用,比如biglogo.rom,有大量LZSS图片数据,但是和代码里的颜色格式对不上,应该是历史遗留。

biglogo

TSGROM 既可以从文件加载、也可以从RAM加载。第一次加载后,就会存到RAM,后续就不用再操作文件了。

PM2008支持 TGA、BPM、PCX的图形文件,TSGROM 格式又臭又长非常无聊,没必要展开分析,我开发了解析TSGROM的脚本 igs-toolkits tsgrom_loader

(base) ➜  tsgrom_loader git:(master) ✗ python ./tsgrom_loader.py -f ./test/resultl.rom -o ./test/resultl --format png
TSGROM Header:
  Header: TSGROM01
  Version: 00.0000.0004
  Length: 0
  Data Zones: 2276
  Data Type Counts:
       SOUND: 1
    ACTBLOCK: 531
    ACTINDEX: 1
    ACT_DATA: 60
    ACT_POOL: 531
    ACT_STEP: 975
    BASEDATA: 1
    BMP_OPSS: 18
    MTV_INAC: 1
    PALETTE1: 1
    TGA_OPSS: 156
Found 174 image data zones

以 IGS Logo 为例,解压后有动画的每一帧的图片

tsgrom_extracted

Action Parser

IGS 的 TSGROM 定义了游戏的各种图形行为,并称之为 action,主程序通过解析 action 来实现功能。如果要在PC运行游戏主程序,游戏的各种事件都和A27协议有关,需要逆向分析对应Action,但是我想尝试最完美的破解方式,dump ASIC ROM 放到模拟器运行,不想分析这种屎山代码。

  • ACT BLOCK 数据块数量
  • ACT INDEX ACT 索引
  • ACT DATA Action 数据
  • ACT POOL Action 数据
  • ACT STEP 动作帧

主程序是符号剥离的,分析起来很费时间,简单记录一下:

在加载 TSGROM 时,程序会加载 act_data 到内存,根据 act_pool 的的数量,将 pool data 也加载到内存; 每个 TSGROM 会被分配独立的 Group ID。Action 系列的函数,都是根据 Group ID 来区分。最多 0x80 个 action group,每个 Group 长度 0x2AA4。每个 Group 还有对应的 index,action index 大小 0xaa9, 列表长度也是 0x80。

在加载 tsgrom 之前,首先创建 action 对象,总共 1024 个 action_data,每个 action_data 长度 0x8D 字节。 接着调用 ActionUse 初始化 act_data,并且分配图形显示资源,用 ActionFace, ActionShow 等配置控制图形显示,最后调用RealTimeEvent统一刷新画面。

actions

TSG ROM 暗桩

IGS 故意损坏了资源文件的某些数据块,需要通过 ASIC 芯片动态修复,这也是 IGS 的反盗版机制,防止破解者修改动画文件实现换皮游戏。

IGS Logo

tsgrom_extracted_broken

Teammark

tsgrom_extracted_broken1

以 IGS Logo 为例,当 buffer 的标识数匹配 1 时,代表数据包类型是资源修复,当遍历到指定的块,此处是7,就将来自 ASIC 27 的 0x400 字节追加到对应的损坏区域,完成资源数据修复。

load_igslogolc_to_memory

写在结尾

IGS将游戏主程序和硬件强关联,如果要将游戏盗版至其他平台,需要付出非常多的时间。

游戏框架、歌曲谱面,逻辑是状态机,比较复杂,不在我的破解目标内。

把逆向工程写到博客,感觉花费了更多的精力,自己逆向只需要记录一些数据,但是形成文章,就要写的让别人看懂。

下一篇主题:IGS Arcade 逆向系列(五)- ASIC27协议Hook和主程序patch工作

纪念血月,赞美女神

逆向工程IGSArcadeCrack鈊象电子E2000

IGS Arcade 逆向系列(三)- Getshell