Contents
- Why do developers need a .NET obfuscator?
- A sample application checks password
- Comparison of original and obfuscated code
- Conclusion
In this article, you will learn how to obfuscate a .NET application in JetBrains Rider.
ArmDot is a famous .NET obfuscator. Rider is a modern IDE for .NET developers from JetBrains.
Why do developers need a .NET obfuscator?
Such managed platforms as .NET provide a lot of fantastic features: reflection, code safety, ability to be launched at any platform, but unfortunately, it is mainly achieved by utilizing some intermediate code, containing metadata in the open format.
A .NET application is compiled to the intermediate code which can’t be executed as-is, that is why the JIT compiler (a part of a .NET runtime) is used to convert it to the machine code, executed by the CPU.
Such tools as obfuscators help developers hide details of their applications, confuse the logic of methods, encrypt and hide string literals. Also, they rename types and methods whose names provide useful information about the internal organization of the application. Applying all these obfuscation techniques reliably protects a .NET application from hackers and prying eyes, from people who want to crack your algorithms, and extract the assets like images and other resources.
ArmDot is a modern obfuscator with a complete arsenal of protection methods: names obfuscation, control flow obfuscation, embedded resources protection, and code virtualization, the most advanced way to protect code.
A sample application checks password
You can find the complete source code on GitHub.
Let’s start Rider and create a new console application:
Once the project is loaded, right-click to project and select Manage NuGet packages. Then start typing ArmDot.Engine.MSBuildTasks. When ArmDot.Engine.MSBuildTasks is shown, right-click on it and select Install. Rider informs when it is installed. Also install ArmDot.Client:
ArmDot.Client provides attributes to tell ArmDot what kind of obfuscation should be applied to a method, a type, or an entire assembly.
ArmDot.Engine.MSBuildTasks provides a task that MSBuild executes to run ArmDot.
In order to add ArmDot to the build process, you need to add a target that is executed after the project is built. This target should execute the task ArmDot.Engine.MSBuildTasks.ObfuscateTask. Right-click to project, select Edit – Edit project. The project file is loaded to the editor. Add the task as shown below:
<Target Name="Protect" AfterTargets="Build"> <ItemGroup> <Assemblies Include="$(TargetDir)$(TargetFileName)" /> </ItemGroup> <ArmDot.Engine.MSBuildTasks.ObfuscateTask Inputs="@(Assemblies)" ReferencePaths="@(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)')" /> </Target>
ArmDot.Engine.MSBuildTasks.ObfuscateTask has several parameters, you can find more information about them here.
The parameter Inputs specifies assemblies to be obfuscated. In this project, the only assembly is obfuscated and this path can be retrieved using $(TargetDir)$(TargetFileName).
The parameter ReferencePaths provides the list of directories where ArmDot searches for referenced assemblies. If a code uses types from external assemblies, ArmDot might need to get more information about such types. In order to do that, ArmDot has to find the assembly where they are defined. ReferencePaths helps ArmDot to locate the assembly. Fortunately, MSBuild collects the list of directories while compiling. So just use it: @(_ResolveAssemblyReferenceResolvedFiles->’%(RootDir)%(Directory)’)
Let’s check how ArmDot is working. Click to Build – Build Solution. You will see that the project is building and ArmDot is obfuscating the assembly. Of course, no methods are obfuscated in fact because a developer should specify which methods should be obfuscated. That’s why ArmDot shows warnings:
It is time to add some code that we will obfuscate. The application will check the entered password and then show whether it is correct or not. In order to check the entered password, the application calculates its hash and compares it with the correct one.
Let’s add a new method: it takes a password, gets its hash, converts to base64 string, and compares with the correct one:
static bool CheckPassword(string value) { using (var sha256 = SHA256.Create()) { byte[] hashValue = sha256.ComputeHash(Encoding.UTF8.GetBytes(value)); return "mZfua8BSQJP337Kuj4Cpl9dVBL/S6Cn1SioM0xcq2tg=" == Convert.ToBase64String(hashValue); } }
The main method asks for a password, checks it, and displays the result:
static void Main(string[] args) { Console.WriteLine("Enter password and press ENTER"); if (CheckPassword(Console.ReadLine())) Console.WriteLine("The password is correct"); else Console.WriteLine("The password is not correct"); }
Run the project, enter armdot, and ensure that the application displays “The password is correct”. Try another password and you will see that the application displays “The password is not correct”. Great! It works as expected.
Comparison of original and obfuscated code
If you look at the produced code you will see that it has become easier to read. All strings are legible, the logic of CheckPassword is understandable.
Open armdot-rider-sample.dll in dotPeek. Here is what you see:
Let’s obfuscate both methods. Just add the attribute ArmDot.Client.VirtualizeCode:
[ArmDot.Client.VirtualizeCode] static void Main(string[] args) { Console.WriteLine("Enter password and press ENTER"); if (CheckPassword(Console.ReadLine())) Console.WriteLine("The password is correct"); else Console.WriteLine("The password is not correct"); } [ArmDot.Client.VirtualizeCode] static bool CheckPassword(string value) { using (var sha256 = SHA256.Create()) { byte[] hashValue = sha256.ComputeHash(Encoding.UTF8.GetBytes(value)); return "mZfua8BSQJP337Kuj4Cpl9dVBL/S6Cn1SioM0xcq2tg=" == Convert.ToBase64String(hashValue); } }
Then build the project, you will see that ArmDot reported that two methods are obfuscated:
[ArmDot] ArmDot [Engine Version 2021.9.0.0] (c) Softanics. All Rights Reserved [ArmDot] No license key specified, or it is empty. ArmDot is working in demo mode. [ArmDot] THIS PROGRAM IN UNREGISTERED. Buy a license at https://www.armdot.com/order.html [ArmDot] ------ Build started: Assembly (1 of 1): armdot-rider-sample.dll (V:\Projects\armdot-rider-sample\armdot-rider-sample\bin\Debug\net5.0\armdot-rider-sample.dll) ------ [ArmDot] Conversion started for method System.Void armdot_rider_sample.Program::Main(System.String[]) [ArmDot] Conversion finished for method System.Void armdot_rider_sample.Program::Main(System.String[]) [ArmDot] Conversion started for method System.Boolean armdot_rider_sample.Program::CheckPassword(System.String) [ArmDot] Conversion finished for method System.Boolean armdot_rider_sample.Program::CheckPassword(System.String) [ArmDot] Writing protected assembly to V:\Projects\armdot-rider-sample\armdot-rider-sample\bin\Debug\net5.0\armdot-rider-sample.dll..
Run the application and enter the correct password armdot and some incorrect ones to ensure that it is working well after obfuscation.
Let’s look at the obfuscated code. It is absolutely illegible now: it has become impossible to understand how the code is functioning, what methods are called, in what order they are called, and how the return values are used. All strings are hidden somewhere:
Conclusion
In the modern world, .NET based projects are widely spread and quite popular. Nobody wants to distribute their applications with the complete source code, but without obfuscators, any .NET application can be converted to a ready-to-build C# project in literally a few seconds.
ArmDot makes any .NET application hard to understand, even a simple method having several lines turns into hundreds of low-level instructions that can’t be transformed to C#.
String literals are encrypted and extracted on demand. Embedded resources are removed and provided to the .NET immediately.
ArmDot demo is fully-functional and can be downloaded from the downloading page.