Hacking Cocos2D Android Games using Frida

Disclaimer

Please note that the information provided in this blog post is intended for educational and informational purposes only. Any application of the techniques and tools mentioned in this post must be carried out responsibly and in accordance with local and international regulations and laws.

Attempting to hack software, including games, without express permission from the owners is strictly illegal and unethical. The author and the publishers of this blog do not condone, encourage or endorse any illegal activity, and any actions taken based on the contents of this blog will be solely at your own risk.

We strongly encourage readers to use this information to enhance their understanding of the underlying mechanisms of software and to promote improved security, rather than for ulterior purposes. This blog, its author, and its publishers bear no responsibility for misuse of the information provided.

Introduction

Hey there! Ever wondered how exciting it would be to peek under the hood of your favorite Android games, particularly the ones built using cocos2d? Well, this blog post will walk you through just that. But we’re not talking about cheats or shortcuts, we’re diving into game hacking — the good kind.

First off, why cocos2d? It’s a user-friendly, open-source software perfect for building games. On the other hand, Frida is a programmer’s secret weapon, akin to a multi-tool. It’s a powerful toolkit that allows you to weave your code seamlessly into an already-running process. This incredible ability to inject and inspect code in real-time, without causing a ruckus, makes Frida an essential instrument for any coder’s toolbelt.

Now, don’t worry if you aren’t a coding whiz. This guide is meant for anyone curious about how games work. A basic grip on the Android system, a touch of Typescript, a little bit of C++ and a whole lot of enthusiasm are all you need to level up. Ready for the ride? Let’s get started!

Create C++ module for hacking

To illustrate, I make use of a 2D game. This game adopts a unique format, defined by its developers, to house its animations. The asset files associated are denoted with a .ROM extension.

Each of these asset files can encompass multiple actions. In turn, each action is made up of several steps. Intriguingly, each step in this context represents an image. These images, which constitute the steps, might exhibit differing dimensions.

I plan on developing a C++ module that can be injected into the process for hacking. A detailed outline of this procedure can be found on my blog post.

Static analysis

Static analysis, also known as code review, is the process of examining source code without executing it, focusing instead on its structure, dependencies, and patterns. It is a widely accepted preliminary step in understanding the functionality of any software or application, including games built on frameworks like cocos2d.

Begin by familiarizing yourself with the cocos2d framework. Cocos2d is an open-source software development kit for developing games and other graphical applications. I can get cocos2d’s code from here: github.com/cocos2d/cocos2d-x.

Thoroughly reviewing the source code of the framework greatly benefits our endeavor to understand software in-depth, an essential step for ethical hacking. However, we should acknowledge that the openness of open-source platforms does present a double-edged sword. Despite its numerous advantages, it also opens a gateway for potential misuse. The exposure of the source code presents an intimate understanding of a software’s underlying mechanisms, which, while essential for improving and learning, can be exploited if it falls into the wrong hands.Reviewing the framework source code is very helpful for our hacking work. So I should say open source has its dark side. source code provides valuable insight into the underlying mechanisms of software.

Extracting information from binary

Typically, Android games developed using the Cocos2D framework incorporate a shared library known as libcocos2dcpp.so. This integral component houses the core logic underlying the game’s operations and interactions, serving as a centerpiece for the game’s design and execution.
I plan to conduct a detailed examination of this file in order to glean more comprehensive insights about the underlying mechanics of this Android game. This in-depth analysis will allow me to understand its structure and functionality more precisely.

file

1
2
$ file libcocos2dcpp.so
libcocos2dcpp.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, stripped

The result derived from executing the file command implies that the file is an ELF file, and is a ARM32 archtecture.

Ghidra

I utilized Ghidra for analyzing the shared library under examination. This comprehensive software reverse-engineering tool revealed that the library exports numerous symbols that can be invoked. A symbol that particularly caught our attention is _ZN7cocos2d14cocos2dVersionEv. This symbol is associated with a C++ function cocos2d::cocos2dVersion().

