Android Game Hacking Series - Level 3
Introduction to Android Game Hacking and its techniques. Hello everyone! Here is our third post on Android Game Hacking.
Introduction to Android Game Hacking and its techniques.
Hello everyone! Here is our third post on Android Game Hacking. In the previous blog post we worked on hacks in native (ARM) libraries and its memory. You can check the previous blog posts here:
Android Game Hacking - Level 1
Android Game Hacking - Level 2
In this blog post we will work on hacking games developed in Java (for Android), we will create some hacks and in the following blog post we will work on a mod menu to patch the application and activate the hacks on runtime.
For the blog post we will use the following rogue like game: Pixeldungeon
As the game is developed in Java we will use Frida (https://frida.re/) to patch the application in runtime. This will be executed directly on the process, which will make the hacks not persistent. In the following blog post we will make the hack persistent with a mod menu.
The use of Frida has several benefits:
The only hard restriction is to have the device or emulator with root capabilities. There is a way to inject Frida in non-rooted devices but we will not cover it on this blog post.
The setup to work is the following one:
About the game
Pixel Dungeon is a roguelike RPG game, and the objective of the game is go through all the levels of a dungeon. The player can choose between four character classes (Warrior, Mage, Rogue & Huntress), which have different abilities and stats. The character has different stats, a character level and HP. The player looses when the character’s HP gets to 0. In order to level-up the character has to kill monsters that will spawn while the user crawls the dungeon.
The dungeon is divided in levels. Each level is divided in different rooms which are connected through corridors, with stairs to go up and down the dungeon. Each level is generated randomly when the user gets into it. The algorithm assures that there is always an accesible stair to go up and down.
The developer of the game added a mechanics related to food consumption. The character needs to eat food regularly in order to not get to the starving state, where the player will loose 1 HP after he does a couple of steps. Food is rarely scarce, so the player needs to manage all the resources in order to avoid getting to that state. Because of this, the easiest strategies of farming monsters and getting levels won’t work. Also as everything is generated randomnly, the player will not be able to find food to give the character, and they will get to the starving state.
Pixel Dungeon concepts and internals
The application has the following classes related to the character:
From this class diagram the most important attributes that will be used in the algorithms are:
The main stats can be found in the Hero class:
In order to make the character do things, the application calls the Hero.call() method. This method checks the type of the HeroAction stored in the curAction attribute and then it calls the actXxxx(HeroAction) based on it. As an example if in the curAction the application stored a HeroAction.Move object, whenever the Hero.call() is invoked, the Hero will end up calling the actMove() method, sending as a parameter the HeroAction.Move from the curAction attribute.
In the following sections we will work with different hacks.
Hack 1: Refill full life
The script to refill the full life of the character is the following one:
Java.perform(function () {
Char = Java.use("com.watabou.pixeldungeon.actors.Char");
Java.choose("com.watabou.pixeldungeon.actors.hero.Hero", {
onMatch: function (hero) {
var char = Java.cast(hero,Char);
char.HP.value = char.HT.value;
},
onComplete: function () { }
});
});
The first thing you will note is that this script is written in plain javascript, which some specific functions that are included by default by Frida to interact with a Java process.
In the previous script we use three Frida APIs. The first one is Java.perform which is used to tell the frida-server to run the content inside of the process Java VM.
The second one is Java.use, which imports a class already loaded in the process VM in order to be used in the script. Frida creates a wrapper on the class in order to abstract the developer of the internals of Java.
The third one is Java.choose, which iterates over the VM heap to list all instances of a desired class (in this case the Hero class). In this specific case we use this method to retrieve the Hero instance in order to change the current health. If you come from a game hacking background, this would be like finding the memory address of the health of the character and changing it.
In order to run it you’ll need to have the frida-server running in the Android device, and connect in the following way (executing frida-cli in the terminal of the host).
frida -U "Pixel Dungeon"
If everything goes right you should see something similar to the following screen:
____
/ _ | Frida 16.1.4 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Android Emulator 5554 (id=emulator-5554)
[Android Emulator 5554::Pixel Dungeon ]->
Then you should paste and execute the content from the example, and you will see the character’s health full again.
Hack 2: True damage
We will generate a hack that will impact the health of the enemies around the character. It could be useful in case we face some kind of ghost, which are not always hittable (unless you have a special sword).
In this case we use the private attribute called _visibleEnemies on the instance of our Hero, and then per each visible enemy we change the HP value. This will be translated as true damage as the armor and modifications will not be applied to the damage:
Java.perform(function () {
Java.choose("com.watabou.pixeldungeon.actors.hero.Hero", {
onMatch: function (hero) {
var amount_damage = 1;
var Mob = Java.use("com.watabou.pixeldungeon.actors.mobs.Mob");
var Char = Java.use("com.watabou.pixeldungeon.actors.Char");
var iterator = hero._visibleEnemies.value.iterator();
while (iterator.hasNext()) {
var enemyMob = iterator.next();
var theMob = Java.cast(enemyMob,Mob);
var theChar = Java.cast(enemyMob,Char);
theChar.damage(amount_damage, theChar);
console.log(theMob.$className);
console.log(theChar.HP.value);
}
},
onComplete: function () { }
});
});
Hack 3: Change character stats
For this hack we simply need to get the Hero instance and work on the different values a hero has, like STR, which is related to the hit damage (for warriors).
Java.perform(function () {
Char = Java.use("com.watabou.pixeldungeon.actors.Char");
Java.choose("com.watabou.pixeldungeon.actors.hero.Hero", {
onMatch: function (hero) {
hero.STR.value = 25;
},
onComplete: function () { }
});
});
Hack 4: Remove bad buffs
In the game there are different buffs. Some of them are positive and some of them are negative, so with this cheat we want to remove from the hero some negative buffs:
var isBadBuff = function (className) {
if (className.includes('Satiety')) return true;
if (className.includes('Haste')) return true;
return false;
}
Java.perform( function () {
Java.choose("com.watabou.pixeldungeon.actors.hero.Hero", {
onMatch: function (hero) {
var Char = Java.use("com.watabou.pixeldungeon.actors.Char");
var character = Java.cast(hero,Char);
var iterator = character._buffs.value.iterator();
var buffToRemove = new Array();
while (iterator.hasNext()) {
var buff = iterator.next();
if (isBadBuff(buff.$className)) {
buffToRemove.push(buff);
}
console.log(buff.$className);
}
for (var i = 0; i < buffToRemove.length; i++) {
character._buffs.value.remove(buffToRemove[i]);
}
},
onComplete: function () { }
});
});
Hack 5: Make the level visible
In order to understand the script created for this hack we need to understand how the map is created and managed in the game. The following diagram shows the main classes related to the level and map creation and update:
The map is divided in discrete units of space, that we will call “pixels”. Each pixel has a terrain type (the information about the values are stored in the Terrain class). Each terrain type is associated with an integer value, which can also be seen in the Terrain type. The map is basically a matrix of pixels. In the game this is stored as an array, and whenever the application needs a position, it makes count to convert the array to a matrix. This can be done, because the width of the map is a constant. Each pixel has an id, that is the position of the pixel in the array, which will be used to execute operations and validations during the game lifecycle.
The following image shows the values of the positions in a Room:
The following script was used to print the positions of the cells:
function printMap(charObj, level) {
var pos = charObj.pos.value;
var y = Math.floor(pos / 32);
var x = pos % 32;
var RANGE = 6;
var minY = Math.max(y-RANGE,0);
var maxY = Math.min(y+RANGE,31);
var minX = Math.max(x-RANGE,0);
var maxX = Math.min(x+RANGE,31);
for (var i = minY; i<= maxY; i++) {
var line = "";
for (var j = minX; j < maxX; j++) {
var posPrint = i * 32 + j;
//values in Terrain.java
if (posPrint == pos) {
line += "* ";
} else {
line += posPrint + " ";
}
}
console.log(line);
}
}
Java.perform(function () {
Java.choose("com.watabou.pixeldungeon.actors.hero.Hero", {
onMatch: function (hero) {
var heroObject = hero;
//cast a Char
var charClass = Java.use("com.watabou.pixeldungeon.actors.Char");
var charObj = Java.cast(hero,charClass);
var Dungeon = Java.use("com.watabou.pixeldungeon.Dungeon");
var level = Dungeon.level.value;
printMap(charObj, level);
},
onComplete: function () { }
});
});
Another important attribute from the Level class is visited, which has an Array of booleans. This array has one item per pixel, and it stores if the user visited the pixel or not. Visit a pixel means it gets into the field of view of the character.
In this case the visited = true is mapped to 1 and the visited = false is mapped to 0. The only change from the above script was the printMap function:
function printMap(charObj, level) {
var pos = charObj.pos.value;
var y = Math.floor(pos / 32);
var x = pos % 32;
var RANGE = 6;
var minY = Math.max(y-RANGE,0);
var maxY = Math.min(y+RANGE,31);
var minX = Math.max(x-RANGE,0);
var maxX = Math.min(x+RANGE,31);
for (var i = minY; i<= maxY; i++) {
var line = "";
for (var j = minX; j < maxX; j++) {
var posPrint = i * 32 + j;
//values in Terrain.java
if (posPrint == pos) {
line += "* ";
} else if (level.visited.value[posPrint]) {
line += "1 ";
} else {
line += "0 ";
}
}
console.log(line);
}
}
The fieldOfView attribute holds which pixels can be seen by the character. It depends on the place where the user is, the max range of sight (8 pixels), and if there is any object that blocks the sight (like a wall or a closed door). This attribute is reloaded each time the character moves.
In this case the visible pixels are mapped to 1 and the others are mapped to 0. The only change from the above script was the printMap function:
function printMap(charObj, level) {
var pos = charObj.pos.value;
var y = Math.floor(pos / 32);
var x = pos % 32;
var RANGE = 6;
var minY = Math.max(y-RANGE,0);
var maxY = Math.min(y+RANGE,31);
var minX = Math.max(x-RANGE,0);
var maxX = Math.min(x+RANGE,31);
for (var i = minY; i<= maxY; i++) {
var line = "";
for (var j = minX; j < maxX; j++) {
var posPrint = i * 32 + j;
//values in Terrain.java
if (posPrint == pos) {
line += "* ";
} else if (level.fieldOfView.value[posPrint]) {
line += "1 ";
} else {
line += "0 ";
}
}
console.log(line);
}
}
With all this information, we can now understand how to change the field of view in order to make everything visible:
Java.perform(function() {
var Dungeon = Java.use("com.watabou.pixeldungeon.Dungeon");
console.log(Dungeon.level.value.LENGTH.value);
var Arrays = Java.use("java.util.Arrays");
var arrayLength = Dungeon.visible.value.length;
for (var i = 0; i < arrayLength; i++) {
Dungeon.visible.value[i] = true;
Dungeon.level.value.visited.value[i] = true;
Dungeon.level.value.mapped.value[i] = true;
Dungeon.level.value.fieldOfView.value[i] = true;
}
var Level = Java.use("com.watabou.pixeldungeon.levels.Level");
Level.updateFieldOfView.implementation = function (char) {
return this.fieldOfView.value;
};
});
Conclusion
We created several scripts in Frida to patch the application in runtime in order to generate cheats for it. In order to do so we need to execute the reverse engineering of the Java classes stored in the apk file. This is required in order to understand what to patch and how to do it.
The remaining exercise for this application is to make the hacks persistent. So far we need to run frida-server in the device, and load the scripts manually (kind of the way cheats are loaded with Game Guardian). This is not the standard way to generate moded applications. In the following post we will go through the process of creation of a mod menu for Frida.
If you want us to create a video about this hacks, reach out on our social networks and we will create the content for you :)
If you are a mobile game developer and you want to have an assessment on your mobile game, reach us, and we can help you to make your game hacker-proof!