Behind the Cheat - Angry Birds Classics

Posted by VulturSec Team, on 14 Feb, 2024

Hello everyone! I wanted to analyze some cheats on mobile videogames to understand the techniques used in this field.

Blog Image

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. 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>”:

Mobile Game Hacking: Angry Birds 1

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:

Mobile Game Hacking: Angry Birds 2

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:

  • shared_prefs: which is used to store xml files with key/value pairs.
  • files: which is used to store random files.

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:

  • FBAdPrefs.xml: Which is related to Fb ads and it does not seem to be related to the coin mod.
  • com.rovio.angrybirds_preferences: This file has some configurations related to the configuration of the Facebook Events library, which is used to track the usage of the application. I highly doubt it would have anything to do with the mod.

The comparison of the files in the files folder showed several differences in files and many new files as shown in the image:

Mobile Game Hacking: Angry Birds 3

  • 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:

Mobile Game Hacking: Angry Birds 4

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:

  • saveLuaFileWrapper: used to store any file in the files folder. I found references to method invoked with settings.lua and highscores.lua. Sadly the true method that would save a file was called “saveLuaFile” which is not in the files defined here but in the libAngryBirdsClassic.so as shown in the following image.

Mobile Game Hacking: Angry Birds 5

  • loadTableFromFile: which loads some values stored in a file to a structure. It is used in the save method in order to load the stored files and then merged with the structures that are in memory as shown in the saveLuaWrapper snippet and also used in the logout functionality to load the content of the settings and highscore values.

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:

  1. Modify the ingame currency in a guest account using some memory editor, like GameGuardian (https://gameguardian.net/forum/topic/23118-angry-birds-classic/).
  2. Closing the game should have stored the files with the changes in currency.
  3. Dump the game files with adb.
  4. Modify the game to add the scripts that will override the files in the private folder (Note that the hacker just dumped all the files and stored them as they were in the datasave file).
  5. Pack the game.

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.