Obfuscation Beyond Renaming: Why Renamed .NET Still Reads Like Source
Identifier renaming is the weakest obfuscation layer: a decompiler on rename-only code still shows your logic, strings and constants verbatim. Here is what renaming does and does not hide, and the layers — string encryption, constant masking, control-flow flattening and virtualization — that actually make .NET hard to read.
Open almost any .NET app in a decompiler and the first thing you notice is how readable it is. Decompilers reconstruct C# that is close to the original, because the compiler preserves far more structure in IL than most developers expect. The usual answer is “obfuscate it,” and the usual obfuscation is renaming — turning ValidateLicense into a and expiryDate into b. It helps a little. It also fools a lot of teams into thinking they are protected, because they never look at the result through a decompiler. When you do, the illusion breaks.
What renaming leaves behind
Consider a trivial licensing gate:
public bool IsActivated(string key)
{
if (string.IsNullOrEmpty(key)) return false;
var parts = key.Split('-');
if (parts.Length != 4 || parts[0] != "LIC") return false;
long expiry = long.Parse(parts[3]);
return expiry > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
After rename-only obfuscation, a decompiler gives something like this:
public bool a(string A_0)
{
if (string.IsNullOrEmpty(A_0)) return false;
string[] b = A_0.Split('-');
if (b.Length != 4 || b[0] != "LIC") return false;
long c = long.Parse(b[3]);
return c > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
The names are gone. Nothing else is. The control flow is identical, the call to DateTimeOffset.UtcNow.ToUnixTimeSeconds() is in the clear (framework names can’t be renamed), the structure of a license key is spelled out, and the magic string "LIC" is sitting right there telling an attacker exactly what to forge. A reader does not need your variable names to understand this method; they need thirty seconds. Renaming raised the cost of reading from “trivial” to “slightly less trivial.”
That is the ceiling of renaming. It hides labels. Your logic, your literals and your constants are not labels.
Layer 1 — string encryption
String literals are the single most valuable thing a reader harvests, because they are self-documenting: error messages, registry paths, API endpoints, the "LIC" prefix above. String encryption removes them from the metadata and replaces each literal with a call that decrypts it at runtime:
if (b.Length != 4 || b[0] != Strings.Get(0x4a1)) return false;
Now a decompiler shows Strings.Get(0x4a1) instead of "LIC". The value still exists in memory when the program runs, so this is not cryptography against a live debugger — but it defeats the overwhelmingly common attack of reading the binary and grepping its strings. The list of literals that used to map your whole program is gone.
Layer 2 — constant masking
The next free gift to a reader is numeric constants: seat limits, timeouts, feature thresholds, the 4 and 3 that describe your key format. Constant masking replaces literal numbers with small expressions evaluated at runtime, so parts.Length != 4 becomes something the decompiler renders as arithmetic over opaque values rather than a printed 4. Individually minor; collectively, it removes the second-easiest way to understand what a method is checking for.
Layer 3 — control-flow flattening
Renaming, string encryption and constant masking all leave the shape of the method intact — the decompiler still prints clean if/else/while blocks. Control-flow flattening breaks that. It rewrites the method as a single loop around a state-machine switch, where each original basic block becomes a case and a dispatcher variable decides what runs next:
int state = 0;
while (true)
{
switch (state)
{
case 0: /* original block A */ state = 7; break;
case 7: /* original block B */ state = 2; break;
// ... blocks reordered, linked only by the dispatcher
case 2: return result;
}
}
The decompiler can no longer reconstruct the original if/while structure, because that structure has been dissolved into data. A human can still trace it, but “trace a flattened state machine with encrypted strings and masked constants” is a different afternoon than “read the renamed method.”
Layer 4 — virtualization, for the code that matters
Everything above still compiles down to IL, and IL has excellent decompilers. For the handful of methods that are genuinely worth protecting — the license check, a pricing algorithm, a core trade secret — method virtualization goes further: it replaces the method’s IL with a custom bytecode and ships a tiny interpreter that executes it. There is no decompiler for a bytecode that did not exist until your build produced it. An attacker must first reverse-engineer the virtual machine, then the program running on it. That is expensive, which is exactly the point.
Virtualization is the heaviest transform, so you apply it narrowly — to the few methods where the reading cost should be measured in days, not minutes — and leave the rest of the assembly on the lighter layers.
The practical takeaway
Renaming is a fine first layer and a poor last one. Treat protection as a stack: rename broadly, encrypt strings and mask constants across anything sensitive, flatten the control flow of your enforcement and IP code, and virtualize the few methods whose logic you most need to keep. The test for whether it worked is simple and the only one that matters: open your protected build in a decompiler and read it. If your licensing check still tells its own story, you stopped at renaming.
Try Nebula.NET
Harden your .NET code in minutes — start with the free edition.