Behind the Cheat - Angry Birds Classics
Hello everyone! I wanted to analyze some cheats on mobile videogames to understand the techniques used in this field.
Hello everyone! I wanted to analyze some cheats on mobile videogames to understand the techniques used in this field. So here it is a series that will go deep on the techniques used on different mods and cheats.
The first game I chose was Angry Birds, as it was a game I liked a lot when it was a boom some years ago. I won´t deliver the exact version of the modded application because I do not want to distribute malicious or adultered content, even if it is something educational, so you´ll need to find it by yourselves.
The application downloaded claimed that it had a cheat related to unlimited money. This type of mods generally changes the way money is subtracted when something is bought. In this case the cool thing of the hack is that you could patch the assembler instruction used to add instead, so it is virtually unlimited money. The second way I saw this is not really unlimited but a change on the save files in order to change the amount of coins to a high number.
As Angry Birds Classics is not available anymore we won’t have the possibility to run it and understand dinamically how the game works and if the claimed hack works efectivelly. We will go through the modding techniques and I’ll explain the process followed in order to understand how the mod was supposed to work.
1. Get the original version
The first thing I like to do to check where is the hack is to compare the modded version vs the original one, so you´ll need to know which is the original version modded. That is generally easy because the mods try to change specific features on a game and not to hide the version of the original game. In some AAA games that is even an added value because it is a way to know that the hack will work on the latest available version. In fact, in some games (given the importance the company adds to mods and cheats), only the last version of the game will let you play online.
In order to check the version of the modded application you can open the game with Jadx, and check the content of the AndroidManifest and view the android:versionName attribute on the first tag “<manifest>”:
With the version I download the correct version from Apkpure or other store. You could also try to download it from google play directly, but as I start with a static analysis I prefer to download it in my PC and then install it later in the emulator or the device.
2. Compare the versions
In order to make a comparison I start with the decoded original and modded version with the Apktool tool. Doing this will give us an idea of where the changes are, as the tool unzips the apks (which can help us to compare assets and native libs), it will generate the Smali of the Dex files (which can help us to compare Java files between the versions), and it will generate the Android Manifest and other xml files (which will help to see if there is any changes in parameters or in the manifest itself).
The objective of this step is to understand where is the change, but not what is the change. It will guide us in further investigations. As an example if the changes detected are in the native libraries, we will need to execute a binary diff. If the changes are in the .smali files we will need to check the Jadx output in order to understand what the change is.
In my case I used WinMerge (as at the time of writing I’m using a Windows OS), but you can use any tool to achieve the same goal. Luckily there were a really small amount of changes, as it can be seen in the following image, where the left side shows the files of the modded app and the right the ones from the original app:
As it can be seen the changes are mainly in the Java layer. We can see a new file called data.save in the assets, then changes in the META-INF folder which is expected as the application is signed again by the modder, so the CERT.* files correspond to the new signing certificate and the MANIFEST.MF to the new signatures of the files given the new certificates. We can ignore these changes.
Then we have three new classes, and the change of the App class. The new classes will be reviewed in JADX in order to avoid reading smali files, but the App class will be seen in this diff tool as it will be easier to understand where are the changes.
3. Understanding the mod
The first thing I reviewed was the App.smali file, which adds the following instructions:
.method public onCreate(Landroid/os/Bundle;)V
.locals 5
.prologue
invoke-static/range {p0 .. p0}, Lcom/savegame/SavesRestoring;->DoSmth(Landroid/content/Context;)V
move-object/from16 v0, p0
invoke-static {v0}, Lcom/android/unityengine/UnityPIayerNativeActivity;->Init(Landroid/content/Context;)V
...
const-string v0, "GREOzg7MjsgICBBTkRST0lELTEuQ09NICA7"
invoke-static {p0, v0}, Lcom/rovio/fusion/Application;->finish(Landroid/content/Context;Ljava/lang/String;)V
This will add the following code to the onCreate method in the App.java class:
public void onCreate(Bundle bundle) {
SavesRestoring.DoSmth(this);
UnityPIayerNativeActivity.Init(this);
...
Application.finish(this, "GREOzg7MjsgICBBTkRST0lELTEuQ09NICA7");
The App class is not any class. It is the MainActivity, which is the activity that is initially launched when the application is started, and the first method to execute in this application is the onCreate one. So the added code will probably always run unless the application is started through any other class. But as the way an end user starts an application is through the main Activity, it will effectively run.
“SaveRestoring” and the “UnityPIayerNativeActivity” are classes added in the mod. Let’s see what each one of those do:
SaveRestoring.DOSmth
Call SmartDataRestoreForYou:
public static void DoSmth(Context context) {
try {
SmartDataRestoreForYou(context, context.getAssets(), context.getPackageName());
} catch (Exception e) {
Log.e(context.getPackageName() + ":savemessages", "Message: " + e.getMessage());
e.printStackTrace();
}
}
In the following method I removed unimportant things and added comments related to what some lines do in order to make it easier to understand:
private static void SmartDataRestoreForYou(Context context, AssetManager assetManager, String str) throws Exception {
//this is a flag that assures that if this method was already run, does not run a new one by setting a flag in a sharedprefs called savegame:
if (context.getSharedPreferences("savegame", 0).getBoolean("notfirst", false)) {
return;
}
//if it is the first time, the first thing to do is set savegame
context.getSharedPreferences("savegame", 0).edit().putBoolean("notfirst", true).commit();
...
//AssetManager lists all the files in the /assets folder
String[] list = assetManager.list("");
for (int i = 0; i < list.length; i++) {
Log.i(str2, "ListFiles[" + i + "] = " + list[i]);
}
//search for the data.save file (which is in out assets folder)
if (ExistsInArray(list, "data.save")) {
Toast.makeText(context, "Restoring save...", 0);
try {
//unzips the file data.save to the root folder of the application (/data/data/pacakge_id)
Log.i(str2, "data.save : Restoring...");
unZipIt(assetManager.open("data.save"), "/data/data/" + context.getPackageName());
Log.i(str2, "data.save: Successfully restored");
} catch (Exception e) {
Log.e(str2, "data.save: Message: " + e.getMessage());
Toast.makeText(context, "Can't restore save", 1);
}
}
//do the same as data.save but with a file in the obb folder
if (ExistsInArray(list, "extobb.save")) {
...
}
//do the same as data.save but with a file in the external storage folder
if (ExistsInArray(list, "extdata.save")) {
...
}
...
}
UnityPIayerNativeActivity.Init
This file has a curious name, as it is like the UnityPlayerNativeActivity activity which is a common classes in Unity games. I assume that this is to hide the behavior from someone who wants to understand how the library works.
The method executes the following:
public static void Init(Context context) {
/* The first time the application is launched the sharedprefs InUnityEngine.xml does not exists, so the getBoolean from that unexistent file will return the default value (which is the second parameter of getBoolean). As it is true, it will get in the if content. */
if (context.getSharedPreferences("IsUnityEngine", 0).getBoolean("Create", true)) {
/* Sets the flag Create as false, so the second time the application is started the value Create will return false and will not get in the if statement */
context.getSharedPreferences("IsUnityEngine", 0).edit().putBoolean("Create", false).commit();
//shows the message 'Android-1.com'
Toast.makeText(context, new String(Base64.decode("ICAgQW5kcm9pZC0xLmNvbSAg", 0)), 1).show();
}
}
Application.finish
The method is the following one, and I will print the content as debugging in order to make it easier to understand:
//receives 'GREOzg7MjsgICBBTkRST0lELTEuQ09NICA7'
public static void finish(Context context, String str) {
SharedPreferences sharedPreferences = context.getSharedPreferences("isAdsReadyForReward", 0);
SharedPreferences.Editor edit = sharedPreferences.edit();
//deletes the first three chars from the string and decodes it: ;8;2; ANDROID-1.COM ;
//then splits it by ;
String[] split = new String(Base64.decode(str.substring(3), 0)).split(";");
int intValue = Integer.valueOf(split[1]).intValue();
int intValue2 = Integer.valueOf(split[2]).intValue();
int i = sharedPreferences.getInt("k", 0);
int i2 = sharedPreferences.getInt("g", 0);
//if i < 8, increment the k value by ine
if (i < intValue) {
edit.putInt("k", i + 1);
edit.apply();
}
//if i2 >= intValue2, do not increment the g value
if (sharedPreferences.getInt("k", 0) != intValue || i2 >= intValue2) {
return;
}
//increment g by one
//as the cap is 2 (from the previous if, it will get here at most two times)
edit.putInt("g", i2 + 1);
edit.apply();
//show the message 'ANDROID-1.COM'
Toast.makeText(context, split[3], 1).show();
}
By the static analysis I could not find what isAdsReadyForReward specifically used for, and what the k and g values works in here. I followed the analysis dynamically in order to understand if the file exists and how it is used (or if it has something to do with the hack itself).
4. Reading the content of the data.save zip file
At this point I understood how the mod was executed, but I wanted to know what was specifically changed in the game files, so I did an extra effort. I unzipped the files stored in the “data.save” folder, and got the following folders:
Reading the content I had an hypothesis on how the cheat worked. It seemed to be kind of a save state of a game. Maybe the person that executed the hack changed a file with the amount of coins as we stated above, so I needed to know which files seemed to be the candidates. In order to do so I run the original game in an emulator. Even when the game didn’t run completely due to problems I assumed were related to the download of assets from a non-existent folder, it could install the application and the common files.
The comparison of the files in the shared_folder folder showed two differences:
The comparison of the files in the files folder showed several differences in files and many new files as shown in the image:
The folder adc3 has information about requests and cache of a lib related to ads. Checking the content I found that it used the following URL: https://adc-ad-assets.adtilt.com in order to download the Javascript files to control the ads. As I didn’t know that library I searched a bit and I got to the following URL (https://clients.adcolony.com/login) which is the owner of the adtilt.com domain. As it seems to be a genuine Ad library and there is no apparent change (luckily all the files were Json ones) I discarded those ones as being the ones with the hack.
ugc.zip and net.hockeyapp.android are in the original version but not in the modded one, so they weren’t of interest for me.
A032DB9BCE0E6D15383C512251BA805133D6F4A7DGC1 vs AD6C37A2B7F3FA97CBE5CF87F987E7F5F8EE9693BGC1. The file names seem to be generated randomly. I run the original application several times and I got new files each time but with the same content. The file content only changed between the modded version vs the non modded one. As I did not have any clue on what the file was I followed up with the other leads.
bi_data.lua, highscore.lua and settings.lua: These files are binary ones. They seemed to be binary Lua scripts, but when I opened them I couldn’t find the initial header for the Lua files: (1b 4c 75 61). So they probably are encrypted somehow. From these three the most interesting one was settings.lua, that might get something that could change the behavior of the game.
daily_changes.json: Json file which configures what can be retrieved from each day of activity. Even when this could have been a possible target by changing the prizes in coins from 20 or 50 to a high number, it was not the case.
cacert.pem: some certificates changed between the modded version and the last one (I suspect in fact that the original version for the mod was a previous one).
fusion.registry: I have no clue on what this file is. I searched for the file and what could it be and I couldn’t find any reference. The game downloads or generates it when it is installed. As the Java packages for the game starts with “com.rovio.fusion” I suspect it is related to the engine used for the game but I’m not sure.
5. Trying to decompile lua files
As my principal lead to the game mod was related to the encrypted lua files, I tried to search how the encryption happened. I installed the Angry Birds 8.0.3 more times in order to understand if the files changed each time the application was installed or it was something that was stable, and I compared the encrypted files in order to see If I could understand if they were all different. The first thing I found was that the file was different each time the application is installed, and that all the files started in the same way:
I searched for known file signature but I couldn’t find any. Another suspicious thing was that each time the application was installed a new file in the folders was generated. It had common header (which I do not know what it was) and different content each time it was installed. My hypothesis here is that this random file might be the key to encrypt the other files. But as I did not have any clue on what they were I had two alternatives:
a- Find in internet someone who had already done this.
b- Reverse the game to find how this files are generated.
The option “a” is not hard and does not require a lot of time, so I tried to find that information, which led me to many different interesting discoveries. The first one was to a forum where people did mods for Angry Birds. There I found that there are multiple other *Lua files that are the core of the game which are also encrypted in the following way: 1- they are zipped with 7z. 2- they are encrypted with a key that is hardcoded somewhere in the application.
I also found several open source applications in Github that could be used to decrypt those files (like https://github.com/AB360-org/LUAManager or https://github.com/jooapa/Angry_Birds_Decompilation/tree/master). I tried the applications on the scripts in assets/data folder and I could decrypt and decompile them (as they were in binary Lua). This could help in case I had to understand how those files are generated. Another interesting thing about this is that the best way to mod or create cheats would probably be this one instead of the one used for the analyzed hack, as anyone could directly work with the Lua scripts instead of with binaries and the hacks created could be much more complex.
Another interesting blogpost I found was related to the encyption of the highscore.lua and settings.lua file (for another version of Angry Birds) or directly for cocos2d (showing the methodology with some Angry Birds game). Apparently the encryption is AES with CBC and PKCS#7 padding. I needed to confirm this and also finding the key to decrypt the files (which got me to the option b). So I followed these steps:
The key used did not seem to be static as each time the application is installed it will encrypt te same file differently, so at least there is something that changes between installation and installation. I will assume the methods for encrypting the files are the same ones
As I followed the examples in the blogposts I tried to find methods related to decryption in the libAngryBirdsClassic.so but I couldn’t find any similar. So this path was closed.
I found a method that used the “highscore.lua” and “settings.lua” scripts but they were pretty complex and I did not want to follow the reversing of all the methods related to this files, given that there was no debugging information in the methods, so it would take a lot of time.
I followed the b path. In this case I decoded all the Lua files that are in the assets/data/scripts files with the Lua Manager in order to understand what was being stored in the files and maybe to know how they were loaded.
By analyzing the code I found two functions that were wrappers and did not have an implementation (at least in the data/scripts folder) were:
The saveLuaWrapper function is the following one:
function saveLuaFileWrapper(_ARG_0_, _ARG_1_, _ARG_2_)
if _G.native.Account and _G.native.Account.isLoggedIn() then
if _ARG_1_ == "settings" then
loadTableFromFile("settings.lua", "tempLocalSettings")
tempLocalSettings = RovioAccountSettingsManager:combineSettings(tempLocalSettings, RovioAccountSettingsManager:cleanSyncableSettings(settings), true)
saveLuaFile("settings.lua", "tempLocalSettings", _ARG_2_)
tempAccountSettings = RovioAccountSettingsManager:cleanLocalSettings(settings)
_ARG_0_ = "settings_" .. _G.native.Account.getAccountId() .. ".lua"
saveLuaFile(_ARG_0_, "tempAccountSettings", _ARG_2_)
return
elseif _ARG_1_ == "highscores" then
_ARG_0_ = "highscores_" .. _G.native.Account.getAccountId() .. ".lua"
saveLuaFile(_ARG_0_, _ARG_1_, _ARG_2_)
return
end
end
saveLuaFile(_ARG_0_, _ARG_1_, _ARG_2_)
end
Another interesting detail related to stored files is that whenever the user logs in, the file used to track the highscores and state of the account are other ones, highscore_<account_id_hash>.lua and settings_<account_id_hash>.lua. I did not find this files because the game crashed at the beginning. From this detail I infered that the hack was made using a guest account, and modifying the original values.
In the saveLuaFileWrapper I could also see that whenever the account was being saved, the application cleaned from the tempLocalSettings the values that should come from the remote configuration. I assume this is because in the settings structure there were currency values, like gems. So cleaning it is a way to “synchronize” values with the server. In the case of the guest account this is not done, so the hacker would be able to modify and persist any value there. This could be a problem in the case of the guest accounts if there were no validation server-side of the bought items.
After reading some code I understood the following:
bi_data collects events related to the use of the game. It must not have anything to do with the unlimited currency.
highscores As the name states, it tracks the performance of the person in each level. It is somehow related to currency as when you have certain score, it will give you more coins or stars, but this event is only triggered when the level is completed.
settings has the state of the user and the game. Even when it was not possible to find statically a method that received settings as a paramenter and added coins (because of the way the applicatios is coded and how LUA uses global scopes and parameters), I found that it hold parameters related to the account such as if it was premium or not, if videos of Ads should be seen and many more.
Because of that information I got pretty sure that the ingame currency should be stored in that file.
Bonus track: What does the unZipIt do?
Basically the method reads a zip file and create the subfolders in the destination folder and unzips the files there.
The method is implemented in the following way. I’ll add comments in some lines and remove the ones that are not important:
private static void unZipIt(InputStream inputStream, String str) throws Exception {
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
/* this is done in the method more than once. I think it is done to obfuscate the method somehow or to let the modder check some conditions and if the conditions weren't met, abort the execution. I think this because daDakdsIID is 0 and PdsjdolaSd is 0 as well, and they are never changed in this game. */
if (daDakdsIID != PdsjdolaSd) {
throw new Exception("System error...");
}
byte[] bArr = new byte[1024];
/*create the destination folder. In our case as the destination is the root application folder in the internal storage, the folder is already created when the app is installed.*/
new File(str).mkdirs();
ZipEntry nextEntry = zipInputStream.getNextEntry();
...
while (nextEntry != null) {
//discard folder list in the zip file
if (nextEntry.isDirectory()) {
nextEntry = zipInputStream.getNextEntry();
} else {
/* on each file listed in the zip take the folder path and create it recursively. e.g.:
- files/ (discarded in the previous if)
- files/folder/ (discarded in the previous if)
- files/folder/file.xml
*/
int lastIndexOf = nextEntry.getName().lastIndexOf(47);
if (lastIndexOf < 0) {
lastIndexOf = 0;
}
/*
following the example take files/folder/ and create it recursevely in the /data/data/package_id/ folder.
*/
new File(str + "/" + nextEntry.getName().substring(0, lastIndexOf)).mkdirs();
FileOutputStream fileOutputStream = new FileOutputStream(new File(str + "/" + nextEntry.getName()), false);
...
//copy file to the destination.
while (true) {
int read = zipInputStream.read(bArr);
if (read <= 0) {
break;
}
fileOutputStream.write(bArr, 0, read);
}
fileOutputStream.close();
nextEntry = zipInputStream.getNextEntry();
}
}
...
}
This method does not have anything fancy. Note that it is prone to ZipSlip, so do not copy and paste it to use it in your application :)
Conclusion
Even when I couldn’t confirm the hypothesis of the way the hack was done, the hacker should have followed the next steps:
Note that it is not that hard to achieve this technique, as there is no much need of knowledge of reversing or coding, as the methods are generic and well documented. There are plenty of ways to detect this type of attacks and to prevent them, as RASP techniques or authoritative servers that validates each transaction.
If you are a game developer and you want to prevent this type of attacks or understand how resilient is your application to hacks, you can reach us and we will happily help you on the security review of your games.