Obfuscating .NET Core

Oct 14, 2020

Babel Obfuscator fully supports the obfuscation of assemblies realized with .NET Core 3. Previous versions of .NET Core are also supported, but you will have to forgo some protections such as Code Encryption which is only available starting from .NET Core 3.0.

This article will use the Babel Obfuscator NuGet package to deploy a protected .NET Core 3.1 console application. We will reference Babel Licensing to show how to handle the merge of an external assembly reference. Both packages are available with the Company edition, so if you have a minor edition, you will not able to compile the sample project. To keep the application very simple, as we are focused on configuration and deployment aspects, we will put all the code inside the Main method.

static void Main(string[] args)
{
    if (!File.Exists("license.xml"))
    {
        RSASignature rsa = new RSASignature();
        rsa.GenerateKeyInfo = true;
        rsa.CreateKeyPair();

        string licenseKey = new XmlLicense()
            .ForAnyAssembly()
            .IssuedAt(DateTime.Now)
            .LicensedTo("babelfor.NET", "info@babelfor.net")
            .WithTrialDays(10)
            .SignWith(rsa).ToReadableString();

        File.WriteAllText("license.xml", licenseKey);
    }

    XmlLicenseManager manager = new XmlLicenseManager();

    try
    {
        XmlLicense lic = manager.Validate(File.ReadAllText("license.xml"));
        Console.WriteLine("Licensed to: {0} ({1})", 
                            lic.Licensee.Name, lic.Licensee.ContactInfo);

        var trial = lic.Restrictions.FirstOrDefault() as TrialRestriction;
        if (trial != null)
            Console.WriteLine("Time left: {0}", trial.TimeLeft);
    }
    catch (BabelLicenseException ex)
    {
        Console.WriteLine("License error: {0}", ex.Message);
    }

    Console.ReadLine();
}

Let’s start by opening Visual Studio and creating a .NET Core 3.1 Console application named Console31. Select the newly created project and choose Manage NuGet Packages… from the context menu. Add the Babel Obfuscator and Babel Licensing NuGet packages, replace the Main method with the code above then add the following using directives:

using System.IO;
using System.Linq;
using Babel.Licensing;

Copy your license file inside the solution folder to get Babel Obfuscator up and running. If you rebuild the solution, you will get the obfuscated assembly directly inside the build folder.

We added the Babel Licensing NuGet package to create and validate a license in our application. This ultimately does nothing more than adding a reference to Babel.Licensing.dll component. The Babel Licensing component can be merged inside the main assembly improving the overall obfuscation and better hiding all the license validation algorithms.

To merge Babel Licensing, we need to configure Babel Obfuscator by editing the Console31.csproj file. Double click the Console31 project node to get the project file opened inside the Visual Studio editor. Before the closing </Project> element add a Target named ConfigureBabel as follow:

  <Target Name="ConfigureBabel">
    <!-- Obfuscation settings releated properties -->
    <ItemGroup>
      <MergeAssembly Include="$(TargetDir)Babel.Licensing.dll" />
    </ItemGroup>
  </Target> 

The ItemGroup defined inside the target will make Babel Obfuscator merge the Babel.Licensing.dll component into the target assembly. Rebuild the solution and inspect the assembly using dnSpy assembly editor. 

Note that the public interface of Babel Licensing is still visible. Babel Obfuscator typically does not rename the public interface of a component. You can configure Babel to internalize all the merged types so that the obfuscation pass will rename those types automatically. Inside the new Target element add a PropertyGroup element containing the following properties:

  <Target Name="ConfigureBabel">
    <!-- Obfuscation settings releated properties -->
    <ItemGroup>
      <MergeAssembly Include="$(TargetDir)Babel.Licensing.dll" />
    </ItemGroup>

    <PropertyGroup>
      <BabelWarningsToIgnore>W00001;EM0003</BabelWarningsToIgnore>
      <MergeInternalize>true</MergeInternalize>      
    </PropertyGroup>
  </Target>

Rebuild and reload the built assembly inside dnSpy. The public interface has been obfuscated and is no longer visible. The size of the built assembly is pretty close to the disk size consumed by the Babel.Licensind.dll. Babel Licensing has many features we are really not using in our application. We can save disk space by enabling dead code removal. This feature will remove from the assembly all the Babel Licensing types we merged and that our application is not using.

    <PropertyGroup>
      <BabelWarningsToIgnore>W00001;EM0003</BabelWarningsToIgnore>
      <MergeInternalize>true</MergeInternalize>
      <DeadCodeElimination>true</DeadCodeElimination>
    </PropertyGroup>

If we now inspect the obfuscated assembly entry point inside dnSpy, we can still see our application code. To better hide the code in the Main method, we have two options: use control flow obfuscation to scramble the code flow and make it unreadable or use code encryption to completely hide the code. To enable control flow obfuscation and code encryption add the following properties:

    <PropertyGroup>
      <BabelWarningsToIgnore>W00001;EM0003</BabelWarningsToIgnore>
      <MergeInternalize>true</MergeInternalize>
      <DeadCodeElimination>true</DeadCodeElimination>
      <ControlFlowObfuscation>if=on;goto=on;switch=on;case=on;call=on;true</ControlFlowObfuscation>
      <MsilEncryption>Console31.*</MsilEncryption>
    </PropertyGroup>

