A plugin for the Godot Engine which obfuscates all GDScripts when exporting a project, resulting in increased difficulty of successful reverse engineering.
- Why does this exist?
- Installation
- Configuration
- Usage
- Limitations and caveats
- Stability
- Issues
- Troubleshooting
- Roadmap
- License
GDScripts are stored as is when exporting a project, which makes it very easy for anyone to gain full access to the original source code, using tools like gdsdecomp. Right now, even comments are preserved. Godot does have built-in encryption, but that will only deter people who don't know about gdke, which extracts the encryption key from the executable.
Thus, it's very easy for people to get the entire GDScript source code of any exported project. This also means that writing cheats for multiplayer games becomes a lot easier, which includes finding and taking advantage of vulnerabilities in server-authoritative code.
This plugin tries to make reverse engineering harder by obfuscating the source code of all exported scripts. Since only the exported 'pck' file gets processed, obfuscation is non-destructive to the project itself. It among other things, generates random names for identifiers, hardcodes enum and constant values and strips out comments and empty lines.
It does of course not make reverse engineering impossible, but will act as a way greater barrier than just encryption for example, which can be easily bypassed by just downloading a publicly available tool.
Source code(left), obfuscated code(right)
Installation ↑
Note: Requires Godot 4.0+. Developed and tested on Godot 4.2-stable.
- Download a release build or the latest main branch.
- Extract the zip file and copy
addons/gdmaim
to the project'sres://addons
directory. - Go to
Project
->Project Settings
->Plugins
and enable GDMaim
Configuration ↑
To configure this plugin, open the GDMaim dock on the bottom left, right next to the file dock.
Enable Obfuscation
: If enabled, obfuscate scripts. Does not affect post-processing.
Inline Constants
: If enabled, accesses to constants will be replaced with hardcoded values. Declarations of constants get removed.
Inline enums
: If enabled, accesses to enum keys will be replaced with hardcoded values. Declarations of enums get removed.
Obfuscate Exports Vars
: If enabled, obfuscate export vars.
Note: Requires all resources which modify export vars to be saved as '.tres' files.
Strip Comments
: If enabled, remove all comments.
Strip Empty Lines
: If enabled, any line without code will be removed.
Strip Extraneous Spacing
: If enabled, spaces and tabs that are not required, get removed.
Process Feature Filters
: If enabled, process automatic filtering of code, based on export template feature tags. For more information, see feature filters.
Strip Lines Matching RegEx
: If enabled, any lines matching the regular expression will be removed.
ID Prefix
: Sets the prefix to use for all generated IDs.
ID Character List
: A list of characters the ID generator is allowed to use.
Target ID Length
: The length of the ID the generator will try to target. The length does not include the prefix.
Seed
: The seed the ID generator will use to generate IDs. A given seed will always produce the same ID for a given name.
Note: The set value will be ignored, if
Use Dynamic Seed
is enabled.
Use Dynamic Seed
: If enabled, automatically generate a random seed every time the project is exported.
Warning: Dynamic seeds will produce unique scripts on each export, which might potentially 'break' delta updates, as used by Steam for example. It also makes debugging harder.
Output Path
: The path where source maps will automatically be saved to upon export.
Max Files
: The maximum amount of saved source maps. When the limit has been reached, replace the oldest one.
Compress
: If true, compress the source maps before export to take up less space.
Inject Name
: If true, searches for the first enabled autoload during export and inserts a print statement into its _enter_tree
method(or creates a new one if it does not exist). The print statement contains the filename of the associated source map that got generated during the export of that build.
Usage ↑
Please read limitations and caveats to make sure your project can export properly.
While enabled, GDMaim automatically obfuscates scripts and resources during export. As mentioned earlier, the obfuscation only affects the exported '.pck' file, but as always, make sure to use version control.
During export, a source map for the current build will be generated and saved. By default Max Files
is set to only keep the 10 latest files, so every time you actually release a build, make sure to copy the associated source map somewhere safe.
Debugging using source maps
Open the source map viewer by navigating to the GDMaim dock and selecting View Source Map
. Next, open the source map you want to view.
On the left side you will find a file tree, containing all exported scripts. Double-clicking on one will open it.
Once a script has been opened, both the source and exported code will be shown. By selecting a line in either code, you can get the equivalent line in the other code.
Feature tags allow configuration on per export template basis.
Using no_gdmaim
as feature tag, will completely disable obfuscation for that specific export template.
Warning: Do not distribute builds made with
no_gdmain
export templates!
If Process Feature Filters
is enabled, custom feature tags may be used to automatically filter code. See: feature filters.
Obfuscation
##OBFUSCATE bool
: Must be at the beginning of a line. Overrides obfuscation for any code after the hint.
##OBFUSCATE false
var my_var : interval
##OBFUSCATE true
var my_var2 : int
=>
var my_var : int
var __6vjJ : int
Note: Globally accessible names are shared with all scripts, which in the above example means that
my_var
declarations will not get obfuscated in any script.
##OBFUSCATE_STRINGS
: Must be at the end of a line. Obfuscates all strings in the same line.
var my_var : int
set("my_var", 1) ##OBFUSCATE_STRINGS
=>
var __pQFC : int
set("__pQFC", 1)
##OBFUSCATE_STRING_PARAMETERS arg_name
: Must be at the beginning of a line and before a function declaration. For all specified string parameters, obfuscate the arguments when the function is called.
##OBFUSCATE_STRING_PARAMETERS name
func custom_set(name : String, value) -> void:
set(name, value)
var my_var : int
custom_set("my_var", 3)
=>
func __U5iZ(name : String, value) -> void:
set(name, value)
var __rEyW : int
__U5iZ("__rEyW", 3)
Feature filters
Feature filters provide a way to dynamically strip out code based on the export template feature tags set. There is currently only one preprocessor hint.
##FEATURE_FUNC feature_tag
: Must be at the beginning of a line and before a function declaration. Removes the entire implementation of the function if the specified feature tag has not been defined in the current export template.
##FEATURE_FUNC server
func server_func() -> int:
var x : int = 0
for i in 10:
x += 1
return x
=> If the feature tag 'server' has not been defined in the current export template:
func __Dg8o() -> int:
printerr("ERROR: illegal call to 'test.__Dg8o'!")
return 0
Limitations and caveats ↑
This section offers solutions to various limitations of this plugin.
The obfuscator does not support parsing and obfuscating binary files.
Thus, if Obfuscate Exports Vars
is enabled, every custom resource containing export vars must be saved as '.tres' and every packed scene modifying export vars or containing signal connections as '.tscn'. Furthermore, built-in scripts are not supported and must be saved to a file instead.
In order for the obfuscator to work as well as possible, I recommend heavy use of static typing. Although this mostly affects just dictionaries at the moment.
Dictionaries
The easiest way to access dictionaries without any issues is by using get
or the []
operator.
By default, the obfuscator will treat the .
operator as property access, and thus potentially obfuscate the specified key by mistake. This can be avoided by statically typing dictionaries, although with a limitation: the dictionary must be a property of the same script, a static variable of a named script, a property of an autoload or a local variable(which includes function parameters).
class_name Test extends RefCounted
const KEY : int
var prop_dict : Dictionary = { "KEY": 0, }
func my_func(param_dict : Dictionary = { "KEY": 0, }) -> void:
var local_dict : Dictionary = { "KEY": 0, }
var inferred_dict := { "KEY": 0, }
local_dict.KEY = 1 # valid
param_dict.KEY = 1 # valid
prop_dict.KEY = 1 # valid
Test.new().prop_dict.KEY = 1 # invalid!
inferred_dict.KEY = 1 # invalid!
Some built-in functions use string names as identifiers, which will not work without workarounds. Most of those functions, however, can be avoided completely.
Signals
Using the Signal
class directly requires no further workarounds.
Object.connect() -> Signal.connect()
Object.disconnect() -> Signal.disconnect()
Object.emit_signal() -> Signal.emit()
Calls
Use Callables
instead of calling methods via string names.
Object.call() -> Callable.call()
Object.callv() -> Callable.callv()
Object.call_deferred() -> Callable.call_deferred()
Set/get
Because properties cannot be referenced directly, setting and getting requires a bit extra work.
Use either ##OBFUSCATE_STRINGS
:
set("test_var", 0) ##OBFUSCATE_STRINGS
...or ##OBFUSCATE_STRING_PARAMETERS
with a wrapper method(recommended):
##OBFUSCATE_STRING_PARAMETERS property
func set_wrapper(property : String, value) -> void:
set(property, value)
set_wrapper("test_var", 0)
Due to the dynamic nature of GDScript, the obfuscator cannot safely assume the script attached to an object. Consequently, all global identifiers, like property and function names for example, will share the same generated symbol name. Therefore, disabling the obfuscation of an identifier, will also do so for every identical identifier anywhere in the project.
The above limitation also applies to built-in classes, types, functions, etc. An identifier called
current_animation
for example, will never get obfuscated, asAnimationPlayer.current_animation
already exists.
When using imported animated meshes in 'tscn' files, animations seem to always get embedded, which may take up a huge amount of space and significantly stall the obfuscation.
To avoid that, save all animations to external files.
This can be done automatically by double-clicking the imported file('.glb' for example) and navigating to Actions
-> Set Animation Save Paths
.
Note:
Set Animation Save Paths
currently ignores the-loop
/-cycle
import hints, so you have to manually set the loop mode for each looped animation.
Stability ↑
Developed using Godot 4.2-stable.
GDMaim manages to successfully export a multiplayer game I have been working on for quite some time now, which currently means ~450 scripts, containing ~43k lines of code.
I also successfully exported the following 4 projects, after doing some minor tweaks:
- Official Third Person Shooter Demo
- GDQuest's Third Person Shooter Demo
- Official 3D Platformer Demo
- Godot 4 FPS Prototype
Issues ↑
This does of course not mean that there aren't any issues. I believe there are tons of edge cases still left which break obfuscation, but I also do think, that with workarounds, every or most projects should be able to export.
If you have any issues or questions, please feel free to contact me! You can of course try to fix issues yourself as well, but that would require you to decipher the abomination this plugin's code is.
Troubleshooting ↑
First, make sure your project respects the limitations and caveats of this plugin.
- Run the console version of your exported project or read the logfile to see which scripts and lines contain the errors.
- Use the source map viewer to open the source map generated by the failing build and navigate to the errors in the exported code.
- Again, make sure the equivalent source code lines adhere to the limitations and caveats.
"Freezing" during export is usually caused by huge '.tres' or '.tscn' files in your project, which take a very long time to obfuscate. Huge file sizes are usually caused by embedded resources, some of which take up a lot of space when serialized in a text format('.tscn'/'.tres'). To fix that, save all the embedded resources taking up a lot of space(like images and animations) in external binary resource files.
Roadmap ↑
- class_name obfuscation: The base is done, just need to properly(yea right) update all symbols contained in named scripts.
- Additional obfuscation techniques.
- Filename obfuscation: Would require manually renaming and linking .remap files as well...
- Converting text resources to binary: Requires obfuscation of binary files or a conversion tool.
License ↑
GDMaim is MIT-licensed. See LICENSE.md for details.