Aggregator
IGS Arcade 逆向系列(二)- 游戏文件恢复
上一篇文章写到游戏有个破坏分区的保护机制,本篇将深入分析。
作为2007年发布的游戏,此游戏加固机制还是比较落后的,主要是靠一些拼接,修改特征的方法来加固。并没有现代APP加固的那么卷的特性。主要是加固的环节有点多,并且每个游戏都不一样。然后由于开发上的特性,
Security Affairs newsletter Round 525 by Pierluigi Paganini – INTERNATIONAL EDITION
G.O.S.S.I.P 广而告之——中电信量子集团安全攻防 & 侧信道攻击工程师招聘
IGS Arcade Reverse Engineering Series (2) - Recovering Game Files
In the previous post, I mentioned that the game has a protection mechanism that destroys partitions. In this post, we’ll dig deeper into it.
For a game released in 2007, its hardening/protection is relatively outdated. It mainly relies on things like concatenation and signature-tweaking. It doesn’t have the “modern app hardening arms race” vibe. The annoying part is that there are many stages in the protection flow, and each game does it differently. On top of that, some development traits (compiler optimizations, coding style) make reverse engineering more difficult.
Extracting the game itself is actually straightforward: once the game is running, you can dump it via a shell from memory or from the filesystem (if the files are written to disk). But if you want to extract multiple different games, that becomes a hassle—so let’s start with static analysis.
All in all, this reversing process feels like solving a CTF Misc challenge: it requires quite a bit of logical deduction.
Reverse Engineering TrapsIn general, when analyzing filesystem contents, you rarely start from the kernel. Usually you start with init-related files.
The first step is typically checking /etc/inittab. The script starts rc first, and then starts the graphical interface:
# 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/IGSThis script sets environment variables, starts X, then starts the card reader, and finally launches the game. Aside from the loop (which is a bit unusual), everything else looks normal. At this point, you’d definitely conclude /PM2008v2/PM2008v2 is the game binary.
#!/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 doneNext, I analyzed PM2008v2. At first glance, it contains a lot of “executable loader” style code. Combined with the fact that many files previously had no magic bytes, I guessed it might be dynamically loading code and reconstructing ELF binaries. Later, Nova told me this thing is not the game at all—only then did I realize how abnormal it is. This file has a lot of glibc fingerprints and feels like a statically linked glibc executable.
For convenience in reversing and later porting the game, I needed to confirm the GCC and glibc versions. PM2008v2 shows GCC: (GNU) 3.3.1, but has no glibc version string.
So I went straight to the system libc.so and tentatively treated it as glibc 2.3.2. It’s hard to build on modern Linux; even in Docker I ran into issues.
Analyzing the “Fake Game” Executable Building GCC 3.3.1The target kernel is i686, so I built it on CentOS 4 running in VMware. To keep the optimized assembly as consistent as possible, I wanted the same GCC version and build settings. Building this environment also helps future analysis of other games on this platform—more upfront work means fewer detours later.
wget http://mirrors.aliyun.com/gnu/gcc/gcc-3.3.1/gcc-3.3.1.tar.gzUsing Aliyun’s CentOS vault repos, install development dependencies first:
yum groupinstall "Development Tools"Configure like this, target i686. With old GCC versions it’s better not to compile in parallel—sometimes it breaks (3.3.1 was fine, 3.2.2 wasn’t). Then remove the system GCC and install this one:
../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 Building 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 needs to be extracted into the glibc directory:
tar -zxvf glibc-2.3.2.tar.gz cd glibc-2.3.2 tar -zxvf ../glibc-linuxthreads-2.3.2.tar.gzWhen building glibc 2.3.2 you may hit some bugs and need patches. Conveniently, the E2000 platform’s Linux is also an LFS-based system, so you can download patches from 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.patchNext, configure build options. For now I’ll keep it like this, because even with fine-grained optimization flags, the final assembly differs a lot from the target binary.
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/includeMain differences:
- Stack frame: most functions in the target binary return with 0xC9 leave, while my build typically does mov esp, ebp then pop ebp.
- Inlining: calls inside some medium-length functions in the target binary are optimized into inline code.
Even if I set optimization to O3, it barely changes. Manually tweaking -fomit-frame-pointer, -finline-limit=n, etc. also didn’t help. Maybe it needs __inline__ somewhere; I didn’t have time to verify.
With this situation, generating FLIRT signatures with FLAIR barely recovers symbols for functions that still contain internal calls.
After analysis, PM2008v2 is basically just a killdisk function, statically linked with glibc.
Therefore, if you execute inittab directly, it’s impossible to launch the actual game.
System Initialization Analysis Community Reverse Engineering NotesNova told me some information earlier. But honestly, from these notes alone I can’t infer the exact loading flow. I can only tell the file header needs to be restored, rc0.d is actually an ELF and will be executed at boot. Also, the Submarine Crisis game files differ somewhat from my PM2008 game files.
https://github.com/batteryshark/igstools/blob/main/scripts/igs_rofsv1_dumpexec.py
This script is used to recover game files. I tried it and it can produce an ELF, but importing it into IDA causes errors. I didn’t know how the generated file is supposed to run, so I still needed to analyze it myself.
Analyzing the Kernel Boot FlowBy analyzing dependencies and other environment variables, I still couldn’t find any path that launches the game. In the previous post, I analyzed filesystem mounting; after mounting, there’s a whole series of operations. IDA may fail to decompile extremely large functions, but that’s not a big problem—the real pain point isn’t here.
In the previous post, since I was analyzing a filesystem that’s a modified fork of open-source code, I could compare against the original, so not recovering other symbols was fine. This kernel is 2.4; the bzImage doesn’t carry a symbol table. A lot of code here is developed by IGS themselves; some syscalls aren’t invoked via int. When syscalls are involved and symbols aren’t recovered, analysis is still annoying. If you want to use BinDiff, you’d need IDA 8, and I didn’t have time to port it to macOS. Linux has a syscall table; if IGS didn’t customize syscalls, you can directly transplant syscall symbols from your own built kernel.
Also, IDA 9 doesn’t parse this old Linux 2.4 kernel very well. Many xrefs and instructions aren’t recognized and need manual fixing.
Fixing Immediate-Value Xrefs 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}") Fixing String DataPossibly because xrefs aren’t fully recognized, string recognition often misses a few bytes at the beginning. You need to manually fix strings.
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 ThreadFrom the code above, we can see the first step of game initialization is to run /bin/zsh with these parameters:
export HOME=/ export TERM=linux export PATH=/bin:/usr/bin:/sbin:/usr/sbin /bin/zsh /etc/rc.d/rc0.d __KERNEL__ -no-print -PM2008v2The next step is running /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 -PM2008v2Finally, it tries these:
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"); // 指向bashThe kernel cmdline can be found in parse_cmdline_early, and is not controlled by LILO.
If you set an external boot cmdline, there’s a backdoor that checks whether the bootloader parameter equals “JBoot”. If it doesn’t, it goes into an infinite loop.
Does this “JBoot” stand for a bootloader written by someone named James? 👀
bootloader=JBootThe built-in kernel boot args:
root=/dev/hdc2 ro console=ttyS1,115200 BOOT_IMAGE=PM2008v2It doesn’t set init=, so it will definitely execute /sbin/init, and then /etc/inittab.
Getting Stuck Restoring ZSH SymbolsThe trail leads to /bin/zsh. Its entry point looks the same as PM2008v2, so I assumed it’s also based on some modified glibc. But after importing a FLIRT signature, only the innermost functions were recognized—likely due to the same optimization issue.
/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.patAt this point I still don’t know exactly which GCC and glibc it was based on. Considering that many executables later may also use glibc, I need to pin down the version and see if there’s a fast way to recover symbols.
Dependency analysisUsing YAFAF (a tool I wrote five years ago), I can quickly find relevant dependencies. rc*.d should be game code.
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)From these fingerprints, these files look like fragments stitched together from ELF files built in multiple different environments.
And /sbin/init also appears to be based on glibc 2.3.x, so I tried building glibc 2.3.2 with GCC 3.2.2. On CentOS 3, parallel compilation fails for this GCC version. Luckily, I’d seen similar errors when building OpenWrt in the past—otherwise I would’ve been stuck for a long time with zero search results about the root cause.
../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 installWhen building glibc, whether O2 or O3, it almost never inlines functions. I still don’t understand why, can’t find answers, and asking LLMs didn’t help either.
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 Recovering ZSH SymbolsI don’t like doing repetitive mechanical work. If I had to reverse zsh directly, I’d be bored to death.
This IDA version isn’t great at recognizing instructions; many places require manual recovery. After using the script below, the next step is to recover function entry points (I had a similar script in the previous post).
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)Since there’s no way to use a glibc signature here, I came up with a somewhat naive approach—but it’s faster than invoking MCP-based analysis.
First, fully recover glibc strings from file A (my self-compiled one) and recover function entry points. For file B, first fix function entry points, then fix immediate-value offsets, fix strings, deduplicate, remove overly short strings, then match strings one-by-one against file A and filter results.
Then traverse xrefs from the filtered results, and pick those that are not duplicated and are functions:
- If those functions in file A have symbols, apply them to file B.
- For functions where strings are pushed as arguments, find all calls in the current function’s scope; if the callee address has no symbol, use this method to recover symbols for argument-pushed callees.
- Recursively apply symbols (e.g., after recovering a callee, scan inside it for other callees). This feels unnecessary because it quickly runs into many edge cases.
These scripts are still rough and need polishing, but they’re “good enough” for now. And they might not even matter much: after these fixes, it becomes obvious that these ELF files aren’t based on a modified glibc—they’re just statically linked. They all use __libc_start_main to enter main.
Fixing GECA and rc0.dRead 0x400 bytes from /dev/hdc1 at offset 0x1B44 * 512. This is an ELF header, and it’s written into /mnt/head.
IGS likely hid this ELF header in unused space inside the FAT partition. Because the partitions are contiguous, you can’t easily spot hidden content from the partition layout alone.
Read 0x400 bytes from the end of /bin/arch and write it to /mnt/GECA.
Then append /etc/init.d/rc0.d to the end of /mnt/GECA.
Recovering Game FilesBefore I started my analysis, Nova shared findings from BatteryShark. BatteryShark had already reconstructed the ELF for Speed Driver 2, but the ELF still had issues and couldn’t be used for other games such as Percussion Master 2008.
So I still had to do it myself, and I also wanted to reverse the code that reconstructs the game files anyway.
During kernel boot, after GECA is repaired by zsh, it is immediately executed with the same environment variables and arguments.
The concatenation order of rc* is configured by a string variable, and it differs per game.
These digits correspond exactly to the file order under /etc/rc.d:
2 1 3 5 8 4 7 6 9GECA’s execution flow: it cuts fragments under /etc/rc.d according to the specified order (with different cut sizes), and writes the outputs to /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/nullThen it mounts a ramfs at /exec, concatenates these files into the real game binary, and replaces the original disk-destruction executable.
After that, the boot process matches what we saw earlier in /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/nullYou can write a script to replicate this process:
#!/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: 344After recovery, although we avoid multi-version glibc fingerprints, the ELF might still have issues. Each game’s main binary recovery algorithm differs slightly. PM2008’s recovery logic takes two more parameters than SD2, so I’ll focus on PM2008 first.
I still need to do dynamic analysis to understand how the in-memory ELF is loaded. But I’ve noticed that plugging in an Ethernet cable or a keyboard causes the cabinet to crash immediately—no idea whether that’s a protection mechanism. In the next post, I’ll analyze how to do dynamic debugging on the device.
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文件是怎么装载的,但是现在发现,在游戏机接网线或者键盘,就会自动死机,不知道是不是安全机制。下一篇将分析如何在设备上动态调试。
Cetus Protocol обокрали на $223 млн, и теперь они умоляют хакера отдать хотя бы часть — за амнистию и чай
Initial Access Brokers Targeted in Operation Endgame 2.0
Law enforcement in a European-led operation against malware often used as a precursor to ransomware took down 300 servers worldwide, police said Friday. The crackdown is the latest action under Operation Endgame targeting ransomware and botnet ecosystem.
Oh My Pentest Report – Tema de Oh My Zsh para Pentesters
Wi-Fi с паролем "123456": в этих отелях ваши данные сдаются раньше, чем номер
Initial Access Brokers Targeted in Operation Endgame 2.0
Law enforcement in a European-led operation against malware often used as a precursor to ransomware took down 300 servers worldwide, police said Friday. The crackdown is the latest action under Operation Endgame targeting ransomware and botnet ecosystem.
Initial Access Brokers Targeted in Operation Endgame 2.0
Law enforcement in a European-led operation against malware often used as a precursor to ransomware took down 300 servers worldwide, police said Friday. The crackdown is the latest action under Operation Endgame targeting ransomware and botnet ecosystem.