How to protect C# code?

Contents

Introduction

For software developers and companies, their code represents valuable intellectual property. Protecting C# code safeguards their investment of time, effort, and resources by preventing unauthorized access, copying, or theft of their proprietary code. Without proper protection, C# code can be reverse engineered, allowing others to comprehend its logic, algorithms, and design. This can lead to software replication or exploitation of vulnerabilities, potentially resulting in financial losses and reputational damage.

There are even tools available that can decompile compiled C# code and reconstruct the source code, enabling anyone to rebuild it! That’s why ensuring C# code protection is essential.

How to choose a C# protector?

You can find many such protectors, so the question is how to choose a good one. We believe that a good C# protector should support all .NET versions, be cross-platform to allow you to build your applications on Windows, Linux, and macOS. Of course, it should also provide several levels of protection to efficiently safeguard your C# code.

There are other useful features you may consider when choosing a protector, such as integration with MSBuild and Visual Studio, support for GitHub Workflow and Azure DevOps, compatibility with legacy .NET frameworks including .NET 2.0 and 4.0, as well as support for .NET Core.

How to protect C# code?

Let’s explore how to protect your code using an example. We will create a console application that validates entered passwords. It will compare the hash of the entered text with the correct one, so the valid password is not stored in the code. However, we also need to conceal the algorithm from prying eyes, especially the hash value.

To create the console application, follow these steps:

  1. Create a directory called ‘CheckPassword’.
  2. Enter the directory and run the following command from the command line:
dotnet new console --use-program-main

Open Program.cs and add the following code that displays the hash value of the entered text:

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

namespace CheckPassword;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Enter password and press Enter\r\n");
        var password = Console.ReadLine();
        Console.WriteLine(GetHash(password));
    }

    public static String GetHash(string value)
    {
        var str = new StringBuilder();

        using var hash = SHA256.Create();
        
        var result = hash.ComputeHash(Encoding.UTF8.GetBytes(value));

        foreach (byte b in result)
            str.Append(b.ToString("x2"));

        return str.ToString();
    }
}

Build and run the program. Type ‘ArmDot’ to obtain its hash value:

Enter password and press Enter
ArmDot
8734db2e2bebc464828d27c3a8d1080ee5c46e3d56e885dd3e29216a762a4eed

Now that we have the hash value of the correct password, let’s modify the Main method by comparing the hashes::

static void Main(string[] args)
{
    Console.WriteLine("Enter password and press Enter\r\n");
    var password = Console.ReadLine();

    if (GetHash(password) == "8734db2e2bebc464828d27c3a8d1080ee5c46e3d56e885dd3e29216a762a4eed")
        Console.WriteLine("Valid");
    else
        Console.WriteLine("Failed");
}

Build and run it to test if it’s working as expected:

Enter password and press Enter
ArmDot
Valid

The problem with this code is that anyone can easily view it. To ensure this, run ILSpy and load CheckPassword.dll (it’s a good idea to remove CheckPassword.pdb, so ILSpy won’t be able to use the debug information):

Non-obfuscated code is easily readable

C# protector to the rescue! Let’s see how it works.

First, you need to add two NuGet packages: ArmDot.Client and ArmDot.Engine.MSBuildTasks:

dotnet add package ArmDot.Client
dotnet add package ArmDot.Engine.MSBuildTasks

ArmDot.Client contains attributes that you can use to instruct the protector on which protection technique to use (we will provide more information about them later). ArmDot.Engine.MSBuildTasks provides a task for MSBuild that is executed to protect the assembly being compiled (this task reads those attributes to properly protect sections of your code).

After adding ArmDot.Engine.MSBuildTasks, modify the project file to enable the protection task as shown below:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="ArmDot.Client" Version="2023.12.0" />
    <PackageReference Include="ArmDot.Engine.MSBuildTasks" Version="2023.12.0" />
  </ItemGroup>

<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>

</Project>

Rebuild the project; you will see that the protector did its job (but didn’t actually protect anything as we didn’t specify any protection attributes). That’s why we need to add protection attributes: without them, the protector simply doesn’t know what to do. But before adding attributes, let’s briefly look at the protection techniques and the corresponding attributes.

Name obfuscation is a protection technique that changes the names of classes, methods, etc. in order to make them meaningless. Use ArmDot.Client.ObfuscateNames for this kind of protection.

Control flow obfuscation is a technique that hides the logic of a method by encrypting conditional and unconditional branches. Use ArmDot.Client.ObfuscateControlFlow to enable control flow obfuscation.

Lastly, virtualization is a technique of protecting a method that converts its original code into a set of instructions for a new virtual CPU. Use ArmDot.Client.VirtualizeCode to virtualize methods.

Now let’s return to our sample. Let’s virtualize all methods by applying ArmDot.Client.VirtualizeCode to the entire assembly. Also, let’s obfuscate the names as ‘GetHash()’ seems too obvious:

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

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

namespace CheckPassword;

Rebuild the project and run to ensure it is working properly.

Open the assembly in ILSpy. Now the code is completely unreadable, take a look:

Obfuscated code is practically impossible to comprehend

Conclusion

Choosing a protector for C# is an important task for C# developers who want to safeguard their intellectual property. .NET code contains a significant amount of information that provides insight into how your application is structured and what algorithms are being utilized. Hackers can exploit this information to gain access to sensitive data such as passwords, strings, and implementation details of methods. The goal of a protector is to obscure these details as effectively as possible. By employing various protection techniques, you can accomplish this.