However, it’s important to note that the symbol name only provides information about the function’s namespace, name, and argument types. We can’t ascertain the return type solely from the symbol’s name. Fortunately though, we have access to the source code. By cross-referencing the function within the source code, I was able to determine that its return type is char*.

The following code illustrates the cocos2d version is cocos2d-x 3.3rc0

1
2
3
4
namespace cocos2d {
char* cocos2dVersion();
}
LOG_INFO(" Cocos2d version: %s", cocos2d::cocos2dVersion());

Once we precisely identify the version of the Cocos2d framework being used, it opens up the possibility to retrieve the exactly corresponding version of the source code.

Define C++ classes

In the process of extracting images from asset files, I lean on the capabilities of a particular C++ class called ActionGroupClass during the Ghidra analysis phase. This class not only streamlines the extraction process but also boasts of several utility methods.
The following is the class define I infered from Ghidra

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

namespace cocos2d{
struct Texture2D;
}

struct ActionGroupClass{
unsigned char _0x0[0x102];
unsigned short _totalActionCount;
ActionGroupClass();
void vQuickLoad(unsigned char*, bool, int, int);
void vGetBaseXY(short, float*, float*);
int GetTotalStep(short);
void vGetVectorXY(short, short, short*, short*);
bool GetPictureIndex(short, short, unsigned char*);
~ActionGroupClass();
cocos2d::Texture2D* GetTexture2DwithAction(short, short);
};
static_assert(offsetof(ActionGroupClass, _totalActionCount) ==0x102, "_totalActionCount not at offset 0x102");

This class, ActionGroupClass, is designed and implemented by game developers, which means it isn’t found directly within the Cocos2D framework.
Game developers have also defined several global variables, each of which is of the ActionGroupClass type. These variables are utilized throughout the game for various purposes, depending on the specific characteristics and methods defined within the ActionGroupClass.
We can utilize these global variables to extract images from asset files. In addition, the shared library should contain code within the .init section. This code is responsible for calling the constructor method of the ActionGroupClass on these global variables. This initialization is essential in setting up the initial state for these global instances.
In addition, modern C++ provides a way to call a constructor on an already allocated memory. This is called placement new. Here’s how it works:
In C++, the new keyword typically allocates memory and then constructs an object in the allocated memory. However, there are times when you want to separate these two operations. For example, when you have already allocated memory and just want to construct an object at that memory location you can use “placement new”.
Here is the general syntax:

1
2
3
4
5
6

#include <new>
...

auto* p = (unsigned char*)&FishActionGroup;
auto* fishActionGroup = new((void*)p) ActionGroupClass();

We can use this way to call constructor method in these global variables.

Dynamic analysis

Dynamic analysis involves the live examination of a system while it’s operational. In terms of hacking a cocos2d Android game using the Frida toolkit, dynamic analysis enables us to inspect and alter the game while it’s being actively played. This can provide insights that are not available during static analysis—when the program or game is not running.

Read string for a pointer to std::string.

When dealing with the Android NDK, there are numerous elements involved when implementing the Standard Template Library (STL). Since I’m utilizing a recent version of the NDK, the conventional method of extracting a string from a std::string variable using std::string::c_str() is unfortunately not applicable. Hence, a different approach is needed. After a detailed analysis of the memory layout connected to a std::string variable within the process, I have developed the following function to accurately extract the required string.

1
2
3
4
5
const char* getStringCStr(std::string& str){                                                                                                 
auto* p = (unsigned char*) &str;
auto* s= *(char**)&p[0x00];
return s;
}

Get RTTI

The shared library has been configured with Run-Time Type Information (RTTI) enabled. This setup allows us to extract RTTI using a pointer referring to a class directly. To streamline this process, I have authored the subsequent function specifically tailored for this task.

1
2
3
4
5
6
#include <typeinfo>
const char* getInstanceTypeName(void* ptr){
void** p = *(void***)ptr;
std::type_info& t =*(std::type_info*)p[-1];
return t.name();
}

