Tutorial on how to reverse engineer Unity games to gain understanding of their inner workings.
I recently discovered a new game called Muse Dash. It’s a rhythm game so after watching a few gameplay videos, I instantly purchased it on Steam. I’m a huge fan of rhythm games so I spent a lot of time playing it. The game has multiple characters that can be unlocked and each character has unique abilities. One of these characters increases the score you gain but drains your health while you are playing. This is similar to other rhythm games like Osu!. I have played numerous hours and have still not having unlocked this character. Therefore I want to investigate the game to see how unlocking characters is implemented.
Initial Analysis
This article is gonna focus on the PC version of the game. Since the game is made in Unity, the core logic should be the same across all platforms. The game is available on Steam and costs US$2.99 or 3.49€ with an expansion pack adding 78 more songs costing US$29.99 or 29.99€.
After installation, we can look into the folder structure of the game. This can be done quickly by going to the properties of the game and pressing the 'Browse local files...'
button in the 'Local Files'
tab. This will yield a view similar to Figure 2.
Here we can see the first clue that the game was developed using Unity. A dll called UnityPlayer.dll
is present in the root directory. This file is used in the Unity runtime. Since Unity was used, reversing the game is a lot easier because the game logic is implemented in C#. We don’t investigate the main MuseDash.exe
executable since this is a wrapper for Unity and doesn’t contain anything useful for us. Looking in the MuseDash_Data
folder will reveal that all game data is stored in this folder as seen on Picture 3.
Readers who are familiar with Unity will most likely recognize the folder structure. The folder we are interested in is called Managed
. This folder contains all DLL files that will get loaded when the game is started. The remainder of the folders contain third-party dependencies and assets. In the Managed
folder, the only two files we are currently interested in are Assembly-CSharp.dll
and Assembly-CSharp-firstpass.dll
. These are both written in C# so in order to reverse them, we need a C# disassembler. I like to use dnSpy but any tool will work.
Digging In
We load Assembly-CSharp.dll
into dnSpy and this will automatically load in all the required dependencies. Like mentioned earlier, we are not interested in these and will stick to Assembly-CSharp.dll
and Assembly-CSharp-firstpass.dll
. The latter of which is a dependency and, as such, will also be loaded. When we open the first DLL, we see that it’s not packed or obfuscated. This is often done to prevent reverse engineering the file because it is possible to achieve a near 1:1 decompilation.
The first goal is to find the code responsible for selecting which item will be given to the player when they level up. There are multiple ways to approach this. Since we know items need to be awarded when the player levels up, we could search for the logic that handles adding xp and levels to the player. To do this, we use the Search
functionality in dnSpy
. Searching for all numbers and strings containing “level” in the current assembly, we will get a dozen results. We can then sift through this results to eventually find a method called DataUpdate
in Assets.Scripts.GameCore.Managers
.
From this method we learn how the current level of the player is stored. Using this knowledge, it is possible to find the method which will handle increasing the level after playing a song using Search
. However, if we look in the Assets.Scripts.GameCore.Managers
namespace, we will notice a conveniently named class named ItemManager
. To verify that this class is what we are looking for, we can look at the properties of the class which seem to indicate the total amount of items is stored. Furthermore, methods called Init
and Reward
are present in the class. By using the Analyze
functionality in dnSpy
, it is possible to find all methods that called ItemManager.Reward
. This can be triggered by clicking on the ItemManager.Reward
method and pressing CTRL+SHIFT+R
.
We see that ItemManager.Reward
is being called from MessageManager.Receive
. Upon examination we find that this method gets called every time the player receives something such as experience, an item or a new stage. This method is called with the type of item passed as an argument and is then responsible for calling the correct method based on the type.
With this knowledge, ItemManager.Reward
can be reversed to get an understanding of how the game awards characters. The prologue of the method initializes certain properties for achievements and so isn’t useful for us. After that, an array of strings is built and then passed to a method called ItemManager.OnStateReward
. This method will attempt to award an item with the given rarity to the player. If this fails, it will attempt it with a different rarity. This means that the game will first award items that are less rare to the player. Only once the player has received all items from a given rarity will the rarity increase and award higher rarity items.
This means that, in order to unlock the character I want, I have to play enough to receive all other items that have a lower rarity than the character I want. This is an example of how games developed using Unity can easily be reverse engineered to gain information about the core logic of the game. This can be used to create cheats or mods for a game. A skilled reverse engineer can edit the Assembly-CSharp.dll
to change the logic of the game and potentionally give themselves an advantage.
Anti-Cheat
To combat this, game developers can utilize a variety of countermeasures to try and stop people from tampering with the game. An easy and often used tactic is to pack or obfuscate important files to make reverse engineering harder. This wasn’t used by the developers of Muse Dash which made it easy to look at the game logic. Reverse engineers can partially circumvent this countermeasure by unpacked the file or using deobfuscators like de4dot.
Another resource developers use are anti-cheats. This is software designed to prevent players from cheating by trying to detect cheating attempts and banning the suspected cheater. Indie developers often don’t have resources to create this themselves so they will use open-source projects or hire an external company to help them. This is also the case in Muse Dash, where anti-cheat assemblies can be found in Assembly-CSharp-firstpass.dll
. These assemblies belong to Anti-Cheat Toolkit, an anti-cheat that is sold on the Unity Asset Store.
This anti-cheat is able to stop attacks from low-skilled cheaters but most cheaters with more knowledge will have no issue bypassing this anti-cheat. The anti-cheat does however include an interesting feature called “Obscured types”.
Obscured Types
Obscured Types allows you to protect certain variables from being modified by third-party software. It also hides the variable from memory scanners such as Cheat Engine. This is done by encrypting the value of the variable in memory. This technique is also used by Riot Games in League Of Legends. However, this can be bypassed if an attacker knows the encryption key that was used to encrypt the variable. In this case, the default key is 444444 and xor is used for encryption.
Closure
Reverse engineering games developed using Unity is trivial. However, there are a lot of techniques available to game developers that make it a lot harder for attackers. Since indie developers don’t have the resources to make their own anti-cheat, they often buy third-party modules that might not be as effective.