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!

Author

Meng Xipeng

Posted on

2023-08-31

Updated on

2024-01-07

Licensed under

Comments