This blog post aims to provide a method for accurately identifying the version of the NDK (Native Development Kit) used in a .so file within the Android platform.
Methodology
In certain situations, it becomes necessary to confirm the specific NDK version utilized in the .so files of an APK. Once achieve this, we can compile our own binary code using the same NDK version and subsequently patch the resulting binary into the .so files in order to accomplish our objectives. It is crucial to note that using a different version of the NDK may lead to ABI (Application Binary Interface) incompatibility issues with our binary.
Test Case: “Rise of Saiyan” Android Game
For the purpose of illustration, we will employ the Android game called “Rise of Saiyan.” You may download the game from the following link: Rise of Saiyan. Within this game, there exists a shared library file named libcocos2dlua.so.
Locating the Magic Word
The initial step involves finding the magic word, which in this case is “Android”. We can accomplish this by executing the following command:
By analyzing the hexdump, we successfully identified the NDK version string as r21e. This information allows us to download the corresponding NDK version.
Conclusion
In conclusion, this blog post presents a straightforward method for identifying the NDK version string within a .so file. By following the outlined steps, developers can accurately determine the NDK version and proceed with their required tasks effectively and efficiently.
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.
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.
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.
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
staticconst 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.
Print all assets names and types
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++.
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:
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;
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:
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:
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:
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!
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:
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 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
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:
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.
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); newNativeFunction(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.
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.
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!