Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 6 min read

What your .NET release looks like to everyone else, and how to change that without breaking it

Build a small .NET app in Release and open the DLL in ILSpy. You get your class names, your string constants and your license check back as C#. This post shows that on a sample, protects the sample, and then checks what

What your .NET release looks like to everyone else, and how to change that without breaking it

The same InvoiceDesk method in ILSpy before and after protection

Build a small .NET app in Release and open the DLL in ILSpy. You get your class names, your string constants and your license check back as C#. This post shows that on a sample, protects the sample, and then checks what changed and what didn't.

What ships today

The sample is InvoiceDesk, an 11 KB console app with three things most commercial products have: a license check, a pricing rule, and a settings class it reads with System.Text.Json. Everything in it is internal, the way application code usually is.

Here is the license validator from the Release DLL, as printed by ilspycmd (ILSpy's command-line build):

internal static class LicenseValidator
{
    private static readonly string ProductSalt = "invoicedesk-pro-2026";

    internal static bool IsValid(string customerEmail, string licenseKey)
    {
        if (string.IsNullOrWhiteSpace(licenseKey) || licenseKey.Length != 29)
        {
            return false;
        }
        string s = ComputeKey(customerEmail);
        return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(s), Encoding.ASCII.GetBytes(licenseKey.ToUpperInvariant()));
    }

    internal static string ComputeKey(string customerEmail)
    {
        string s = customerEmail.Trim().ToLowerInvariant() + ":" + ProductSalt;
        byte[] inArray = SHA256.HashData(Encoding.UTF8.GetBytes(s));
        string hex = Convert.ToHexString(inArray).Substring(0, 25);
        return string.Join("-", from i in Enumerable.Range(0, 5)
            select hex.Substring(i * 5, 5));
    }
}

The comments and a couple of local names are gone. Everything else is there, salt included. Anyone with the installer can write a key generator from this in a few minutes.

Why it looks like that

A managed assembly is IL plus metadata, and the metadata describes every type, member and signature so the runtime can load it without header files. Microsoft's docs call this self-describing. ILSpy reads the same metadata and turns the IL back into C#. A private repository protects your source. It has no effect on the binary you hand to customers.

Obfuscation raises the cost of reading and patching that binary. It doesn't make a secret in client code safe, and a determined analyst with a debugger still gets there.

The trap: names are sometimes a file format

The reason many teams give up on obfuscation is in the third class of the sample:

internal sealed class AppSettings
{
    public string CompanyName { get; set; } = "Example Ltd";
    public string DefaultCurrency { get; set; } = "EUR";
    public int PaymentTermDays { get; set; } = 30;

    public string ToJson() => JsonSerializer.Serialize(this);
    public static AppSettings FromJson(string json) =>
        JsonSerializer.Deserialize<AppSettings>(json) ?? new();
}

A customer's settings file looks like {"CompanyName":"Contoso Kft","DefaultCurrency":"HUF","PaymentTermDays":45}. The property names are the file format. Rename CompanyName to a and the app compiles, starts, and reads every existing settings file as defaults. Nothing throws. Contoso's invoices go out under "Example Ltd" and you find out from a support ticket.

Reflection by name, convention-based dependency injection, XAML bindings and plugin interfaces fail the same way. The build stays green in all of them.

Two things protect you here, and you need both.

Check before you protect. In the ProofCloak workspace, the profile is checked against the uploaded file before a job exists. Each requested protection gets its own verdict. In the November 8 precheck on an earlier build of InvoiceDesk, the selected profile asked for a protection the input did not qualify for.

Precheck result: Not supported for this profile and route

The profile went no further. It did not run with that protection quietly dropped. With the protection removed, the same screen read "Supported for this profile and route" and listed each requested protection as allowed.

A precheck does not find every place your app depends on a name. Keep your own list of what you serialize, reflect over and load dynamically.

Run the protected file, not the build. InvoiceDesk reads a settings file written by an "earlier release" and prints what it found. Before and after protection:

License valid: True
Total (HU): 1567.73
Settings file: Contoso Kft, HUF, 45 days
Settings saved: {"CompanyName":"Contoso Kft","DefaultCurrency":"HUF","PaymentTermDays":45}

In this run, both builds read the same settings and printed the same serialized JSON. Four lines is a toy test. The point is that it ran against the protected DLL.

The same class, protected

The profile was renaming, string protection, control-flow rewriting and anti-tamper checks. This is ilspycmd on the output:

