前言

2010年是街机的黄金时代,随着移动终端和家用游戏机普及,街机行业也逐渐走向没落,尽管国内出台了一些政策鼓励游戏游艺设备行业发展,但此行业在未来一直都不被资本看好。2020 疫情更是给电玩行业带来了很大的打击。

在以前,100元可能只能买50个币,现在100元可以买200个币。

我喜欢赛车游戏,湾岸Midnight、头文字D、Speed Driver是近几年电玩城最火的游戏,因为它有账号功能,具备社交属性。前段时间在破解极速5的玩家APP,才知道IGS (鈊象电子)就是我小时候玩的三国战记和西游的厂商,IGS反破解在整个行业都是顶尖的,竞速游戏的极速系列更是号称无人可破解。

上个月,我家旁边的大玩家电玩城倒闭了,我感觉迟早有一天极速系列游戏会消失,便想挑战破解极速系列游戏。

IGS 极速系列发布顺序如下:

Game Device Model Year
Speed Driver:Evolution Evolution 2004
Speed Driver 2 E2000 2007
极速 3 E3000, E3100 2010
极速 4 E3000, E3100, S3000 2013
极速 5 S3000 2019

我计划从E2000平台开始破解,难度肯定比PGM(PolyGame Master)更低,因为是基于PC开发的,不需要额外模拟声卡和显卡。硬件几乎没有物理保护,CPU指令集是x86,OS是Linux,没有反调试,也没有VMP,这不就是新手村吗。

Speed Driver系列在亚洲影响力媲美湾岸Midnight,头文字D,雷动G(都被破解了)。IGS的反破解是最成功的,在整个产品生命周期内都未被破解。

硬件分析

Arcade-docs 可以查询到每个设备的硬件信息和支持的游戏
凭借我擅长的淘电子垃圾能力,已经搞到一台E2000主机。国外有不少爱好者购买,所以价格涨上去了。

外部接口分析

正面有两个RS-485接口,并且CF卡可以从外部拆卸

背面接口,具有千禧年PC的接口特点

  • 12V DC
  • 2 x DB9 COM
  • 4 x USB 2.0
  • RJ45 LAN
  • 3.5mm Audio Out
  • 25pin + 30pin 的I/O口

主板分析

考虑到这个破解难度不高,其他贴了贴纸遮盖丝印的元件就不分析了,太麻烦

我买的这个主机安装了 Percussion Master 2008 游戏,但极速2也能运行

  • 主板型号:I-JOIN E2000-V256 IH-02 (研华)
  • A - CPU:Celeron M 370 (1.5 GHz)
  • B - 北桥:未知
  • C - GPU:NVIDIA GeForce 6200 (256 MB, GDDR2)
  • D - 2 x DDR 333 256MB
  • E - Chipset: Intel 852GME (ICH4-M)
  • F - PCI9030,是GPIO芯片,可能用于游戏控制器的信号传输。
  • G - I/O 控制器 (大概率有LPC接口)
  • H - 某I/O控制器
  • I - IH-C02 集成控制器
  • J - 4MB的 Uniform Sector Flash,DIP 48-Pin 封装,据说是BIOS,这个芯片居然专门贴了游戏名称的标签,说明BIOS ROM 也和游戏有关。可能会有跟V21芯片相关的内容
  • K - V21芯片,也许是一颗FPGA或者ASIC,用于加密处理控制信号的输入输出
  • L - 64K x 16 HIGH-SPEED CMOS STATIC RAM x3
  • M - CF卡:ADATA 2GB
  • N - IDE硬盘接口

我有很多种办法拿硬件的root shell,但我对游戏加载的过程更感兴趣,因此先做软件逆向分析吧。

I/O板分析

这几块板子又称控制板,应该是用于连接游戏控制器,比如方向盘、刹车油门、投币器之类的。

通过连接主板的25pin + 30pin 的I/O口 进行通信

价格可能比主机本体还贵

