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 | ad4dac Android (7714059, based on r416183c1) clang version 12.0.8 (https://android.googlesource.com/toolchain/llvm-project 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 | const _frida_log = new NativeCallback(function(sp:NativePointer){ |
- _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:
- Create a subdirectory named
jni
. - Navigate to the
jni
directory. - Create a C++ file named
mousebot.cc
with the following content:1
2
3
4
5
6
7
8
9
10
11
12extern "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.
- Create
Android.mk
file with the following content:1
2
3
4
5
6
7
8
9
10
11LOCAL_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.
- Create
Application.mk
file with the following content:1
2APP_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
- Create
Makefile
file with the following content: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 running1
2
3
4
5
6
7
8
9
10
11ifndef NDKPATH
$(error NDKPATH not set)
endif
all: build_android
build_android:
${NDKPATH}/ndk-build V=1
clean:
${NDKPATH}/ndk-build cleanmake
, the shared library will be built, and you can find it in thelibs/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:
- Parse the .so file using LIEF library
- 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 | const soname ='libmain.so' |
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 | const m = Process.getModuleByName(soname); |
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 | ################################################## |
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 - Part2
http://mengxipeng1122.github.io/2023/08/29/Hacking-Android-game-using-Frida-Part2/