C# Obfuscation in Godot

Code obfuscation (in this context) refers to the practice of disguising code after it is built to make it more difficult to understand or reverse-engineer. There's a whole debate about whether obfuscation is worth the effort or not, but I won't get into that here. Instead, I want to talk about how to obfuscate C# code specifically for a Godot project, since I found very little information online on how to do this successfully.
Caveats:
- This is specifically about C# and is not relevant to GDScript.
- This information is for Godot 4.3 / 4.4 and may not work exactly the same with other versions, especially Godot 3.
- I am in no way an expert on this subject, so follow my steps at your own risk, but I wanted to log my research and experiences in case they are useful to anyone else. Credit to Github user Armynator for establishing a starting point in this issue.
To start with, you'll need an obfuscator application for .NET. There are a ton of these around, but many are either very outdated or very expensive. I used Obfuscar, which is free and open source. It seems to be intended for use with Visual Studio, but it works from the command line as well. I am basing the following information on Obfuscar, but the configuration process should be similar for any obfuscator.
Setup
On Windows, you can open and extract the Obfuscar NuGet package with an archive application like 7-Zip. The executable is tools/Obfuscar.Console.exe. You will need to create an XML config file and run the executable with one parameter containing the path to the XML. Everything else is specified in the XML. The config documentation explains how to set it up, but it won't work out of the box.
The file that you want to obfuscate is the .dll in your exported project's data directory that is named after your .NET assembly, e.g. MyProject.dll. Don't try to obfuscate your executable, it won't work. Obfuscar needs this .dll to stay where it is, but it helps to make a clean working copy of it in the same directory while you're trying to get the configuration right. Make this file the target in the XML (the InPath shown is for a Windows build):
<var name="InPath" value="<build path>\data_MyProject_x86_64"></var> <Module file="$(InPath)\MyProject_working.dll"></Module>
Take note of where your project writes log files, as obfuscation is prone to causing errors until you get the configuration right.
Execution
Once you run Obfuscar, it will generate an obfuscated MyProject_working.dll in the OutPath directory you specify, along with a Mapping.txt that shows what components were renamed and how. Keep this mapping file, even for your published builds! You'll need it to understand the obfuscated stack trace from your application logs.
Copy the new .dll over the original MyProject.dll in your data directory and run your project. With the empty configuration above, the application won't run, and you will get an error in your log file about GodotPlugins. This namespace needs to be completely excluded from obfuscation, so add the following inside the module tags:
<SkipNamespace name="GodotPlugins*" />
If you obfuscate now, the project might work without further configuration. I had some additional issues with Minerva Labyrinth, so I'll explain how I solved them. This isn't a comprehensive list of possible issues, but generally, you just want to test your exported project and observe any errors. The log files will point you to elements that you need to either refactor or exclude from obfuscation. Anything that relies on reflection or similar string trickery (including in Godot's generated code) is particularly at risk.
Nonexistent signal
At first, my project started, but nothing worked. The log file showed a bunch of errors about connecting callables to nonexistent signals. I found that if I excluded all methods from obfuscation, it worked, but I didn't want to have such a broad exclusion. Eventually I narrowed the problem down to custom signals that were defined in code, but connected in the Godot signal inspector. After poking around in Mapping.txt, I noticed that the classes that contained the problem signals included an obfuscation of a method called GetGodotSignalList. Skipping this method for all types solved this problem. I'm guessing that this method is called somewhere in the Godot layer using reflection, so renaming it broke that call.
<SkipMethod type="*" name="GetGodotSignalList"/>
Deserialization
The next problem came with deserializing JSON data from disk, specifically saved games and custom control settings. The reason here is straightforward: I'm using C#'s built-in serialization libraries to map directly between JSON and the fields in some structs, so changing the names of those fields broke the deserialization. Fortunately, all of the structs that I use for serialization have the naming convention *SerialBlock, so it was a simple matter to just skip them all.
<SkipType name="*SerialBlock*" skipFields="true"/>
Enum string matching
My last major problem involved several enums related to stats and equipment. I actually didn't notice this immediately, but it became apparent when I saw that my equipment screen wasn't working correctly, and the log showed errors about nonexistent enum values. This is due to some inspector-facing code that matches strings to enum names. Obfuscar is kind of weird about enums; you can add <SkipEnums value="true"/> to skip all enums, but I didn't want to do that. Eventually I worked out that this option will obfuscate the name of an enum class, but leave the actual enums alone:
<SkipField type="EquipSlot" rx=".*" />
However, if you have a lot of problematic enums, using SkipEnums might be easier than listing them all.
Private vs. Public
By default, Obfuscar only obfuscates private members and leaves public and protected ones alone. You can configure it to obfuscate these as well, but this increases the chances of something going wrong. In my case, the enum and deserialization issues only appeared when I toggled on public obfuscation, but private obfuscation didn't actually cover very much. I think if you're going to the trouble of obfuscating in the first place, it's worth turning this on, if you can.
Character Sets
There are also options to use Unicode or Korean characters for obfuscated names. I found that the Korean set tentatively worked, but enabling the Unicode set for some reason caused Obfuscar to not respect the SkipNamespace, which sent me back to the GodotPlugins error. I decided not to bother with these.
ScriptPath
For some reason, Godot adds a ScriptPath annotation to all of your built classes, which is visible in obfuscated code and shows Godot's internal resource path to the script. I don't know if this is possible to obfuscate automatically, or if doing so would break the application.
ILSpy
Finally, how do you know your obfuscation worked and what it looks like? You can use ILSpy to browse your obfuscated .dll. This is a good way to check your obfuscator's work and see if you're satisfied with your configuration.
Test It
Obviously, testing is key. Obfuscation is delicate and not all errors will be immediately obvious, so be sure you test your application thoroughly afterwards. I have a lot of testing work coming up for Minerva Labyrinth before release, and I wanted to look into this topic well in advance.
Minerva Labyrinth
A dark magical girl dungeon crawler
Status | In development |
Author | Midnight Spire Games |
Genre | Role Playing |
Tags | blobber, Dark Fantasy, Dungeon Crawler, Fantasy, First-Person, magical-girl, Retro, Singleplayer, Turn-based, Turn-Based Combat |
Languages | English |
Accessibility | Configurable controls, Interactive tutorial |
More posts
- v0.4.10 - Fix for subway lock1 hour ago
- Demo available now!16 hours ago
- Demo Work18 days ago
- Trailer + almost there38 days ago
- Update on Godot MigrationOct 31, 2024
- Progress with GodotNov 09, 2023
- Where We AreOct 21, 2023
- First Gameplay FootageJul 29, 2023
- The Unexpected Hassles of Colorful TextApr 30, 2023
Leave a comment
Log in with itch.io to leave a comment.