We used a regular expression inside the MsilEncryption property to encrypt every method inside the Console31 namespace. As we have only the Main method, the obfuscation log will show that only one method has been encrypted. Inspecting the entry point of our application we will see that the main method has been totally hidden by the code encryption feature.

We have fully protected our assembly and we can now create a publish profile to deploy the final application. Create a disk publish profile inside Visual Studio and press Publish. If you open the publish folder you will notice that the main assembly has not been obfuscated at all. This because Visual Studio spawns a new build to publish the assembly and it picks the assembly from the obj intermediate folder. To get our published assembly obfuscated insert the following PropertyGroup before the one defined containing Babel settings:

    <!-- Publish releated properties -->
    <PropertyGroup>
      <BabelInput>$(IntermediateOutputPath)$(TargetFileName)</BabelInput>
      <BabelOutput>$(IntermediateOutputPath)$(TargetFileName)</BabelOutput>

      <!-- Do not obfuscate if already obfuscated -->
      <DetectIfObfuscated>exit</DetectIfObfuscated>
      <Use>tagassembly=true</Use>
      
      <!-- Do not publish deps file and PDB -->
      <GenerateDependencyFile>false</GenerateDependencyFile>
      <CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
    </PropertyGroup>

Note that we pointed Babel to obfuscate the assembly inside the intermediate output path (the obj folder) saving the obfuscated assembly to the same place. This will make Visual Studio to use the obfuscated assembly during the deployment step. Then we set a couple of properties to tag the obfuscated assembly and skip the obfuscation in case the assembly has already been obfuscated. We also configured Visual Studio to not generate a .deps file and to not deploy PDB symbols.

Clean the publish folder and run again Publish from within Visual Studio. If you inspect the publish folder you will notice that the Console31.dll is now fully obfuscated but we still have the Babel.Licensing.dll component. This because Visual Studio knows that Babel Licensing has been referenced and will therefore copy the referenced assembly to the publish folder. We can configure the publish process to remove any merged or embedded assembly from the set of published files. Add a new Target called RemoveFilesFromFileToPublish. This target must run after the target ComputeFilesToPublish so that we can change the set of deployed assemblies by removing every merged or embedded DLL.

<Target Name="RemoveFilesFromFileToPublish" AfterTargets="ComputeFilesToPublish">
    <PropertyGroup>
      <MergedAssembliesRegEx>@(MergeAssembly->'%(FileName)%(Extension)', '|')</MergedAssembliesRegEx>
      <EmbedAssembliesRegEx>@(EmbedAssembly->'%(FileName)%(Extension)', '|')</EmbedAssembliesRegEx>
    </PropertyGroup>
    
    <ItemGroup>
      <!-- Remove embedded assemblies -->
      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish->'%(Identity)')" Condition="('$(EmbedAssembliesRegEx)' != '') And $([System.Text.RegularExpressions.Regex]::IsMatch('%(FileName)%(Extension)', '$(EmbedAssembliesRegEx)'))" />
      
      <!-- Remove merged assemblies -->
      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish->'%(Identity)')" Condition="('$(MergedAssembliesRegEx)' != '') And $([System.Text.RegularExpressions.Regex]::IsMatch('%(FileName)%(Extension)', '$(MergedAssembliesRegEx)'))" />      
    </ItemGroup>

    <Message Text="Publish files: @(ResolvedFileToPublish)" Importance="high" />
  </Target>

There isn’t much magic here. We created two properties MergedAssembliesRegEx and EmbedAssembliesRegEx concatenating all the names of merged and embedded DLL files respectively. These expressions will be used to match and remove every deployed assembly file name present inside the collection of ResolvedFileToPublish created by Visual Studio during ComputeFileToPublish.

Before doing a new publish remember to clean up the publish folder as Visual Studio will not do that for you. Running Publish once again within Visual Studio, we will have that the publish folder now contains only the main assembly and no longer the merged Babel.Licensing.dll. Note that the Console31.exe file is a native PE executable created by Visual Studio to run your .NET Core assembly Console31.dll.

Recent versions of NET Core supports the ReadyToRun (R2R) technology. You can edit your publish profile and enable the ReadyToRun compilation check to get a fully obfuscated assembly optimized by native code. 

You can also enable the Produce a single file check to get only one .exe executable file to deploy. The difference with merging we made using Babel is that the single file produced by Visual Studio is rather a packaged version of your application. Using Babel Obfuscator to merge the referenced assembly has improved the overall obfuscation of the deployed application.

In this article, we discussed how to protect a .NET Core application with Babel Obfuscator, showing how to merge references to better obfuscate the resulting assembly and use the deployment process of Visual Studio. Here you can download the Visual Studio solution of the Console31 application.

Pin It on Pinterest

Share This