应该可以使用逻辑分析仪来捕获这些传感器信号,然后实现控制功能。但据说控制信号由V21芯片(ASIC)处理,可能会加密。我也不打算去仿制一个控制板。这种街机游戏没有地平线5好玩。现在电玩城也便宜,真想玩的话,就直接花钱去电玩城,或者买一套方向盘在家里玩。

文件系统分析

直接dump CF卡,文件大小2GB,file命令的回显如下:

1
DOS/MBR boot sector, LInux i386 boot LOader; partition 1 : ID=0x1, active, start-CHS (0x0,1,1), end-CHS (0x2,63,63), startsector 63, 12033 sectors; partition 2 : ID=0x83, start-CHS (0x3,0,1), end-CHS (0x22,63,63), startsector 12096, 129024 sectors; partition 3 : ID=0x83, start-CHS (0x62,0,1), end-CHS (0x399,63,63), startsector 395136, 3322368 sectors; partition 4 : ID=0x83, start-CHS (0x23,0,1), end-CHS (0x61,63,63), startsector 141120, 254016 sectors

启动过程,LILO作为MBR,直接安装在第一个扇区,用来引导Linux

1
2
3
4
5
6
+----------------------------------------+
| Master Boot Record Operating system |
|----------------------------------------|
| LILO ---------------> Linux |
| ---> other OS |
+----------------------------------------+

通过fdisk查看分区信息。是Linux操作系统,四个分区,并首尾连续,最后一个分区之后的内容,都是0x00,不存在隐藏分区

1
2
3
4
5
6
7
8
9
10
11
12
Disk ./percussion_master_2008.img: 1.77 GiB, 1903878144 bytes, 3718512 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x6e4a3fef

Device Boot Start End Sectors Size Id Type
./percussion_master_2008.img1 * 63 12095 12033 5.9M 1 FAT12
./percussion_master_2008.img2 12096 141119 129024 63M 83 Linux
./percussion_master_2008.img3 395136 3717503 3322368 1.6G 83 Linux
./percussion_master_2008.img4 141120 395135 254016 124M 83 Linux

E2000平台的分区有一个特点,第三分区和第四分区,顺序是反的。

第二分区和第四分区,7z无法完全提取,binwalk能识别出一些压缩区域,因此大概率是经过了魔改的文件系统。

每个分区的用途如下(最后我找到了分区2,4的原始文件系统类型):

1
2
3
4
Partition 1: Ext2, Bootloader, kernel
Partition 2: IGS CRAMFS, RootFS
Partition 3: Ext3, Log Data
Partition 4: IGS SquashFS, Game Data

Kernel 逆向

目前未找到rootfs和游戏程序,他们大概率位于第二分区和第四分区。
我认为逆向kernel可以少走不少弯路。不依赖内核,其实可以直接硬逆出文件系统的格式,但这样很无聊,而且最终写出来的extractor,肯定比不了开源的文件系统解压工具。
首先看第一个分区,有一个 loader 和 kernel。显示了内核版本,但是这里不一定是正确的版本。

1
2
SD2:        LILO (LInux LOder)
SD2-OS-61P: Linux kernel x86 boot executable, bzImage, version 2.4.31-IGS_V0.5a (james@Code_Server.linnet.net.tw) #1 Thu Aug 30 19:06:24 CST 2007, RO-rootFS, root_dev 0X305, Normal VGA, setup size 512*6, syssize 0x3b88, jump 0x230 0xe800040000000000 instruction, protocol 2.3

LILO经过了IGS 定制开发,会显示,IGS Loader v2.0 Boot Menu 2007/04,可判断版本是lilo-22.8

这里没什么好分析的,直接开始提取vmlinux,内核文件是bzImage格式,需要解压

1
7z x ./SD2-OS-61P -okernel.decomped

接下来就是普通的内核逆向,第一步计算基址,基址有很多种确定方法,可以参考我其他文章。

这里多看两眼就猜出来了,0xC0100000

基址配置正确,IDA就可以自动识别出一些交叉引用,剩下的需要手动还原。IDA 9.0修改了一些API,又得重新写一下脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import ida_bytes
import ida_segment
import ida_funcs
import ida_ida

