Obfuscation of .NET Single Publish Files

How to use obfuscation for single-file deployment in .NET?

Contents

Bundling an obfuscated C# application into a single file.

With the release of .NET Core 3.0, .NET can bundle an application into a single file. This feature simplifies the .NET applications distribution because it doesn’t require installing a particular .NET runtime version on an end-user machine. Before the single-file deployment had been introduced, it might have happened that your .NET application failed to run as an incorrect .NET runtime version was installed, or even no .NET runtimes were installed at all.

Often developers ask how to enable obfuscation to a process of building and publishing an application. In this guide, we will create a sample application that checks the entered password, and will demonstrate how to obfuscate the application while publishing.

A sample application that validates a password.

The source code is available on GitHub: https://github.com/Softanics/obfuscation-dotnet-single-publish-files

Use your favorite IDE or the command line to create the sample application. In this tutorial, we use Visual Studio 2022, the most recent version of the famous IDE from Microsoft.

Run Visual Studio 2022, choose C# Console App, enter CheckPassword as the project name, select .NET 6.0 as framework, and let Visual Studio to create initial files.

Open Program.cs and modify it as shown below:

using System.Security.Cryptography;
using System.Text;

Console.WriteLine("Enter password: ");
var password = Console.ReadLine();
var passwordBinaryForm = Encoding.UTF8.GetBytes(password);
var hash = SHA256.Create().ComputeHash(passwordBinaryForm);
var hashInStringForm = Convert.ToBase64String(hash);

Console.WriteLine($"Correct hash: {hashInStringForm}");
Console.WriteLine("Press any key to exit...");
Console.ReadKey();

Start debugging, enter ArmDot when the application asks for a password and then copy the correct hash. Then we will use this hash to validate the entered password, as storing password as-is may be a really bad practice:

Copy the correct hash

The correct hash is hzTbLivrxGSCjSfDqNEIDuXEbj1W6IXdPikhanYqTu0=

Now we are ready to check the password hash:

using System.Security.Cryptography;
using System.Text;

while (true)
{ 
    Console.WriteLine("Enter password (or press Ctrl+C to exit): ");
    var password = Console.ReadLine();
    var passwordBinaryForm = Encoding.UTF8.GetBytes(password);
    var hash = SHA256.Create().ComputeHash(passwordBinaryForm);
    var hashInStringForm = Convert.ToBase64String(hash);

    if ("hzTbLivrxGSCjSfDqNEIDuXEbj1W6IXdPikhanYqTu0=" == hashInStringForm)
        Console.WriteLine($"The password is correct");
    else
        Console.WriteLine($"The password is incorrect");
}

Rebuild the application and test whether it is working as expected:

The application validates the password

What’s the problem with this code? It is easy to read! Just run ildasm, the standard tool to disassemble IL code and load CheckPassword.dll. You can see all implementation details:

Without obfuscation, the code is available for everyone

That is why you need an obfuscator. It scrambles and encodes the original code, hides used strings, and makes it hard to understand how a .NET application works.

Publishing the .NET application.

To publish the application, switch to Solution View, right-click the project, and choose Publish. Then choose publishing to a local folder. Click to Next and Finish. Now the project is ready to be published. Click to Publish. After the building and publishing are finished, you will find files in \bin\Release\net6.0\publish. You can review the code of CheckPassword.dll once again to ensure that the code is readable.

How to add obfuscation to the publishing process?

First of all, you need to add ArmDot, a cross-platform obfuscation tool, to the project. switch to Solution View, right-click the project, and choose Manage NuGet packages…. Then switch to Browse and type ArmDot. You will see two packages; install both:

Add ArmDot to the project

Additionally, you need to modify the project file to enable obfuscation:

<Target Name="Protect" AfterTargets="AfterCompile" BeforeTargets="BeforePublish">
    <ItemGroup>
        <Assemblies Include="$(ProjectDir)$(IntermediateOutputPath)$(TargetFileName)" />
    </ItemGroup>
    <ArmDot.Engine.MSBuildTasks.ObfuscateTask
    Inputs="@(Assemblies)"
    ReferencePaths="@(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)')"
    SkipAlreadyObfuscatedAssemblies="true"
    />
</Target>

Publish the project to ensure that ArmDot works as expected. You will see the following in the Output Window:

Build started...
1>------ Build started: Project: CheckPassword, Configuration: Release Any CPU ------
1>V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\Program.cs(8,53,8,61): warning CS8604: Possible null reference argument for parameter 's' in 'byte[] Encoding.GetBytes(string s)'.
1>[ArmDot] ArmDot [Engine Version 2022.7.0.0] (c) Softanics. All Rights Reserved
1>[ArmDot] ------ Build started: Assembly (1 of 1): CheckPassword.dll (V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\obj\Release\net6.0\CheckPassword.dll) ------
1>V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\CheckPassword.csproj(19,9): warning : [ArmDot] warning ARMDOT0003: No methods to protect in the assembly V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\obj\Release\net6.0\CheckPassword.dll
1>[ArmDot] Writing protected assembly to V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\obj\Release\net6.0\CheckPassword.dll...
1>[ArmDot] Finished
1>CheckPassword -> V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\bin\Release\net6.0\CheckPassword.dll
1>Done building project "CheckPassword.csproj".
2>------ Publish started: Project: CheckPassword, Configuration: Release Any CPU ------
2>[ArmDot] ArmDot [Engine Version 2022.7.0.0] (c) Softanics. All Rights Reserved
2>[ArmDot] Skipped the assembly V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\obj\Release\net6.0\CheckPassword.dll as it is already obfuscated
2>[ArmDot] Finished
2>CheckPassword -> V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\bin\Release\net6.0\CheckPassword.dll
2>CheckPassword -> V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\bin\Release\net6.0\publish\
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
========== Publish: 1 succeeded, 0 failed, 0 skipped ==========

You may notice that ArmDot has produced the warning “No methods to protect in the assembly”. Indeed, we have not specified any obfuscation options.

The easiest way to specify such options is to use obfuscation attributes provided by ArmDot.Client (we’ve just added it via NuGet):

using System.Security.Cryptography;
using System.Text;

[assembly: ArmDot.Client.ObfuscateControlFlow]
[assembly: ArmDot.Client.VirtualizeCode]

while (true)
{ 
    Console.WriteLine("Enter password (or press Ctrl+C to exit): ");
    var password = Console.ReadLine();
    var passwordBinaryForm = Encoding.UTF8.GetBytes(password);
    var hash = SHA256.Create().ComputeHash(passwordBinaryForm);
    var hashInStringForm = Convert.ToBase64String(hash);

    if ("hzTbLivrxGSCjSfDqNEIDuXEbj1W6IXdPikhanYqTu0=" == hashInStringForm)
        Console.WriteLine($"The password is correct");
    else
        Console.WriteLine($"The password is incorrect");
}

ObfuscateControlFlow tells ArmDot to apply control flow obfuscation to methods of the assembly; VirtualizeCode tells ArmDot to virtualize code. Virtualization is not possible for all methods, so those ones that will be skipped, will be obfuscated by control flow obfuscation because we’ve specified ObfuscateControlFlow.

Let’s publish once again; this time you will see:

1>[ArmDot] ------ Build started: Assembly (1 of 1): CheckPassword.dll (V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\obj\Release\net6.0\CheckPassword.dll) ------
1>[ArmDot] Conversion started for method System.Void Program::<Main>$(System.String[])
1>[ArmDot] Conversion finished for method System.Void Program::<Main>$(System.String[])
1>[ArmDot] Conversion started for method System.Void Program::.ctor()
1>[ArmDot] Conversion finished for method System.Void Program::.ctor()
1>[ArmDot] Writing protected assembly to V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\obj\Release\net6.0\CheckPassword.dll...
…
2>CheckPassword -> V:\Projects\obfuscation-dotnet-single-publish-files\CheckPassword\bin\Release\net6.0\CheckPassword.dll

Open CheckPassword.dll in ildasm. You will see new strange items in the assembly:

Obfuscated code

Open Program and double-click Main. Now you can see the obfuscated code that is really hard to understand; tons of local variables, raw pointers, conversions, jumps back and forth:

Obfuscated code

Conclusion

Since .NET code is easy to read, a .NET obfuscator like ArmDot becomes an essential tool for every .NET developer. Adding obfuscation to the publishing process gives the option to produce obfuscated .NET applications that are hard to decrypt. Obfuscators protect your intellectual property, making it almost impossible to understand implementation details of your .NET applications.