Secure77

Techblog and Security

How to Patch a Android Unity Game

Misc

This post is a follow-up of the How to mod a simple Android App post and heavily based on this great article https://hacking.kurcin.com/android/use-frida-to-controll-unity-app-il2cpp/

Background about this Project

I chose a game that I know and that I played myself for a while, this helps to identify useful methods and properties where you want to hook into. As this Game is currently online and active played, I decided to not publish the name of the game to prevent cheating.

Requirements

I used a rooted physical android device with a running frida-server as root. Emulators may won’t work (based on the architecture and different .so loads) and also patching the app with objection will maybe not work.

Decompilation

First, we can confirm that the APK is really using the Unity engine by checking the entry point.

It’s very common that Unity games using this method as main activity com.unity3d.player.UnityPlayerActivity

Next we are going to decompile the app (using the APK Tool Gui) and extract the libil2cpp.so we will find this file in lib\arm64-v8a folder. We also need the global-metadata.dat which we can find at assets\bin\Data\Managed\Metadata

Now we want to decompile the libil2cpp.so to find the definitions and offsets. For this we are going to use the Il2CppInspector (https://github.com/djkaty/Il2CppInspector)

as our App is using a UnitEngine Version 29 we need to use a custom build for this: https://github.com/ElCapor/Il2CppInspector/releases/tag/2021.2

We can drag and drop both files (libil2cpp.so and global-metadata.dat) into the Gui to start the decompiling.

We are using File per assembly and select all for our export

You can also drag and drop the APK directly to tool, so you don’t need to decompile it by yourself

Depending on your target device you may want to choose another image

Investigation

After exporting the c# files we can start to investigate. Usually the created Assembly-CSharp.cs is a good start, as this is the namespace where most of the calls will come from.

This part takes a lot of analyze, searching and code reading, your goal should be to find a method name which you can associate with something in the game. For example: if your game involves some fishing or you can gain experience, search for methods or properties which contains words like fishing or experience. It is also recommended to search for a method which you can trigger in your game on demand, e.g. random drop is probably not a good starting method. You also want to search over all your exported files to find the correct namespace.

In my project I found some interesting method with the name AddHeroIsFishingMarkerToHero which I could assume will be called as soon I start fishing in my app

We are interested in the offset address of this method

Hooking

Now its time to jump over to frida and start to hook into this function. I used this create template from the page mentioned at the beginning of this post.

function awaitForIL2CPPLoad(callback) {
    var i = setInterval(function () {
        var addr = Module.findBaseAddress('libil2cpp.so');
        if (addr) {
            clearInterval(i);
            callback(+addr)
        }
    }, 0);
}

var il2cpp = null;

Java.perform(function () {
    awaitForIL2CPPLoad(function (base) {
        il2cpp = ptr(base)
        console.log("base: " + base)
        attachHacks()
    })
})

// AddHeroIsFishingMarkerToHero in the Game.ECS.cs
var startFishingAddress = 0x00D0B410

    Interceptor.attach(il2cpp.add(startFishingAddress), {
        onEnter: function (args) {
            // On enter function
            this.instance = args[0];
        },
        onLeave: function (resoult) {
            // On enter function   
            console.log("start fishing...")      
        }
    })

We are launching the game with frida and our script.

 frida -U -l .\hack.js -f <target-app>

As soon we start „fishing“ in our game we can see our print statement in the console log 🎉

This means we are on the correct path, next we want to try some more useful stuff by changing values. For this I was looking for some speed changing values and finally found a get property

Overwrite Return Values

If we attach our Interceptor to this property, we can see that it will be called like every second, this means some other class or method will use it to ask for the current movement speed. To investigate in a more clean way, we will save the value and only print it if we start fishing, this way we prevent spawning our terminal with log messages.

[snip]

var currentMovementSpeed

    Interceptor.attach(il2cpp.add(movementSpeed), {
        onEnter: function (args) {
            // On enter function
            this.instance = args[0];
        },
        onLeave: function (resoult) {
           currentMovementSpeed = ptr(resoult.toString())
        }
    })

    Interceptor.attach(il2cpp.add(startFishingAddress), {
        onEnter: function (args) {
            // On enter function
            this.instance = args[0];
        },
        onLeave: function (resoult) {
            // On enter function   
            console.log("start fishing...")
            console.log("current movement speed: " + currentMovementSpeed) 
        }
    })

as we can see, the return value is 0x3ff999999999999a. the GUI of the app tells us that the speed value is currently at Level 16. If we play a little bit an level up our speed to level 17 we will get the following movement speed 0x3ffb333333333333 and for level 18 we will get 0x3ffccccccccccccd. If we take a look at the first to bytes we can see the value change:

0x3ff9 = 16377
0x3ffb = 16379
0x3ffc = 16380

It seems the level ups will change the movement speed by 1 or 2 (maybe higher levels will change the speed even more). But lets try to increase the first 2 bytes to something higher.

With frida its also possible to overwrite the return value. I am not sure how the rest of the return values need to be formatted but we just change to increase the speed by 20, so we take the value 16400 = 0x4010 and ad some random bytes so our final value will be this 0x401099999999999a

Interceptor.attach(il2cpp.add(movementSpeed), {
        onEnter: function (args) {
            // On enter function
            this.instance = args[0];
        },
        onLeave: function (resoult) {
	        // movementspeed + 20
           resoult.replace(0x401099999999999a)
           currentMovementSpeed = ptr(resoult).toString()
        }
    })

We can’t see the level update in our GUI, but we can recognize that we are moving much faster now. Of course, this hack is not persistent, as it only works until Frida is running and overwriting this function. As soon we restart our game the movement speed is the original one.

So let’s try to figure out if we find something which has some lasting impact.

Overwrite Method Parameters

Instead of changing the return value of getter methods, we can try to find some methods which increase something permanently. By looking at the code, I found the method which is called by add an item (like gems or coins)

Here we can see that the AddAmount method is called with a simple BigDouble parameter. Frida offers a really useful function to print all parameters and register in Json format, when our target function get called

    Interceptor.attach(il2cpp.add(currencies), {
        onEnter: function (args) {
            // On enter function
            this.instance = args[0];
            console.log('Context information:');
            console.log('Context  : ' + JSON.stringify(this.context));
        },
        onLeave: function (resoult) {

            console.log("Currency added!)            
        }
    })

we can see at x1 the value 0x4000000000000000

    "pc": "0x7b050a65e4",
    "sp": "0x7b03de4000",
    "nzcv": 1610612736,
    "x0": "0x79e05d1300",
    "x1": "0x4000000000000000"

with further tracking I figured out how the values are stored:

0x4000000000000000 = 1 item
0x4010000000000000 = 2 items
0x4018000000000000 = 3 items
0x4020000000000000 = 4 items

This means we can increase our collect item amount (x * 8) to whatever we want if we overwrite the parameter

    Interceptor.attach(il2cpp.add(currencies), {
        onEnter: function (args) {
            // On enter function
            this.instance = args[0];
            args[1] = ptr(0x4100000000000000)
        },
        onLeave: function (resoult) {

            console.log("Currency added!)            
        }
    })

And indeed, as soon we collect something in our game we can see that we now get a huge amount of items. As this is „add“ action, this will be stored to our character and also be saved. So this will be persistent also after a game restart without frida.

I found several other methods which lead to some significant game changes:

  • catchingSpeed
  • currentCargo
  • rodsCount
  • addExp

look for methods which return (or take) a bool, string or number as value, manipulating these values is much easier then manipulate something like a struct, dictionary or object

Patch the Game

Even if we hook with frida in some function who change our game progress, our save-game is only stored on our rooted device.
To store our changes into the APK we need to mod the .so file.

This is the cherry on the cake, for my project I chose the get property method which will change my current movement speed.

irst, we should check with Frida what the return value of this method is and overwrite until we have our desired value. In my case 0x400099999999999a was the default value and 0x405099999999999a was a good amount to move fast enough in the game.

Next, we want to write a return function in armv8 (which is the 64bit version of arm). The easiest way is to use an online Code to ARM compiler like https://godbolt.org/
Now we just write a simple function which will return our value, as this is a very big number we need to select a long function. Also we need to select a armv8 compiler

        mov     x0, #-7378697629483820647
        movk    x0, #39322
        movk    x0, #16464, lsl #48
        ret

As we can see, the compiler translate our code into nice arm assembly code, which we now only need to convert into hex, for this we can use this online compiler: https://shell-storm.org/online/Online-Assembler-and-Disassembler/

Replace some instructions

Now we only need to replace our target function with our. For this we can use a hex editor, open the libil2cpp.so and jump to our offset 0x00C82B6C (Ctrl + g in HxD), select the next 16 bytes and overwrite these with our function

e0 e7 01 b2 40 33 93 f2 00 0a e8 f2 c0 03 5f d6

finally, we will save the file (delete .bak from the lib folder) and recompile the APP

If everything was correct, we can now install the app on every device and our movement speed should be fine 😎

With Frida we can confirm if our return value is patched.

    Interceptor.attach(il2cpp.add(movementSpeed), {
        onEnter: function (args) {
            // On enter function
            this.instance = args[0];
        },
        onLeave: function (resoult) {
          console.log(resoult);
        }
    })

To make this working on x86 devices also, we need to adjust the libil2cpp.so in the armeabi-v7a folder, retrieve the offsets again from this file and also write a ARMv7 assembly function and (maybe) adjust the return value

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert