Help us improve Softanics
We use analytics cookies to understand which pages and downloads are useful. No ads. Privacy Policy
Artem Razin
Low-level software protection engineer with 20+ years in native and managed code security. Creator of ArmDot, protecting commercial .NET applications since 2014.

Why .NET Code Is Vulnerable to Reverse Engineering

Yes, .NET code can be decompiled. Not in theory, not by specialists with expensive tooling - by any developer with five minutes and an internet connection. The question is not whether your .NET assembly can be read by someone who wants to read it. It can. The question is what you are going to do about it.

This page explains why the vulnerability exists, how deep it goes, and what it actually looks like in practice. If you are evaluating whether obfuscation is necessary for your application, this is the place to start.

.NET compiles to an intermediate language, not to machine code

When you build a C# or VB.NET project, the compiler does not produce native machine code the way a C++ compiler does. It produces Common Intermediate Language - CIL, sometimes called MSIL. CIL is a platform-neutral bytecode that the .NET runtime JIT-compiles to native instructions at execution time.

This design is what makes .NET cross-platform and what powers reflection, dynamic loading, and the entire generics system. It is also what makes your compiled assembly readable.

CIL is not a binary format in the sense of being opaque. It is a structured, documented format specified in ECMA-335. Every .NET assembly - every .dll and .exe you ship - is a Portable Executable file with two distinct regions: the CIL bytecode itself, and a metadata section. The metadata section is where the vulnerability is most severe.

The metadata section preserves your source structure verbatim

The .NET metadata format stores your code's structure in a set of tables. The TypeDef table lists every class and struct in the assembly, by name. The MethodDef table lists every method, by name, with its parameter names and return type. The Field table lists every field. The MemberRef table records every external reference your code makes - every API call, every type you import.

These are not hashed, not encrypted, not obfuscated in any way by the standard toolchain. They are stored as plain strings in the metadata string heap, exactly as you typed them. The name LicenseValidator, the method CheckSerialKey, the field _trialDaysRemaining - all of it is in the file, in plaintext, by design.

This is not a bug or an oversight. It is how .NET's reflection system works. Reflection reads these tables at runtime to locate and invoke types by name. Remove the names and reflection breaks. The same structure that makes .NET's runtime so powerful is exactly what makes a shipped assembly so readable.

What a decompiler actually sees

ILSpy is the dominant open-source .NET decompiler. Since Visual Studio 2022 it has been embedded directly into the IDE under "Decompile source code" - you can right-click any external type in the editor and see its decompiled source without installing anything. dnSpy is a separate tool that adds a debugger and a binary editor on top of ILSpy's decompilation engine: it can set breakpoints in a running .NET process and patch bytecode directly in memory.

Open any unprotected .NET assembly in ILSpy and you see a project tree that looks exactly like a Visual Studio solution. Classes in the namespace hierarchy. Methods with readable names and reconstructed C# bodies. String literals inline. Comments are gone, but everything else is effectively there.

The reconstruction quality depends on the complexity of the original code. Simple methods come back almost exactly as written. Methods with complex iterator blocks, async state machines, or heavy LINQ chains come back in a slightly more verbose form - but the intent, the structure, and the data are all recoverable.

The practical implication is that shipping an unprotected .NET binary is roughly equivalent to publishing your source code with the comments removed.

I remember my reaction the first time Microsoft started promoting .NET at my university - handing out free books, evangelizing the platform. I had spent years before that doing security research on native applications, working with tools like SoftICE to understand how real programs protected themselves at the lowest level. My immediate thought when I saw CIL for the first time was: who would write commercial software on this? Everything is just sitting there in plain sight.

That instinct turned out to be correct - and twenty years later, the fundamental situation has not changed. The tooling for reading .NET assemblies has only gotten better and more accessible. What required real skill and patience in 2002 takes five minutes today.

How quickly can an attacker find what they are looking for?