internal static class AvaebCaldbbwcdbJbgbCd
{
    [CompilerGenerated]
    private sealed class ApaVbWdscRaaecaAebbwb
    {
        public string hex;

        internal string AQaYaUdzdmeAebaqeIava(int i)
        {
            return hex.Substring(i * 5, 5);
        }
    }

    private static readonly string ADaPdreGapeTbUcMbXcNc = ArcJaBddbgbibiajaZbLd.AqdhbaagckcPaIcAaybCa(608177074, 145956453, -2074505810, 392448148, cacheResult: true);

    internal static bool AueLdCaUcecNbAaIdYbDe(string customerEmail, string licenseKey)
    {
        int num = -231558403;
        while (true)
        {
            switch ((num - 2000301672) ^ 0x7AF88495)
            {
            case 2:
                return false;
            case 3:
            {
                string s = ACaxbydBdpeMcHbmbhcFe(customerEmail);
                return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(s), Encoding.ASCII.GetBytes(licenseKey.ToUpperInvariant()));
            }
            default:
                num = (string.IsNullOrWhiteSpace(licenseKey) ? (-231558401) : (-231558404));
                break;
            case 1:
                num = ((licenseKey.Length == 29) ? (-231558402) : (-231558401));
                break;
            }
        }
    }

    internal static string ACaxbydBdpeMcHbmbhcFe(string customerEmail)
    {
        string s = customerEmail.Trim().ToLowerInvariant() + ":" + ADaPdreGapeTbUcMbXcNc;
        byte[] inArray = SHA256.HashData(Encoding.UTF8.GetBytes(s));
        string hex = Convert.ToHexString(inArray).Substring(0, 25);
        return string.Join("-", from i in Enumerable.Range(0, 5)
            select hex.Substring(i * 5, 5));
    }
}

What changed:

  • The class, both methods and the field have generated names. Nothing says "license" any more.
  • The salt is gone from the file. The field is filled by a decryptor call, and a search for invoicedesk-pro-2026 in the DLL finds nothing.
  • The branch in IsValid is a state machine driven by an opaque integer.

What didn't, and you should know this before you rely on it:

  • Framework calls are still visible. You can see that something is hashed with SHA-256 and compared in fixed time. This profile doesn't hide call targets.
  • Parameter names (customerEmail, licenseKey) are still there.
  • In AppSettings, the class name and the backing fields were renamed, and the public properties CompanyName, DefaultCurrency and PaymentTermDays were not. That is deliberate: public properties are treated as a possible data contract, and it is why the settings file above still loads.

One more thing we learned the hard way. The first version of this sample declared the salt as a const. The compiler stores a const as a literal in metadata, and the profile we used doesn't encrypt constant fields, so the salt was still readable in the protected file. For this demonstration we changed the sample value to static readonly and inspected the protected output again. We only caught it because we opened the protected file, which is the argument for the next section.

Every protected build explains itself

The job page compares the input and the output with the same inspection schema, and the job ends with an execution receipt:

Job page before/after panel and receipt excerpt

Input Protected
Size 11,264 bytes 19,968 bytes
Types 7 10
Methods 40 62
Fields 11 25
String literals encrypted 17
Methods with rewritten control flow 8
Method bodies encrypted 0 (not requested)

The receipt records the engine version (0.1.0-alpha.333), the SHA-256 of the input and of every output, the attempt number and the measured CPU time (2.24 s here). It is signed by the broker that ran the job.

The hashes matter more than they look. Protection is not deterministic: an earlier build of this sample went through the same profile three times and came out with three different hashes. If someone asks which protected file went into release 4.2, "the one from Tuesday" is not an answer. The receipt is.

These counts are not a security score. They tell you what the engine did, so that a zero where you expected seventeen is visible before you ship.

A short checklist

  1. Open your current release in ILSpy. Decide what in it is worth protecting.
  2. List the names that are contracts: JSON and XML models, reflection targets, plugin interfaces, anything bound from XAML or config.
  3. Protect a build and run your real tests against the protected files. We wrote more about that in A Green Build Is Not a Release Check.
  4. Open the protected file too. Keep the receipt with the release.

Try it

ProofCloak is free during the open beta: one project and five protection jobs a month. You need to sign up, and no payment is taken. It targets .NET 10 managed DLL and EXE files. Try it on a sample first:

Protect your first .NET build free

If the after-listing above still looks too readable to you, tell us in the comments what you would expect to see instead. That feedback is why this is a beta.

This article was drafted with AI using ProofCloak’s InvoiceDesk sample. The listings and measurements come from the sample run described above.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.