Disclaimer: this article is about a vulnerability I found in a Unity game. I disclosed this to the developers of the game which approved of this post but asked me to censor it so there would not be instructions to pirating their game associated with its name publicly.
Some time ago after a long day of work I browsed the Steam Store and looked for some new games to try and play. I was used to seeing a lot of mostly simplistic “Early Access” games and did my best to avoid them as I made quite a few unfortunate experiences with those (an
infamous example among those being DayZ) but one of those titles stood out to me as it was also available as a free demo. This way I could at least have a look at the game and its gameplay before deciding to buy it or not. So I downloaded it, started it and actually spent quite some fun time with it. Personally I did not expect much from the game but I was happy to see that it was one of few “Early Access” games I played that were not only playable but also being actively developed and quite polished.
It did not take long for this reverse-engineering habit of mine to kick in though. So I browsed the game’s files and found a familiar folder structure.
1. The Files and Assembly
This was a Unity game! In the past I hacked quite a few Unity games (Human Fall Flat, Ravenfield, DeadCore, Totally Accurate Battlegrounds) and even wrote a short tutorial on how to get started in hacking those.
At this point I wanted to take a closer look at the game out of curiosity: people write code very differently and often game-developers come up with new and interesting ideas in their code. Sometimes though they just don’t know any better and produce horrible code. Either way, browsing it is usually very interesting.
Knowing that with Unity games most of their interesting code resides in “<Game>\<Game>_Data\Managed\Assembly-CSharp.dll”, I started up dnSpy and loaded the file. First I tried to simply browse the code but once I found the main namespace of the game that all the logic and types were implemented in, I found myself confronted with more than 1.300 classes (and enums) to browse. I did not want to spend a whole lot of time digging through mostly uninteresting code and simply proceeded to inspect what libraries this game references.
The references listed one very unusual reference: Harmony. This is “
a library for patching, replacing and decorating .NET and Mono methods during runtime” and when hacking Unity games I only ever used it myself to hook methods but I had not seen any game using it. To find out where it was used I used dnSpy’s “Export to Project”-feature that created a Visual Studio project and opened in Visual Studio Code (which is a tool I like to use for browsing code). Searching for “Harmony” cleared things up: it was used in a modding-system the developers implemented.
Proceeding the same way with the “LZ4” reference I found out that this compression algorithm was used for savegames. At this point I was wondering whether the demo version of this game was actually a stripped down version of the game (lacking assets of the full-version) or not, having some kind of artificial “demo”-switch that disabled features.
2. Patching code
Naturally, the next term I searched for was “demo” which yielded 428 hits in 68 files. Searching for “.demo” (indicating a member-function or -variable) yielded only 41 hits in 22 files – this looked promising. During all of my Unity hacking I learned that in order to find interesting code more efficiently one should look for (and at) class names that indicate very high-level functions such as “*Manager”, “Main*”, “Game*”, “Player*” (where “*” is a wildcard). I actually found “.demo” in a class called “MainMenu” and browsed its code:
It turned out that this variable could be traced back to the static singleton-like instance implemented in “GameVersion”.
This class held other interesting members like enums that specified “build”-types (publisher-specific) and “release”-types (private/public alpha, press release). And these very lines of code controlled the initialization of the singleton. Since dnSpy allows to edit code, one can simply change members to out liking (including private members!):
GameVersion._defaultAsset.demo = false;
Once saved (“File” -> “Save Module…”) the game would start into the regular game mode and allow using all the features that were disabled before, including saving and loading games. Being able to circumvent actually paying real money for the full version of the game so easily made me wonder: do the developers know about this?
3. Disclosing the Vulnerability
One thing I love about IT conventions is that many popular ones have their talks recorded and uploaded to popular sites like YouTube. I remember watching David Kriesel’s talk “Never trust a scan you did not fake yourself” (original title “Traue keinem Scan, den du nicht selbst gefälscht hast”, YouTube) that was about how he identified a severe bug in XEROX printers and disclosed this to their support. One of the key take-away notes was: “Don’t be a dick.” Nobody benefits from toxic reactions. While this should be common sense people tend to behave toxic in the hacking-scene with the gamehacking-scene often surpassing others.
Being a Unity and C# developer myself and being a big fan of Unity games, I decided to contact the game’s developers about this issue. Luckily, there was a public Discord server about the game maintained by the game’s developers so joined it and quickly got in touch with a developer, explaining to them what I found and what the steps were necessary to effectively crack their demo.
They were rather surprised: “So you just changed those lines and that’s it?” Only two days later they updated the demo changing the implementation. The “demo”-variable was removed and replaced with a public getter-method that simply returned “true”. Again, I could use dnSpy and easily change it to return “false” instead. This worked very well!
Only after the update I realized that one could have circumvented this “switch” without even touching the assembly. In the code of the singleton posted above it reads the data from “Resources”. There’s a file called “resources.assets” in the “_Data”-folder of the game. Loading the file into HxD and searching for the string “Version” (as suggested by the call made in the singleton-code) yielded more than 3.000 hits. I patiently traversed this list and found this:
File: resources.assets Offset(h) 00 04 08 0C 0CC4FE60 00000000 07000000 56657273 696F6E00 ........Version. 0CC4FE70 02000000 01000000 00000000 08000000 ................ 0CC4FE80 00000000 01000000 61000000 01000000 ........a....... 0CC4FE90 2A000000 04000000 32393131 20030000 *.......2911 ... 0CC4FEA0 00000000 00000000 00000000 00000000 ................
Generated by HxD
The file contains uncompressed, serialized resources. Knowing the structure of GameVersion I could interpret the data:
Offset (hex) | Content |
0CC4FE60 | [Padding?] |
0CC4FE64 | Length of Resource-name: 7 |
OCC4FE68 | Content of Resource-name: “Version” |
OCC4FE70 | “Build”-type: 2 (meaning “STEAM”) |
0CC4FE74 | “Release”-type: 1 (meaning “PUBLIC_ALPHA”) |
0CC4FE78 | “Major” version: 0 |
0CC4FE7C | “Minor” version: 8 |
0CC4FE80 | “Revision”: 0 |
0CC4FE84 | Length of “versionAppendedText”: 1 |
0CC4FE88 | Content of “versionAppendedText”: “a” |
0CC4FE8C | Length of “modIndicatorText”: 1 |
0CC4FE90 | Content of “modIndicatorText”: “*” |
OCC4FE94 | Length of “build”: 4 |
0CC4FE98 | Content of “build”: “2911” |
In an earlier build of the game the value of the “demo”-field would have been at offset 0CC4FE78 – changing a single bit would have unlocked the full-version of the game.
4. Countermeasures
In the sense of protection against cracking, games are no different than other software: one can try to protect their software all they want – given enough dedication, reverse-engineers will eventually crack it. One can only try to make that so hard that reverse-engineers won’t be willing to spend a whole lot of time.
There are obfuscators and packers available for use with Unity games but they come with quite some cons: first of all, developers would have to get those – this can cost as little as nothing (open source obfuscators such as ConfuserEx), to very little (e.g. BeeByte’s obfuscator) and quite a lot depending on whatever solution on the market they try. Secondly, every additional build-step slows down deployment to players and depending on the software used there can be noticable performance-impacts (as was just recently matter of public discussion with Denuvo). Then, every change done to the final assembly can introduce new bugs that can be very hard to track down – this is something an “Early Access” game should avoid at all costs so this doesn’t affect ratings of the game.
The developers of the game had a clear opinion on this matter: “I stopped caring about it. It’s a fight I can never win. People will always find ways to pirate, and they simply don’t care about all the damage it does to us indies.”
After I presented the second demo-bypass they replied: “[It] Doesn’t matter anyways. If people go to bed with the conscience clean after pirating the game, it’s their problem”
I agree with them: people will always find a way to pirate games and most of them probably won’t care about the damage they do to the industry. While one could utilize open source software for basic protection, the battle against pirates is a battle one will not win.