Gorgias
IGS Arcade 逆向系列(四)- ASIC27协议和TSGROM文件静态分析
IGS的反盗版技术上不难,但是非常诡异,可能是代码写的太烂了。
IGS E2000 本质上,是 PC + 游戏基板 的组合(研华设计)。既要考虑到 Anti-Copy (反盗版),又需要考虑到软件工程上的复用。ASIC相当于一个完全黑盒的计算模块,将游戏关键逻辑放在里面,既能提升性能,也可以防止破解。
主程序流程分析游戏主程序会开辟一段 0x200034的栈空间,其中缓冲区占0x200000,而且这个栈开辟出来后不会恢复,这会导致IDA Pro无法反编译,不知道是不是故意的。
首先需要patch这个缓冲区大小,减小函数的栈帧,然后将函数 undefine,最后重新识别,就可以成功反编译。
- Kernel mount_root 时,以及游戏程序启动时 会校验 BIOS 版本信息,开发者却说是获取CRC结果,结果我愣是找遍了所有CRC的位置,也没找到任何反盗版有关的CRC计算逻辑;
- BIOS 信息校验失败就校验PCI的驱动信息,如果失败,好像也没有任何操作,但其他地方也插入了许多一样的校验桩,校验失败就会阻止运行;
- 系统初始化:屏幕、音频、图形、文字、语言、ASIC、Timer、PLXPCI、游戏、音乐、控制器、摄像机、台账、控制、投币、混合器等等;
- 刷新4次ASIC,why???;
- 加载基础 action 文件(TSGROM格式),每次加载都刷新一次ASIC;
- 游戏版本校验,显示第一屏,加载字体,加载声音;
- 加载读卡器;
- 游戏Loop,4种状态(Game, Test, Setting, Demo),可通过ASIC控制
游戏使用了SDL 1.2.7开发,SDL(Simple DirectMedia Layer)是一个跨平台的多媒体开发库,主要用于提供对音频、输入设备(键盘/鼠标/游戏手柄)和图形硬件的底层访问。但是这个性能比较低,只适合2D游戏。Percussion Master 2008 是2D游戏。Speed Driver 2 是 3D游戏,两者的差别可能很大。
开发者在游戏主程序每一处与 ASIC 交互的位置,都插了代码桩,暂时把他称作 RealTimeEvent,应该是统一事件处理程序,每次逻辑的变化、动画的变化,都需要刷新事件。用来实现各种复杂的控制功能,也附带了一些反盗版功能。不得不吐槽,这个代码质量真糟糕,每次都要做大量的计算,性能很差,和用纯 js + html 开发单页面应用一样。
状态检查桩的逻辑
- 更新时钟
- 计时器检查
- Action处理
- 音乐处理
- 音频处理
- 按键状态以及控制输入
- 台账
- 游戏币处理
- PLX PCI 状态处理
- SDL 事件处理
- 绘制动态五边形动画
- PCI 控制写入
- ASIC 27 命令写入
- PCI 数据读取
- 图形刷新
percussion master 2008 支持7个地区,3种语言;简体中文、繁体中文、英文。
ROIO BIOS 信息校验内核会运行一个驱动 /dev/roio, 游戏程序通过此驱动对比内置的版本信息表,实现校验功能。内核本身和游戏程序均内置了表格,原理应该是开发者通过某些工具解析了BIOS信息,然后将物理偏移硬编码到程序和内核里面。
内核和主程序的 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
ROIO 的 Magic Number 有三种:
- 0xfc 获取目标地址的32位数值,小端
- 0xfd 获取目标地址的32位数值,大端
- 0xfe 获取目标地址的8位数值
最后再 xor 0xC2E83AB8
这里的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 : reservedBIOS芯片封装是PLCC 32,使用RT809H成功dump。
使用系统启动后,BIOS ROM的一些数据被解析到了内存里,不是1:1 copy,偏移地址是 0xF0000,。
PCCard 随机数校验我实在不理解这个代码的目的是什么,驱动代码里有SPY的关键词,可能是反嗅探用的暗桩?在启动程序、初始化游戏和print日志会触发,如果上面的BIOS Check 失败了,也会触发此校验。通过ioctl来请求/dev/pccard0,获取结果或者不获取结果。
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_FLAGrequest 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]),幻数就是命中的那个值。我感觉可能是用来初始化驱动的,实在想不到其他作用了。为什么写的那么复杂?
在游戏主程序的某处,发现了残留的代码,异或的内容是 0xD4AA268A,在 percussion master 2008 未发现触发逻辑,应该是另一个游戏的暗桩。可以更确定,这个功能就是为了反盗版的。(虽然设计的很烂)
ASIC 27 协议 A27 初始化游戏主程序与I/O板的通信,经过 PLX PCI 9030 芯片,以共享内存的方式,进行数据交换。
游戏启动后,ASIC 27 初始化之前,会先加载PCI 9030驱动,并开辟一段缓冲区,专门用来存放 ASIC Buffer,里面有各种数据状态。开发者称作 CommandPort。
接下来初始化 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: 调整音量
发送数据到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,0x1eSystem 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 ChecksumASIC 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 # size: 0x400 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)。
也有一些 rom 没有携带版本信息,不知道有什么作用,比如biglogo.rom,有大量LZSS图片数据,但是和代码里的颜色格式对不上,应该是历史遗留。
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 为例,解压后有动画的每一帧的图片
Action ParserIGS 的 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统一刷新画面。
TSG ROM 暗桩IGS 故意损坏了资源文件的某些数据块,需要通过 ASIC 芯片动态修复,这也是 IGS 的反盗版机制,防止破解者修改动画文件实现换皮游戏。
IGS Logo
Teammark
以 IGS Logo 为例,当 buffer 的标识数匹配 1 时,代表数据包类型是资源修复,当遍历到指定的块,此处是7,就将来自 ASIC 27 的 0x400 字节追加到对应的损坏区域,完成资源数据修复。
写在结尾IGS将游戏主程序和硬件强关联,如果要将游戏盗版至其他平台,需要付出非常多的时间。
游戏框架、歌曲谱面,逻辑是状态机,比较复杂,不在我的破解目标内。
把逆向工程写到博客,感觉花费了更多的精力,自己逆向只需要记录一些数据,但是形成文章,就要写的让别人看懂。
下一篇主题:IGS Arcade 逆向系列(五)- ASIC27协议Hook和主程序patch工作
纪念血月,赞美女神
IGS Arcade 逆向系列(四)- ASIC27协议和TSGROM文件静态分析
IGS的反盗版技术上不难,但是非常诡异,可能是代码写的太烂了。
IGS E2000 本质上,是 PC + 游戏基板 的组合(研华设计)。既要考虑到 Anti-Copy (反盗版),又需要考虑到软件工程上的复用。ASIC相当于一个完全黑盒的计算模块,将游戏关键逻辑放在里面,既能提升性能,也可以防止破解。
主程序流程分析游戏主程序会开辟一段 0x200034的栈空间,其中缓冲区占0x200000,而且这个栈开辟出来后不会恢复,这会导致IDA Pro无法反编译,不知道是不是故意的。
首先需要patch这个缓冲区大小,减小函数的栈帧,然后将函数 undefine,最后重新识别,就可以成功反编译。
- Kernel mount_root 时,以及游戏程序启动时 会校验 BIOS 版本信息,开发者却说是获取CRC结果,结果我愣是找遍了所有CRC的位置,也没找到任何反盗版有关的CRC计算逻辑;
- BIOS 信息校验失败就校验PCI的驱动信息,如果失败,好像也没有任何操作,但其他地方也插入了许多一样的校验桩,校验失败就会阻止运行;
- 系统初始化:屏幕、音频、图形、文字、语言、ASIC、Timer、PLXPCI、游戏、音乐、控制器、摄像机、台账、控制、投币、混合器等等;
- 刷新4次ASIC,why???;
- 加载基础 action 文件(TSGROM格式),每次加载都刷新一次ASIC;
- 游戏版本校验,显示第一屏,加载字体,加载声音;
- 加载读卡器;
- 游戏Loop,4种状态(Game, Test, Setting, Demo),可通过ASIC控制
游戏使用了SDL 1.2.7开发,SDL(Simple DirectMedia Layer)是一个跨平台的多媒体开发库,主要用于提供对音频、输入设备(键盘/鼠标/游戏手柄)和图形硬件的底层访问。但是这个性能比较低,只适合2D游戏。Percussion Master 2008 是2D游戏。Speed Driver 2 是 3D游戏,两者的差别可能很大。
开发者在游戏主程序每一处与 ASIC 交互的位置,都插了代码桩,用来实现各种复杂的控制功能,也附带了一些反盗版功能。不得不吐槽,这个代码质量真糟糕,每次都要做大量的计算,性能很差。
状态检查桩的逻辑
- 更新时钟
- 计时器检查
- Action处理
- 音乐处理
- 音频处理
- 按键状态以及控制输入
- 台账
- 游戏币处理
- PLX PCI 状态处理
- SDL 事件处理
- 绘制动态五边形动画
- PCI 控制写入
- ASIC 27 命令写入
- PCI 数据读取
- 图形刷新
percussion master 2008 支持7个地区,3种语言;简体中文、繁体中文、英文。
ROIO BIOS 信息校验内核会运行一个驱动 /dev/roio, 游戏程序通过此驱动对比内置的版本信息表,实现校验功能。内核本身和游戏程序均内置了表格,原理应该是开发者通过某些工具解析了BIOS信息,然后将物理偏移硬编码到程序和内核里面。
内核和主程序的 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
ROIO 的 Magic Number 有三种:
- 0xfc 获取目标地址的32位数值,小端
- 0xfd 获取目标地址的32位数值,大端
- 0xfe 获取目标地址的8位数值
最后再 xor 0xC2E83AB8
这里的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 : reservedBIOS芯片封装是PLCC 32,使用RT809H成功dump。
使用系统启动后,BIOS ROM的一些数据被解析到了内存里,不是1:1 copy,偏移地址是 0xF0000,。
PCCard 随机数校验我实在不理解这个代码的目的是什么,驱动代码里有SPY的关键词,可能是反嗅探用的暗桩?在启动程序、初始化游戏和print日志会触发,如果上面的BIOS Check 失败了,也会触发此校验。通过ioctl来请求/dev/pccard0,获取结果或者不获取结果。
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_FLAGrequest 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]),幻数就是命中的那个值。我感觉可能是用来初始化驱动的,实在想不到其他作用了。为什么写的那么复杂?
在游戏主程序的某处,发现了残留的代码,异或的内容是 0xD4AA268A,在 percussion master 2008 未发现触发逻辑,应该是另一个游戏的暗桩。可以更确定,这个功能就是为了反盗版的。(虽然设计的很烂)
ASIC 27 协议 A27 初始化游戏主程序与I/O板的通信,经过 PLX PCI 9030 芯片,以共享内存的方式,进行数据交换。
游戏启动后,ASIC 27 初始化之前,会先加载PCI 9030驱动,并开辟一段缓冲区,专门用来存放 ASIC Buffer,里面有各种数据状态。开发者称作 CommandPort。
接下来初始化 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: 调整音量
发送数据到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,0x1eSystem 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 ChecksumASIC 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存放了游戏的多媒体资源,有脚本、音频、2D动画。
PM2008 的 TSGROM 版本支持不低于 00.0000.0004,和PM1一致。代码写的简单粗暴,全是while(1)。
也有一些 rom 没有携带版本信息,不知道有什么作用,比如biglogo.rom,有大量LZSS图片数据,但是和代码里的颜色格式对不上,应该是历史遗留。
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 为例,解压后有动画的每一帧的图片
Action ParserIGS 的 TSGROM 定义了游戏的各种图形行为,并称之为 action,主程序通过解析 action 来实现功能。如果是要重写一个ASIC27软件,那么需要逆向分析 action parser,但是我想尝试最完美的破解方式,dump ASIC ROM 放到模拟器运行。不想分析这种屎山代码。
ACT BLOCK 数据块数量 ACT INDEX ACT 索引 ACT DATA Action 数据 ACT POOL Action 数据 ACT STEP 动作帧
TSG ROM 暗桩IGS 故意损坏了资源文件的某些数据块,需要通过 ASIC 芯片动态修复,这也是 IGS 的反盗版机制,防止破解者修改动画文件实现换皮游戏。
IGS Logo
Teammark
以 IGS Logo 为例,当 buffer 的标识数匹配 1 时,代表数据包类型是资源修复,当遍历到指定的块,此处是7,就将来自 ASIC 27 的 0x400 字节追加到对应的损坏区域,完成资源数据修复。
写在结尾IGS将游戏主程序和硬件强关联,如果要将游戏盗版至其他平台,需要付出非常多的时间。
游戏框架、歌曲谱面,逻辑是状态机,比较复杂,不在我的破解目标内。
要把逆向工程写到博客,感觉花费了更多的精力,自己逆向只需要记录一些数据,但是形成文章,就要写的让别人看懂。
下一篇主题:IGS Arcade 逆向系列(五)- ASIC27协议Hook和主程序patch工作
纪念血月,赞美女神
IGS Arcade 逆向系列(三)- Getshell
IGS Arcade 逆向系列(三)- Getshell
这段时间有不少进展,但是遇到瓶颈了,大概还有三篇的量,最近很忙,先更新第三篇。
我把芯片贴纸拆开并做了详细的分析工作,第一篇硬件分析方向基本上没错,更新了对IGS的芯片描述,第二篇文章代码有一些bug,已更新。
要高效分析游戏主程序,最好是设备上动态调试,首先需要shell权限。除了CF卡和IO口,我目前几乎没有任何对这个设备输入内容的途径。
如果走串口调试,第一步找到硬件调试口,下一步修改kernel内置的boot command。很麻烦,还要重打包回去,不够优雅。
CF卡有两个分区我可以直接写入,一个就是Kernel所在的Boot分区,另一个是日志和临时文件分区,那个说不定可以getshell,但是Pwn这个硬件不是我的目的。
IGS CRAMFS 重打包之前我根据IGS魔改的cramfs特征,二次开发了cramfs-tools,但是它只能解包,不能重打包。它是不完美的,我有一点强迫症,想要让它作为真正的toolkits。
IGS 魔改了cramfs的结构体,在cramfs_inode加入了校验值,也就是每次读取inode就会检查。
struct cramfs_super { unsigned int magic1; unsigned int future; /* future = CRAMFS_MAGIC ^ IGS_MAGIC_MASK2 ^ IGS_MAGIC_MASK1 */ char igs_info[64]; unsigned int size; unsigned int magic2; unsigned int flags; unsigned int padding; struct cramfs_info fsid; char name[64]; struct cramfs_inode root; }; struct cramfs_inode { u32 inode_magic; u32 namelen:CRAMFS_NAMELEN_WIDTH, offset:CRAMFS_OFFSET_WIDTH; u32 size:CRAMFS_SIZE_WIDTH, gid:CRAMFS_GID_WIDTH; u32 mode:CRAMFS_MODE_WIDTH, uid:CRAMFS_UID_WIDTH; }; struct cramfs_info { u32 crc; u32 edition; u32 blocks; u32 files; };在get_cramfs_inode,有一个暗桩,用inode_magic异或根inode的magic,判断是否等于0x705DE1,如果不等于就出错。虽然简单,但是每个游戏都不一样,重复的patch工作也挺烦人的。
下图的可读性看起来不错,是因为我做了一些费时间的工作。其实这里IDA的反编译一开始是识别错误的,和汇编完全不一样。要把结构体先还原了,这里的分支才会正确,不然就会丢失很多代码。
我已经实现了IGS cramfs重打包的功能 igs-toolkits cramfs-tools
Unpack
sudo ./igs-toolkits/cramfs-tools/cramfsck -v -x./out ./test.imgRepack
sudo ./igs-toolkits/cramfs-tools/mkcramfs ./out test.img 修复 Shell 环境IGS 在新版的PM2008,做了很多阻止破解的措施。这个Kernel非常老,我花了大量时间解决兼容性问题。操作系统,编译工具链,源代码反复测试了许多版本,终于找到了稳定的编译环境。
修复agetty现在的系统初始化后是没有tty shell的,因为IGS移除了agetty。这个组件位于util-linux。需要重新编译,静态链接。
CC="/opt/gcc_3.2.2/bin/gcc" LDFLAGS="-static" DESTDIR=/root/build-linux-utils export LDFLAGS CC DESTDIR ./configure --enable-static make make install然后在 /etc/inittab,增加下面这行命令。看起来是有115200的串口输出,我还没有在COM口尝试。因为在卧室里研究,场地有限,我喜欢更优雅的调试方式。
1:4:respawn:/sbin/agetty ttyS1 115200 vt102 修复SSH ServicesIGS移除了ssh服务,本来想用现成的dropbear,结果没一个能运行。花了大量时间找到个兼容的dropbear 0.53版本,完成静态编译(解决了很多错误),很多特性是在代码里写死的,我甚至做了一些patch,但还会出现奇怪的问题。 于是只能选择ssh,新版的ssh由于弃用了过时加密算法,连接旧版ssh都要指定算法。因此要选择较新版本的openssh,以及openssl。
编译好后生成密钥对,因为算法和格式有差异,所以要重新生成
ssh-keygen -q -t rsa -f ssh_host_key ssh-keygen -q -t rsa -f ssh_host_rsa_keysshd_config 需要手动配置这些
HostKey /home/ssh_host_key HostKey /home/ssh_host_rsa_key SyslogFacility AUTHPRIV LogLevel INFO PermitRootLogin yes AuthorizedKeysFile .ssh/authorized_keys PermitEmptyPasswords yes X11Forwarding yes Subsystem sftp /home/sftp-server最后需要修复sshd的权限,否则会出错
mkdir /var/empty echo "sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin" >> /etc/passwd在前文提到的启动脚本添加ssh启动命令
/home/sshd -f /home/sshd_config -h /home/ssh_host_rsa_key -p 22 -E /PM2008v2/pm2_data/sshd.log > /PM2008v2/pm2_data/sshd_run.log 2>&1 修复ifconfig并且启用网络IGS 禁止了网络连接,我在游戏代码里看到是可以联网的,游戏有一个OnlineMode,早期是有全球排名的,但当前版本不支持,代码里有明显的修改痕迹。没有地方可以触发进入OnlineMode。
系统甚至还有DHCP服务。配置IP的逻辑写在代码里。
ifconfig被失效了,应该也是被魔改了,自己重新编译一个。
/home/ifconfig eth0 up mtu 1500 >> /PM2008v2/pm2_data/game_stdout.log 2>&1 /home/ifconfig eth0 192.168.2.128 netmask 255.255.255.0 broadcast 192.168.2.255 >> /PM2008v2/pm2_data/game_stdout.log 2>&1这样就可以连接外部网络了。
我还编译了一版 busybox,遇到大量兼容性问题,做了一些patch,也花了很多时间才编译成功。二进制文件可以在这里下载
https://github.com/gorgiaxx/igs-toolkits/tree/master/E2000_binaries
将这些文件放到指定目录,然后重打包,写入CF卡,重启设备,ssh服务就可以直接访问了。
由于没有回显,只能将stdout输出到rdisk4s4,也就是ext3分区,读取并挂载查看回显。花了很多时间才调试成功,这两条命令用了无数次。
sudo dd if=/dev/rdisk4s4 of=./rdisk4s4.img bs=1M && rm -rf ./part4 && 7z x ./rdisk4s4.img -o./part4 sudo ../igs-toolkits/cramfs-tools/mkcramfs ./out test.img && sudo dd if=./test.img of=/dev/rdisk4s2 bs=1M 游戏环境分析E2000主机有两个I/O接口,控制指令来自外部,即使拿到root也玩不了游戏。
游戏主程序游戏主程序实际运行在/exec/PM2008v2 会启动3个进程
根据地址空间,可以判断出,此程序运行在X11,OpenGL渲染。并且可以控制读卡器。调用了/dev/plx/Pci9030-0,访问方式是DMA转虚拟地址,应该是游戏的物理I/O。
[IGS_Linux]root ~# cat /proc/120/maps 08048000-084b6000 r-xp 00000000 00:08 98 /exec/PM2008v2 084b6000-084cb000 rw-p 0046e000 00:08 98 /exec/PM2008v2 084cb000-0b044000 rwxp 00000000 00:00 0 40000000-40013000 r-xp 00000000 16:02 11153856 /lib/ld-2.3.2.so 40013000-40014000 rw-p 00012000 16:02 11153856 /lib/ld-2.3.2.so 40014000-40015000 rw-p 00000000 00:00 0 40015000-4001d000 r-xp 00000000 16:02 57643504 /usr/sbin/cardread/lib/libcasmcard.so 4001d000-4001e000 rw-p 00007000 16:02 57643504 /usr/sbin/cardread/lib/libcasmcard.so 4001e000-40020000 rwxp 00000000 00:0b 2217 /dev/zero 40020000-4002d000 r-xp 00000000 16:02 12504408 /lib/libpthread-0.10.so 4002d000-4002e000 rw-p 0000d000 16:02 12504408 /lib/libpthread-0.10.so 4002e000-40070000 rw-p 00000000 00:00 0 40070000-40072000 r-xp 00000000 16:02 11995640 /lib/libdl-2.3.2.so 40072000-40073000 rw-p 00001000 16:02 11995640 /lib/libdl-2.3.2.so 40073000-400de000 r-xp 00000000 16:02 28323568 /usr/X11R6/lib/libGL.so.1.0.8762 400de000-400f7000 rwxp 0006b000 16:02 28323568 /usr/X11R6/lib/libGL.so.1.0.8762 400f7000-400f8000 rwxp 00000000 00:00 0 400f8000-400f9000 rw-p 00000000 00:00 0 400f9000-4021e000 r-xp 00000000 16:02 11262808 /lib/libc-2.3.2.so 4021e000-40223000 rw-p 00124000 16:02 11262808 /lib/libc-2.3.2.so 40223000-40225000 rw-p 00000000 00:00 0 40225000-40246000 r-xp 00000000 16:02 12066128 /lib/libm-2.3.2.so 40246000-40247000 rw-p 00020000 16:02 12066128 /lib/libm-2.3.2.so 40247000-40254000 r-xp 00000000 16:02 34514852 /usr/X11R6/lib/libXext.so.6.4 40254000-40255000 rw-p 0000c000 16:02 34514852 /usr/X11R6/lib/libXext.so.6.4 40255000-40331000 r-xp 00000000 16:02 33143504 /usr/X11R6/lib/libX11.so.6.2 40331000-40334000 rw-p 000db000 16:02 33143504 /usr/X11R6/lib/libX11.so.6.2 40334000-40342000 r-xp 00000000 16:02 57803624 /usr/sbin/cardread/lib/libpcsclite.so.0.0.1 40342000-40343000 rw-p 0000d000 16:02 57803624 /usr/sbin/cardread/lib/libpcsclite.so.0.0.1 40343000-40344000 rw-p 00000000 00:00 0 40344000-40ad2000 r-xp 00000000 16:02 28719620 /usr/X11R6/lib/libGLcore.so.1.0.8762 40ad2000-40b02000 rwxp 0078d000 16:02 28719620 /usr/X11R6/lib/libGLcore.so.1.0.8762 40b02000-40b06000 rwxp 00000000 00:00 0 40b06000-40b07000 rw-p 00000000 00:00 0 40b07000-40b08000 r-xp 00000000 16:02 40614972 /usr/X11R6/lib/libnvidia-tls.so.1.0.8762 40b08000-40b09000 rw-p 00000000 16:02 40614972 /usr/X11R6/lib/libnvidia-tls.so.1.0.8762 40b09000-40b6b000 rw-p 00000000 00:00 0 40b6b000-40b6c000 rw-s 00000000 00:0b 2004 /dev/plx/Pci9030-0 40b6c000-40b8c000 rw-s 00000000 00:0b 2004 /dev/plx/Pci9030-0 40b8c000-40b8d000 rw-s 00000000 00:0b 2004 /dev/plx/Pci9030-0 40b8d000-4158e000 rw-p 00000000 00:00 0 4158e000-41596000 r-xp 00000000 16:02 34498192 /usr/X11R6/lib/libXcursor.so.1.0 41596000-41597000 rw-p 00007000 16:02 34498192 /usr/X11R6/lib/libXcursor.so.1.0 41597000-4159e000 r-xp 00000000 16:02 37237160 /usr/X11R6/lib/libXrender.so.1.2 4159e000-4159f000 rw-p 00006000 16:02 37237160 /usr/X11R6/lib/libXrender.so.1.2 4159f000-415a0000 rw-s e8001000 00:0b 1990 /dev/nvidia0 415a0000-415a1000 rw-s e8c02000 00:0b 1990 /dev/nvidia0 415a1000-415aa000 r-xp 00000000 16:02 12388932 /lib/libnss_files-2.3.2.so 415aa000-415ab000 rw-p 00008000 16:02 12388932 /lib/libnss_files-2.3.2.so 415ab000-415c2000 rw-s 00000000 00:04 0 /SYSV00000000 (deleted) 415c2000-416ee000 rw-s d0000000 00:0b 1990 /dev/nvidia0 416ee000-41750000 rw-p 00000000 00:0b 2217 /dev/zero 41750000-41791000 rw-p 00000000 00:00 0 41791000-41893000 rw-s e0011000 00:0b 1990 /dev/nvidia0 41893000-41894000 rw-s 16fc2000 00:0b 1990 /dev/nvidia0 41894000-41895000 rw-s 17027000 00:0b 1990 /dev/nvidia0 41895000-41896000 rw-s df93b000 00:0b 1990 /dev/nvidia0 41896000-4189a000 rw-s 17025000 00:0b 1990 /dev/nvidia0 4189a000-4189b000 rw-s df939000 00:0b 1990 /dev/nvidia0 4189b000-4189c000 rw-s 17021000 00:0b 1990 /dev/nvidia0 4189c000-4199c000 rw-s e0114000 00:0b 1990 /dev/nvidia0 4199c000-4199d000 rw-s 00000000 00:04 98305 /SYSV00000000 (deleted) 4199d000-4199e000 rw-s 00000000 00:04 131074 /SYSV00000000 (deleted) 4199e000-41a56000 rw-p 00000000 00:00 0 41a56000-41a57000 ---p 00000000 00:00 0 41a57000-41c56000 rwxp 00001000 00:00 0 41c56000-43096000 rw-p 00003000 00:00 0 43096000-43097000 r--s 00000000 00:07 2709 /tmp/pcsc/.pcscpub 43097000-43098000 r--s 00001000 00:07 2709 /tmp/pcsc/.pcscpub 43098000-43099000 r--s 00002000 00:07 2709 /tmp/pcsc/.pcscpub 43099000-4309a000 r--s 00003000 00:07 2709 /tmp/pcsc/.pcscpub 4309a000-4309b000 r--s 00004000 00:07 2709 /tmp/pcsc/.pcscpub 4309b000-4309c000 r--s 00005000 00:07 2709 /tmp/pcsc/.pcscpub 4309c000-4309d000 r--s 00006000 00:07 2709 /tmp/pcsc/.pcscpub 4309d000-4309e000 r--s 00007000 00:07 2709 /tmp/pcsc/.pcscpub 4309e000-4309f000 r--s 00008000 00:07 2709 /tmp/pcsc/.pcscpub 4309f000-430a0000 r--s 00009000 00:07 2709 /tmp/pcsc/.pcscpub 430a0000-430a1000 r--s 0000a000 00:07 2709 /tmp/pcsc/.pcscpub 430a1000-430a2000 r--s 0000b000 00:07 2709 /tmp/pcsc/.pcscpub 430a2000-430a3000 r--s 0000c000 00:07 2709 /tmp/pcsc/.pcscpub 430a3000-430a4000 r--s 0000d000 00:07 2709 /tmp/pcsc/.pcscpub 430a4000-430a5000 r--s 0000e000 00:07 2709 /tmp/pcsc/.pcscpub 430a5000-430a6000 r--s 0000f000 00:07 2709 /tmp/pcsc/.pcscpub 430a6000-4352f000 rw-p 00000000 00:00 0 43530000-441c9000 rw-p 0048a000 00:00 0 4423c000-44322000 rw-p 01196000 00:00 0 443a7000-444b0000 rw-p 01301000 00:00 0 bfde2000-c0000000 rwxp ffde3000 00:00 0游戏程序本身有大量字符串,看起来像是编码后的数据,在此之前,我从来没接触过Big5编码,直接让deepseek帮我写了个脚本识别。
import chardet from encodings.aliases import aliases def try_all_encodings(hex_str): all_encodings = set(aliases.values()) byte_data = bytes.fromhex(hex_str.replace(" ", "")) print(f"原始16进制数据: {hex_str}") print(f"字节长度: {len(byte_data)} bytes\n") detected = chardet.detect(byte_data) print(f"自动检测结果: {detected['encoding']} (置信度: {detected['confidence']:.2%})") common_encodings = [ 'gbk', 'gb18030', 'gb2312', 'utf-8', 'utf-16', 'big5', 'hz', 'iso-2022-jp', 'euc-kr' ] print("\n=== 常见编码测试 ===") for enc in common_encodings: try: decoded = byte_data.decode(enc) print(f"[{enc.upper()}]: {decoded}") except: pass print("\n=== 完整编码测试 ===") for enc in sorted(all_encodings): try: decoded = byte_data.decode(enc) if decoded.isprintable(): print(f"[{enc}]: {decoded}") except: continue if __name__ == "__main__": hex_data = "C2 F7 B6 7D 3A A6 50 AE C9 BA 56 C0 BB 31 50 A4 CE 32 50 B9 AA AD B1" try_all_encodings(hex_data)游戏是台湾地区研发的,使用了繁体中文,IDA不能自动识别。需要手动添加。
Option -> Strings -> Default(8-bit) -> Insert(Right Click) -> Big5
因为非ASCII,也没有太好的识别方式,就还是参考上篇文章的修复字符串思路吧。
运行游戏主程序,只会打印这些日志。
[IGS_Linux]root /proc/sys# /exec/PM2008v2 Device Handle 4 Version Major 4,Minor 3,Rev 0 Get Virtual address!! [CommandPortAddresss]=0x40b8c000,[ShareRAMAddress]=0x40b6c000 Clear CommandPort Complete. start代码里将dprintf逻辑移除了,所以不会打印日志。
瓶颈现在遇到瓶颈了,因为节奏游戏的命中判定,是放到ASIC,ASIC把判定状态传递到CPU。主程序是没有判定逻辑的。目前把ASIC27协议分析出来了,自己写判定逻辑倒是没问题,但这就不叫破解了。只有破解了ASIC,才能完美模拟这些游戏。接下来需要Dump ASIC的固件,难度很高,不知道今年能不能搞定。
接下来还有两篇已经完成的工作,等有空再写。
- IGS Arcade 逆向系列(四)- ASIC27协议和TSGROM文件静态分析
- IGS Arcade 逆向系列(五)- ASIC27协议Hook和主程序patch工作
上个月我都Linux电脑CPU缩肛了,死因是长期运行,散热不好,我花了大量时间排查,迁移工具,然后把博客环境迁移到新电脑。
以前写博客都是nvm到旧版node,运行Hexo框架生成静态页面,8年没有动过。因为从那时起,hexo的依赖就很烦人,更新必出问题,导致我不敢更新。
旧环境在MacOS很难跑起来,不管是源码安装还是brew安装都不行。我不理解一个nodejs项目为什么要用到python3.8。
那一堆乱七八糟的依赖,用最新版Hexo也不行,很多主题都不兼容。我都用asdf了,怎么还能这样?果断放弃Hexo,迁移博客到Hugo用了很多时间。
IGS Arcade 逆向系列(三)- Getshell
这段时间有不少进展,但是遇到瓶颈了,大概还有三篇的量,最近很忙,先更新第三篇。
我把芯片贴纸拆开并做了详细的分析工作,第一篇硬件分析方向基本上没错,更新了对IGS的芯片描述,第二篇文章代码有一些bug,已更新。
要高效分析游戏主程序,最好是设备上动态调试,首先需要shell权限。除了CF卡和IO口,我目前几乎没有任何对这个设备输入内容的途径。
如果走串口调试,第一步找到硬件调试口,下一步修改kernel内置的boot command。很麻烦,还要重打包回去,不够优雅。
CF卡有两个分区我可以直接写入,一个就是Kernel所在的Boot分区,另一个是日志和临时文件分区,那个说不定可以getshell,但是Pwn这个硬件不是我的目的。
IGS CRAMFS 重打包之前我根据IGS魔改的cramfs特征,二次开发了cramfs-tools,但是它只能解包,不能重打包。它是不完美的,我有一点强迫症,想要让它作为真正的toolkits。
IGS 魔改了cramfs的结构体,在cramfs_inode加入了校验值,也就是每次读取inode就会检查。
struct cramfs_super { unsigned int magic1; unsigned int future; /* future = CRAMFS_MAGIC ^ IGS_MAGIC_MASK2 ^ IGS_MAGIC_MASK1 */ char igs_info[64]; unsigned int size; unsigned int magic2; unsigned int flags; unsigned int padding; struct cramfs_info fsid; char name[64]; struct cramfs_inode root; }; struct cramfs_inode { u32 inode_magic; u32 namelen:CRAMFS_NAMELEN_WIDTH, offset:CRAMFS_OFFSET_WIDTH; u32 size:CRAMFS_SIZE_WIDTH, gid:CRAMFS_GID_WIDTH; u32 mode:CRAMFS_MODE_WIDTH, uid:CRAMFS_UID_WIDTH; }; struct cramfs_info { u32 crc; u32 edition; u32 blocks; u32 files; };在get_cramfs_inode,有一个暗桩,用inode_magic异或根inode的magic,判断是否等于0x705DE1,如果不等于就出错。虽然简单,但是每个游戏都不一样,重复的patch工作也挺烦人的。
下图的可读性看起来不错,是因为我做了一些费时间的工作。其实这里IDA的反编译一开始是识别错误的,和汇编完全不一样。要把结构体先还原了,这里的分支才会正确,不然就会丢失很多代码。
我已经实现了IGS cramfs重打包的功能 igs-toolkits cramfs-tools
Unpack
sudo ./igs-toolkits/cramfs-tools/cramfsck -v -x./out ./test.imgRepack
sudo ./igs-toolkits/cramfs-tools/mkcramfs ./out test.img 修复 Shell 环境IGS 在新版的PM2008,做了很多阻止破解的措施。这个Kernel非常老,我花了大量时间解决兼容性问题。操作系统,编译工具链,源代码反复测试了许多版本,终于找到了稳定的编译环境。
修复agetty现在的系统初始化后是没有tty shell的,因为IGS移除了agetty。这个组件位于util-linux。需要重新编译,静态链接。
CC="/opt/gcc_3.2.2/bin/gcc" LDFLAGS="-static" DESTDIR=/root/build-linux-utils export LDFLAGS CC DESTDIR ./configure --enable-static make make install然后在 /etc/inittab,增加下面这行命令。看起来是有115200的串口输出,我还没有在COM口尝试。因为在卧室里研究,场地有限,我喜欢更优雅的调试方式。
1:4:respawn:/sbin/agetty ttyS1 115200 vt102 修复SSH ServicesIGS移除了ssh服务,本来想用现成的dropbear,结果没一个能运行。花了大量时间找到个兼容的dropbear 0.53版本,完成静态编译(解决了很多错误),很多特性是在代码里写死的,我甚至做了一些patch,但还会出现奇怪的问题。 于是只能选择ssh,新版的ssh由于弃用了过时加密算法,连接旧版ssh都要指定算法。因此要选择较新版本的openssh,以及openssl。
编译好后生成密钥对,因为算法和格式有差异,所以要重新生成
ssh-keygen -q -t rsa -f ssh_host_key ssh-keygen -q -t rsa -f ssh_host_rsa_keysshd_config 需要手动配置这些
HostKey /home/ssh_host_key HostKey /home/ssh_host_rsa_key SyslogFacility AUTHPRIV LogLevel INFO PermitRootLogin yes AuthorizedKeysFile .ssh/authorized_keys PermitEmptyPasswords yes X11Forwarding yes Subsystem sftp /home/sftp-server最后需要修复sshd的权限,否则会出错
mkdir /var/empty echo "sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin" >> /etc/passwd在前文提到的启动脚本添加ssh启动命令
/home/sshd -f /home/sshd_config -h /home/ssh_host_rsa_key -p 22 -E /PM2008v2/pm2_data/sshd.log > /PM2008v2/pm2_data/sshd_run.log 2>&1 修复ifconfig并且启用网络IGS 禁止了网络连接,我在游戏代码里看到是可以联网的,游戏有一个OnlineMode,早期是有全球排名的,但当前版本不支持,代码里有明显的修改痕迹。没有地方可以触发进入OnlineMode。
系统甚至还有DHCP服务。配置IP的逻辑写在代码里。
ifconfig被失效了,应该也是被魔改了,自己重新编译一个。
/home/ifconfig eth0 up mtu 1500 >> /PM2008v2/pm2_data/game_stdout.log 2>&1 /home/ifconfig eth0 192.168.2.128 netmask 255.255.255.0 broadcast 192.168.2.255 >> /PM2008v2/pm2_data/game_stdout.log 2>&1这样就可以连接外部网络了。
我还编译了一版 busybox,遇到大量兼容性问题,做了一些patch,也花了很多时间才编译成功。二进制文件可以在这里下载
https://github.com/gorgiaxx/igs-toolkits/tree/master/E2000_binaries
将这些文件放到指定目录,然后重打包,写入CF卡,重启设备,ssh服务就可以直接访问了。
由于没有回显,只能将stdout输出到rdisk4s4,也就是ext3分区,读取并挂载查看回显。花了很多时间才调试成功,这两条命令用了无数次。
sudo dd if=/dev/rdisk4s4 of=./rdisk4s4.img bs=1M && rm -rf ./part4 && 7z x ./rdisk4s4.img -o./part4 sudo ../igs-toolkits/cramfs-tools/mkcramfs ./out test.img && sudo dd if=./test.img of=/dev/rdisk4s2 bs=1M 游戏环境分析E2000主机有两个I/O接口,控制指令来自外部,即使拿到root也玩不了游戏。
游戏主程序游戏主程序实际运行在/exec/PM2008v2 会启动3个进程
根据地址空间,可以判断出,此程序运行在X11,OpenGL渲染。并且可以控制读卡器。调用了/dev/plx/Pci9030-0,访问方式是DMA转虚拟地址,应该是游戏的物理I/O。
[IGS_Linux]root ~# cat /proc/120/maps 08048000-084b6000 r-xp 00000000 00:08 98 /exec/PM2008v2 084b6000-084cb000 rw-p 0046e000 00:08 98 /exec/PM2008v2 084cb000-0b044000 rwxp 00000000 00:00 0 40000000-40013000 r-xp 00000000 16:02 11153856 /lib/ld-2.3.2.so 40013000-40014000 rw-p 00012000 16:02 11153856 /lib/ld-2.3.2.so 40014000-40015000 rw-p 00000000 00:00 0 40015000-4001d000 r-xp 00000000 16:02 57643504 /usr/sbin/cardread/lib/libcasmcard.so 4001d000-4001e000 rw-p 00007000 16:02 57643504 /usr/sbin/cardread/lib/libcasmcard.so 4001e000-40020000 rwxp 00000000 00:0b 2217 /dev/zero 40020000-4002d000 r-xp 00000000 16:02 12504408 /lib/libpthread-0.10.so 4002d000-4002e000 rw-p 0000d000 16:02 12504408 /lib/libpthread-0.10.so 4002e000-40070000 rw-p 00000000 00:00 0 40070000-40072000 r-xp 00000000 16:02 11995640 /lib/libdl-2.3.2.so 40072000-40073000 rw-p 00001000 16:02 11995640 /lib/libdl-2.3.2.so 40073000-400de000 r-xp 00000000 16:02 28323568 /usr/X11R6/lib/libGL.so.1.0.8762 400de000-400f7000 rwxp 0006b000 16:02 28323568 /usr/X11R6/lib/libGL.so.1.0.8762 400f7000-400f8000 rwxp 00000000 00:00 0 400f8000-400f9000 rw-p 00000000 00:00 0 400f9000-4021e000 r-xp 00000000 16:02 11262808 /lib/libc-2.3.2.so 4021e000-40223000 rw-p 00124000 16:02 11262808 /lib/libc-2.3.2.so 40223000-40225000 rw-p 00000000 00:00 0 40225000-40246000 r-xp 00000000 16:02 12066128 /lib/libm-2.3.2.so 40246000-40247000 rw-p 00020000 16:02 12066128 /lib/libm-2.3.2.so 40247000-40254000 r-xp 00000000 16:02 34514852 /usr/X11R6/lib/libXext.so.6.4 40254000-40255000 rw-p 0000c000 16:02 34514852 /usr/X11R6/lib/libXext.so.6.4 40255000-40331000 r-xp 00000000 16:02 33143504 /usr/X11R6/lib/libX11.so.6.2 40331000-40334000 rw-p 000db000 16:02 33143504 /usr/X11R6/lib/libX11.so.6.2 40334000-40342000 r-xp 00000000 16:02 57803624 /usr/sbin/cardread/lib/libpcsclite.so.0.0.1 40342000-40343000 rw-p 0000d000 16:02 57803624 /usr/sbin/cardread/lib/libpcsclite.so.0.0.1 40343000-40344000 rw-p 00000000 00:00 0 40344000-40ad2000 r-xp 00000000 16:02 28719620 /usr/X11R6/lib/libGLcore.so.1.0.8762 40ad2000-40b02000 rwxp 0078d000 16:02 28719620 /usr/X11R6/lib/libGLcore.so.1.0.8762 40b02000-40b06000 rwxp 00000000 00:00 0 40b06000-40b07000 rw-p 00000000 00:00 0 40b07000-40b08000 r-xp 00000000 16:02 40614972 /usr/X11R6/lib/libnvidia-tls.so.1.0.8762 40b08000-40b09000 rw-p 00000000 16:02 40614972 /usr/X11R6/lib/libnvidia-tls.so.1.0.8762 40b09000-40b6b000 rw-p 00000000 00:00 0 40b6b000-40b6c000 rw-s 00000000 00:0b 2004 /dev/plx/Pci9030-0 40b6c000-40b8c000 rw-s 00000000 00:0b 2004 /dev/plx/Pci9030-0 40b8c000-40b8d000 rw-s 00000000 00:0b 2004 /dev/plx/Pci9030-0 40b8d000-4158e000 rw-p 00000000 00:00 0 4158e000-41596000 r-xp 00000000 16:02 34498192 /usr/X11R6/lib/libXcursor.so.1.0 41596000-41597000 rw-p 00007000 16:02 34498192 /usr/X11R6/lib/libXcursor.so.1.0 41597000-4159e000 r-xp 00000000 16:02 37237160 /usr/X11R6/lib/libXrender.so.1.2 4159e000-4159f000 rw-p 00006000 16:02 37237160 /usr/X11R6/lib/libXrender.so.1.2 4159f000-415a0000 rw-s e8001000 00:0b 1990 /dev/nvidia0 415a0000-415a1000 rw-s e8c02000 00:0b 1990 /dev/nvidia0 415a1000-415aa000 r-xp 00000000 16:02 12388932 /lib/libnss_files-2.3.2.so 415aa000-415ab000 rw-p 00008000 16:02 12388932 /lib/libnss_files-2.3.2.so 415ab000-415c2000 rw-s 00000000 00:04 0 /SYSV00000000 (deleted) 415c2000-416ee000 rw-s d0000000 00:0b 1990 /dev/nvidia0 416ee000-41750000 rw-p 00000000 00:0b 2217 /dev/zero 41750000-41791000 rw-p 00000000 00:00 0 41791000-41893000 rw-s e0011000 00:0b 1990 /dev/nvidia0 41893000-41894000 rw-s 16fc2000 00:0b 1990 /dev/nvidia0 41894000-41895000 rw-s 17027000 00:0b 1990 /dev/nvidia0 41895000-41896000 rw-s df93b000 00:0b 1990 /dev/nvidia0 41896000-4189a000 rw-s 17025000 00:0b 1990 /dev/nvidia0 4189a000-4189b000 rw-s df939000 00:0b 1990 /dev/nvidia0 4189b000-4189c000 rw-s 17021000 00:0b 1990 /dev/nvidia0 4189c000-4199c000 rw-s e0114000 00:0b 1990 /dev/nvidia0 4199c000-4199d000 rw-s 00000000 00:04 98305 /SYSV00000000 (deleted) 4199d000-4199e000 rw-s 00000000 00:04 131074 /SYSV00000000 (deleted) 4199e000-41a56000 rw-p 00000000 00:00 0 41a56000-41a57000 ---p 00000000 00:00 0 41a57000-41c56000 rwxp 00001000 00:00 0 41c56000-43096000 rw-p 00003000 00:00 0 43096000-43097000 r--s 00000000 00:07 2709 /tmp/pcsc/.pcscpub 43097000-43098000 r--s 00001000 00:07 2709 /tmp/pcsc/.pcscpub 43098000-43099000 r--s 00002000 00:07 2709 /tmp/pcsc/.pcscpub 43099000-4309a000 r--s 00003000 00:07 2709 /tmp/pcsc/.pcscpub 4309a000-4309b000 r--s 00004000 00:07 2709 /tmp/pcsc/.pcscpub 4309b000-4309c000 r--s 00005000 00:07 2709 /tmp/pcsc/.pcscpub 4309c000-4309d000 r--s 00006000 00:07 2709 /tmp/pcsc/.pcscpub 4309d000-4309e000 r--s 00007000 00:07 2709 /tmp/pcsc/.pcscpub 4309e000-4309f000 r--s 00008000 00:07 2709 /tmp/pcsc/.pcscpub 4309f000-430a0000 r--s 00009000 00:07 2709 /tmp/pcsc/.pcscpub 430a0000-430a1000 r--s 0000a000 00:07 2709 /tmp/pcsc/.pcscpub 430a1000-430a2000 r--s 0000b000 00:07 2709 /tmp/pcsc/.pcscpub 430a2000-430a3000 r--s 0000c000 00:07 2709 /tmp/pcsc/.pcscpub 430a3000-430a4000 r--s 0000d000 00:07 2709 /tmp/pcsc/.pcscpub 430a4000-430a5000 r--s 0000e000 00:07 2709 /tmp/pcsc/.pcscpub 430a5000-430a6000 r--s 0000f000 00:07 2709 /tmp/pcsc/.pcscpub 430a6000-4352f000 rw-p 00000000 00:00 0 43530000-441c9000 rw-p 0048a000 00:00 0 4423c000-44322000 rw-p 01196000 00:00 0 443a7000-444b0000 rw-p 01301000 00:00 0 bfde2000-c0000000 rwxp ffde3000 00:00 0游戏程序本身有大量字符串,看起来像是编码后的数据,在此之前,我从来没接触过Big5编码,直接让deepseek帮我写了个脚本识别。
import chardet from encodings.aliases import aliases def try_all_encodings(hex_str): all_encodings = set(aliases.values()) byte_data = bytes.fromhex(hex_str.replace(" ", "")) print(f"原始16进制数据: {hex_str}") print(f"字节长度: {len(byte_data)} bytes\n") detected = chardet.detect(byte_data) print(f"自动检测结果: {detected['encoding']} (置信度: {detected['confidence']:.2%})") common_encodings = [ 'gbk', 'gb18030', 'gb2312', 'utf-8', 'utf-16', 'big5', 'hz', 'iso-2022-jp', 'euc-kr' ] print("\n=== 常见编码测试 ===") for enc in common_encodings: try: decoded = byte_data.decode(enc) print(f"[{enc.upper()}]: {decoded}") except: pass print("\n=== 完整编码测试 ===") for enc in sorted(all_encodings): try: decoded = byte_data.decode(enc) if decoded.isprintable(): print(f"[{enc}]: {decoded}") except: continue if __name__ == "__main__": hex_data = "C2 F7 B6 7D 3A A6 50 AE C9 BA 56 C0 BB 31 50 A4 CE 32 50 B9 AA AD B1" try_all_encodings(hex_data)游戏是台湾地区研发的,使用了繁体中文,IDA不能自动识别。需要手动添加。
Option -> Strings -> Default(8-bit) -> Insert(Right Click) -> Big5
因为非ASCII,也没有太好的识别方式,就还是参考上篇文章的修复字符串思路吧。
运行游戏主程序,只会打印这些日志。
[IGS_Linux]root /proc/sys# /exec/PM2008v2 Device Handle 4 Version Major 4,Minor 3,Rev 0 Get Virtual address!! [CommandPortAddresss]=0x40b8c000,[ShareRAMAddress]=0x40b6c000 Clear CommandPort Complete. start代码里将dprintf逻辑移除了,所以不会打印日志。
瓶颈现在遇到瓶颈了,因为节奏游戏的命中判定,是放到ASIC,ASIC把判定状态传递到CPU。主程序是没有判定逻辑的。目前把ASIC27协议分析出来了,自己写判定逻辑倒是没问题,但这就不叫破解了。只有破解了ASIC,才能完美模拟这些游戏。接下来需要Dump ASIC的固件,难度很高,不知道今年能不能搞定。
接下来还有两篇已经完成的工作,等有空再写。
- IGS Arcade 逆向系列(四)- ASIC27协议和TSGROM文件静态分析
- IGS Arcade 逆向系列(五)- ASIC27协议Hook和主程序patch工作
上个月我都Linux电脑CPU缩肛了,死因是长期运行,散热不好,我花了大量时间排查,迁移工具,然后把博客环境迁移到新电脑。
以前写博客都是nvm到旧版node,运行Hexo框架生成静态页面,8年没有动过。因为从那时起,hexo的依赖就很烦人,更新必出问题,导致我不敢更新。
旧环境在MacOS很难跑起来,不管是源码安装还是brew安装都不行。我不理解一个nodejs项目为什么要用到python3.8。
那一堆乱七八糟的依赖,用最新版Hexo也不行,很多主题都不兼容。我都用asdf了,怎么还能这样?果断放弃Hexo,迁移博客到Hugo用了很多时间。
IGS Arcade 逆向系列(一)- E2000平台分析
IGS Arcade 逆向系列(二)- 游戏文件恢复
上一篇文章写到游戏有个破坏分区的保护机制,本篇将深入分析。
作为2007年发布的游戏,此游戏加固机制还是比较落后的,主要是靠一些拼接,修改特征的方法来加固。并没有现代APP加固的那么卷的特性。主要是加固的环节有点多,并且每个游戏都不一样。然后由于开发上的特性,
IGS Arcade 逆向系列(二)- 游戏文件恢复
IGS Arcade 逆向系列(二)- 游戏文件恢复
上一篇文章写到游戏有个破坏分区的保护机制,本篇将深入分析。
作为2007年发布的游戏,此游戏加固机制还是比较落后的,主要是靠一些拼接,修改特征的方法来加固。并没有现代APP加固的那么卷的特性。主要是加固的环节有点多,并且每个游戏都不一样。然后由于开发上的特性,(编译优化,代码风格),使得逆向分析变麻烦了。
要提取游戏其实比较简单,等游戏运行时通过shell从内存或者从文件系统(如果文件落地)dump就行了。但是如果要提取不同游戏,这样就太麻烦了,还是先静态分析吧。
总之,这个逆向过程像是在做MISC题一样,需要一些逻辑推理。
逆向陷阱一般情况下,分析文件系统的内容,很少会先从内核入手,一般先看init相关文件。 第一步一般都是看 /etc/inittab,脚本首先启动rc,然后启动图形界面
# Begin /etc/inittab id:4:initdefault: si::sysinit:/etc/rc.d/init.d/rc x:4:respawn:/etc/X11/IGS &> /dev/null # End /etc/inittab /etc/rc.d/init.d/rc #!/bin/bash PATH=/bin:/sbin:/usr/bin:/usr/sbin export PATH mount -n -o remount,rw / mount -n -t ramfs tmp /tmp mount -n -t proc proc /proc mount -n -t usbdevfs usbdevfs /proc/bus/usb #echo "copy for etc" cp -a /etc/* /tmp mount -n -t ramfs etc /etc cp -a /tmp/* /etc rm -rf /tmp/* #echo "copy for dev" cp -a /dev/* /tmp mount -n -t ramfs dev /dev cp -a /tmp/* /dev rm -rf /tmp/* mount -n -t devpts pts /dev/pts mount -n -t tmpfs shm /dev/shm #echo "copy for var" cp -a /var/* /tmp mount -n -t ramfs var /var cp -a /tmp/* /var rm -rf /tmp/* #echo "copy for root" cp -a /root/.b* /tmp mount -n -t ramfs root /root cp -a /tmp/.b* /root rm -rf /tmp/.b* /sbin/hdparm -c1 -d1 -k1 -Xudma4 /dev/hdc &> /dev/null /etc/X11/IGS配置环境变量,启动X,然后启动读卡器,最后启动游戏,除了这个循环有一点反常,其他都非常正常。到这个时候肯定就认为/PM2008v2/PM2008v2是游戏本体了
#!/bin/sh TZ="UCT" TERM="xterm" TempFile="/tmp/XTemp" HZ="100" PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin LD_LIBRARY_PATH=/usr/X11R6/lib:/usr/X11R6/lib/modules/extensions DISPLAY=:0 export PATH LD_LIBRARY_PATH DISPLAY TERM HZ TZ ps -A | grep XFree86 | ( while read pid tty time command; do kill -9 $pid; done ) XFree86 &> /dev/null& mwm &> /dev/null & /usr/X11R6/bin/xsetroot -cursor /usr/X11R6/bitmaps/empty_ptr /usr/X11R6/bitmaps/empty_ptr if [ -f $TempFile ];then rm -rf $TempFile sleep 10 exit 0 else touch $TempFile fi /etc/rc.d/init.d/cardreader &> /dev/null& export TZ="CST" #Run Game cd /PM2008v2 while [ 1 ] do ./PM2008v2 &> /dev/null sleep 5 done接下来分析 PM2008v2,第一眼看上去,里面有很多程序装载的代码。联想到之前有很多文件没有magic,猜测可能是拿来动态加载代码文件还原成elf的。但是后来 Nova 和我说这东西不是游戏,我才知道了它的异常。这个文件有很多glibc特征,感觉就是一个静态链接glibc的程序。
为了方便逆向和后期的游戏移植,我需要确认GCC和GLibc版本,PM2008v2: GCC: (GNU) 3.3.1,但是没有GLibc的版本信息。 因此我直接找到系统的libc.so,版本暂定为glibc 2.3.2。在最新的linux下不好编译,docker下也出了问题。
分析伪游戏程序 编译 GCC 3.3.1由于目标版本内核是i686的,我使用CentOS4编译,运行在VMware,为了保证优化之后的汇编代码一致,我要使用相同GCC的版本,然后编译。并且搭建这个环境,也能方便以后对辅助分析该平台其他游戏,也有一些帮助。前期准备工作多一点,后期就可以少走一些弯路。
wget http://mirrors.aliyun.com/gnu/gcc/gcc-3.3.1/gcc-3.3.1.tar.gz使用阿里云的vault源,先安装开发环境依赖。
yum groupinstall "Development Tools"使用下列配置,指定版本为i686,旧版的gcc,最好不要并发编译,可能会出问题(3.3.1没有遇到,3.2.2遇到了),然后删除系统自带的gcc,再安装。
../gcc-3.3.1/configure --prefix=/opt/gcc_3.3.1 --infodir=/usr/share/info --enable-shared --enable-threads=posix --disable-checking --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --host=i686-pc-gnu-linux --build=i686-pc-linux-gnu --target=i686-pc-linux-gnu make -j8 yum remove gcc make install 编译 Glibc 2.3.2 wget http://mirrors.aliyun.com/gnu/glibc/glibc-2.3.2.tar.gz wget http://mirrors.aliyun.com/gnu/glibc/glibc-linuxthreads-2.3.2.tar.gzlinuxthreads要解压到glibc目录
tar -zxvf glibc-2.3.2.tar.gz cd glibc-2.3.2 tar -zxvf ../glibc-linuxthreads-2.3.2.tar.gzGCC 2.3.2编译可能会遇到一些bug,需要打patch,刚好E2000平台的Linux也是LFS版本,可以去这里下载 LFS Glibc patches
patch -p1 < ../patches/glibc-2.3.2-sscanf-1.patch patch -p1 < ../patches/glibc-2.3.2-inlining_fixes-2.patch patch -p1 < ../patches/glibc-2.3.2-test_lfs-1.patch接下来配置编译选项,先暂时这么用吧,因为即使设置细分的优化选项,最后的汇编内容都和目标文件差异很大。
CC=/opt/gcc_3.3.1/bin/gcc CFLAGS="-march=pentium4 -O2" ../glibc-2.3.2/configure --prefix=/lib --disable-profile --enable-add-ons --libexecdir=/usr/lib --with-headers=/usr/include差异项:
- 栈帧:目标程序大部分的函数返回都是 0xC9 leave,而我编译的都是先move esp, ebp,然后pop ebp。
- 内链函数:目标程序函数内部的call,一些中等长度的函数,会被优化成内联函数。
上述内容,即使我将编译优化级别设为O3,也几乎没有变化。手动配置fomit-frame-pointer、-finline-limit=n等信息,也没用。也许需要手动设置__inline__吧,我没时间去验证。 这个情况,用flare生成signature,几乎还原不了那种函数内部带有call的符号。
经过分析,PM2008v2这个程序,就是一个killdisk函数,静态编译了glibc。
因此,如果执行inittab,是不可能启动游戏的。
系统初始化分析 网友的逆向成果Nova之前告诉了我一些信息,实际上,仅从这个笔记,我看不出具体的加载流程,只能看出文件头需要还原,然后rc.0实际上是elf文件,启动时会执行。而且Submarine Crisis游戏文件和我的PM2008游戏文件又一些差异。
https://github.com/batteryshark/igstools/blob/main/scripts/igs_rofsv1_dumpexec.py
这个脚本是用于还原游戏文件的,我尝试了一下,可以生成一个ELF,但是放到IDA分析会出错。我并不知道生成的文件要如何运行。因此还是需要自己分析一遍。
分析内核启动过程通过分析依赖和其他环境变量,并没有找到任何能启动游戏的路径。在上一篇文章,分析了文件系统挂载,在挂载之后,还有一系列操作。IDA遇到长度过大的函数,可能就会出错,无法反编译。但是问题不大,麻烦的不在这里。
上一篇文章由于是分析基于开源代码魔改的filesystem,可以直接对照逆向,因此不还原其他符号,问题也不大。该内核版本是2.4,bzImage没有携带符号表。这里的代码很多是IGS自己开发的,有些系统调用不是通过int来调用的。如果有用到syscall,在符号没还原的情况下分析,还是有一些麻烦,如果用bindiff,就需要在ida 8下用。我没有时间去移植到Mac上。Linux Kernel有一个syscall table,如果IGS没有自定义过syscall,那么可以直接将自己编译的kernel的syscall 符号照搬过来。
另外,IDA9对这个老的Linux 2.4内核解析不太好,很多交叉引用和汇编指令都没有识别出来,需要手动修复。
修复立即数交叉引用 import ida_ida import ida_bytes import ida_ua import idautils def find_immediate_values_and_convert_to_offset(start_range, end_range): converted_count = 0 checked_count = 0 min_ea = ida_ida.inf_get_min_ea() max_ea = ida_ida.inf_get_max_ea() print(f"EA range: 0x{start_range:X} - 0x{end_range:X}") print(f"Immediate Value Range:0x{min_ea:X} - 0x{max_ea:X}") for ea in idautils.Heads(): if not ida_bytes.is_code(ida_bytes.get_flags(ea)): continue insn = ida_ua.insn_t() if ida_ua.decode_insn(insn, ea) == 0: continue if start_range <= ea and ea <= end_range: for op_num in range(ida_ida.UA_MAXOP): op = insn.ops[op_num] if op.type == ida_ua.o_void: break if op.type == ida_ua.o_imm: imm_value = op.value if op.value > 0xFFFFFFFF: imm_value = (0xFFFFFFFF & op.value) checked_count += 1 if min_ea <= imm_value and imm_value <= max_ea: if idc.op_offset(ea, op_num, REF_OFF32): converted_count += 1 else: print(f" -> Convert Failed: 0x{ea:X}[{op_num}]") print(f"Immediate Value: {checked_count}, Converted: {converted_count}") 修复字符串数据可能是因为交叉引用没有识别完全,字符串识别总是会错失前面几个bytes,需要手动修复字符串。
def get_the_firsstr_ea(ea): addr = ea - 1 last_byte = ida_bytes.get_byte(addr) if 32 < last_byte and last_byte < 127: ea = get_the_firsstr_ea(addr) return ea def find_str_address(start_ea, end_ea): current_ea = start_ea found_count = 0 while current_ea < end_ea: if current_ea == ida_idaapi.BADADDR: break address_flags = ida_bytes.get_flags(current_ea) if ida_bytes.is_strlit(address_flags): str_size = ida_bytes.get_item_size(current_ea) the_first_str_addr = get_the_firsstr_ea(current_ea) if the_first_str_addr != current_ea: len = current_ea - the_first_str_addr + str_size ida_bytes.create_strlit(the_first_str_addr, len, 0) print(f"Fix str at 0x{current_ea:X}, before: {str_size}, after: {len}") current_ea += ida_bytes.get_item_size(current_ea) continue Kernel Thread根据上图代码,可以得知游戏初始化的第一步是先运行/bin/zsh,并且携带这些参数。
export HOME=/ export TERM=linux export PATH=/bin:/usr/bin:/sbin:/usr/sbin /bin/zsh /etc/rc.d/rc0.d __KERNEL__ -no-print -PM2008v2下一步就是运行/mnt/GECA文件
export HOME=/ export TERM=linux export PATH=/bin:/usr/bin:/sbin:/usr/sbin /mnt/GECA /etc/rc.d/rc0.d __KERNEL__ -no-print -PM2008v2最后运行这些
if ( execute_command ) run_init_process((const char *)execute_command); run_init_process("/sbin/init"); // 存在 run_init_process("/etc/init"); // 不存在 run_init_process("/bin/init"); // 不存在 run_init_process("/bin/sh"); // 指向bashKernel Cmdline可以在parse_cmdline_early找到,并非LILO控制。
如果从外部设置了bootcmdline,其中有一个暗桩,会检测bootloader参数是否等于”JBoot“,如果不等于,就进入死循环。
这个JBoot是不是代表James写的Bootloader?👀
bootloader=JBoot内核自带的启动命令
root=/dev/hdc2 ro console=ttyS1,115200 BOOT_IMAGE=PM2008v2并没有设置init=,因此肯定会执行/sbin/init,然后执行/etc/inittab
恢复ZSH符号受阻线索到了/bin/zsh,这个程序入口和PM2008v2一样,我判断也是基于glibc改的,但是我导入FLIRT Signature,只能识别出最里层的函数,还是那个编译优化的原因。
/Applications/IDA\ Professional\ 9.1.app/Contents/MacOS/tools/flair/sigmake ~/RE/igs/libc.pat ~/RE/igs/libc2.3.2.o2.sig /Applications/IDA\ Professional\ 9.1.app/Contents/MacOS/tools/flair/pelf ~/RE/igs/libc.a ~/RE/igs/libc.pat目前不知道到底是基于什么GCC和GLibc改的,考虑到后面可能有很多程序也会用到glibc,所以需要确定是什么版本,看看有没有快速恢复符号的办法。
依赖关系分析使用我五年前开发的YAFAF,可以很快找到相关的依赖关系。rc*.d应该是游戏的代码。
rc0.d
GLIBC_2.1 GLIBC_2.0 GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)rc2.d
GCC_3.0 GLIBC_2.0 GLIBC_2.1 GLIBC_2.2.3 GLIBC_2.1.3 GLIBC_2.3 GLIBC_2.2 GLIBC_2.3.2 GLIBC_2.0 GLIBC_2.1 GLIBCPP_3.2 GLIBC_2.2 GLIBC_2.1.3 GLIBC_2.3 GLIBC_2.3.2rc9
GCC: (GNU) 3.3.1 GCC: (GNU) 3.2.1 20021207 (Red Hat Linux 8.0 3.2.1-2) GCC: (GNU) 3.2.1 20030202 (Red Hat Linux 8.0 3.2.1-7) GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-4) GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)从这些特征来看,这几个文件,应该是多个不同环境编译的elf文件的碎片构成。
并且/sbin/init也是基于glibc2.3.X,所以用gcc3.2.2编译glibc2.3.2吧,基于centos3,编译这一版GCC,不能用并发,否则会出错。还好我以前编译openwrt遇到过类似错误,不然又要卡很久,搜都搜不到是啥原因。
../gcc-3.2.2/configure --prefix=/opt/gcc_3.2.2 --infodir=/usr/share/info --enable-shared --enable-threads=posix --disable-checking --with-system-zlib --enable-__cxa_atexit make make install编译GLibc,不管是用O2还是O3,最后几乎都没有内联函数优化。目前还搞不明白是什么原因,也搜不到,问AI也没用。
CC=/opt/gcc_3.2.2/bin/gcc CFLAGS="-march=pentium4 -O2" ../glibc-2.3.2/configure --prefix=/lib --disable-profile --enable-add-ons --libexecdir=/usr/lib --with-headers=/usr/include CC=/opt/gcc_3.2.2/bin/gcc CFLAGS="-O3" ../glibc-2.3.2/configure --prefix=/lib --disable-profile --enable-add-ons --libexecdir=/usr/lib --with-headers=/usr/include 恢复分析 ZSH 符号我不喜欢做机械而重复的工作,如果要我直接逆向zsh,我会觉得非常无聊。 这个版本ida对识别instruments不是太好,很多地方需要手动恢复。用下图脚本恢复完后,下一步就是恢复function entry,在我上一篇文章有类似的脚本。
def find_and_make_instrument(start_ea, end_ea): image_base = idaapi.get_imagebase() current_ea = start_ea found_count = 0 while current_ea < end_ea: if current_ea == ida_idaapi.BADADDR: break address_flags = ida_bytes.get_flags(current_ea) if ida_bytes.is_code(address_flags): current_ea += ida_bytes.get_item_size(current_ea) continue else: ida_bytes.del_items(0x8052606, 0, 1) if ida_ua.can_decode(current_ea): print("Decode instruments at 0x{:X}".format(current_ea)) insn_size = ida_ua.decode_insn(ida_ua.insn_t(), current_ea) ida_bytes.del_items(current_ea, 0, insn_size) offset = ida_ua.create_insn(current_ea) if offset > 0: found_count += 1 if idc.get_func_flags(current_ea) != -1: current_ea += offset continue else: print("Decode instruments failed at 0x{:X}".format(current_ea)) return else: print("Create instruments failed at 0x{:X}".format(current_ea)) current_ea += 1 continue print("Search finished, {} instruments created".format(found_count)) find_and_make_instrument(0x080480B4, 0x0808F000)因为没有办法用glibc的signature,我想了一个比较傻的办法,但是速度比调用MCP分析要快。
先把文件A(自己编译的)的glibc字符串完全恢复,恢复函数入口。 在文件B,先修复函数入口,修复立即数偏移,修复字符串,去重,去除过短字符串,然后逐个匹配文件A的字符串,并筛选。 从筛选结果遍历交叉引用,将不重复且为函数的筛选出来。
- 如果文件A的这些函数有符号,就恢复到文件B;
- 字符串作为参数入栈的函数,找到当前函数空间的所有call,如果目标地址没有符号,也用这个办法去还原入栈的函数符号。
- 递归设置符号,比如恢复了这个call的函数,然后再去call函数内部寻找其他的函数。这个感觉没什么必要,因为会遇到很多情况。
这些这几个脚本比较糙,还需打磨,主要是现在够用了。而且可能作用不是很大,因为做了这几项优化后,很快就能看出这几个ELF,实际上没有魔改Glibc,只是静态链接了而已。并且都是用 libc_start_main 来启动主函数。
修复GECA和rc0.d从 /dev/hdc1 的 0x1B44 * 512 位置读取 0x400 字节,这个是一个ELF Header,写入到 /mnt/head 里。
IGS应该是把这个ELF头藏在了FAT分区的空白处。因为分区是连续的,无法直接从分区布局看出藏了内容。
从 /bin/arch 的末尾的位置读取 0x400 字节,写入到 /mnt/GECA 里。
然后将 /etc/init.d/rc0.d 追加到 /mnt/GECA 末尾
修复游戏文件在我开始分析之前,Nova 把他和 BatteryShark 研究的进度分享给我了,他们已经将 Speed Driver 2 的ELF还原,但是这个ELF还是有问题的,并且不能用在其他游戏,比如Percussion Master 2008。 IGS Dump EXEC
因此还是要自己动手,恰好我也要逆向分析还原游戏文件的代码。
在内核启动阶段,GECA 被 zsh 修复后,立刻就会用相同的环境变量和参数运行。
rc* 的拼接顺序,是通过一个字符串变量来设定的,每个游戏都不一样。
这个数字字符,刚好对应 /etc/rc.d 的文件顺序
2 1 3 5 8 4 7 6 9GECA 代码运行过程:先将/etc/rc.d的碎片,根据不同顺序来设置剪切尺寸,输出到/mnt目录
dd if=/etc/rc.d/rc2.d of=/mnt/rc2 bs=1K count=[file_size / 1024 - 400] &> /dev/null dd if=/etc/rc.d/rc1.d of=/mnt/rc1 bs=1K count=[file_size / 1024 - 400 + 7] &> /dev/null dd if=/etc/rc.d/rc3.d of=/mnt/rc3 bs=1K count=[file_size / 1024 - 400 + 14] &> /dev/null dd if=/etc/rc.d/rc5.d of=/mnt/rc5 bs=1K count=[file_size / 1024 - 400 + 21] &> /dev/null dd if=/etc/rc.d/rc8.d of=/mnt/rc8 bs=1K count=[file_size / 1024 - 400 + 28] &> /dev/null dd if=/etc/rc.d/rc4.d of=/mnt/rc4 bs=1K count=[file_size / 1024 - 400 + 35] &> /dev/null dd if=/etc/rc.d/rc7.d of=/mnt/rc7 bs=1K count=[file_size / 1024 - 400 + 42] &> /dev/null dd if=/etc/rc.d/rc6.d of=/mnt/rc6 bs=1K count=[file_size / 1024 - 400 + 49] &> /dev/null dd if=/etc/rc.d/rc9.d of=/mnt/rc9 bs=1 count=[file_size - 400 * 1024] &> /dev/null然后挂载ramfs在/exec,将这些文件拼接成真正的游戏文件,替换掉原本的硬盘破坏程序。 接下来的启动过程就符合前面的 /etc/X11/IGS 了。
mount -n -t ramfs GameExecution /exec &> /dev/null cat /mnt/head /mnt/rc2 /mnt/rc1 /mnt/rc3 /mnt/rc5 /mnt/rc8 /mnt/rc4 /mnt/rc7 /mnt/rc6 /mnt/rc9 > /exec/PM2008v2 && chmod 777 /exec/PM2008v2 &> /dev/null # 删除临时文件 umount /mnt &>/dev/null rm -rf /mnt &>/dev/null可以编写一个脚本来实现这个过程
#!/usr/bin/env python3 import os import argparse def main(): parser = argparse.ArgumentParser(description='Recover game from IGS E2000 platform') parser.add_argument('head_file', type=str, help='head file to read') parser.add_argument('rc_dir', type=str, help='game parts dir to read') parser.add_argument('game_file', type=str, help='game file to write') args = parser.parse_args() rc_order = "213584769" rc_order_len = len(rc_order) block_num = 400 ignore_count = 7 step_type = 2 head = open(args.head_file, 'rb').read() with open(args.game_file, 'wb') as game_fd: game_fd.write(head) for i in range(rc_order_len): rc_file_path = os.path.join(args.rc_dir, f"rc{rc_order[i]}.d") rc_data = open(rc_file_path, 'rb').read() rc_data_size = len(rc_data) write_size = rc_data_size - block_num * 1024 print(f"rc{rc_order[i]}.d size: 0x{rc_data_size:08X}, write 0x{write_size:08X} bytes to game file, write block {int(write_size/1024)} skip block_num: {block_num}") # head = head + rc_data[0:write_size] if step_type == 2: block_num -= ignore_count else: block_num += ignore_count game_fd.write(rc_data[0:write_size]) if __name__ == "__main__": main() (base) ➜ python ./recover_game.py ./head.img ./part2/etc/rc.d ./pm2008_game rc2.d size: 0x00080000, write 0x0001C000 bytes to game file, write block 112 skip block_num: 400 rc1.d size: 0x00080000, write 0x0001DC00 bytes to game file, write block 119 skip block_num: 393 rc3.d size: 0x00080000, write 0x0001F800 bytes to game file, write block 126 skip block_num: 386 rc5.d size: 0x00080000, write 0x00021400 bytes to game file, write block 133 skip block_num: 379 rc8.d size: 0x00080000, write 0x00023000 bytes to game file, write block 140 skip block_num: 372 rc4.d size: 0x00080000, write 0x00024C00 bytes to game file, write block 147 skip block_num: 365 rc7.d size: 0x00080000, write 0x00026800 bytes to game file, write block 154 skip block_num: 358 rc6.d size: 0x00080000, write 0x00028400 bytes to game file, write block 161 skip block_num: 351 rc9.d size: 0x003CA6D8, write 0x003746D8 bytes to game file, write block 3537 skip block_num: 344修复后,虽然避开了多个版本的glibc特征,但ELF可能还有问题,因为每个游戏的主程序恢复算法还是有略微差异。PM2008的恢复逻辑输入就比SD2多两个参数,先分析PM2008吧。
还需要动态分析一下内存里的ELF文件是怎么装载的,但是现在发现,在游戏机接网线或者键盘,就会自动死机,不知道是不是安全机制。下一篇将分析如何在设备上动态调试。
IGS Arcade 逆向系列(二)- 游戏文件恢复
上一篇文章写到游戏有个破坏分区的保护机制,本篇将深入分析。
作为2007年发布的游戏,此游戏加固机制还是比较落后的,主要是靠一些拼接,修改特征的方法来加固。并没有现代APP加固的那么卷的特性。主要是加固的环节有点多,并且每个游戏都不一样。然后由于开发上的特性,(编译优化,代码风格),使得逆向分析变麻烦了。
要提取游戏其实比较简单,等游戏运行时通过shell从内存或者从文件系统(如果文件落地)dump就行了。但是如果要提取不同游戏,这样就太麻烦了,还是先静态分析吧。
总之,这个逆向过程像是在做MISC题一样,需要一些逻辑推理。
逆向陷阱一般情况下,分析文件系统的内容,很少会先从内核入手,一般先看init相关文件。 第一步一般都是看 /etc/inittab,脚本首先启动rc,然后启动图形界面
# Begin /etc/inittab id:4:initdefault: si::sysinit:/etc/rc.d/init.d/rc x:4:respawn:/etc/X11/IGS &> /dev/null # End /etc/inittab /etc/rc.d/init.d/rc #!/bin/bash PATH=/bin:/sbin:/usr/bin:/usr/sbin export PATH mount -n -o remount,rw / mount -n -t ramfs tmp /tmp mount -n -t proc proc /proc mount -n -t usbdevfs usbdevfs /proc/bus/usb #echo "copy for etc" cp -a /etc/* /tmp mount -n -t ramfs etc /etc cp -a /tmp/* /etc rm -rf /tmp/* #echo "copy for dev" cp -a /dev/* /tmp mount -n -t ramfs dev /dev cp -a /tmp/* /dev rm -rf /tmp/* mount -n -t devpts pts /dev/pts mount -n -t tmpfs shm /dev/shm #echo "copy for var" cp -a /var/* /tmp mount -n -t ramfs var /var cp -a /tmp/* /var rm -rf /tmp/* #echo "copy for root" cp -a /root/.b* /tmp mount -n -t ramfs root /root cp -a /tmp/.b* /root rm -rf /tmp/.b* /sbin/hdparm -c1 -d1 -k1 -Xudma4 /dev/hdc &> /dev/null /etc/X11/IGS配置环境变量,启动X,然后启动读卡器,最后启动游戏,除了这个循环有一点反常,其他都非常正常。到这个时候肯定就认为/PM2008v2/PM2008v2是游戏本体了
#!/bin/sh TZ="UCT" TERM="xterm" TempFile="/tmp/XTemp" HZ="100" PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin LD_LIBRARY_PATH=/usr/X11R6/lib:/usr/X11R6/lib/modules/extensions DISPLAY=:0 export PATH LD_LIBRARY_PATH DISPLAY TERM HZ TZ ps -A | grep XFree86 | ( while read pid tty time command; do kill -9 $pid; done ) XFree86 &> /dev/null& mwm &> /dev/null & /usr/X11R6/bin/xsetroot -cursor /usr/X11R6/bitmaps/empty_ptr /usr/X11R6/bitmaps/empty_ptr if [ -f $TempFile ];then rm -rf $TempFile sleep 10 exit 0 else touch $TempFile fi /etc/rc.d/init.d/cardreader &> /dev/null& export TZ="CST" #Run Game cd /PM2008v2 while [ 1 ] do ./PM2008v2 &> /dev/null sleep 5 done接下来分析 PM2008v2,第一眼看上去,里面有很多程序装载的代码。联想到之前有很多文件没有magic,猜测可能是拿来动态加载代码文件还原成elf的。但是后来 Nova 和我说这东西不是游戏,我才知道了它的异常。这个文件有很多glibc特征,感觉就是一个静态链接glibc的程序。
为了方便逆向和后期的游戏移植,我需要确认GCC和GLibc版本,PM2008v2: GCC: (GNU) 3.3.1,但是没有GLibc的版本信息。 因此我直接找到系统的libc.so,版本暂定为glibc 2.3.2。在最新的linux下不好编译,docker下也出了问题。
分析伪游戏程序 编译 GCC 3.3.1由于目标版本内核是i686的,我使用CentOS4编译,运行在VMware,为了保证优化之后的汇编代码一致,我要使用相同GCC的版本,然后编译。并且搭建这个环境,也能方便以后对辅助分析该平台其他游戏,也有一些帮助。前期准备工作多一点,后期就可以少走一些弯路。
wget http://mirrors.aliyun.com/gnu/gcc/gcc-3.3.1/gcc-3.3.1.tar.gz使用阿里云的vault源,先安装开发环境依赖。
yum groupinstall "Development Tools"使用下列配置,指定版本为i686,旧版的gcc,最好不要并发编译,可能会出问题(3.3.1没有遇到,3.2.2遇到了),然后删除系统自带的gcc,再安装。
../gcc-3.3.1/configure --prefix=/opt/gcc_3.3.1 --infodir=/usr/share/info --enable-shared --enable-threads=posix --disable-checking --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --host=i686-pc-gnu-linux --build=i686-pc-linux-gnu --target=i686-pc-linux-gnu make -j8 yum remove gcc make install 编译 Glibc 2.3.2 wget http://mirrors.aliyun.com/gnu/glibc/glibc-2.3.2.tar.gz wget http://mirrors.aliyun.com/gnu/glibc/glibc-linuxthreads-2.3.2.tar.gzlinuxthreads要解压到glibc目录
tar -zxvf glibc-2.3.2.tar.gz cd glibc-2.3.2 tar -zxvf ../glibc-linuxthreads-2.3.2.tar.gzGCC 2.3.2编译可能会遇到一些bug,需要打patch,刚好E2000平台的Linux也是LFS版本,可以去这里下载 LFS Glibc patches
patch -p1 < ../patches/glibc-2.3.2-sscanf-1.patch patch -p1 < ../patches/glibc-2.3.2-inlining_fixes-2.patch patch -p1 < ../patches/glibc-2.3.2-test_lfs-1.patch接下来配置编译选项,先暂时这么用吧,因为即使设置细分的优化选项,最后的汇编内容都和目标文件差异很大。
CC=/opt/gcc_3.3.1/bin/gcc CFLAGS="-march=pentium4 -O2" ../glibc-2.3.2/configure --prefix=/lib --disable-profile --enable-add-ons --libexecdir=/usr/lib --with-headers=/usr/include差异项:
- 栈帧:目标程序大部分的函数返回都是 0xC9 leave,而我编译的都是先move esp, ebp,然后pop ebp。
- 内链函数:目标程序函数内部的call,一些中等长度的函数,会被优化成内联函数。
上述内容,即使我将编译优化级别设为O3,也几乎没有变化。手动配置fomit-frame-pointer、-finline-limit=n等信息,也没用。也许需要手动设置__inline__吧,我没时间去验证。 这个情况,用flare生成signature,几乎还原不了那种函数内部带有call的符号。
经过分析,PM2008v2这个程序,就是一个killdisk函数,静态编译了glibc。
因此,如果执行inittab,是不可能启动游戏的。
系统初始化分析 网友的逆向成果Nova之前告诉了我一些信息,实际上,仅从这个笔记,我看不出具体的加载流程,只能看出文件头需要还原,然后rc.0实际上是elf文件,启动时会执行。而且Submarine Crisis游戏文件和我的PM2008游戏文件又一些差异。
https://github.com/batteryshark/igstools/blob/main/scripts/igs_rofsv1_dumpexec.py
这个脚本是用于还原游戏文件的,我尝试了一下,可以生成一个ELF,但是放到IDA分析会出错。我并不知道生成的文件要如何运行。因此还是需要自己分析一遍。
分析内核启动过程通过分析依赖和其他环境变量,并没有找到任何能启动游戏的路径。在上一篇文章,分析了文件系统挂载,在挂载之后,还有一系列操作。IDA遇到长度过大的函数,可能就会出错,无法反编译。但是问题不大,麻烦的不在这里。
上一篇文章由于是分析基于开源代码魔改的filesystem,可以直接对照逆向,因此不还原其他符号,问题也不大。该内核版本是2.4,bzImage没有携带符号表。这里的代码很多是IGS自己开发的,有些系统调用不是通过int来调用的。如果有用到syscall,在符号没还原的情况下分析,还是有一些麻烦,如果用bindiff,就需要在ida 8下用。我没有时间去移植到Mac上。Linux Kernel有一个syscall table,如果IGS没有自定义过syscall,那么可以直接将自己编译的kernel的syscall 符号照搬过来。
另外,IDA9对这个老的Linux 2.4内核解析不太好,很多交叉引用和汇编指令都没有识别出来,需要手动修复。
修复立即数交叉引用 import ida_ida import ida_bytes import ida_ua import idautils def find_immediate_values_and_convert_to_offset(start_range, end_range): converted_count = 0 checked_count = 0 min_ea = ida_ida.inf_get_min_ea() max_ea = ida_ida.inf_get_max_ea() print(f"EA range: 0x{start_range:X} - 0x{end_range:X}") print(f"Immediate Value Range:0x{min_ea:X} - 0x{max_ea:X}") for ea in idautils.Heads(): if not ida_bytes.is_code(ida_bytes.get_flags(ea)): continue insn = ida_ua.insn_t() if ida_ua.decode_insn(insn, ea) == 0: continue if start_range <= ea and ea <= end_range: for op_num in range(ida_ida.UA_MAXOP): op = insn.ops[op_num] if op.type == ida_ua.o_void: break if op.type == ida_ua.o_imm: imm_value = op.value if op.value > 0xFFFFFFFF: imm_value = (0xFFFFFFFF & op.value) checked_count += 1 if min_ea <= imm_value and imm_value <= max_ea: if idc.op_offset(ea, op_num, REF_OFF32): converted_count += 1 else: print(f" -> Convert Failed: 0x{ea:X}[{op_num}]") print(f"Immediate Value: {checked_count}, Converted: {converted_count}") 修复字符串数据可能是因为交叉引用没有识别完全,字符串识别总是会错失前面几个bytes,需要手动修复字符串。
def get_the_firsstr_ea(ea): addr = ea - 1 last_byte = ida_bytes.get_byte(addr) if 32 < last_byte and last_byte < 127: ea = get_the_firsstr_ea(addr) return ea def find_str_address(start_ea, end_ea): current_ea = start_ea found_count = 0 while current_ea < end_ea: if current_ea == ida_idaapi.BADADDR: break address_flags = ida_bytes.get_flags(current_ea) if ida_bytes.is_strlit(address_flags): str_size = ida_bytes.get_item_size(current_ea) the_first_str_addr = get_the_firsstr_ea(current_ea) if the_first_str_addr != current_ea: len = current_ea - the_first_str_addr + str_size ida_bytes.create_strlit(the_first_str_addr, len, 0) print(f"Fix str at 0x{current_ea:X}, before: {str_size}, after: {len}") current_ea += ida_bytes.get_item_size(current_ea) continue Kernel Thread根据上图代码,可以得知游戏初始化的第一步是先运行/bin/zsh,并且携带这些参数。
export HOME=/ export TERM=linux export PATH=/bin:/usr/bin:/sbin:/usr/sbin /bin/zsh /etc/rc.d/rc0.d __KERNEL__ -no-print -PM2008v2下一步就是运行/mnt/GECA文件
export HOME=/ export TERM=linux export PATH=/bin:/usr/bin:/sbin:/usr/sbin /mnt/GECA /etc/rc.d/rc0.d __KERNEL__ -no-print -PM2008v2最后运行这些
if ( execute_command ) run_init_process((const char *)execute_command); run_init_process("/sbin/init"); // 存在 run_init_process("/etc/init"); // 不存在 run_init_process("/bin/init"); // 不存在 run_init_process("/bin/sh"); // 指向bashKernel Cmdline可以在parse_cmdline_early找到,并非LILO控制。
如果从外部设置了bootcmdline,其中有一个暗桩,会检测bootloader参数是否等于”JBoot“,如果不等于,就进入死循环。
这个JBoot是不是代表James写的Bootloader?👀
bootloader=JBoot内核自带的启动命令
root=/dev/hdc2 ro console=ttyS1,115200 BOOT_IMAGE=PM2008v2并没有设置init=,因此肯定会执行/sbin/init,然后执行/etc/inittab
恢复ZSH符号受阻线索到了/bin/zsh,这个程序入口和PM2008v2一样,我判断也是基于glibc改的,但是我导入FLIRT Signature,只能识别出最里层的函数,还是那个编译优化的原因。
/Applications/IDA\ Professional\ 9.1.app/Contents/MacOS/tools/flair/sigmake ~/RE/igs/libc.pat ~/RE/igs/libc2.3.2.o2.sig /Applications/IDA\ Professional\ 9.1.app/Contents/MacOS/tools/flair/pelf ~/RE/igs/libc.a ~/RE/igs/libc.pat目前不知道到底是基于什么GCC和GLibc改的,考虑到后面可能有很多程序也会用到glibc,所以需要确定是什么版本,看看有没有快速恢复符号的办法。
依赖关系分析使用我五年前开发的YAFAF,可以很快找到相关的依赖关系。rc*.d应该是游戏的代码。
rc0.d
GLIBC_2.1 GLIBC_2.0 GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)rc2.d
GCC_3.0 GLIBC_2.0 GLIBC_2.1 GLIBC_2.2.3 GLIBC_2.1.3 GLIBC_2.3 GLIBC_2.2 GLIBC_2.3.2 GLIBC_2.0 GLIBC_2.1 GLIBCPP_3.2 GLIBC_2.2 GLIBC_2.1.3 GLIBC_2.3 GLIBC_2.3.2rc9
GCC: (GNU) 3.3.1 GCC: (GNU) 3.2.1 20021207 (Red Hat Linux 8.0 3.2.1-2) GCC: (GNU) 3.2.1 20030202 (Red Hat Linux 8.0 3.2.1-7) GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-4) GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)从这些特征来看,这几个文件,应该是多个不同环境编译的elf文件的碎片构成。
并且/sbin/init也是基于glibc2.3.X,所以用gcc3.2.2编译glibc2.3.2吧,基于centos3,编译这一版GCC,不能用并发,否则会出错。还好我以前编译openwrt遇到过类似错误,不然又要卡很久,搜都搜不到是啥原因。
../gcc-3.2.2/configure --prefix=/opt/gcc_3.2.2 --infodir=/usr/share/info --enable-shared --enable-threads=posix --disable-checking --with-system-zlib --enable-__cxa_atexit make make install编译GLibc,不管是用O2还是O3,最后几乎都没有内联函数优化。目前还搞不明白是什么原因,也搜不到,问AI也没用。
CC=/opt/gcc_3.2.2/bin/gcc CFLAGS="-march=pentium4 -O2" ../glibc-2.3.2/configure --prefix=/lib --disable-profile --enable-add-ons --libexecdir=/usr/lib --with-headers=/usr/include CC=/opt/gcc_3.2.2/bin/gcc CFLAGS="-O3" ../glibc-2.3.2/configure --prefix=/lib --disable-profile --enable-add-ons --libexecdir=/usr/lib --with-headers=/usr/include 恢复分析 ZSH 符号我不喜欢做机械而重复的工作,如果要我直接逆向zsh,我会觉得非常无聊。 这个版本ida对识别instruments不是太好,很多地方需要手动恢复。用下图脚本恢复完后,下一步就是恢复function entry,在我上一篇文章有类似的脚本。
def find_and_make_instrument(start_ea, end_ea): image_base = idaapi.get_imagebase() current_ea = start_ea found_count = 0 while current_ea < end_ea: if current_ea == ida_idaapi.BADADDR: break address_flags = ida_bytes.get_flags(current_ea) if ida_bytes.is_code(address_flags): current_ea += ida_bytes.get_item_size(current_ea) continue else: ida_bytes.del_items(0x8052606, 0, 1) if ida_ua.can_decode(current_ea): print("Decode instruments at 0x{:X}".format(current_ea)) insn_size = ida_ua.decode_insn(ida_ua.insn_t(), current_ea) ida_bytes.del_items(current_ea, 0, insn_size) offset = ida_ua.create_insn(current_ea) if offset > 0: found_count += 1 if idc.get_func_flags(current_ea) != -1: current_ea += offset continue else: print("Decode instruments failed at 0x{:X}".format(current_ea)) return else: print("Create instruments failed at 0x{:X}".format(current_ea)) current_ea += 1 continue print("Search finished, {} instruments created".format(found_count)) find_and_make_instrument(0x080480B4, 0x0808F000)因为没有办法用glibc的signature,我想了一个比较傻的办法,但是速度比调用MCP分析要快。
先把文件A(自己编译的)的glibc字符串完全恢复,恢复函数入口。 在文件B,先修复函数入口,修复立即数偏移,修复字符串,去重,去除过短字符串,然后逐个匹配文件A的字符串,并筛选。 从筛选结果遍历交叉引用,将不重复且为函数的筛选出来。
- 如果文件A的这些函数有符号,就恢复到文件B;
- 字符串作为参数入栈的函数,找到当前函数空间的所有call,如果目标地址没有符号,也用这个办法去还原入栈的函数符号。
- 递归设置符号,比如恢复了这个call的函数,然后再去call函数内部寻找其他的函数。这个感觉没什么必要,因为会遇到很多情况。
这些这几个脚本比较糙,还需打磨,主要是现在够用了。而且可能作用不是很大,因为做了这几项优化后,很快就能看出这几个ELF,实际上没有魔改Glibc,只是静态链接了而已。并且都是用 libc_start_main 来启动主函数。
修复GECA和rc0.d从 /dev/hdc1 的 0x1B44 * 512 位置读取 0x400 字节,这个是一个ELF Header,写入到 /mnt/head 里。
IGS应该是把这个ELF头藏在了FAT分区的空白处。因为分区是连续的,无法直接从分区布局看出藏了内容。
从 /bin/arch 的末尾的位置读取 0x400 字节,写入到 /mnt/GECA 里。
然后将 /etc/init.d/rc0.d 追加到 /mnt/GECA 末尾
修复游戏文件在我开始分析之前,Nova 把他和 BatteryShark 研究的进度分享给我了,他们已经将 Speed Driver 2 的ELF还原,但是这个ELF还是有问题的,并且不能用在其他游戏,比如Percussion Master 2008。 IGS Dump EXEC
因此还是要自己动手,恰好我也要逆向分析还原游戏文件的代码。
在内核启动阶段,GECA 被 zsh 修复后,立刻就会用相同的环境变量和参数运行。
rc* 的拼接顺序,是通过一个字符串变量来设定的,每个游戏都不一样。
这个数字字符,刚好对应 /etc/rc.d 的文件顺序
2 1 3 5 8 4 7 6 9GECA 代码运行过程:先将/etc/rc.d的碎片,根据不同顺序来设置剪切尺寸,输出到/mnt目录
dd if=/etc/rc.d/rc2.d of=/mnt/rc2 bs=1K count=[file_size / 1024 - 400] &> /dev/null dd if=/etc/rc.d/rc1.d of=/mnt/rc1 bs=1K count=[file_size / 1024 - 400 + 7] &> /dev/null dd if=/etc/rc.d/rc3.d of=/mnt/rc3 bs=1K count=[file_size / 1024 - 400 + 14] &> /dev/null dd if=/etc/rc.d/rc5.d of=/mnt/rc5 bs=1K count=[file_size / 1024 - 400 + 21] &> /dev/null dd if=/etc/rc.d/rc8.d of=/mnt/rc8 bs=1K count=[file_size / 1024 - 400 + 28] &> /dev/null dd if=/etc/rc.d/rc4.d of=/mnt/rc4 bs=1K count=[file_size / 1024 - 400 + 35] &> /dev/null dd if=/etc/rc.d/rc7.d of=/mnt/rc7 bs=1K count=[file_size / 1024 - 400 + 42] &> /dev/null dd if=/etc/rc.d/rc6.d of=/mnt/rc6 bs=1K count=[file_size / 1024 - 400 + 49] &> /dev/null dd if=/etc/rc.d/rc9.d of=/mnt/rc9 bs=1 count=[file_size - 400 * 1024] &> /dev/null然后挂载ramfs在/exec,将这些文件拼接成真正的游戏文件,替换掉原本的硬盘破坏程序。 接下来的启动过程就符合前面的 /etc/X11/IGS 了。
mount -n -t ramfs GameExecution /exec &> /dev/null cat /mnt/head /mnt/rc2 /mnt/rc1 /mnt/rc3 /mnt/rc5 /mnt/rc8 /mnt/rc4 /mnt/rc7 /mnt/rc6 /mnt/rc9 > /exec/PM2008v2 && chmod 777 /exec/PM2008v2 &> /dev/null # 删除临时文件 umount /mnt &>/dev/null rm -rf /mnt &>/dev/null可以编写一个脚本来实现这个过程
#!/usr/bin/env python3 import os import argparse def main(): parser = argparse.ArgumentParser(description='Recover game from IGS E2000 platform') parser.add_argument('head_file', type=str, help='head file to read') parser.add_argument('rc_dir', type=str, help='game parts dir to read') parser.add_argument('game_file', type=str, help='game file to write') args = parser.parse_args() rc_order = "213584769" rc_order_len = len(rc_order) block_num = 400 ignore_count = 7 step_type = 2 head = open(args.head_file, 'rb').read() with open(args.game_file, 'wb') as game_fd: game_fd.write(head) for i in range(rc_order_len): rc_file_path = os.path.join(args.rc_dir, f"rc{rc_order[i]}.d") rc_data = open(rc_file_path, 'rb').read() rc_data_size = len(rc_data) write_size = rc_data_size - block_num * 1024 print(f"rc{rc_order[i]}.d size: 0x{rc_data_size:08X}, write 0x{write_size:08X} bytes to game file, write block {int(write_size/1024)} skip block_num: {block_num}") # head = head + rc_data[0:write_size] if step_type == 2: block_num -= ignore_count else: block_num += ignore_count game_fd.write(rc_data[0:write_size]) if __name__ == "__main__": main() (base) ➜ python ./recover_game.py ./head.img ./part2/etc/rc.d ./pm2008_game rc2.d size: 0x00080000, write 0x0001C000 bytes to game file, write block 112 skip block_num: 400 rc1.d size: 0x00080000, write 0x0001DC00 bytes to game file, write block 119 skip block_num: 393 rc3.d size: 0x00080000, write 0x0001F800 bytes to game file, write block 126 skip block_num: 386 rc5.d size: 0x00080000, write 0x00021400 bytes to game file, write block 133 skip block_num: 379 rc8.d size: 0x00080000, write 0x00023000 bytes to game file, write block 140 skip block_num: 372 rc4.d size: 0x00080000, write 0x00024C00 bytes to game file, write block 147 skip block_num: 365 rc7.d size: 0x00080000, write 0x00026800 bytes to game file, write block 154 skip block_num: 358 rc6.d size: 0x00080000, write 0x00028400 bytes to game file, write block 161 skip block_num: 351 rc9.d size: 0x003CA6D8, write 0x003746D8 bytes to game file, write block 3537 skip block_num: 344修复后,虽然避开了多个版本的glibc特征,但ELF可能还有问题,因为每个游戏的主程序恢复算法还是有略微差异。PM2008的恢复逻辑输入就比SD2多两个参数,先分析PM2008吧。
还需要动态分析一下内存里的ELF文件是怎么装载的,但是现在发现,在游戏机接网线或者键盘,就会自动死机,不知道是不是安全机制。下一篇将分析如何在设备上动态调试。
IGS Arcade 逆向系列(一)- E2000平台分析
IGS Arcade 逆向系列(一)- E2000平台分析
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 - A11 BIOS芯片 SST 49LF004B(PLCC32)
- I - IH-C02 ALTERA EPM3032ALC44-10N CPLD (PLCC44), 控制器,里面有ROM,用途不详
- J - IGS EV29LV640-90PCR 8MB EEPROM,DIP 48-Pin 封装,IGS定制的芯片。这个芯片专门贴了游戏名称的标签,说明BIOS ROM 也和游戏有关。可能会有跟V21芯片相关的内容。
- K - V21,IGS036E,也许是一颗FPGA或者ASIC,用于加密处理控制信号的输入输出
- L - 64K x 16 HIGH-SPEED CMOS STATIC RAM x3
- M - CF卡:ADATA 2GB (266X)
- N - IDE硬盘接口
我有很多种办法拿硬件的root shell,但我对游戏加载的过程更感兴趣,因此先做软件逆向分析吧。
I/O板分析这几块板子又称控制板,应该是用于连接游戏控制器,比如方向盘、刹车油门、投币器之类的。
通过连接主板的25pin + 30pin 的I/O口 进行通信
价格可能比主机本体还贵
应该可以使用逻辑分析仪来捕获这些传感器信号,然后实现控制功能。但据说控制信号由V21芯片(ASIC)处理,可能会加密。我也不打算去仿制一个控制板。这种街机游戏没有地平线5好玩。现在电玩城也便宜,真想玩的话,就直接花钱去电玩城,或者买一套方向盘在家里玩。
文件系统分析直接dump CF卡,文件大小2GB,file命令的回显如下:
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
+----------------------------------------+ | Master Boot Record Operating system | |----------------------------------------| | LILO ---------------> Linux | | ---> other OS | +----------------------------------------+通过fdisk查看分区信息。是Linux操作系统,四个分区,并首尾连续,最后一个分区之后的内容,都是0x00,不存在隐藏分区
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 LinuxE2000平台的分区有一个特点,第三分区和第四分区,顺序是反的。
第二分区和第四分区,7z无法完全提取,binwalk能识别出一些压缩区域,因此大概率是经过了魔改的文件系统。
每个分区的用途如下(最后我找到了分区2,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。显示了内核版本,但是这里不一定是正确的版本。
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.3LILO经过了IGS 定制开发,会显示,IGS Loader v2.0 Boot Menu 2007/04,可判断版本是lilo-22.8
这里没什么好分析的,直接开始提取vmlinux,内核文件是bzImage格式,需要解压
7z x ./SD2-OS-61P -okernel.decomped接下来就是普通的内核逆向,第一步计算基址,基址有很多种确定方法,可以参考我其他文章。
这里多看两眼就猜出来了,0xC0100000
基址配置正确,IDA就可以自动识别出一些交叉引用,剩下的需要手动还原。IDA 9.0修改了一些API,又得重新写一下脚本。
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魔改文件系统对应的原始文件系统版本
- rofs:cramfs
- shfs: squashfs 2.2
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魔改的部分。
对于这种找不到ramdisk或者rootfs的固件,第一步肯定是找到kernel里的sys_mount和boot parameters
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的逻辑,位于此处
prepare_namespace ->rd_load_disk ->rd_load_image ->identify_ramdisk_image在此次可以确定IGS 支持 squashfs 和 igs rofs 格式的ramdisk。并且可以确定它们的magic。
分析过程就不记录了,他们把 FS 的header魔改了,分析起来有一些恶心🤢。
提取文件系统我分析了 E2000 平台的三种自定义文件系统 header,内容如下。
IGS SHFS V1 Header 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 为例
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 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就用上加密了。
玩家 @novacosmic00 之前请我帮忙提取 GoGoBall 的文件,但他说他已经解密了其他游戏的文件。唯独 GoGoBall 解不了。用我修改的工具尝试提取,发现文件CRC不对,但还是能提取出大部分。
在我提取完固件之后,和玩家 @novacosmic00 沟通,才知道两年前有人研究过这个,并写了提取脚本。
https://github.com/batteryshark/igstools/tree/main/scripts
作者可能是根据文件系统内容,找出地址规律反推结构体的,这也是一种办法,但缺点是提取出来的文件可能比较混乱,而且无法检测CRC错误。
shadow-shadow文件,默认都清空了root密码
root:x:13130:0:99999:7::: test::13074:0:99999:7:::这是原始的文件
root:$1$qAw/5vcb$x9rPCAwLMdRBQXwlq1zG70:13130:0:99999:7::: test:$1$JjP1oLAJ$xilZIedv3S3jbs8oTZAad1:13074:0:99999:7::: 反破解/PM2008v2/PM2008v2,符号去除了,并且根据字符串特征,也像是正常的loader
但实际上,如果在自己电脑运行,他会把你分区前512k写0。我当时居然运行了,还好我没给root,而且硬盘是NVME的。谁能想到这个18年前的游戏,还有这一手🙈。以前从来没接触过这个领域,据说街机的反破解有很多自杀式逻辑。
下一篇文章将分析游戏内部的保护机制
参考IGS Arcade 逆向系列(一)- E2000平台分析
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 - A11 BIOS芯片 SST 49LF004B(PLCC32)
- I - IH-C02 ALTERA EPM3032ALC44-10N CPLD (PLCC44), 控制器,里面有ROM,用途不详
- J - IGS EV29LV640-90PCR 8MB EEPROM,DIP 48-Pin 封装,IGS定制的芯片。这个芯片专门贴了游戏名称的标签,说明BIOS ROM 也和游戏有关。可能会有跟V21芯片相关的内容。
- K - V21,IGS036E,也许是一颗FPGA或者ASIC,用于加密处理控制信号的输入输出
- L - 64K x 16 HIGH-SPEED CMOS STATIC RAM x3
- M - CF卡:ADATA 2GB (266X)
- N - IDE硬盘接口
我有很多种办法拿硬件的root shell,但我对游戏加载的过程更感兴趣,因此先做软件逆向分析吧。
I/O板分析这几块板子又称控制板,应该是用于连接游戏控制器,比如方向盘、刹车油门、投币器之类的。
通过连接主板的25pin + 30pin 的I/O口 进行通信
价格可能比主机本体还贵
应该可以使用逻辑分析仪来捕获这些传感器信号,然后实现控制功能。但据说控制信号由V21芯片(ASIC)处理,可能会加密。我也不打算去仿制一个控制板。这种街机游戏没有地平线5好玩。现在电玩城也便宜,真想玩的话,就直接花钱去电玩城,或者买一套方向盘在家里玩。
文件系统分析直接dump CF卡,文件大小2GB,file命令的回显如下:
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
+----------------------------------------+ | Master Boot Record Operating system | |----------------------------------------| | LILO ---------------> Linux | | ---> other OS | +----------------------------------------+通过fdisk查看分区信息。是Linux操作系统,四个分区,并首尾连续,最后一个分区之后的内容,都是0x00,不存在隐藏分区
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 LinuxE2000平台的分区有一个特点,第三分区和第四分区,顺序是反的。
第二分区和第四分区,7z无法完全提取,binwalk能识别出一些压缩区域,因此大概率是经过了魔改的文件系统。
每个分区的用途如下(最后我找到了分区2,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。显示了内核版本,但是这里不一定是正确的版本。
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.3LILO经过了IGS 定制开发,会显示,IGS Loader v2.0 Boot Menu 2007/04,可判断版本是lilo-22.8
这里没什么好分析的,直接开始提取vmlinux,内核文件是bzImage格式,需要解压
7z x ./SD2-OS-61P -okernel.decomped接下来就是普通的内核逆向,第一步计算基址,基址有很多种确定方法,可以参考我其他文章。
这里多看两眼就猜出来了,0xC0100000
基址配置正确,IDA就可以自动识别出一些交叉引用,剩下的需要手动还原。IDA 9.0修改了一些API,又得重新写一下脚本。
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魔改文件系统对应的原始文件系统版本
- rofs:cramfs
- shfs: squashfs 2.2
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魔改的部分。
对于这种找不到ramdisk或者rootfs的固件,第一步肯定是找到kernel里的sys_mount和boot parameters
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的逻辑,位于此处
prepare_namespace ->rd_load_disk ->rd_load_image ->identify_ramdisk_image在此次可以确定IGS 支持 squashfs 和 igs rofs 格式的ramdisk。并且可以确定它们的magic。
分析过程就不记录了,他们把 FS 的header魔改了,分析起来有一些恶心🤢。
提取文件系统我分析了 E2000 平台的三种自定义文件系统 header,内容如下。
IGS SHFS V1 Header 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 为例
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 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就用上加密了。
玩家 @novacosmic00 之前请我帮忙提取 GoGoBall 的文件,但他说他已经解密了其他游戏的文件。唯独 GoGoBall 解不了。用我修改的工具尝试提取,发现文件CRC不对,但还是能提取出大部分。
在我提取完固件之后,和玩家 @novacosmic00 沟通,才知道两年前有人研究过这个,并写了提取脚本。
https://github.com/batteryshark/igstools/tree/main/scripts
作者可能是根据文件系统内容,找出地址规律反推结构体的,这也是一种办法,但缺点是提取出来的文件可能比较混乱,而且无法检测CRC错误。
shadow-shadow文件,默认都清空了root密码
root:x:13130:0:99999:7::: test::13074:0:99999:7:::这是原始的文件
root:$1$qAw/5vcb$x9rPCAwLMdRBQXwlq1zG70:13130:0:99999:7::: test:$1$JjP1oLAJ$xilZIedv3S3jbs8oTZAad1:13074:0:99999:7::: 反破解/PM2008v2/PM2008v2,符号去除了,并且根据字符串特征,也像是正常的loader
但实际上,如果在自己电脑运行,他会把你分区前512k写0。我当时居然运行了,还好我没给root,而且硬盘是NVME的。谁能想到这个18年前的游戏,还有这一手🙈。以前从来没接触过这个领域,据说街机的反破解有很多自杀式逻辑。
下一篇文章将分析游戏内部的保护机制
参考VW ID.4 ICAS1 车控分析
VW ID.4 ICAS1 车控分析
VW ID.4 ICAS1 车控分析
2021年在360搭了一套ID.4台架环境,本来快要出成果了(拿到了ODIS内网权限,还有ICAS3的root),被安排出差去搭建展示车,后续工作计划被打乱。这期间不管是工作还是生活都遇到了许多糟心事,便停止了研究。
算是一个怨念项目吧,今年5月我在机缘巧合下又研究了ID.4的ICAS1的车控逻辑,还有一个怨念项目是CAN-Pick NG,写了一半又搁置了,不知道2025可不可以完成。
ID.4 是大众MEB平台,EE架构总共有两个域控制器,ICAS1, ICAS3。其中ICAS1(J533)与车身控制有关。 下图是J533的位置,位于序号13。将线控模块装载到此处,可以实现车身监测和控制。
车内ECU拓扑Classic CAN: 500k
CAN-FD:仲裁域500k, 数据域2M。若要分析CAN-FD,ZLG设备最合适。
通过分析 ELSA电路图 和 大众内部培训资料,发现总线命名混乱,必须重新整理才能得到正确的CAN拓扑图。
J533总共有9路CAN,4路LIN(暂不需要),关键ECU如下。
- Running-Gear CAN (CAN-FD)
- J104 - ABS
- J500 - EPS,转向助力控制单元
- NX6 - 制动控制器 (Brake Booster)
- Powertrain CAN (CAN-FD)
- J623 - 电机控制模块 Engine/Motor Control Module
- J841/J944 - 电驱单元
- J234 - 气囊 (不能在这里Fuzzing,危险)
- Driver Assistance CAN,CAN-FAS (CAN-FD)
- J428 - 车距调节控制单元
- J446 - 泊车雷达控制单元
- J769/J770 - 行驶换道辅助系统控制单元
- J928 - 全景摄像头控制单元
- Convenience CAN (Classic)
- J527 - 电控换档模块 (Steering Column Electronics Control Module)
- J605 - 行李箱盖控制单元
- J764 - 转向柱锁
- …其他车身控制
- CAN-EV (CAN-FD)
- J979 - 加热与空调控制模块 (Heater and Air Conditioning Control Module)
T40a 连接器和表格对应如下。
总线安全策略 网关隔离多功能方向盘控制信号,由Convenience CAN接收,但是底盘CAN的ECU也能收到。底盘CAN发出的多功能方向盘控制信号,不会被J533转发到目标ECU。
CRC 校验分析CAN总线变化,可以看到大部份报文的变化规律,第一字节随机,第二字节有规律递增。第一字节是CRC,第二字节是计数器。
CRC 初始值参考OpenDBC github /opendbc/can/common.cc volkswagen_mqb_checksum
可以获取VM MQB平台的 Checksum 初始值,但是ID.4有很多新的CRC初始值,我通过逆向分析并记录在下面了。
大部份控制信号有CRC和计数器,ECU不会校验某些不重要功能的CRC,比如天窗,车窗,鸣笛。但是会校验转向助力,行李箱盖开启,雨刮等影响驾驶的功能。
Signal CAN ID CRC Seed AAA_01 0x12DD5502 0x62,0x14,0x7c,0xa1,0x49,0x95,0x43,0x04,0x78,0x46,0x74,0x19,0x39,0x17,0x9f,0x1c ACC_18 0x14D 0x1a,0x65,0x81,0x96,0xc0,0xdf,0x11,0x92,0xd3,0x61,0xc6,0x95,0x8c,0x29,0x21,0xb5 Airbag_01 0x040 0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40 Airbag_02 0x520 0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44 APS_Master 0x380 0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13 AWV_03 0x0DB 0x09,0xfa,0xca,0x8e,0x62,0xd5,0xd1,0xf0,0x31,0xa0,0xaf,0xda,0x4d,0x1a,0x0a,0x97 BEM_06 0x48B 0x54,0xaf,0x8a,0xfb,0x0d,0x87,0x6a,0x0f,0x47,0x78,0x31,0x4f,0x35,0x28,0x82,0x6d Blinkmodi_02 0x366 0xa9,0xbd,0xfb,0x3c,0x95,0x0f,0x75,0x3a,0x4f,0x19,0x59,0x6d,0xb2,0xe9,0xd1,0x97 EA_01 0x1A4 0x69,0xbb,0x54,0xe6,0x4e,0x46,0x8d,0x7b,0xea,0x87,0xe9,0xb3,0x63,0xce,0xf8,0xbf EA_02 0x1F0 0x2f,0x3c,0x22,0x60,0x18,0xeb,0x63,0x76,0xc5,0x91,0x0f,0x27,0x34,0x04,0x7f,0x02 ELV_01 0x656 0xab,0x2f,0xd3,0x39,0x6f,0x37,0xfa,0x59,0xa4,0x70,0xce,0x11,0x54,0x82,0x62,0x56 EM1_01 0x0C0 0x2f,0x44,0x72,0xd3,0x07,0xf2,0x39,0x09,0x8d,0x6f,0x57,0x20,0x37,0xf9,0x9b,0xfa EML_02 0x1A555541 0x3e,0xb4,0x25,0xc1,0x31,0x1f,0xf1,0xd7,0xb1,0xbe,0xcc,0xe0,0x0f,0x46,0x51,0xb2 EML_06 0x20A 0x9d,0xe8,0x36,0xa1,0xca,0x3b,0x1d,0x33,0xe0,0xd5,0xbb,0x5f,0xae,0x3c,0x31,0x9f ESC_50 0x102 0xd7,0x12,0x85,0x7e,0x0b,0x34,0xfa,0x16,0x7a,0x25,0x2d,0x8f,0x04,0x8e,0x5d,0x35 ESC_51 0x0FC 0x77,0x5c,0xa0,0x89,0x4b,0x7c,0xbb,0xd6,0x1f,0x6c,0x4f,0xf6,0x20,0x2b,0x43,0xdd ESP_10 0x116 0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac ESP_20 0x65D 0xac,0xb3,0xab,0xeb,0x7a,0xe1,0x3b,0xf7,0x73,0xba,0x7c,0x9e,0x06,0x5f,0x02,0xd9 ESP_21 0x0FD 0xb4,0xef,0xf8,0x49,0x1e,0xe5,0xc2,0xc0,0x97,0x19,0x3c,0xc9,0xf1,0x98,0xd6,0x61 ESP_24 0x31B 0x67,0x8a,0xae,0x22,0x4d,0xd0,0x51,0x80,0x5c,0xb9,0xce,0x1e,0xdf,0x02,0x2d,0xd4 Getriebe_11 0x0AD 0x3f,0x69,0x39,0xdc,0x94,0xf9,0x14,0x64,0xd8,0x6a,0x34,0xce,0xa2,0x55,0xb5,0x2c GRA_ACC_01 0x12B 0x6a,0x38,0xb4,0x27,0x22,0xef,0xe1,0xbb,0xf8,0x80,0x84,0x49,0xc7,0x9e,0x1e,0x2b HCA_01 0x126 0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda HVL_01 0x12DD553D 0x1d,0x82,0x7b,0x79,0xa5,0xee,0x3a,0xb9,0xb7,0xf9,0xe4,0x67,0x7f,0x97,0x11,0xad IPA_01 0x138 0x77,0x4e,0x14,0x87,0xf2,0xf8,0xb2,0x61,0xf6,0xa4,0x52,0x94,0xd4,0x81,0x2a,0xb1 IPA_02 0x16A9545F 0xc6,0x7f,0x85,0xb6,0xe6,0xae,0xf8,0x26,0xb0,0x8c,0x19,0x10,0x5b,0x33,0x64,0x6c Klemmen_Status_01 0x3C0 0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3 LH_EPS_02 0x11D 0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c LH_EPS_03 0x09F 0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5 Licht_Anf_01 0x3D5 0xc5,0x39,0xc7,0xf9,0x92,0xd8,0x24,0xce,0xf1,0xb5,0x7a,0xc4,0xbc,0x60,0xe3,0xd1 LWI_01 0x086 0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86 Motor_14 0x3BE 0x1f,0x28,0xc6,0x85,0xe6,0xf8,0xb0,0x19,0x5b,0x64,0x35,0x21,0xe4,0xf7,0x9c,0x24 Motor_51 0x10B 0x77,0x5c,0xa0,0x89,0x4b,0x7c,0xbb,0xd6,0x1f,0x6c,0x4f,0xf6,0x20,0x2b,0x43,0xdd Motor_54 0x14C 0x16,0x35,0x59,0x15,0x9a,0x2a,0x97,0xb8,0x0e,0x4e,0x30,0xcc,0xb3,0x07,0x01,0xad Motor_Code_01 0x641 0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47 Parken_01 0x206 0x09,0xfa,0xca,0x8e,0x62,0xd5,0xd1,0xf0,0x31,0xa0,0xaf,0xda,0x4d,0x1a,0x0a,0x97 PLA_04 0x407 0xef,0x60,0x04,0xa8,0x0c,0x1c,0xda,0x07,0x36,0xd7,0x28,0x92,0xa9,0x88,0x2c,0x4a QFK_01 0x13D 0x20,0xca,0x68,0xd5,0x1b,0x31,0xe2,0xda,0x08,0x0a,0xd4,0xde,0x9c,0xe4,0x35,0x5b RCTA_01 0x2B7 0x5e,0xc7,0x04,0x11,0x4d,0x27,0x0d,0x31,0x91,0xb8,0x62,0x76,0x64,0x09,0xeb,0xec SAL_01 0x12DD54C9 0xde,0xa9,0x83,0x0b,0x0c,0x64,0x79,0x44,0x0f,0xf6,0xc6,0xc7,0x05,0x45,0xb7,0x59 SAM_01 0x205 0x19,0x36,0xd4,0x1e,0x80,0x22,0xf4,0xb8,0xad,0x41,0x0b,0x3f,0x87,0x42,0x25,0x40 SMLS_01 0x3D4 0xc3,0x79,0xbf,0xdb,0xe9,0x11,0x46,0x86,0x69,0xb6,0x9b,0x29,0x15,0x9c,0x45,0x0d TA_01 0x26B 0xce,0xcc,0xbd,0x69,0xa1,0x3c,0x18,0x76,0x0f,0x04,0xf2,0x3a,0x93,0x24,0x19,0x51 TSG_FT_02 0x3E5 0xc4,0x6a,0x69,0x30,0xcf,0x61,0x58,0x51,0x1b,0x86,0x99,0xd3,0xf6,0x1d,0x9a,0x37 VMM_01 0x105 0xde,0x0e,0xa7,0x1d,0xc3,0x83,0xbd,0x82,0x8c,0xa2,0x0c,0x7b,0x4d,0x3c,0x58,0x79 VMM_02 0x139 0xed,0x03,0x1c,0x13,0xc6,0x23,0x78,0x7a,0x8b,0x40,0x14,0x51,0xbf,0x68,0x32,0xba CRC 算法 MEB_Kennungsfolge = { # LWI_01 Steering Angle 0x86: [0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86], # LH_EPS_03 Electric Power Steering 0x9F: [0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5], # HCA_01 Heading Control Assist 0x126: [0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA], # GRA_ACC_01 Steering wheel controls for ACC 0x12B: [0x6A, 0x38, 0xB4, 0x27, 0x22, 0xEF, 0xE1, 0xBB, 0xF8, 0x80, 0x84, 0x49, 0xC7, 0x9E, 0x1E, 0x2B] } def gen_crc_lookup_table_8(poly): crc_lut = [0] * 256 for i in range(256): crc = i for j in range(8): if crc & 0x80: crc = (crc << 1) ^ poly else: crc <<= 1 crc_lut[i] = crc & 0xFF return crc_lut def volkswagen_mqb_checksum(address, data): global crc8_lut_8h2f crc = 0xFF # CRC8 8H2F/AUTOSAR for i in range(1, len(data)): crc = crc ^ data[i] # print(hex(crc), hex(i)) crc = crc8_lut_8h2f[crc] counter = data[1] & 0x0F if address in MEB_Kennungsfolge: crc ^= MEB_Kennungsfolge[address][counter] else: # 无需校验 crc ^= [0x00] * 16 # 默认情况下,返回全 0 crc = crc8_lut_8h2f[crc] # 标准 CRC8 8H2F/AUTOSAR 最后一个 XOR return crc ^ 0xFF can_id = 0x126 data = bytearray(bytes(8)) crc8_lut_8h2f = gen_crc_lookup_table_8(0x2f) crc = volkswagen_mqb_checksum(can_id, data) data[0] = crc CAN 信号- Convenience CAN有以下信号通信,可用于状态读取,车身功能基本上可以通过此路CAN进行控制。包括空调,前后大灯,鸣笛,雨刮,车窗,车锁,车辆状态等信息。
DBC已经分析出来了,暂时不公开。
0x184 1. 锁车 1. 第二字节 后视镜开 0x20 开 2. 第三字节 后视镜关 0x40 关 2. 车窗 1. 第六字节 车窗 04 左前下,02 左前上,40 左后下,08右前上,10右前下 2. 第七字节,01右后下 0x185 1. 车窗控制,10 20 up 40 80 down 1. 第一字节 前排 2. 第二字节 后排 0x598 1. 天窗控制 1. 第三字节 0x20 无 0x21 tap 0x22 滑动 0x25 长按 2. 第七字节:04 关一下,08 一直关,0C 开一下,10 一直开, 14,1c其他 控制转向要求档位在D/B或者R档。以下五个信号可以用于控制方向盘。
- LWI_01 Lenkwinkelsensor
- LH_EPS_03 Lenkhilfe Electric Power Steering
- HCA_01 Heading Control Assist
- GRA_ACC_01 Geschwindigkeitsregelanlage & Adaptive Cruise Control
- PLA_05 Park Lane Assist
提示
- 组合使用0x86, 0x9f, 0x126, 0x12b, 0x302信号,在CAN-FAS发送,可以实现泊车方向盘转向功能。
- 组合使用0x86, 0x9f, 0x302信号,在Gear-Running CAN发送,可以实现方向盘转向。
- 其中0x9f(LH_EPS_03)和0x302(PLA_05)两个信号都可以直接控制方向盘。
- 0x302没有CRC校验,因此控制方向盘比0x9f容易
控制方向盘Demo,仅让方向盘动起来,真正要实现远控,需要有力学的知识,还要设计控制算法。
import os import can import time can0 = can.interface.Bus(channel = 'can0', bustype = 'socketcan', fd=True, bitrate=500000) # msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) # can0.send(msg) MEB_Kennungsfolge = { # LWI_01 Steering Angle 0x86: [0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86], # LH_EPS_03 Electric Power Steering 0x9F: [0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5], # HCA_01 Heading Control Assist 0x126: [0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA], # GRA_ACC_01 Steering wheel controls for ACC 0x12B: [0x6A, 0x38, 0xB4, 0x27, 0x22, 0xEF, 0xE1, 0xBB, 0xF8, 0x80, 0x84, 0x49, 0xC7, 0x9E, 0x1E, 0x2B] } def volkswagen_mqb_checksum(address, data): global crc8_lut_8h2f crc = 0xFF # CRC8 8H2F/AUTOSAR for i in range(1, len(data)): crc = crc ^ data[i] # print(hex(crc), hex(i)) crc = crc8_lut_8h2f[crc] counter = data[1] & 0x0F if address in MEB_Kennungsfolge: crc ^= MEB_Kennungsfolge[address][counter] else: print("Attempt to CRC check undefined Volkswagen message 0x%02X" % address) crc ^= [0x00] * 16 # 默认情况下,返回全 0 crc = crc8_lut_8h2f[crc] # 标准 CRC8 8H2F/AUTOSAR 最后一个 XOR return crc ^ 0xFF def gen_crc_lookup_table_8(poly): crc_lut = [0] * 256 for i in range(256): crc = i for j in range(8): if crc & 0x80: crc = (crc << 1) ^ poly else: crc <<= 1 crc_lut[i] = crc & 0xFF return crc_lut def meb_can_send(can_id, interval, data): global hca_01_counter data[1] = data[1] | hca_01_counter crc = volkswagen_mqb_checksum(can_id, data) data[0] = crc hca_01_counter += 1 for j in data: print(hex(j), end=", ") time.sleep(interval) msg = can.Message(is_extended_id=False, arbitration_id=can_id, data=data, is_fd=True) can0.send(msg) def can_send(can_id, interval, data): time.sleep(interval) msg = can.Message(is_extended_id=False, arbitration_id=can_id, data=data, is_fd=True) can0.send(msg) crc8_lut_8h2f = gen_crc_lookup_table_8(0x2f) def set_value(data, value, start_pos, value_length): end_pos = start_pos + value_length for bit_index in range(start_pos, end_pos): byte_index = bit_index // 8 bit_offset = bit_index % 8 bit_value = (value >> (bit_index - start_pos)) & 1 # print("\n") # print("index", bit_index, start_pos, end_pos, "byte_index:", byte_index, "bit_offset:", bit_offset, "v:", bit_value) if bit_value: data[byte_index] |= 1 << bit_offset else: data[byte_index] &= ~(1 << bit_offset) # for i in data: # print(bin(i), end=",") return data def set_bit(b, bit, bit_index): if bit_index < 0 or bit_index > 7: raise ValueError("bit_index must be between 0 and 7") mask = 1 << bit_index flipped_byte = b ^ mask return flipped_byte def create_steering_control(apply_steer, lkas_enabled): data = bytearray(bytes(8)) if lkas_enabled: # HCA_01_Sendestatus data = set_value(data, 5, 30, 1) # HCA_01_Status_HCA data = set_value(data, 1, 32, 4) else: # HCA_01_Sendestatus data = set_value(data, 3, 30, 1) # HCA_01_Status_HCA data = set_value(data, 0, 32, 4) # HCA_01_LM_Offset print("create_steering_control", apply_steer) data = set_value(data, abs(apply_steer), 16, 9) # HCA_01_LM_OffSign v = 1 if apply_steer < 0 else 0 data = set_value(data, v, 31, 1) # HCA_01_Vib_Freq data = set_value(data, 3, 12, 4) # HCA_01_Vib_Amp data = set_value(data, 10, 36, 4) return data def create_pla_control(sendestatus, positive, degree): data = bytearray(bytes(24)) data[1] = 0x40 # PLA_QFK_Spuerb data[2] = 0xFA # PLA_QFK_KruemmSoll data = set_value(data, degree, 32, 15) # PLA_QFK_KruemmSoll_VZ if positive: data = set_value(data, 1, 47, 1) else: data = set_value(data, 0, 47, 1) # PLA_05_Sendestatus if sendestatus: data = set_value(data, 1, 74, 1) else: data = set_value(data, 0, 74, 1) return data hca_01_counter = 0 degree = 0 for i in range(20): # data = create_steering_control(10 * i, 1) # meb_can_send(0x126, 0.1, data) degree += 0x10 data = create_pla_control(1, 1, degree) can_send(0x302, 0.1, data) 注意事项- 离开车辆之前,确保CAN总线处于接通状态,否则会大量耗电。
- 不要对Powertrain 和Running Gear CAN 进行 Fuzzing,可能直接会导致人身伤害,另外,也可能导致ECU异常,产生潜在隐患。比如转向灯失效,后视镜无法打开。
VW ID.4 ICAS1 车控分析
2021年在360搭了一套ID.4台架环境,本来快要出成果了(拿到了ODIS内网权限,还有ICAS3的root),被安排出差去搭建展示车,后续工作计划被打乱。这期间不管是工作还是生活都遇到了许多糟心事,便停止了研究。
算是一个怨念项目吧,今年5月我在机缘巧合下又研究了ID.4的ICAS1的车控逻辑,还有一个怨念项目是CAN-Pick NG,写了一半又搁置了,不知道2025可不可以完成。
ID.4 是大众MEB平台,EE架构总共有两个域控制器,ICAS1, ICAS3。其中ICAS1(J533)与车身控制有关。 下图是J533的位置,位于序号13。将线控模块装载到此处,可以实现车身监测和控制。
车内ECU拓扑Classic CAN: 500k
CAN-FD:仲裁域500k, 数据域2M。若要分析CAN-FD,ZLG设备最合适。
通过分析 ELSA电路图 和 大众内部培训资料,发现总线命名混乱,必须重新整理才能得到正确的CAN拓扑图。
J533总共有9路CAN,4路LIN(暂不需要),关键ECU如下。
- Running-Gear CAN (CAN-FD)
- J104 - ABS
- J500 - EPS,转向助力控制单元
- NX6 - 制动控制器 (Brake Booster)
- Powertrain CAN (CAN-FD)
- J623 - 电机控制模块 Engine/Motor Control Module
- J841/J944 - 电驱单元
- J234 - 气囊 (不能在这里Fuzzing,危险)
- Driver Assistance CAN,CAN-FAS (CAN-FD)
- J428 - 车距调节控制单元
- J446 - 泊车雷达控制单元
- J769/J770 - 行驶换道辅助系统控制单元
- J928 - 全景摄像头控制单元
- Convenience CAN (Classic)
- J527 - 电控换档模块 (Steering Column Electronics Control Module)
- J605 - 行李箱盖控制单元
- J764 - 转向柱锁
- …其他车身控制
- CAN-EV (CAN-FD)
- J979 - 加热与空调控制模块 (Heater and Air Conditioning Control Module)
T40a 连接器和表格对应如下。
总线安全策略 网关隔离多功能方向盘控制信号,由Convenience CAN接收,但是底盘CAN的ECU也能收到。底盘CAN发出的多功能方向盘控制信号,不会被J533转发到目标ECU。
CRC 校验分析CAN总线变化,可以看到大部份报文的变化规律,第一字节随机,第二字节有规律递增。第一字节是CRC,第二字节是计数器。
CRC 初始值参考OpenDBC github /opendbc/can/common.cc volkswagen_mqb_checksum
可以获取VM MQB平台的 Checksum 初始值,但是ID.4有很多新的CRC初始值,我通过逆向分析并记录在下面了。
大部份控制信号有CRC和计数器,ECU不会校验某些不重要功能的CRC,比如天窗,车窗,鸣笛。但是会校验转向助力,行李箱盖开启,雨刮等影响驾驶的功能。
Signal CAN ID CRC Seed AAA_01 0x12DD5502 0x62,0x14,0x7c,0xa1,0x49,0x95,0x43,0x04,0x78,0x46,0x74,0x19,0x39,0x17,0x9f,0x1c ACC_18 0x14D 0x1a,0x65,0x81,0x96,0xc0,0xdf,0x11,0x92,0xd3,0x61,0xc6,0x95,0x8c,0x29,0x21,0xb5 Airbag_01 0x040 0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40 Airbag_02 0x520 0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44 APS_Master 0x380 0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13,0x13 AWV_03 0x0DB 0x09,0xfa,0xca,0x8e,0x62,0xd5,0xd1,0xf0,0x31,0xa0,0xaf,0xda,0x4d,0x1a,0x0a,0x97 BEM_06 0x48B 0x54,0xaf,0x8a,0xfb,0x0d,0x87,0x6a,0x0f,0x47,0x78,0x31,0x4f,0x35,0x28,0x82,0x6d Blinkmodi_02 0x366 0xa9,0xbd,0xfb,0x3c,0x95,0x0f,0x75,0x3a,0x4f,0x19,0x59,0x6d,0xb2,0xe9,0xd1,0x97 EA_01 0x1A4 0x69,0xbb,0x54,0xe6,0x4e,0x46,0x8d,0x7b,0xea,0x87,0xe9,0xb3,0x63,0xce,0xf8,0xbf EA_02 0x1F0 0x2f,0x3c,0x22,0x60,0x18,0xeb,0x63,0x76,0xc5,0x91,0x0f,0x27,0x34,0x04,0x7f,0x02 ELV_01 0x656 0xab,0x2f,0xd3,0x39,0x6f,0x37,0xfa,0x59,0xa4,0x70,0xce,0x11,0x54,0x82,0x62,0x56 EM1_01 0x0C0 0x2f,0x44,0x72,0xd3,0x07,0xf2,0x39,0x09,0x8d,0x6f,0x57,0x20,0x37,0xf9,0x9b,0xfa EML_02 0x1A555541 0x3e,0xb4,0x25,0xc1,0x31,0x1f,0xf1,0xd7,0xb1,0xbe,0xcc,0xe0,0x0f,0x46,0x51,0xb2 EML_06 0x20A 0x9d,0xe8,0x36,0xa1,0xca,0x3b,0x1d,0x33,0xe0,0xd5,0xbb,0x5f,0xae,0x3c,0x31,0x9f ESC_50 0x102 0xd7,0x12,0x85,0x7e,0x0b,0x34,0xfa,0x16,0x7a,0x25,0x2d,0x8f,0x04,0x8e,0x5d,0x35 ESC_51 0x0FC 0x77,0x5c,0xa0,0x89,0x4b,0x7c,0xbb,0xd6,0x1f,0x6c,0x4f,0xf6,0x20,0x2b,0x43,0xdd ESP_10 0x116 0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac,0xac ESP_20 0x65D 0xac,0xb3,0xab,0xeb,0x7a,0xe1,0x3b,0xf7,0x73,0xba,0x7c,0x9e,0x06,0x5f,0x02,0xd9 ESP_21 0x0FD 0xb4,0xef,0xf8,0x49,0x1e,0xe5,0xc2,0xc0,0x97,0x19,0x3c,0xc9,0xf1,0x98,0xd6,0x61 ESP_24 0x31B 0x67,0x8a,0xae,0x22,0x4d,0xd0,0x51,0x80,0x5c,0xb9,0xce,0x1e,0xdf,0x02,0x2d,0xd4 Getriebe_11 0x0AD 0x3f,0x69,0x39,0xdc,0x94,0xf9,0x14,0x64,0xd8,0x6a,0x34,0xce,0xa2,0x55,0xb5,0x2c GRA_ACC_01 0x12B 0x6a,0x38,0xb4,0x27,0x22,0xef,0xe1,0xbb,0xf8,0x80,0x84,0x49,0xc7,0x9e,0x1e,0x2b HCA_01 0x126 0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda,0xda HVL_01 0x12DD553D 0x1d,0x82,0x7b,0x79,0xa5,0xee,0x3a,0xb9,0xb7,0xf9,0xe4,0x67,0x7f,0x97,0x11,0xad IPA_01 0x138 0x77,0x4e,0x14,0x87,0xf2,0xf8,0xb2,0x61,0xf6,0xa4,0x52,0x94,0xd4,0x81,0x2a,0xb1 IPA_02 0x16A9545F 0xc6,0x7f,0x85,0xb6,0xe6,0xae,0xf8,0x26,0xb0,0x8c,0x19,0x10,0x5b,0x33,0x64,0x6c Klemmen_Status_01 0x3C0 0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3,0xc3 LH_EPS_02 0x11D 0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c,0x1c LH_EPS_03 0x09F 0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5,0xf5 Licht_Anf_01 0x3D5 0xc5,0x39,0xc7,0xf9,0x92,0xd8,0x24,0xce,0xf1,0xb5,0x7a,0xc4,0xbc,0x60,0xe3,0xd1 LWI_01 0x086 0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86 Motor_14 0x3BE 0x1f,0x28,0xc6,0x85,0xe6,0xf8,0xb0,0x19,0x5b,0x64,0x35,0x21,0xe4,0xf7,0x9c,0x24 Motor_51 0x10B 0x77,0x5c,0xa0,0x89,0x4b,0x7c,0xbb,0xd6,0x1f,0x6c,0x4f,0xf6,0x20,0x2b,0x43,0xdd Motor_54 0x14C 0x16,0x35,0x59,0x15,0x9a,0x2a,0x97,0xb8,0x0e,0x4e,0x30,0xcc,0xb3,0x07,0x01,0xad Motor_Code_01 0x641 0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47,0x47 Parken_01 0x206 0x09,0xfa,0xca,0x8e,0x62,0xd5,0xd1,0xf0,0x31,0xa0,0xaf,0xda,0x4d,0x1a,0x0a,0x97 PLA_04 0x407 0xef,0x60,0x04,0xa8,0x0c,0x1c,0xda,0x07,0x36,0xd7,0x28,0x92,0xa9,0x88,0x2c,0x4a QFK_01 0x13D 0x20,0xca,0x68,0xd5,0x1b,0x31,0xe2,0xda,0x08,0x0a,0xd4,0xde,0x9c,0xe4,0x35,0x5b RCTA_01 0x2B7 0x5e,0xc7,0x04,0x11,0x4d,0x27,0x0d,0x31,0x91,0xb8,0x62,0x76,0x64,0x09,0xeb,0xec SAL_01 0x12DD54C9 0xde,0xa9,0x83,0x0b,0x0c,0x64,0x79,0x44,0x0f,0xf6,0xc6,0xc7,0x05,0x45,0xb7,0x59 SAM_01 0x205 0x19,0x36,0xd4,0x1e,0x80,0x22,0xf4,0xb8,0xad,0x41,0x0b,0x3f,0x87,0x42,0x25,0x40 SMLS_01 0x3D4 0xc3,0x79,0xbf,0xdb,0xe9,0x11,0x46,0x86,0x69,0xb6,0x9b,0x29,0x15,0x9c,0x45,0x0d TA_01 0x26B 0xce,0xcc,0xbd,0x69,0xa1,0x3c,0x18,0x76,0x0f,0x04,0xf2,0x3a,0x93,0x24,0x19,0x51 TSG_FT_02 0x3E5 0xc4,0x6a,0x69,0x30,0xcf,0x61,0x58,0x51,0x1b,0x86,0x99,0xd3,0xf6,0x1d,0x9a,0x37 VMM_01 0x105 0xde,0x0e,0xa7,0x1d,0xc3,0x83,0xbd,0x82,0x8c,0xa2,0x0c,0x7b,0x4d,0x3c,0x58,0x79 VMM_02 0x139 0xed,0x03,0x1c,0x13,0xc6,0x23,0x78,0x7a,0x8b,0x40,0x14,0x51,0xbf,0x68,0x32,0xba CRC 算法 MEB_Kennungsfolge = { # LWI_01 Steering Angle 0x86: [0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86], # LH_EPS_03 Electric Power Steering 0x9F: [0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5], # HCA_01 Heading Control Assist 0x126: [0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA], # GRA_ACC_01 Steering wheel controls for ACC 0x12B: [0x6A, 0x38, 0xB4, 0x27, 0x22, 0xEF, 0xE1, 0xBB, 0xF8, 0x80, 0x84, 0x49, 0xC7, 0x9E, 0x1E, 0x2B] } def gen_crc_lookup_table_8(poly): crc_lut = [0] * 256 for i in range(256): crc = i for j in range(8): if crc & 0x80: crc = (crc << 1) ^ poly else: crc <<= 1 crc_lut[i] = crc & 0xFF return crc_lut def volkswagen_mqb_checksum(address, data): global crc8_lut_8h2f crc = 0xFF # CRC8 8H2F/AUTOSAR for i in range(1, len(data)): crc = crc ^ data[i] # print(hex(crc), hex(i)) crc = crc8_lut_8h2f[crc] counter = data[1] & 0x0F if address in MEB_Kennungsfolge: crc ^= MEB_Kennungsfolge[address][counter] else: # 无需校验 crc ^= [0x00] * 16 # 默认情况下,返回全 0 crc = crc8_lut_8h2f[crc] # 标准 CRC8 8H2F/AUTOSAR 最后一个 XOR return crc ^ 0xFF can_id = 0x126 data = bytearray(bytes(8)) crc8_lut_8h2f = gen_crc_lookup_table_8(0x2f) crc = volkswagen_mqb_checksum(can_id, data) data[0] = crc CAN 信号- Convenience CAN有以下信号通信,可用于状态读取,车身功能基本上可以通过此路CAN进行控制。包括空调,前后大灯,鸣笛,雨刮,车窗,车锁,车辆状态等信息。
DBC已经分析出来了,暂时不公开。
0x184 1. 锁车 1. 第二字节 后视镜开 0x20 开 2. 第三字节 后视镜关 0x40 关 2. 车窗 1. 第六字节 车窗 04 左前下,02 左前上,40 左后下,08右前上,10右前下 2. 第七字节,01右后下 0x185 1. 车窗控制,10 20 up 40 80 down 1. 第一字节 前排 2. 第二字节 后排 0x598 1. 天窗控制 1. 第三字节 0x20 无 0x21 tap 0x22 滑动 0x25 长按 2. 第七字节:04 关一下,08 一直关,0C 开一下,10 一直开, 14,1c其他 控制转向要求档位在D/B或者R档。以下五个信号可以用于控制方向盘。
- LWI_01 Lenkwinkelsensor
- LH_EPS_03 Lenkhilfe Electric Power Steering
- HCA_01 Heading Control Assist
- GRA_ACC_01 Geschwindigkeitsregelanlage & Adaptive Cruise Control
- PLA_05 Park Lane Assist
提示
- 组合使用0x86, 0x9f, 0x126, 0x12b, 0x302信号,在CAN-FAS发送,可以实现泊车方向盘转向功能。
- 组合使用0x86, 0x9f, 0x302信号,在Gear-Running CAN发送,可以实现方向盘转向。
- 其中0x9f(LH_EPS_03)和0x302(PLA_05)两个信号都可以直接控制方向盘。
- 0x302没有CRC校验,因此控制方向盘比0x9f容易
控制方向盘Demo,仅让方向盘动起来,真正要实现远控,需要有力学的知识,还要设计控制算法。
import os import can import time can0 = can.interface.Bus(channel = 'can0', bustype = 'socketcan', fd=True, bitrate=500000) # msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) # can0.send(msg) MEB_Kennungsfolge = { # LWI_01 Steering Angle 0x86: [0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86, 0x86], # LH_EPS_03 Electric Power Steering 0x9F: [0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5, 0xF5], # HCA_01 Heading Control Assist 0x126: [0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA], # GRA_ACC_01 Steering wheel controls for ACC 0x12B: [0x6A, 0x38, 0xB4, 0x27, 0x22, 0xEF, 0xE1, 0xBB, 0xF8, 0x80, 0x84, 0x49, 0xC7, 0x9E, 0x1E, 0x2B] } def volkswagen_mqb_checksum(address, data): global crc8_lut_8h2f crc = 0xFF # CRC8 8H2F/AUTOSAR for i in range(1, len(data)): crc = crc ^ data[i] # print(hex(crc), hex(i)) crc = crc8_lut_8h2f[crc] counter = data[1] & 0x0F if address in MEB_Kennungsfolge: crc ^= MEB_Kennungsfolge[address][counter] else: print("Attempt to CRC check undefined Volkswagen message 0x%02X" % address) crc ^= [0x00] * 16 # 默认情况下,返回全 0 crc = crc8_lut_8h2f[crc] # 标准 CRC8 8H2F/AUTOSAR 最后一个 XOR return crc ^ 0xFF def gen_crc_lookup_table_8(poly): crc_lut = [0] * 256 for i in range(256): crc = i for j in range(8): if crc & 0x80: crc = (crc << 1) ^ poly else: crc <<= 1 crc_lut[i] = crc & 0xFF return crc_lut def meb_can_send(can_id, interval, data): global hca_01_counter data[1] = data[1] | hca_01_counter crc = volkswagen_mqb_checksum(can_id, data) data[0] = crc hca_01_counter += 1 for j in data: print(hex(j), end=", ") time.sleep(interval) msg = can.Message(is_extended_id=False, arbitration_id=can_id, data=data, is_fd=True) can0.send(msg) def can_send(can_id, interval, data): time.sleep(interval) msg = can.Message(is_extended_id=False, arbitration_id=can_id, data=data, is_fd=True) can0.send(msg) crc8_lut_8h2f = gen_crc_lookup_table_8(0x2f) def set_value(data, value, start_pos, value_length): end_pos = start_pos + value_length for bit_index in range(start_pos, end_pos): byte_index = bit_index // 8 bit_offset = bit_index % 8 bit_value = (value >> (bit_index - start_pos)) & 1 # print("\n") # print("index", bit_index, start_pos, end_pos, "byte_index:", byte_index, "bit_offset:", bit_offset, "v:", bit_value) if bit_value: data[byte_index] |= 1 << bit_offset else: data[byte_index] &= ~(1 << bit_offset) # for i in data: # print(bin(i), end=",") return data def set_bit(b, bit, bit_index): if bit_index < 0 or bit_index > 7: raise ValueError("bit_index must be between 0 and 7") mask = 1 << bit_index flipped_byte = b ^ mask return flipped_byte def create_steering_control(apply_steer, lkas_enabled): data = bytearray(bytes(8)) if lkas_enabled: # HCA_01_Sendestatus data = set_value(data, 5, 30, 1) # HCA_01_Status_HCA data = set_value(data, 1, 32, 4) else: # HCA_01_Sendestatus data = set_value(data, 3, 30, 1) # HCA_01_Status_HCA data = set_value(data, 0, 32, 4) # HCA_01_LM_Offset print("create_steering_control", apply_steer) data = set_value(data, abs(apply_steer), 16, 9) # HCA_01_LM_OffSign v = 1 if apply_steer < 0 else 0 data = set_value(data, v, 31, 1) # HCA_01_Vib_Freq data = set_value(data, 3, 12, 4) # HCA_01_Vib_Amp data = set_value(data, 10, 36, 4) return data def create_pla_control(sendestatus, positive, degree): data = bytearray(bytes(24)) data[1] = 0x40 # PLA_QFK_Spuerb data[2] = 0xFA # PLA_QFK_KruemmSoll data = set_value(data, degree, 32, 15) # PLA_QFK_KruemmSoll_VZ if positive: data = set_value(data, 1, 47, 1) else: data = set_value(data, 0, 47, 1) # PLA_05_Sendestatus if sendestatus: data = set_value(data, 1, 74, 1) else: data = set_value(data, 0, 74, 1) return data hca_01_counter = 0 degree = 0 for i in range(20): # data = create_steering_control(10 * i, 1) # meb_can_send(0x126, 0.1, data) degree += 0x10 data = create_pla_control(1, 1, degree) can_send(0x302, 0.1, data) 注意事项- 离开车辆之前,确保CAN总线处于接通状态,否则会大量耗电。
- 不要对Powertrain 和Running Gear CAN 进行 Fuzzing,可能直接会导致人身伤害,另外,也可能导致ECU异常,产生潜在隐患。比如转向灯失效,后视镜无法打开。