def find_and_make_function(start_ea, end_ea, pattern_str):
image_base = idaapi.get_imagebase()
pattern = ida_bytes.compiled_binpat_vec_t()
err = ida_bytes.parse_binpat_str(pattern, image_base, pattern_str, 16)
if err:
return

current_ea = start_ea
found_count = 0

while current_ea < end_ea:
current_ea, index = ida_bytes.bin_search(
current_ea,
end_ea,
pattern,
ida_bytes.BIN_SEARCH_FORWARD | ida_bytes.BIN_SEARCH_NOSHOW)

if current_ea == ida_idaapi.BADADDR:
break

if current_ea % 4 != 0:
current_ea += 1
continue

if ida_bytes.get_flags(current_ea) & ida_bytes.FF_CODE:
if idc.get_func_flags(current_ea) != -1:
current_ea += 1
continue

seg = ida_segment.getseg(current_ea)
if not seg:
current_ea += 1
continue

if not ida_funcs.add_func(current_ea):
print("Create function failed at 0x{:X}".format(current_ea))
else:
found_count += 1

current_ea += 4

print("Search finished, {} functions created".format(found_count))


find_and_make_function(0xC0100000, 0xC0508000, "55 89 E5")

# 可以根据实际情况自己定义 function entry opcode

当然到这里还没有完全识别出全部交叉引用,在data段,还有一些字符串和offset还原的不是很好,但影响不大,实在是没有思路的话,就把这些内容还原,说不定会有新发现。

接下来就是找分析切入点,实在没啥技术含量,我有很多种方案,比如从文件系统特征入手之类的,但全写出来就像说“茴字有五种写法”一样搞笑。

可以确认内核版本是 2.4.31,并且IGS版本是v1.0,而不是bzImage显示的v0.5。

igs_strings

根据下列字符串,可以确定IGS魔改文件系统对应的原始文件系统版本

  • rofs:cramfs
  • shfs: squashfs 2.2

rofs_strings

Linux 2.4.31 没有squashfs,直接找squashfs的patch,IGS魔改SHFS是基于squashfs2.2。
https://master.dl.sourceforge.net/project/squashfs/OldFiles/squashfs2.2r2.tar.gz

Linux 2.4没有支持kallsysm,因此需要手动还原符号。一些printf,memcpy,str之类的函数可以快速一眼看出来,但是我要分析系统启动过程,所以需要还原一些内核专用的符号。

这种老设备,在新的linux下没有办法交叉编译,我安装了一个CentOS 5.10的32位虚拟机,成功编译,提取出 System.map 和 vmlinux,这样就可以方便对比IGS魔改的部分。

symbols

对于这种找不到ramdisk或者rootfs的固件,第一步肯定是找到kernel里的sys_mount和boot parameters

1
2
3
root=/dev/hdc2 ro console=ttyS1,115200
sys_mount("/dev/hdc3", "/PM2008v2", "shfs", flag, data)
sys_mount("/dev/hdc4", "/PM2008v2/pm2_data", "ext3", flag, data)

这里可以确定,rootfs应该是第二分区,游戏数据在第三分区。

挂在ramdisk的逻辑,位于此处

1
2
3
4
prepare_namespace
->rd_load_disk
->rd_load_image
->identify_ramdisk_image

在此次可以确定IGS 支持 squashfs 和 igs rofs 格式的ramdisk。并且可以确定它们的magic。

identify_ramdisk_image

分析过程就不记录了,他们把 FS 的header魔改了,分析起来有一些恶心🤢。

提取文件系统

我分析了 E2000 平台的三种自定义文件系统 header,内容如下。

