Skip to content
← All posts
· Delta1 Labs Nebula.NETObfuscationGuide

Keeping obfuscated names stable across releases with Nebula's seed map

By default every obfuscation run renames from scratch, so a method that was "a" in 1.4.0 becomes "q" in 1.4.1 and every archived deobfuscation map rots the moment you ship. Nebula.NET's seedMapFile fixes the output in place: unchanged members keep the obfuscated name they had last release, only new code gets new names. Here is how the symbol map works, why stable names make crash symbolication and binary diffs tractable, and how to wire incremental obfuscation into CI.

Ship an obfuscated .NET build and you get a mapping: MyApp.Billing.Invoice.Recalculate became n.a.b, and you tuck away the map so you can decode the crash reports that will arrive. Ship 1.4.1 a week later with a one-line fix, and that same method is now q.c.f. Nothing about it changed, but its obfuscated name did — because the default obfuscation run renames the whole assembly from scratch, and the renamer’s numbering shifted the moment you added a field three classes over.

That churn is quietly expensive. Your 1.4.0 map cannot read a 1.4.1 stack trace. A binary diff between the two releases is a sea of renames with your actual fix buried in it. And you can never look at a raw obfuscated trace and think “oh, that’s the invoice bug again,” because the name is different every build. Nebula.NET’s seed map removes the churn: feed last release’s map back in, and unchanged members keep the names they already had. This post is how that works and why it is worth wiring up.

What a run already emits

You do not have to turn anything on to get the raw material. Every Nebula run writes a symbol map next to the obfuscated assembly — MyApp.dll produces MyApp.symbols.json — recording every rename it performed:

{
  "tool": "Nebula.NET",
  "toolVersion": "1.1.6",
  "generatedUtc": "2026-10-10T09:00:00Z",
  "assembly": "MyApp",
  "entries": [
    { "kind": "Method", "original": "MyApp.Billing.Invoice.Recalculate", "obfuscated": "n.a.b", "token": "0x06000123" },
    { "kind": "Type",   "original": "MyApp.Billing.Invoice",              "obfuscated": "n.a",   "token": "0x02000011" },
    { "kind": "Field",  "original": "MyApp.Billing.Invoice._subtotal",    "obfuscated": "n.a.d", "token": "0x04000045" }
  ]
}

Each entry carries four things: the symbol kind, the original fully-qualified name, the obfuscated name it received, and the metadata token — the MethodDef/TypeDef/FieldDef RID in the output module. That token is what makes the map a precise two-way index rather than a fuzzy name list, and it is what nebula deobfuscate --map MyApp.symbols.json reads to turn an obfuscated stack trace back into real names. So far, so normal — you archive this file with each release and decode traces against it.

The problem is that the next release’s map is a different file with different obfuscated values for the same original names. The archive grows one incompatible map per release.

Seeding: carry the names forward

The seed map closes the loop. Point seedMapFile at the previous release’s .symbols.json and Nebula consults it before assigning names:

{
  "preservePublicApi": true,
  "controlFlowObfuscation": true,
  "encryptStrings": true,
  "obfuscateConstants": true,
  "metadataHardening": true,
  "seedMapFile": "maps/MyApp-1.4.0.symbols.json"
}

Now the renamer runs in two phases. For every member that still exists under the same original name, it looks up the obfuscated name the seed assigned and reuses it verbatim. Only members with no entry in the seed — genuinely new code, or code whose identity changed — fall through to fresh name generation, and those fresh names are chosen to avoid colliding with any name the seed already handed out. The result is an assembly where the renaming is incremental: the diff against last release is your actual change, not a global renumber.

1.4.0 map (seed)Recalculate → n.a.bInvoice → n.a_subtotal → n.a.darchived with the1.4.0 releaseseedMapFile1.4.1 (seeded)Recalculate → n.a.bInvoice → n.a_subtotal → n.a.dApplyCredit → n.a.e (new)unchanged names reused1.4.1 (no seed)Recalculate → q.c.fInvoice → q.c_subtotal → q.c.hApplyCredit → q.c.jevery name moved

Why stable names pay off

Crash symbolication across versions. The practical win is that one archived map decodes more than one build. When Recalculate is n.a.b in 1.4.0, 1.4.1, and 1.4.2, a trace that lands in n.a.b resolves against any of those maps, and you learn to recognize the obfuscated name on sight — n.a.b is “the invoice recalculation,” full stop. A crash-report pipeline can group by obfuscated frame and show you that the same method is responsible across a span of releases before anyone runs nebula deobfuscate, because the symbol is the stable identity you have been collecting against all along.

Diffs that show the change, not the churn. Delta patchers (ClickOnce, Squirrel, your own binary-diff updater) produce tiny updates only when most of the assembly’s bytes are unchanged. A from-scratch rename moves nearly every name and therefore nearly every byte, so a one-line fix ships as a near-full download. Seeded obfuscation keeps the unchanged members byte-for-byte comparable, so the patch is proportional to the actual change. The same is true when a human reviews two decompiled builds side by side to confirm a hotfix did only what it claimed: with stable names the diff is legible; without them it is noise.

A recognizable mapping you can reason about. Over a release series the map becomes an append-mostly ledger. New entries appear as you add code; existing entries stay put. You can diff two maps and read, in plain terms, exactly which members are new this release — a surprisingly useful signal in its own right.

Wiring it into CI

The mechanics are: build, obfuscate seeded from the last release’s map, and archive this build’s map so the next build can seed from it. The map lives outside the source tree because it names your symbols; an artifact store or a protected branch is the usual home.

# Pseudocode for a release pipeline step.
steps:
  - run: dotnet build -c Release
  # Pull the previous release's map; on the very first release this is a no-op
  # and Nebula renames from scratch (there is nothing to seed from yet).
  - run: fetch-artifact MyApp-latest.symbols.json -> maps/MyApp-prev.symbols.json
  - run: nebula protect --config nebula.json   # config sets seedMapFile: maps/MyApp-prev.symbols.json
  # Archive THIS build's map as the new "latest" so the next release seeds from it,
  # and keep a version-stamped copy for decoding this build's crash reports.
  - run: publish-artifact MyApp.symbols.json as MyApp-latest.symbols.json
  - run: publish-artifact MyApp.symbols.json as MyApp-${VERSION}.symbols.json

Two practical notes. First, the very first release has no seed — that is fine; Nebula renames from scratch and emits the first map, which becomes the seed for release two. Second, keep a version-stamped copy of every map (MyApp-1.4.1.symbols.json) in addition to the rolling latest, because that is what decodes crash reports from that specific build. The rolling latest is for seeding the next build; the stamped copies are for symbolication forever.

What seeding does not do

Seeding is an operational tool, not a security knob, and it is worth being precise about that. It fixes which obfuscated name a member gets; it does not weaken the obfuscation of any individual build. An attacker holding your 1.4.0 assembly already has a fully protected binary — string-encrypted, control-flow-flattened, metadata-hardened. Reusing the same names in 1.4.1 tells that attacker nothing the 1.4.0 binary did not already tell them, because the names are opaque in both. The renaming remains deterministic and collision-free, every other pass runs exactly as configured, and there is no original-name leakage in the output — the map that holds the originals is yours, archived on your side, and never ships in the assembly. What you gain is purely on your side of the fence: maps that keep working, diffs that stay small, and crash reports you can read at a glance across a whole release series.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.