Hacking Android Game using Frida - Part3

Hacking Android Game with Frida (Part3)

In the previous blog post, we discussed how to create a NDK project to interact with an Android game. In this follow-up article, we will delve deeper into the game’s internals by call fucntions in libmain.so.

Analyzing libmain.so

We have chosen a game for analysis because its libmain.so contains a multitude of interesting functions. To examine the symbols exported by the library, execute the following command:

1
readelf -sW libmain.so

In our case, the library exports over 36,000 symbols. In theory, we can call each exported symbol using C++. This presents an exciting opportunity for exploration.

Conducting Static and Dynamic Analysis of libmain.so

Before calling these functions from C++, it is essential to establish a goal. In our current scenario, our goal is relatively simple: we want to list all assets in the game, including their names and types. To accomplish this, we need to extract relevant class definitions from libmain.so. For static analysis, we will employ Ghidra, while Frida will assist us in adding hooks to key functions for inspecting the game’s internal state and memory data. Please note that this process is complex and time-consuming, but the results are well worth the effort. While we won’t delve into the entire analysis process in this blog, we will share the final results. The following are the classes we extracted for listing all assets in the game:

Note: These class definitions are highly dependent on the version of libmain.so used. The MD5 hash of the libmain.so file we worked with is e721395e3e327899a5a55ea4fb422a1c. Additionally, to ensure compatibility with a C compiler and facilitate analysis in Ghidra, I have utilized __cplusplus macros in the code. Ghidra supports C code, and by using these macros, we can import the code into Ghidra for easier analysis. This allows us to leverage Ghidra’s capabilities while working with the codebase.

VuAsset

1
2
3
4
5
6
7
8
9
10
11
// VuAsset
struct VuAsset
{
void * _vtab; // 0x00
#if defined(__cplusplus)
std::string _name; // 0x08
#else
unsigned char _name[0x18];
#endif

};

The VuAsset class has a member variable, _name, of type std::string at offset 0x08, used to store the names of assets. In the ARM64 platform, a pointer occupies 8 bytes.

VuAssetDB

1
2
3
4
5
6
7
8
9
10
11
12
13
// VuAssetDB  size 0xe0
struct VuAssetDB{

unsigned char _0x0[0x40 ]; // 0x00

#if defined(__cplusplus)
std::map<std::string, std::vector<std::string>> _assetNames; // 0x40
#else
unsigned char _assets[0x18];
#endif

// ...
};

The VuAssetDB class contains a member variable of type std::map<std::string, std::vector<std::string>> at offset 0x40, used to store the names of assets. The key of this variable is of type std::string, storing asset type names, while the value is of type std::vector<std::string>, storing asset names of the same type. We will utilize this class to print all asset names and types in the game.

VuAssetFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// VuAssetFactory size 0x190
struct VuAssetFactory {
unsigned char _0x0[0x38 ]; // 0x00

#if defined(__cplusplus)
std::vector<std::string> _assetTypeNames; // 0x38
std::map<std::string, void*> _assetTypeInfos; // 0x50
#else
unsigned char _asssetTypeNames[0x18];
unsigned char _asssetTypeInfos[0x18];
#endif

VuAssetDB* _vuAssetDB; // 0x68

unsigned char _0x70[0x8];

#if defined(__cplusplus)
std::unordered_map<unsigned int, VuAsset*> _loadedAssets; // 0x78
#else
unsigned char _asssetTypeNames[0x28];
#endif


#if defined(__cplusplus)
static VuAssetFactory* mpInterface;

#endif
// ...
};

The VuAssetFactory class includes a member variable of type std::vector<std::string> at offset 0x38, used to store asset type names. We can utilize this member variable to print all asset type names. Additionally, this class has a member variable of type VuAssetDB* at offset 0x68 to store the pointer to VuAssetDB. Furthermore, it contains a member variable of type std::unordered_map<unsigned int, VuAsset*> at offset 0x78, used to store loaded assets. The key of this variable is of type unsigned int, representing the asset hash calculated by asset name, and the valueis of type VuAsset*, storing the asset pointer. Lastly, the class includes a static member variable of type VuAssetFactory* to store the global instance of VuAssetFactory. This class follows the singleton design pattern, allowing us to obtain the global VuAssetFactory instance using mpInterface.