Extract images from a asset file

Moving forward, let’s deftly invoke the inherent functions of our key class, ActionGroupClass, to systematically acquire all pertinent information concerning the graphic elements.

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

char* romdata = read_file_into_memory("/data/local/tmp/weapon1.rom") ;
if(romdata){
LOG_INFOS("%p", romdata);
fishActionGroup->vQuickLoad((unsigned char*)romdata, true, 30, 30);
free(romdata);
romdata = NULL;
auto totalActionCount = fishActionGroup->_totalActionCount;
LOG_INFOS(" totalActionCount %d", totalActionCount);
for(auto action=0; action<totalActionCount; action++){
auto totalStep = fishActionGroup->GetTotalStep(action);
float x, y;
fishActionGroup->vGetBaseXY(action, &x, &y);
LOG_INFOS(" action %d ,total step %d %f %f ", action, totalStep, x, y );
short stepx, stepy;
for(auto step = 0; step < totalStep; step++){
fishActionGroup->vGetVectorXY(action, step, &stepx, &stepy);
unsigned char pic;
auto success = fishActionGroup->GetPictureIndex(action, step, &pic);
LOG_INFOS(" action %d/%d step %d/%d base %f %f step %d %d pic %d ", action, totalActionCount, step, totalStep, x, y, stepx, stepy, pic);
auto* tex = fishActionGroup->GetTexture2DwithAction(action, step);
auto w = tex->getPixelsWide();
auto h = tex->getPixelsHigh();
auto* format = tex->getStringForFormat();
LOG_INFOS(" action %d/%d step %d/%d texture %p %d %d %s", action, totalActionCount, step, totalStep, tex, w, h, format);
}
}
}

Here’s an illustrative breakdown of its operations:

The first segment is devoted to reading a ROM file, specifically ‘weapon1.rom’, and storing it into memory. The in-memory address of this acquired data is logged for debugging purposes.

Next, the vQuickLoad method of fishActionGroup, an instance of ActionGroupClass, is invoked. This quickly loads in the binary content from the ROM data into our action group. Meanwhile, the parameters ‘30’ represent the default size in pixels for each sprite in the action.

The memory allocated to ‘romdata’ is subsequently cleared using ‘free’ to prevent memory leaks, and its pointer is nullified for safety.

Thereafter, we acquire the total count of actions stored within our action group by calling the _totalActionCount function. For every action, two tasks are performed: Firstly, the total number of steps related to that action are gathered. Secondly, the base coordinates (x, y) for that action are obtained via vGetBaseXY.

Each step per action is scrutinized next. This includes determining its directional vector (stepx, stepy) and the picture index related to each step. Moreover, the texture associated with each step of the action is also retrieved.

Finally, this section delves into extracting comprehensive details about said texture: the dimensions (width and height in pixels) and the format. This information, alongside other relevant details, is logged for every step in every action.

This meticulous process guarantees a thorough understanding of the movements, graphical elements, sizes, and properties involved, which immensely aids in manipulating or changing game assets for your desired outcome as part of your game hacking initiative.
After obtaining the pointers to Texture2D, it becomes possible to invoke the built-in functions within the class to save the graphic data to disk. However, elaborating on the specifics of this process falls outside the boundaries of this blog post. Therefore, we won’t delve into that here.

Conclusion

In this blog post, we explored the world of hacking Cocos2D Android games using Frida. We emphasized responsible and ethical use of the techniques and tools discussed. We covered static analysis, examining source code, and dynamic analysis for inspecting games while running. We discussed the extraction of images from asset files using the ActionGroupClass. Remember, unauthorized hacking is illegal and unethical. Use this knowledge responsibly to enhance your understanding of software and promote improved security. Happy exploring!

Author

Meng Xipeng

Posted on

2023-10-28

Updated on

2024-01-07

Licensed under

Comments