IGS SHFS V1 Header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct squashfs_super_block_22_v1 {
unsigned int s_magic; // 0xD4AA2682
unsigned int block_size_1:16;
unsigned int block_log:16;
unsigned int major_number;
unsigned int minor_number;
unsigned int inodes;
unsigned int bytes_used;
unsigned int uid_start;
unsigned int guid_start;
unsigned int inode_table_start;
unsigned int directory_table_start;
unsigned int flags:8;
unsigned int no_uids:8;
unsigned int no_guids:8;
int mkfs_time /* time of filesystem creation */;
squashfs_inode root_inode;
unsigned int block_size;
unsigned int fragments;
unsigned int fragment_table_start;
} __attribute__ ((packed));

IGS SHFS V2 Header

以 Percussion Master 2008 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct squashfs_super_block_22_v2 {
unsigned int s_magic; // 0xD4AA2682
char igs_info[64]; // banner
unsigned int block_size_1:16;
unsigned int block_log:16;
unsigned int igs_fs_version_major; // 59 17 23 96
unsigned int igs_fs_version_minor; // E1 5D 70 00
unsigned int major_number; // 31 64 52 E5
unsigned int minor_number; // 92 2C 03 68
unsigned int inodes;
unsigned int bytes_used;
unsigned int uid_start;
unsigned int guid_start;
unsigned int inode_table_start;
unsigned int directory_table_start;
unsigned int flags:8;
unsigned int no_uids:8;
unsigned int no_guids:8;
int mkfs_time /* time of filesystem creation */;
squashfs_inode root_inode;
unsigned int block_size;
unsigned int fragments;
unsigned int fragment_table_start;
} __attribute__ ((packed));

IGS ROFS V1 Header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct cramfs_inode {
unsigned int inode_magic;
unsigned int namelen:6, offset:26;
unsigned int size:24, gid:8;
unsigned int mode:16, uid:16;
};

struct cramfs_info {
unsigned int crc;
unsigned int edition;
unsigned int blocks;
unsigned int files;
};

struct igs_rofs_super_block {
unsigned int magic1; // 0x81006e6a
unsigned int future;
char igs_info[64];
unsigned int size;
unsigned int magic2; // 0xD4AA2682
unsigned int flags;
unsigned int padding;
cramfs_info fsid;
char name[64];
cramfs_inode root[2];
};

igs_rofs_super_block File;

将上述文件header,移植到cramfs-tools和squashfs-tools,就可以完美提取文件系统,并且还可以发现文件是否损坏。我已经将代码上传至github。https://github.com/gorgiaxx/igs-toolkits

这个版本提取难度不高,但下个版本E3000就用上加密了。

extracted_files

玩家 @novacosmic00 之前请我帮忙提取 GoGoBall 的文件,但他说他已经解密了其他游戏的文件。唯独 GoGoBall 解不了。用我修改的工具尝试提取,发现文件CRC不对,但还是能提取出大部分。

在我提取完固件之后,和玩家 @novacosmic00 沟通,才知道两年前有人研究过这个,并写了提取脚本。

https://github.com/batteryshark/igstools/tree/main/scripts

作者可能是根据文件系统内容,找出地址规律反推结构体的,这也是一种办法,但缺点是提取出来的文件可能比较混乱,而且无法检测CRC错误。

shadow-

shadow文件,默认都清空了root密码

1
2
root:x:13130:0:99999:7:::   
test::13074:0:99999:7:::

这是原始的文件

1
2
root:$1$qAw/5vcb$x9rPCAwLMdRBQXwlq1zG70:13130:0:99999:7:::
test:$1$JjP1oLAJ$xilZIedv3S3jbs8oTZAad1:13074:0:99999:7:::

反破解

/PM2008v2/PM2008v2,符号去除了,并且根据字符串特征,也像是正常的loader

pitch_symbols

但实际上,如果在自己电脑运行,他会把你分区前512k写0。我当时居然运行了,还好我没给root,而且硬盘是NVME的。谁能想到这个18年前的游戏,还有这一手🙈。以前从来没接触过这个领域,据说街机的反破解有很多自杀式逻辑。

kill_disk

下一篇文章将分析游戏内部的保护机制

参考

萌娘百科 - Speed_Driver系列
https://github.com/shizmob/arcade-docs/