Decompilation is the first step, not the last. An attacker who has decompiled your assembly can then search it. ILSpy supports full-text search across all decompiled types and members. dnSpy supports search by string literal, by method name, by type name, by IL opcode.

Looking for your license check? Search for serial, license, activate, trial. Looking for your API key? Search for string literals that look like tokens. Looking for your encryption implementation? Search for AES, Rijndael, CryptoStream.

In a typical commercial .NET application, an experienced attacker can locate the license validation routine, understand it, and identify the simplest patch to bypass it in under an hour. The binary patch itself - changing a single conditional branch instruction from brtrue to br so that the validation always falls through - takes seconds in dnSpy.

This is not a hypothetical. It is the standard workflow documented in public reverse engineering communities, applied routinely to commercial .NET software.

Does Native AOT fix the problem?

Native AOT, introduced in .NET 7 and stabilized in .NET 8, compiles your application to a self-contained native binary with no CIL and no JIT. This eliminates the most readable part of the vulnerability - the CIL bytecode itself.

But it does not eliminate the metadata. Native AOT binaries still carry type and method information needed for diagnostics, exception handling, and stack trace generation. The symbol names are present in the binary. The structure of your application is recoverable through static analysis of the native code, which is harder than reading CIL but is a well-understood discipline with mature tooling.

More practically: Native AOT has significant constraints. It does not support runtime code generation, imposes restrictions on reflection, increases binary size substantially, and extends build times. Many .NET applications cannot adopt it without architectural changes.

For most .NET applications - desktop software, libraries, Blazor WASM, Unity games, MAUI apps - Native AOT is not available or not practical. For those where it is, it raises the reverse engineering cost relative to plain CIL, but it does not eliminate it.

Does IL2CPP protect Unity games?

IL2CPP is Unity's ahead-of-time compilation path. It converts C# IL to C++ source, then compiles that to native code for the target platform. The resulting binary contains no CIL.

However, IL2CPP generates a file called global-metadata.dat that ships with every build. This file contains the type system metadata - class names, method names, field names - that the IL2CPP runtime needs to support reflection and exception handling. The file format is publicly documented by the reverse engineering community. Tools like Il2CppDumper can extract readable type and method symbols from any IL2CPP build in seconds, producing a header file that makes the native binary far more readable than it would otherwise be.

IL2CPP raises the bar compared to plain Mono builds. It does not protect your code.

The actual scope of what is exposed

To be concrete about what an attacker recovers from an unprotected .NET assembly:

  • Every class, struct, interface, and enum name in your codebase
  • Every method signature - name, parameter names, parameter types, return type
  • Every field and property name
  • All string literals - including API keys, connection strings, license validation strings, error messages, and any other hardcoded text
  • The complete call graph - what calls what
  • The logic of every method body, reconstructed as C# from CIL
  • Every external dependency your code references, by name and version

What is not recoverable: original comments, original local variable names (unless debug symbols are included), and the original formatting of the source. That is all.

What this means for your application

If your .NET application contains any of the following, distributing it without protection exposes that information directly:

  • Proprietary algorithms or business logic with commercial value
  • Hardcoded credentials, API keys, or connection strings
  • License validation or trial enforcement logic
  • Anything you would not publish on GitHub

The good news is that this is a solved problem. .NET obfuscation exists precisely because this vulnerability is structural and well-understood. The techniques available range from simple symbol renaming - which permanently removes the readable names from your assembly - to code virtualization, which transforms selected methods into a custom bytecode that decompilers cannot reconstruct.

Continue reading: .NET Obfuscation Techniques: A Technical Overview →

Back to: .NET Obfuscation: The Complete Developer Guide →

Protect your .NET application with ArmDot

ArmDot is a cross-platform .NET obfuscator that addresses the vulnerabilities described on this page: symbol renaming, string encryption, control flow obfuscation, and code virtualization. It integrates via NuGet and runs on Windows, Linux, and macOS build agents.