Reversing AmongUs APK

Gabrio Tognozzi
6 min readJan 1, 2021

I’ve started my journey into the software world when, a long time ago, I was exposed to the game hacking community of Metin2. Honestly I never understood what was actually going on under the hood. Today, that I’ve grown up, I want to challenge myself to see if I’m actually able to do something fun myself, without copying and pasting pieces of software as a script kiddie.

In this post I will do some reverse engineering and dynamic instrumentation on the AmongUs APK, on a running android device, to get my player move faster than it should ( aka speed hack ).

Setup the Environment

I was trying to do this experiment on a AVD ( Android Virtual Device ) but Frida was having troubles handling the translation between Arm and Intel architectures, and was not finding one of the fundamental Shared Libraries we will work on at runtime. The emulator would probably be a good fit for a purely Java/Kotlin APK. Anyway, AmongUs is written using Unity (with Il2cpp), therefore we will have to work with a compiled Arm binary, and I didn’t find any alternative to use a physical device.

For this experiment we will be using Apktool, Frida, Il2CppDumper, and Ghidra.

Reversing The IL2CPP.so Library

First things first, I don’t understand how this IL2CPP (Intermediate Language to C++) works. I only know that Unity games can be shipped in a way that all the game logic gets compiled into an il2cpp.so library, that gets loaded at runtime from the APK. IL2CPP was implemented for performances reasons and it is about compiling the intermediate language ( by default Unity uses C# ) into C++ and then run it as compiled binaries. This obviously increases the difficulties when reverse engineering it, the classic Unity games, written in C# are really easier to reverse engineer ( for references about reverse engineering C# I suggest you to read about this amazing tool dnSpy )

Anyway, I read that thanks to IL2CPPDumper it is possible to retrieve symbols for the various functions that the game implements. Therefore I used IL2CPPDumper to generate a script.json passing to it the files it needs (to use it read the docs, it needs the executable .so and the global metadata files). IL2CPPDumper also comes with a Ghidra plugin ghidra.py (you will find it inside the Release folders) that imports the symbols into the project. Give Ghidra a couple of minutes, then you can search for for speed in the function names

Search for functions with Speed in their name

The methods that catch my attention are the one related to PlayerPhysics, specifically the one that isn’t related to Ghost mode. Double clicking on the PlayerPhysics.get_TrueSpeed function we can read the disassembly of it

Decompiled code of get_TrueSpeed method of the PlayerPhysics object

We can assume that the first parameter is a pointer to an object of type PlayerPhysics , this object is used to read, at a displacement of 0x30 bytes from it, the value of fVar2 that contributes to the final result of the PlayerPhysics speed. Therefore our goal is well defined: we want to increase the value of the float that is placed at the memory address (param1 + 0x30) . In order to being able of reading the value of the parameter passed to the function we need to use a technique that is called Dynamic Instrumentation, to help us there exists an amazing tool called Frida.

Injecting the Frida Gadget

Given that I was not able to find a rooted device, or get all the things work properly on an AVD, I had to inject the Frida Gadget into the main activity of the AmongUs APK. To inject the Gadget I had to unpack the APK using adb and apktool

$ adb devices # find out if the device is correctly recognized
$ adb shell pm list packages -f | grep spacemafia
$ adb pull /path/to/the/apk amongus.apk
$ apktool d amongus.apk

Now that I have the apk unpacked i could read the manifest searching for the Main Activity

$ cd amongus# this command shows that the activity name is UnityPlayerActivity
$ cat AndroidManifest.xml | grep action.MAIN -B2 | grep android:name

# this command greps for the file containing the class declaration
$ grep "com.unity3d.player.UnityPlayerActivity" . -Rn | grep "class public" | cut -d: -f1

Once found that our Main activity is at ./com/uniy3d/player/UnityPlayerActivity.smali we can patch it injecting the code that loads the frida gadget. The below code calls System.loadLibrary("frida") from the activity constructor, that results in loading the library /lib/arm64-v8a/libfrida.so and allows us to use the Frida CLI for Dynamic Instrumentation.

# direct methods
.method public constructor <init>()V
.locals 0
.line 16
invoke-direct {p0}, Landroid/app/Activity;-><init>()V
const-string v0, "frida"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

return-void
.end method

Once we have injected the Frida Gadget we can pack the APK back, sign it and push it on the device ( about signing an APK there are plenty of examples around the internet, just read one of them )

$ apktool b amongus# sign it with any tool (e.g. jarsigner)$ adb uninstall com.innersloth.spacemafia
$ adb install amongus/dist/amongus.apk

I was afraid that they could have implemented some anti-tampering or anti-frida techniques but there were no issues in starting the patched apk and attach to it with the Frida command line

# Start the APK on the device
$ frida -U Gadget
# This should spawn a frida cli without issues

Patching the Player Speed

Once we are in the Frida cli we can use the Interceptor module to read the value of param1.

To be able to read the value of the parameter of the function, that will be found in register x0 , we need the address of the function. In Ghidra it is shown that the address of the function is 0x103da84 . But in order to find the actual offset of this function inside the il2cpp.so library we have to subtract the image base (Ghidra simulates a dummy image base address when loading binaries in memory). To read the image base address open window->Memory Map and you will find it written on the header of the modal that opens. In my case it is 0x10000 therefore the actual offset from the il2cpp.so library to the get_TrueSpeed method is 0x102da84.

The only thing we are missing is the base address of the il2cpp.so library inside the RAM of the running process. To find it we just need to run from the Frida CLI

[MYPHONE ::Gadget]-> Process.enumerateModules().forEach(m=>{if(m.name.includes("il2cpp")){console.log(m.name,m.base);}})

In my case the base address of the il2cpp.so library was 0x6f4d3d3000 , therefore the address of the method we want to intercept is 0x6f4d3d3000+0x102da84 (base of the module + offset of the method from base) that results in the address 0x6f4e400a84.

We’ve found that the address of getTrueSpeed is0x6f4e400a84 . We now need to use Interceptor to read the parameter value (passed through the register x0)

Interceptor.attach(ptr("0x7867135a84"),{
onEnter: function (args) {
console.log("registers: ",JSON.stringify(this.context));
},
onLeave: function (value){}
});

This dumps the values of all the registers, we are interested in the x0 register, following the Arm64 calling conventions. In my case its value was 0x6f4fa82be0, which from the Ghidra snippet is 0x30 away from the value that contributes to the player speed. The actual address we are interested in is therefore 0x6f4fa82c10 . Using the Memory module we can read and write to such address

> Memory.readFloat(ptr("0x6f4fa82c10"))
2.5
# We can patch it to give a speedup to the player
> Memory.writeFloat(ptr("0x6f4fa82c10"),12)

Eureka! we can now start running like mads around the game, and we’ve learned something new.

Disclaimer

The information provided by this post is for educational purposes only. All information is provided in good faith, however we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability or completeness of any information.

Under no circumstance shall we have any liability to you for any loss or damage of any kind incurred as a result of the use of the code above or reliance on any information provided on the post.We’re not responsible for your use of the information contained in or linked from this blog post.

--

--