Helper Function for Obtaining the Actual Class Name of VuAsset Children

Based on our analysis, we have observed that VuAsset acts as a base class for other asset-related classes. To simplify our tasks, we have implemented a helper function that allows us to retrieve the actual class names. Please find the code snippet below:

1
2
3
4
5
6
static const std::type_info& getTypeInfoOfInstance_ndk(void* p)               
{
p = *(void**)p;
p = ((void**)p)[-1];
return *(std::type_info*)p;
}

Given that libgame.so is compiled with the RTTI (Run-Time Type Information) option, we can utilize the getTypeInfoOfInstance_ndk function to obtain the class information of instances. It’s important to note that we cannot directly use the typeid operator to retrieve runtime type information (RTTI) of an object. This is because libgame.so contains the RTTI information, and when we write our C++ code in libmousebot.so, the typeid operator will provide the RTTI information from libmousebot.so, which may lead to incorrect results. Since we don’t have visibility into how the derived classes of VuAsset are defined, the getTypeInfoOfInstance_ndk function helps us retrieve the accurate class information from libgame.so.

Now, it’s time to rock! In this section, we will demonstrate how to print all asset names and types using functions provided by libgame.so. This will serve as a great starting point for exploring Frida’s capabilities and the powerful combination of Frida and C++.

  1. Get global pointer to the instance of VuAssetFactory
    To begin, we need to obtain the global pointer to the instance of VuAssetFactory. This can be achieved with the following code snippet:

    1
    auto* pVuAssetFactory = VuAssetFactory::mpInterface; 
  2. Get the instance of VuAssetDB
    Next, we retrieve the instance of VuAssetDB using the pointer obtained in the previous step. The code is as follows:

    1
    auto* pVuAssetDB = pVuAssetFactory->_vuAssetDB;
  3. List all assets in VuAssetDB
    In this step, we iterate through all the assets in VuAssetDB and print their names and types. The code snippet below demonstrates this process:

    1
    2
    3
    4
    5
    6
    7
     for( auto it = pVuAssetDB->_assetNames.begin(); it != pVuAssetDB->_assetNames.end(); ++it){                                                              
    auto& assetType = it->first;
    auto& names = it->second;
    for(auto it1=names.begin(); it1!=names.end(); ++it1){
    LOG_INFOS(" %s : %s", assetType.c_str(), it1->c_str());
    }
    }

    Here, the LOG_INFOS macro is used to print the asset information, which internally calls the _frida_log function.
    The resulting output will be similar to the following:

    1
    2
    3
    4
    5
    <...>
    [/mnt/work/work.2023/frida-hackinggame/jni/mousebot.cc:104] VuTextureAsset : UI/Icons/Inventory_Prize
    [/mnt/work/work.2023/frida-hackinggame/jni/mousebot.cc:104] VuTextureAsset : UI/Icons/Inventory_Skin
    [/mnt/work/work.2023/frida-hackinggame/jni/mousebot.cc:104] VuTextureAsset : UI/Icons/KeyCard_A
    <...>
  4. List All Loaded Assets in the Game
    In this final step, we will list all the loaded assets in the game. Although VuAssetDB contains all the assets, it only provides basic information for each asset. During the game runtime, the actual asset data is loaded on demand.
    The code snippet below demonstrates how to iterate through the loaded assets and print their information:

    1
    2
    3
    4
    5
    6
    7
    8
    auto& v = pVuAssetFactory->_loadedAssets;
    for(auto it = v.begin(); it != v.end(); ++it){
    auto* pAsset = it->second;
    auto& k = it->first;
    auto& assetTypeInfo = getTypeInfoOfInstance_ndk(pAsset);
    const char* assetTypeName = assetTypeInfo.name();
    LOG_INFOS(" %x pAsset %p assetTypeName %s assetName %s", k, pAsset, assetTypeName, pAsset->_name.c_str());
    }

    In this code, we use the getTypeInfoOfInstance_ndk function to retrieve the actual class name of each asset instance. The resulting output will be similar to the following:

    1
    2
    3
    4
    5
    <...>
    [/mnt/work/work.2023/frida-hackinggame/jni/mousebot.cc:118] e1fc0284 pAsset 0x77e86e1e40 assetTypeName 18VuStaticModelAsset assetName Env/Hall/Straight_16m
    [/mnt/work/work.2023/frida-hackinggame/jni/mousebot.cc:118] 93b383af pAsset 0x77c03b1f00 assetTypeName 15VuMaterialAsset assetName Paper/Cardboard_Wall
    [/mnt/work/work.2023/frida-hackinggame/jni/mousebot.cc:118] 9f8e3cbb pAsset 0x77b8be9810 assetTypeName 15VuTemplateAsset assetName Tile_Hall_Hazard/Hall_Hazard_Stomper_2x_16m
    <...>

You can find the complete source code in the repository

Conclusion

Congratulations! This marks the end of our exploration series. In this final log, we have successfully printed all asset information in the game using the functions provided by libgame.so. This serves as a solid foundation for further exploration of Frida’s capabilities. The combination of Frida and C++ opens up a world of possibilities, allowing us toperform various interesting tasks. With the knowledge gained from this series, you can continue your journey of hacking and exploring the game.

Remember, Frida and C++ together are a powerful toolset that can enable us to accomplish a wide range of tasks. The ability to access and manipulate game assets opens up exciting possibilities for customization, analysis, and experimentation.

Happy hacking, and enjoy your adventures in the world of game exploration!

Hacking Android game using Frida - Part2

Hacking Android Games with Frida (Part2)

In the previous blog post, we discussed how to create a Frida Typescript project to interact with an Android game. In this follow-up article, we will delve deeper into the game’s internals by using the NDK (Native Development Kit) and explore the process of hacking Android games using Frida.

NDK Version enumeration

To begin, we need to identify the NDK version used to compile the game’s native library (libmain.so). Follow these steps:
1. Navigate to the game data directory on the Android device. The path may vary, but an example path is:

1
cd /data/app/com.vectorunit.mercury.googleplay-hN_B8AKQmUeVXiBbDZ2Vvg==/lib/arm64

Note: Replace com.vectorunit.mercury.googleplay-hN_B8AKQmUeVXiBbDZ2Vvg== with the actual game ID on your device.
2. In the game data directory, we can find a file libmain.so. Copy it to PC.
3. Extract strings from libmain.so using the following command:

1
strings -tx libmain.so

Look for the version strings and commit IDs in the command output. For example:

1
2
3
ad4dac Android (7714059, based on r416183c1) clang version 12.0.8 (https://android.googlesource.com/toolchain/llvm-project c935d99d7cf2016289302412d708641d52d2f7ee)
ad4e4a Android (4691093 based on r316199) clang version 6.0.2 (https://android.googlesource.com/toolchain/clang 183abd29fc496f55536e7d904e0abae47888fc7f) (https://android.googlesource.com/toolchain/llvm 34361f192e41ed6e4e8f9aca80a4ea7e9856f327) (based on LLVM 6.0.2svn)
ad4f52 Linker: LLD 12.0.8 (/buildbot/src/android/llvm-r416183/out/llvm-project/lld c935d99d7cf2016289302412d708641d52d2f7ee)

Perform a Google search using the version strings and commit IDs to determine the NDK version. In this example, I assume the game was compiled with NDK r17c.
4. Download the according NDK from here, and uncompress it to your PC.

NOTE: NDK r17c is an older version and may not work with newer versions of Linux. Refer this page for potential issues of to run clang++ on NDK r17c. The provided solution in that post can help resolve the issue.

Create helper function in Typescript for C++ code

Next, we will create two helper functions in Typescript that can be called from C++ code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const _frida_log = new NativeCallback(function(sp:NativePointer){
console.log(sp.readUtf8String());
}, 'void', ['pointer']);

const _frida_hexdump = new NativeCallback(function(sp:NativePointer, sz:number){
console.log(
hexdump(sp, {
offset: 0,
length: sz,
header: true,
ansi: false,
})
);
}, 'void', ['pointer','uint']);
  • _frida_log: This function outputs a string and accepts a pointer to the string.
  • _frida_hexdump:This function performs a hexdump of a block of memory and accepts a pointer to the memory and its length.

Build the NDK .so file

To build the NDK shared library (.so file), follow these steps:

  1. Create a subdirectory named jni.
  2. Navigate to the jni directory.
  3. Create a C++ file named mousebot.cc with the following content:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    extern "C" {
    void _frida_log(const char* p);
    void _frida_hexdump(void* p, unsigned int i);
    }

    extern "C" int test(void* base){

    _frida_log("Hello from C++ !");
    _frida_hexdump(base, 0x20);

    return 0;
    }
  • The declarations for the helper functions are enclosed in extern “C” to avoid C++ name mangling.
  • The test function accepts a pointer to memory, calls the helper functions, and returns 0.
  1. Create Android.mk file with the following content:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    LOCAL_PATH := $(call my-dir)

    include $(CLEAR_VARS)
    LOCAL_MODULE:= mousebot
    LOCAL_SRC_FILES := mousebot.cc
    LOCAL_C_INCLUDES :=
    LOCAL_LDLIBS :=
    LOCAL_CFLAGS=
    LOCAL_ALLOW_UNDEFINED_SYMBOLS := true
    LOCAL_SHARED_LIBRARIES =
    include $(BUILD_SHARED_LIBRARY)
  • This file instructs NDK to build a shared library. I set LOCAL_ALLOW_UNDEFINED_SYMBOLS to true to avoid compiler to complain about undefined symbols for our defined helper functions. We implements the helper functions in Typescript. We have no library for compiler to link.
  1. Create Application.mk file with the following content:
    1
    2
    APP_PLATFORM=android-27
    APP_ABI=arm64-v8a
  • To determine the Android API level of your device, use the following command:
    1
    adb shell getprop  | grep api_level
  • To determine the CPU ABI, use the following command:
    1
    adb shell getporop ro.product.cpu.abi
  1. Create Makefile file with the following content:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ifndef NDKPATH
    $(error NDKPATH not set)
    endif

    all: build_android

    build_android:
    ${NDKPATH}/ndk-build V=1

    clean:
    ${NDKPATH}/ndk-build clean
    This Makefile instructs the NDK to build the shared library, while enabling verbose mode with the V=1 flag. Make sure to export the NDKPATH environment variable with the path to your actual NDK installation. By running make, the shared library will be built, and you can find it in the libs/arm64-v8a/libmousebot.so directory.

Convert .so file to typescript module

Next, we’ll convert the generated .so file into a TypeScript module. To accomplish this, we have provided a Python script called so2ts.py, which you can find here. The script performs the following steps:

  1. Parse the .so file using LIEF library
  2. Generate a .ts file that loads the .so file manually:
  • Allocates memory for the .so file, and loads it into the allocated memory, set ting the permissions to rwx.
  • Applies hot patches to the .so file using the information from the relocation sectin.
  • Invokes constoructors of the .so file.

To run the script, execute the following command:

1
./utils/so2ts.py -b libs/arm64-v8a/libmousebot.so -o modinfos/libmousebot.ts

The generated TypeScript file will be located at modinfos/libmousebot.ts.

Call the Test function in Typescript

Now that we have the converted TypeScript module, we can proceed to call the C++ function from TypeScript. Here’s how you can accomplish this:

import the generated .ts file

In your TypeScript code index.ts, import the generated .ts file as follows:

1
import {mod as libmousebotinfo} from './modinfos/libmousebot'

Load the .so file

Add the following code to index.ts to load the .so file:

1
2
3
4
5
6
7
8
const soname ='libmain.so'
const lib = libmousebotinfo.load([
soname,
],{
_frida_log ,
_frida_hexdump ,

})

In the code snippet above, soname represents the name of the original libmain.so file. Since the game process loads this .so file during boot, we pass soname to libmousebotinfo.load to enable our .so file to resolve symbols from this library. Additionally, we pass _frida_log and _frida_hexdump to libmousebotinfo.load to provide these two functions to our .so file.

Call test function

To call the test function in libmousebot.so, use the following TypeScript code:

1
2
const m = Process.getModuleByName(soname);
new NativeFunction(lib.symbols.test, 'int', ['pointer'])(m.base);

In the code snippet above, we obtain the base address of libmain.so using Process.getModuleByName. Then, we invoke the test function in libmousebot.so using lib.symbols.test, where lib.symbols includes all the symbols exported by libmousebot.so.

Recompile index.ts and inject the Frida script

With all the necessary code prepared, we can recompile index.ts and inject the Frida script into the game process. Use the following command:

1
frida -U -l _agent.js -n 'MouseBot'

When executed, the console will display the following output:

1
2
3
4
5
##################################################
Hello from C++ !
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a26ace000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF............
7a26ace010 03 00 b7 00 01 00 00 00 d4 04 72 00 00 00 00 00 ..........r.....

Conclusion

In this tutorial, we have learned how to build a shared library using the Android NDK and call C++ functions from TypeScript. By leveraging Frida, we successfully integrated the functionalities of libmousebot.so with the existing libmain.so of the game process. This opens up possibilities for further exploration and utilization of Frida’s capabilities.

In the next blog post, we will delve deeper into Frida’s extensive feature set and explore how to call functions in libmain.so from our custom libmousebot.so.

Thank you for following along! If you have any further questions or need assistance, feel free to reach out.

Hacking Android game using Frida - Part1

Hacking Android Games with Frida (Part1)

Disclaimer:

The techniques and information presented in this serial of blogs are strictly for educational purposes. Unauthorized use of these techniques for illicit activities, including game hacking, is strictly prohibited. The author and publisher do not condone or support any form of illegal or unethical behavior. It is essential to respect the intellectual property rights of game developers and adhere to the terms of service. Any actions taken based on the knowledge gained from this blog are solely at the reader’s own risk, and the author and publisher are not liable for any misuse or legal consequences that may arise.

Introduction:

Mobile gaming has become a massive industry, and many players strive to gain an edge by tweaking or modifying Android games. One powerful tool for game hacking is Frida, an open-source dynamic instrumentation framework. In this serial of blogs, we will guide you through the process of creating a Frida project using TypeScript, enabling you to hack Android games and unlock new possibilities.
I will put all source codes in here.

The Game for Testing:

For the purpose of testing and learning, we will use “MouseBot”, a popular and addictive runner game developed by Vector Unit. You can download the Android version of the game for free from the Play Store here. It’s free.

Prerequisites:

To follow along with this tutorial, you’ll need the following:

  • A basic understanding of TypeScript.
  • Node.js installed on your machine.
  • A rooed Android device or emulator.
  • Frida installed on your machine.
  • Test game , MouseBot, installed in your machine.

Step 1: Setting Up the Project

  • Create a new directory for your Frida project.
  • Open a terminal or command prompt and navigate to the project directory.
  • Create a file named package.json in the project directory. You can view its content from here.
  • Create a file named tsconfig.json in the project directory. You can view its content from here.
  • Install the Frida library for TypeScript and frida-compile by executing the following command, I have write these libraries in this package.json:
    1
    npm i 

Step 2: Writing the Frida Script

  • Inside your project directory, create a new TypeScript file, index.ts:
  • Open the file in your preferred code editor.
    Write the Frida script to print Hello world to the console.
    1
    2
    3
    4
    5
    6
    const test = ()=>{
    console.log('hello world')
    }

    console.log('##################################################')
    test()

Step 3: Running the Frida Script

  • Save the game-hack.ts file.
  • Open a terminal or command prompt and navigate to the project directory.
  • Create a file named Makefile in the project directory. Its content is shown as follows:
    1
    2
    all:
    ./node_modules/frida-compile/bin/compile.js -o _agent.js index.ts
  • Compile the TypeScript file into JavaScript using the following command:
    1
    make
    This command will create a file named _agent.js in the project directory.
  • Run the Frida script using the following command:
  • Setup your Android device, start frida-server. This page has detailed instructions for this step.
  • Launch MouseBot on your Android device or emulator.
  • Execute the following command to inject the Frida script into the game process:
    1
    frida -U -l _agent.js -n 'MouseBot'
    If everything goes well, Frida will inject the script into the game process. and you will see Hello world in the console.

Conclusion:

In this blog, we explored the initial steps of creating a Frida project using TypeScript and injecting a basic Frida script into an Android game. We emphasized the importance of using this knowledge responsibly and for educational purposes only. In the next blog of this series, we will delve deeper into Frida’s capabilities and explore more advanced game hacking techniques. Stay tuned and happy hacking!

Hacking Pandora's Box Game Cartridge 2

Disclaimer

This blog is intended solely for educational purposes. Please refrain from using this information for malicious purposes.

Introduction

I recently acquired a new Pandora’s Box game cartridge, which utilizes the RK3128 system-on-a-chip (SOC) from Rockchip. I wanted to share my experience with hacking it.
The manufacturer has overclocked it to 1.5GHz to ensure smooth gameplay with HD video filtering. However, due to the increased performance, an additional fan is required for cooling. Fortunately, the cartridge uses an SD card for booting, making it easy to dump the firmware.Here is a photo of the PCB:
PCB

loader

The system initiates the “loader” program to extract necessary files and launch the main program, “mkemu.” The manufacturer has placed the “loader” in the initram filesystem of the kernel, which is in CPIO format. Extracting the “loader” binary is a straightforward process.
The following code snippet from /etc/init.d/rcS in the initram reveals how the system starts the “loader” program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/sh
PATH=/bin:/sbin:/usr/bin:/usr/sbin
runlevel=S
prevlevel=N
umask 022
export PATH runlevel prevlevel

mount -t tmpfs mdev /dev
mkdir /dev/pts
mkdir /dev/shm

mount -a

echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

touch /dev/mdev.log

cd /tmp
mkdir -m 1777 .X11-unix

cd /lib/modules/3.4.39-h3
insmod elib.ko
#insmod ch341.ko

while true
do
/home/games/loader
done

Analyzing the Loader Program

I loaded the “loader” program into Ghidra and performed an automated analysis. At address 0x11b10, it becomes evident that the “loader” attempts to mount a LUKS filesystem file:

1
2
sprintf(uStack_620,"echo -n \"%s\" | cryptsetup luksOpen /dev/dm-1 pass -",&DAT_00028c94);
system(uStack_620);

The LUKS key is stored in ASCII format at address DAT_00028c94 and is calculated using a number read from /dev/elib. The function at 0x14720 includes the code to read the original number from /dev/elib:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int read_rand_seed(void *data)

{
int __fd;
int iVar1;

__fd = open("/dev/elib",0);
elib_fd = __fd;
if (__fd < 0) {
iVar1 = -1;
}
else {
iVar1 = ioctl(__fd,0x80045a13,data);
iVar1 = iVar1 >> 0x1f;
close(__fd);
elib_fd = -1;
}
return iVar1;
}

Fortunately, the programmer compiled the Linux driver into an individual .ko file instead of including it in the kernel binary. This allows us to easily locate the code responsible for returning the number within the small .ko file.

Analyzing elib.ko

We can find elib.ko in the CPIO filesystem. By loading it into Ghidra, we can locate the code that handles the 0x80045a13 ioctl code in the elib_ioctl function. Ghidra’s decompiled C code provides the following information:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
if (param_2 == 0x80045a13) {
local_2c = (undefined *)0x0;
memset(&local_28,0,0x10);
pcVar2 = strstr(saved_command_line,"custom_id=");
if (pcVar2 != (char *)0x0) {
pcVar2 = pcVar2 + 10;
pcVar3 = strchr(pcVar2,0x20);
if (pcVar3 == (char *)0x0) {
__n = strlen(pcVar2);
}
else {
__n = (int)pcVar3 - (int)pcVar2;
}
if (__n < 0x10) {
strncpy((char *)&local_28,pcVar2,__n);
local_2c = (undefined *)simple_strtoul(&local_28,0,0x10);
}
}
if (local_2c == (undefined *)0x0) {
local_2c = (undefined *)0x87654321;
}
printk("\x011custom id: %x\n",(uint)local_2c);
puVar6 = *(undefined **)(((uint)local_38 & 0xffffe000) + 8);
bVar12 = (undefined *)0xfffffffb < param_3;
if (!bVar12) {
bVar12 = puVar6 < param_3 + 4 || (uint)((int)(param_3 + 4) - (int)puVar6) < (uint)bVar12;
}
if (!bVar12) {
puVar6 = (undefined *)0x0;
}
if (puVar6 != (undefined *)0x0) {
return 4;
}
ppuVar4 = &local_2c;
uVar5 = 4;
goto LAB_00010668;
}
if (param_2 != 0x80045a0b) goto LAB_00010674;

And the programmer is lazy, as he hardcoded this sensitive number. It’s 0x87654321. I noticed there is an MCU on the PCB, and I suspected the programmer might have stored the number in the MCU, but he didn’t.

Find the key for the LUKS file

Now, it’s time to calculate the key for the LUKS file. I decided to use unicorn engine for this task, I wrote a python script for this task.
This script requires the preparation of the file rocky.rsa and the number 0x87654321. The thumb instructions call the standard libc functions, memset and memcpy. I simulate these two functions in the block_hook section. The following is the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(address==0x10cb4):  
# void * memset(void *__s,int __c,size_t __n)
s = uc.reg_read(UC_ARM_REG_R0)
c = uc.reg_read(UC_ARM_REG_R1)
n = uc.reg_read(UC_ARM_REG_R2)
print('hook memset', hex(s), c, n)
uc.mem_write(s, bytes([c]*n))
uc.reg_write(UC_ARM_REG_R0, s)
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
elif(address==0x10d2c):
# void * memcpy(void *__dest,void *__src,size_t __n)
__dest = uc.reg_read(UC_ARM_REG_R0)
__src = uc.reg_read(UC_ARM_REG_R1)
__n = uc.reg_read(UC_ARM_REG_R2) #TODO: hack
print('hook memcpy', hex(__dest), hex(__src), __n)
dat = uc.mem_read(__src,__n)
uc.mem_write(__dest,bytes(dat));
uc.reg_write(UC_ARM_REG_R0, __dest)
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))

If the script succeeds, we can get the LUKS key at address 0x28c94. It is a string, and we can use this key to mount the LUKS file on a PC.

Decrypt config.dat file

The function at 0x011dc8 is a decryption function. loader uses it to decrypt config.dat and other configuration files for gaming. I wrote a Python script to perform this decryption. The script saves the resulting file to /tmp/tt.bin.

mkemu

loader starts mkemu using system. We can find the related instruction at address 0x01153c in loader. The Ghidra decompiled C code is as follows:

1
2
3
4
5
do {
system("/home/games/mkemu");
puts("User app exit.");
usleep(10000);
} while( true );

Analyzing the mkemu program

mkemu is the essential program for this game cartridge. It displays the game selection menu and allows users to play their selected game. After analyzing it using Ghidra, I believe it utilizes SDL.

Decrypt asset

The programmer encrypted and packaged the assets in the file asset.dat. Therefore, we need to decrypt the assets using the Thumb instructions in mkemu. You can find the complete Python script in here. This script will save each decrypted asset file in the target folder.

Conclusion

This board isn’t very difficult, and I feel like hacking it is similar to participating in a Capture The Flag (CTF) competition. Every key is like a flag. The only difference is that the board is real.

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.