Hacking a IL2Cpp Game
About Il2Cpp
Il2Cpp is a converter for c# that translates from IL (Intermediate Language) to normal native assembly. I think it’s even made for unity. But yeah you will find it often in mobile apps and some games.
So we’ve got two unity il2cpp games currently being very popular: Fall Guys and… Among Us.
I started playing Among Us with a few guys and thought “mhh what shenanigans could one do?”.
And I hope you’re not doing the big evil piracy just because the devs are to lazy to implement proper security right? Like that would be very evil and probably wouldn’t even give you all the DLCs.
My first approach
Dumping the game
While we can basically reverse the game like usual with Ghidra or IDA, we can get a huge headstart here.
C# usually can be almost completely decompiled and recompiled again including variable names and method bodies.
Here we don’t have that luxury, but il2cpp saves the function names and classes (including fields) in a metadata file (called global-metadata). That means we have to reverse the method bodies on our own though.
Okay first we need the game and a program called Il2CppDumper.
We can run it and get a few prompts or use this:
Il2CppDumper.exe <executable-file> <global-metadata> <output-directory>
The executable is here not the Among Us.exe
, but the GameAssembly.dll
. The global metadata can be found (relative from the base directory) in Among Us_Data\il2cpp_data\Metadata\
.
So the its this:
Il2CppDumper.exe "GameAssembly.dll" "Among Us_Data\il2cpp_data\Metadata\global-metadata.dat" <output-directory>
We get a few output files including some DummyDlls, which we could use in VS or dnSpy, a script.json
, a header and some scripts like ida.py
and ghidra.py
(they are actually in the base directory and don’t get generated).
We can now analyze the dll in Ghidra (which takes long af) and then run the ghidra.py (and select the script.json
).
After some long time, we can see that the script did a suboptimal job at importing the functions, but at least we know where which function is and how the classes are built.
Actually using the gathered info
We could start a new c++ project, import the header and do our typical stuff like changing values etc. I mean we have the offsets, so that would be easy. I found something else after that, but more on that later.
Calling Methods
We can read out of dnSpy something like this:
[Token(Token = "0x600020E")]
[Address(RVA = "0x20C0E0", Offset = "0x20AAE0", VA = "0x1020C0E0")]
public void CompleteTask(PlayerControl pc, uint taskId);
The Method CompleteTask
is then located at GameAssembly.dll
+ RVA (0x20C0E0
). We can also find the signature in the script.json
:
{
"Address": 2146528,
"Name": "GameData$$CompleteTask",
"Signature": "void GameData__CompleteTask (GameData_o* __this, PlayerControl_o* pc, uint32_t taskId, const MethodInfo* method);"
},
Notice the third parameter const MethodInfo* method
. If we look at a call in Ghidra, in most cases we will just put there 0x0. Don’t ask me why, it’s some il2cpp stuff.
Accessing Static fields
I found this playerlist as a field of GameData
:
// Token: 0x0400034F RID: 847
[Token(Token = "0x40001D9")]
[FieldOffset(Offset = "0x24")]
public System.Collections.Generic.List<GameData.PlayerInfo> AllPlayers;
Lucky for us GameData
has a static Instance
field:
[Token(Token = "0x40001D8")]
[FieldOffset(Offset = "0x0")]
public static GameData Instance;
Okay I tried to find the solution, but I only found this (for me at the time) useless thread on Github.
Look at script.py to find something like “Class${namespace}{classname}”
I have no idea what I’m supposed to to with that info.
You could probably just use this struct (from the header) and look at the script.json address.
struct GameData_o {
GameData_c *klass;
void *monitor;
GameData_Fields fields;
};
Then access it via GameData->klass->static_fields.Instance
.
Btw spoiler: You probable won’t be able to access GameData
, because it was always null for me.
The second (for me) better way
While I was looking for a way to use the static fields, I found this program called Il2CppInspector which promised a better Ghidra experience, that was on-par with the IDA one (meaning with full parameters and all).
Dumping the game
Okay it has a nice gui and all that, but it also gives you the ability to generate a dll injection project. That sounds like exactly what I need!
Okay let’s go: Select metadata file, select GameAssembly.dll
, blabla.
We then can select the Python Script for dissassemblers (Ghidra for me) or something else (like the DLL Injection project).
Let’s go with the Ghidra route for now:
If you don’t wanna use the GUI you can use this command:
Il2CppInspector-cli.exe -t Ghidra -i "GameAssembly.dll" -m "Among Us_Data\il2cpp_data\Metadata\global-metadata.dat" -p <Output Dir>
In the output directory we have the Ghidra.py
and the metadata.json
and a cpp
folder (which is the DLL injection project). To import it into Ghidra:
- Open up Ghidra
- Add GameAssembly.dll
- No auto analysis (it gets automatically analysed after the script import and doesn’t create conflicts)
- File > Parse C Source
- New Config, add
cpp/appdata/il2cpp-types.h
- Parse options:
-D_GHIDRA_
- Click Parse
- Add the Output folder as Script Folder
- Run the Ghidra.py
- Wait for like 1 hour
Okay now we have a proper reversing environment.
Using the stuff
The generated project already has some folder structure:
- appdata (the actual data for the game like function offsets and structs)
- framework (dll entry point and il2cpp init helpers)
- user (where we will put our code)
Calling Methods
That’s pretty easy actually. You can just call something like GaugeRandomizer_Update(gauze, 0x0)
. The last parameter is the MethodInfo* again.
Note that some functions can’t be called from other threads (like Present).
Static types
Most classes with static fields are nicely mapped to <class>__TypeInfo
from il2cpp-types-ptr.h
.
We can use the usage in Ghidra:
auto instance = GameData__TypeInfo->static_fields->Instance;
Example
Here is some code I wrote in first to test around
void Run()
{
while (true)
{
Sleep(100);
if (IsKeyDown(VK_F5))
{
auto playerControlType = app::PlayerControl__TypeInfo;
if (playerControlType != nullptr)
{
auto gameOptions = playerControlType->static_fields->GameOptions;
if (gameOptions != nullptr)
{
gameOptions->fields.PlayerSpeedMod = 2.f;
}
}
}
}
Of course this is not good code, but I wanted to test around till I got the hang of it.
If you want to use the code in other files, include il2cpp-appdata.h
. You may need to include it before any other headers. Also the precompiled header didn’t fucking work for me.
Some fun ideas
This game is written with pure security in mind
- Just see all the bad guys, I mean what’s this game all about right?
- Kill People from whereever you are
- Kill yourself, this works surprisingly great
- Gotta go fast
- Complete tasks from whereever you are
- Kill Timer? Nah
- Jump into those vents, just to be sus. Oh you’re innocent? Doesn’t matter
I haven’t found a way to say no to murder yet. If you hook the local function, you won’t die, but the other players believe you did. Also you can’t really force being a imposter.
What to do from here?
Hook DirectX, draw a nice menu (guess what I didn’t do), play around to see what shit you can do. And don’t use it online. This game has no anti cheat. Just don’t ruin the fun. Yes, I am saying this under a basically step to step guide how to do this. Fight me.
Other Stuff I found
Melon Loader
The World’s First Universal Mod Loader for Unity Games
Supposed to work with every Unity game including Mono AND Il2Cpp games.
Combined with CppExplorer (a ingame explorer and debugging tool) that sounded great.
Queue 2 hours of me trying to find out why MelonLoader didn’t even start.
The answer: Oh wow it doesn’t work with 32-bit games. I needed to dig in the forgotten discord server archives to find that. Thanks for nothing.