Hacking pandroa's box game cartridge 1

Disclaimer

This blog is only for educational purposes. Never use these info for bad.

Introduction

Pandora’s box multi-game cartridge is a popular product from China. This cartridge usually has 1000+ retro arcade games. It supports Jamma and HDMI. Users can enjoy old-time games with it. And some newer versions of it have more than 10,000 games, and support some home console games. Many manufactures from China have the ability to design and product Pandora’s box multi-game cartridge. They always add software protection to prevent others from cloning their efforts.
Recently, I got a newer version of PB boards. And I hacked it. I will introduce the steps of how I hacked this board.
The PCB is as follows:
PCB
I uploaded all code to github

Tools/Packages I used

  • strings is a GNU util to output all printable strings in files.
  • file is a GNU util to determine files type.
  • binwalk is a easy way to extract firmware from an image file.
  • Ghidra is a great suite for disassembling and decompiling binary from variety platforms.
  • Unicorn is a lightweight multi-platform, multi-architecture CPU emulator framework.

Inspect image

This board uses Amlogic s805. This SOC is old, and not very powerful for running some 3D games. I think the manufaturer selected this SOC only beacause it’s cheap. This SOC only cost near to $1 if they select to use second-hard ICs.
This board has s805, a memory IC, and no EMMC IC. It has a TF card of size 32GB, this board uses TF card boot mode.
The TF card has 2 partitions, shown as the following:

1
2
3
4
5
6
7
8
9
10
11
$ fdisk -l pb1.img
Disk pb1.img: 29.57 GiB, 31739999744 bytes, 61992187 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: 0x00007577

Device Boot Start End Sectors Size Id Type
pb1.img1 8192 1441791 1433600 700M 6 FAT16
pb1.img2 1441792 60440575 58998784 28.1G c W95 FAT32 (LBA)

I mounted partition 1 to check its files:

1
2
3
4
5
6
7
8
9
10
11
$ sudo mount -oloop,ro,offset=$((8192*512)) pb1.img /mnt/disk
$ ls /mnt/disk -l
total 684384
-rwxr-xr-x 1 root root 1843272 9月 26 2021 logicalcircuit
-rwxr-xr-x 1 root root 60268 9月 26 2021 logo_en.png
-rwxr-xr-x 1 root root 60268 9月 26 2021 logo_ko.png
-rwxr-xr-x 1 root root 60268 9月 26 2021 logo.png
-rwxr-xr-x 1 root root 6678528 9月 26 2021 rxnv
-rwxr-xr-x 1 root root 314572800 1月 3 18:26 rxte
drwxr-xr-x 2 root root 16384 4月 18 2022 'System Volume Information'
-rwxr-xr-x 1 root root 377487360 1月 3 18:26 user

And checked the file type using file

1
2
3
4
5
6
7
8
9
$ file /mnt/disk/*
/mnt/disk/logicalcircuit: PC bitmap, Adobe Photoshop with alpha channel mask, 1280 x 720 x 16
/mnt/disk/logo_en.png: PNG image data, 500 x 120, 8-bit/color RGBA, non-interlaced
/mnt/disk/logo_ko.png: PNG image data, 500 x 120, 8-bit/color RGBA, non-interlaced
/mnt/disk/logo.png: PNG image data, 500 x 120, 8-bit/color RGBA, non-interlaced
/mnt/disk/rxnv: data
/mnt/disk/rxte: LUKS encrypted file, ver 1 [aes, xts-plain64, sha256] UUID: 3929e3d8-a638-4b24-93d7-a459b77ac901
/mnt/disk/System Volume Information: directory
/mnt/disk/user: LUKS encrypted file, ver 1 [aes, xts-plain64, sha256] UUID: 85dbb20b-1880-422f-b4e7-6564b1d13abb

It shows file rxnv maybe encrypted.
I uploaded the TF card image file here. And I striped partition 2 to save room.

Analyse U-boot

U-boot has many versions. We should find U-boot in s805 SDK. I find Odroid C1 is using s805. And I can find partitions table here.

As this page shows, we can download U-boot source from github

1
$ git clone https://github.com/hardkernel/u-boot.git -b odroidc-v2011.03

Extract U-boot binary

We can extract U-boot from the image accord to partitions table

1
2
3
4
$ dd if=pb1.img skip=64 count=$((8192-64)) of=uboot.bin
# 8192 is the start block of the paration 1
$ file uboot.bin
uboot.bin: UCL compressed data

If we compile U-boot successfully, we can get a tool build/tools/uclpack to decompress UCL data. I guess UCL is the special package format for s805.

1
2
3
4
5
6
7
8
9
10
11
12
$ build/tools/uclpack -d  uboot.bin uboot.bin.decompressed

UCL data compression library (v1.03, Jul 20 2004).
Copyright (C) 1996-2004 Markus Franz Xaver Johannes Oberhumer
http://www.oberhumer.com/opensource/ucl/

uclpack: block-size is 262144 bytes
uclpack: decompressed 340268 into 740288 bytes

$ file uboot.bin.decompressed
uboot.bin.decompressed: data

file can not recognize U-boot type. But strings can still give us some useful info.

1
2
3
4
5
6
7
8
$ strings -tx uboot.bin.decompressed 
<...>
a9b72 switch_bootmode=if test ${reboot_mode} = factory_reset; then run recovery;else if test ${reboot_mode} = update; then run update; else if test ${reboot_mode} = usb_burning; then run usb_burning;else if test ${wipe_data} = failed; then echo wipe_data=${wipe_data}; run recovery;else fi;fi;fi;fi
a9c98 prepare=logo size ${outputmode}; video open; video clear; video dev open ${outputmode};if fatload mmc 0 0x13000000 logicalcircuit; then bmp display 0x13000000; bmp scale;fi;
a9d46 storeboot=echo Rx Booting...; if unifykey get usid; then setenv bootargs ${bootargs} androidboot.serialno=${usid};fi;echo [Maple] read info...; if mmcinfo; then if fatload mmc 0 ${loadaddr} rxnv; then setenv bootargs ${bootargs} bootfromsd; bootm;fi;fi; echo failed...; run recovery
a9e62 recovery=echo enter recovery;if mmcinfo; then if fatload mmc 0 ${loadaddr} recovery.img; then bootm;fi;fi; if usb start 0; then if fatload usb 0 ${loadaddr} recovery.img; then bootm; fi;fi;if imgread kernel recovery ${loadaddr}; then bootm; else echo no recovery in flash; fi;
a9f77 usb_burning=update 1000
<...>

At 0xa9d46, storeboot environment variable shows U-boot load rxnv file using fatload command, and runs it using bootm. Now I can be sure rxnv actually is the kernel, and it should be encrypted. fatload should have code to decrypt it, or the kernel will not boot.

Locate decrypt codes in U-boot

Now, open uboot.bin.decompressed with Ghidra. I can get U-boot will be loaded at 0x10000000 by check file build/u-boot.map in U-boot source code.
That’s the funnest part during the whole hacking. Inspect disassemble code with source code. To make analysis easier, I wrote a header file uboot.h. This file includes some structure definitions. We can import this file into ghidra.After hours of efforts, I found the codes to decrpyt rxnv, it’s at 0x10064aec-0x10064f2c, shown as follows:
Screenshot.
And these codes mainly do the following things:

  • Check the name of the file currently loading, if it is rxnv then decrytpt;
  • It seems to prepare key in the first loop;
  • Decrypt data in the sencode loop;
    And I found ghidra’s register renaming makes it not very easy to know the actual register name in every assembly instruction, just like instruction mov maxsize, #0x4 at 0x10064af0. We can use Patch Instruction menu item in the context menu to check the original instructions.

Decrypt data using code in U-boot

Let’s decrypt rxnv using the code we found. I worte a python script for this work. This python script uses unicorn to emulate ARM instructions in U-boot to decrypt rxnv file.

  • Create an ARM emulator
    1
    mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
  • Allocate memory
    1
    2
    3
    4
    5
    6
    7
    8
    mu.mem_map(code_ptr, 0x10000000); # code_ptr= 0x10000000, I will put U-boot binary in here

    mu.mem_map(data_ptr, 0x10000000); # data_ptr= 0x20000000, I will put encrypted data in here;

    mu.mem_map(alloc_ptr, 0x10000000); # alloc_ptr= 0x30000000, this rangion if for malloc/free functions

    mu.mem_map(0 , 0x00200000); # this is stack, and set sp_ptr = 0x00100000

  • Load data
    1
    2
    3
    4
    5
    6
    #  load uboot
    mu.mem_write(code_ptr, open('uboot.bin','rb').read())
    # load data
    data = open('rxnv','rb').read();
    datalen = len(data);
    mu.mem_write(data_ptr, data);
  • Write hook codes
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # callback for tracing basic blocks
    def hook_block(uc, address, size, user_data):
    if address == 0x10016248: # hook malloc, emulate malloc function,
    sz = uc.reg_read(UC_ARM_REG_R0);
    global alloc_ptr;
    ret = alloc_ptr;
    alloc_ptr+=sz;
    uc.reg_write(UC_ARM_REG_R0, ret); # write return value, a pointer to allocated memory
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR));
    elif address == 0x10015fec: # hook free, do nothing
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR));
    elif address == 0x10015bc8: # hook printf , actual printf function will access to serial port, so I emulate this function to avoid hardware operations. Unicorn is a great tool to emulate CPU, but is not very easy to implement serial ports.
    fmt = uc.reg_read(UC_ARM_REG_R0);
    bs = uc.mem_read(fmt, 0x100);
    s = bs.decode('utf-8').split('\0')[0];
    print(s) # only print the first arguments
    uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR));
    elif address == 0x10064d14: # This is at the begin of the second loop, print loop variable and total loop counter to show decrypt progress
    n1=struct.unpack('I', uc.mem_read(uc.reg_read(UC_ARM_REG_SP)+0x18,4))[0]
    n2=struct.unpack('I', uc.mem_read(uc.reg_read(UC_ARM_REG_SP)+0x28,4))[0]
    print('go here', n1, n2);
    # tracing all basic blocks with customized callback
    mu.hook_add(UC_HOOK_BLOCK, hook_block)
  • Initialize registers and variables in the stack.
    1
    2
    3
    4
    5
    6
    7
    mu.reg_write(UC_ARM_REG_R5,     datalen ) # datalen is length of the encrpyted data
    mu.reg_write(UC_ARM_REG_R4, 0x100abc80) # d = 0x100641d4+[0x10064f48]
    mu.reg_write(UC_ARM_REG_R0, 0 ) #
    mu.reg_write(UC_ARM_REG_SP, sp_ptr ) # set stack register
    # set maxsize
    mu.mem_write(sp_ptr+0x34, struct.pack('I', 0x800000)); # this variable should be greater than datalen;
    mu.mem_write(sp_ptr+0x2c, struct.pack('I', data_ptr)); # data_ptr is the pointer to encrypted data;
  • Emulate
    1
    2
    3
    ADDRESS0 = 0x10064b00
    ADDRESS1 = 0x10064f2c
    mu.emu_start(ADDRESS0, ADDRESS1, count=-1)
  • Save decrypted data
    1
    open('rxnv.decrypted','wb').write(mu.mem_read(data_ptr, datalen)) # now, data_ptr points to the decrypted data
    Please note: this script may take a long time to run. I tested it with my PC,(Intel(R) Core(TM) i5-9400 CPU @ 2.90GHz , 6 cores, 16GB DDR), and it took 15 minutes, so I printf loop variable at 0x10064d14.
    Now, we check decrypted rxnv.
    1
    2
    $ file rxnv.decrypted 
    rxnv.decrypted: Android bootimg, kernel (0x10008000), ramdisk (0x11000000), second stage (0x10f00000), page size: 2048
    It’s a packaged Android kernel.

Analyse Kernel

I use binwalk to extract data from rxnv.decrypted

1
2
3
4
5
6
7
8
9
$ binwalk -e rxnv.decrypted 

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Android bootimg, kernel size: 2986048 bytes, kernel addr: 0x10008000, ramdisk size: 3664373 bytes, ramdisk addr: 0x11000000, product name: ""
2048 0x800 uImage header, header size: 64 bytes, header CRC: 0x71E73C6F, created: 2022-05-19 17:19:01, image size: 2985984 bytes, Data Address: 0x208000, Entry Point: 0x208000, data CRC: 0x9502FBF, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: gzip, image name: "Linux-3.10.99"
2112 0x840 gzip compressed data, has original file name: "ccImage", from Unix, last modified: 2022-05-19 17:19:00
2990080 0x2DA000 gzip compressed data, from Unix, last modified: 2022-05-19 17:19:01
6656000 0x659000 device tree image (dtb)

The -e flag will extract recognized data to floder _rxnv.decrypted.extracted;
Check extracted files:

1
2
3
4
5
6
7
$ ls -l _rxnv.decrypted.extracted/
total 13232
-rw-rw-r-- 1 mxp mxp 7429120 1月 4 14:05 2DA000
-rw-rw-r-- 1 mxp mxp 6117776 1月 4 14:05 ccImage
$ file _rxnv.decrypted.extracted/*
_rxnv.decrypted-0.extracted/2DA000: ASCII cpio archive (SVR4 with no CRC)
_rxnv.decrypted-0.extracted/ccImage: data

2DA000 is a cpio archive. Use the following commands to depack it;

1
2
3
4
$ mkdir cpio.files
$ cd cpio.files/
$ sudo cpio -i < ../2DA000
14510 blocks

ccImage should be the kernel binary.

Anslyse init script in Kernel

I checked the init file in the cpio archive. and it’s actually a bash script, and I found commands to mount rxte and user

1
2
3
4
5
6
7
8
debug_msg "校验运行1"
cryptsetup luksOpen /flash/rxte rxsys -d /usr/lib/system.key &
debug_msg "校验运行2"
cryptsetup luksOpen /flash/user user -d /usr/lib/system.key &
debug_msg "准备挂载"
mount_part "/dev/mapper/rxsys" "/sysroot" "rw,loop"
debug_msg "挂载user"
mount_part "/dev/mapper/user" "/sysroot/userdata" "rw,loop"

Recover true root file system

Run the same commands to mount rxte and user file, we can get actual rootfs.

1
2
3
4
sudo cryptsetup luksOpen rxte rxsys -d _rxnv.decrypted.extracted/cpio.files/usr/lib/system.key
sudo cryptsetup luksOpen user user -d _rxnv.decrypted.extracted/cpio.files/usr/lib/system.key
sudo mount /dev/mapper/rxsys /mnt/disk -o "rw,loop"
sudo mount /dev/mapper/user /mnt/disk1 -o "rw,loop"

Now we have hacked this board.

Conclusion

Finally, I hacked this board, The primary part is to decrypt rxnv using unicorn. We need not to rewrite instructions in C or Python. All we need to do is emulate the ARM instructions correctly.

Author

Meng Xipeng

Posted on

2023-01-04

Updated on

2023-10-05

Licensed under

Comments