Skip to content
← All posts
· Delta1 Labs Nebula.NETObfuscation.NETDeep Dive

Encrypting embedded resources: the data your obfuscator forgets

You renamed every type, encrypted every string, and virtualized the hot methods — and then someone opened your assembly in a resource viewer and read your embedded SQL, your licensing key material, and the full text of your error messages straight out of the .resources blob. Managed resources are not code, so method-level protection never touches them; they sit in the manifest as length-prefixed values anyone can dump. This is how embedded resources are actually stored in a PE, why renaming leaves them untouched, and how Nebula encrypts them at build time and transparently decrypts them through a resolver so your calls to ResourceManager and GetManifestResourceStream keep working unchanged.

A protection pass tends to focus on the exciting surface: the method bodies. You rename the type graph, flatten control flow, encrypt string literals, maybe virtualize the methods that enforce licensing. Then you ship, and a week later someone emails you the exact text of your internal error messages, the template of a connection string, and a data file you considered proprietary — all pulled out of the assembly without decompiling a single method. They did not break your obfuscation. They opened the resources, which your obfuscation never touched, because resources are not code.

This is the blind spot. Embedded resources are stored as data in a part of the PE that method-level transforms have no reason to visit. This post is about what is really in that blob, why every code-focused protection leaves it in the clear, and how Nebula encrypts resources at build time while keeping your ResourceManager and GetManifestResourceStream calls working exactly as written.

Where resources actually live

A managed resource is not a member. When you mark a file as an embedded resource, the compiler writes its bytes into a resource blob and adds a row to the ManifestResource metadata table naming it and giving its offset. A .resx file is compiled into a .resources binary stream and embedded the same way. Nothing about this process involves IL, method bodies, or member names — the two subsystems barely know about each other.

code sideTypeDef · MethodDef · IL bodiesrenamed, flattened, string-encryptedstring literals → decrypt at runtimeobfuscation covers thisdata sideManifestResource → resource bloblength-prefixed values, type tableSQL · key material · .resx stringsuntouched → a reader walks it

Because the resource blob is a separate region, every code transform you run — renaming, control flow, string encryption, virtualization — passes it by. There is no name to mangle and no IL to rewrite. The blob you shipped is byte-for-byte the blob the compiler produced.

What a reader sees

The .resources format is deliberately simple and the BCL ships the reader. You do not need a decompiler; you need System.Resources.ResourceReader, the same class the framework uses internally:

using var reader = new ResourceReader("MyApp.Strings.resources");
foreach (DictionaryEntry entry in reader)
    Console.WriteLine($"{entry.Key} = {entry.Value}");

// ConnectionTemplate = Server={0};Database=acme;User Id={1};Password={2};
// ActivationSeed     = 7f3a9c... (your "secret" byte[] in plain hex)
// Error_LicenseVoid  = This build's licence was revoked on {0:d}.

For a raw embedded file (not a .resx), it is even more direct — GetManifestResourceStream hands back the exact bytes:

var asm = Assembly.LoadFrom("MyApp.dll");
using var s = asm.GetManifestResourceStream("MyApp.Data.rules.bin");
using var ms = new MemoryStream();
s!.CopyTo(ms);
File.WriteAllBytes("rules.bin", ms.ToArray()); // your proprietary file, extracted

Nothing here touches your protected methods. The resource names are listed in the stream’s hash index, the string values are UTF-8 with a 7-bit length prefix, and arbitrary objects come back through their serializer. A resource viewer is just a GUI over this API. “I embedded it in the DLL” bought you nothing against it.

Encrypting the blob at build time

Nebula treats resources as a first-class protection target. Enabled in the config, the resource-encryption pass walks the ManifestResource entries you selected, replaces the plaintext bytes with ciphertext, and injects a resolver that decrypts on demand. The config names what to protect:

<Nebula>
  <ResourceEncryption Enabled="true">
    <!-- Raw embedded files -->
    <Include Pattern="MyApp.Data.*" />
    <!-- .resx-backed resource sets -->
    <Include Pattern="MyApp.Strings.resources" />
    <!-- Leave framework/designer resources that must stay plain -->
    <Exclude Pattern="*.g.resources" />
  </ResourceEncryption>
</Nebula>

At build time the pass rewrites each matching resource. Conceptually the transform is this — read the plaintext, encrypt under a key the protected code can reproduce, and write the ciphertext back into the blob:

// Build-time transform (inside the Nebula pass, shown for intuition)
foreach (var res in module.Resources.OfType<EmbeddedResource>())
{
    if (!selector.Matches(res.Name)) continue;

    byte[] plain  = res.GetResourceData();
    byte[] cipher = ResourceCrypto.Encrypt(plain, deriveKeyFor(res.Name));
    module.Replace(res, new EmbeddedResource(res.Name, cipher));
}

The key is not written out as a constant field. It is produced by a small decryption routine that Nebula injects and then subjects to the same protections as the rest of your code — string encryption on its constants, control-flow flattening, optionally virtualization — so an attacker cannot grep for a static key or a recognisable algorithm identifier. That is the whole point: the resource ciphertext is only as recoverable as the obfuscated code that unwraps it.

Decryption stays transparent to your code

The pass would be useless if it forced you to rewrite every GetString and GetManifestResourceStream call. It does not. For raw embedded files, Nebula installs a hook so the protected names return a decrypting stream, and your existing call site is unchanged:

// Your code — unchanged. The resolver decrypts behind GetManifestResourceStream.
using var s = Assembly.GetExecutingAssembly()
    .GetManifestResourceStream("MyApp.Data.rules.bin");
var rules = RuleSet.Parse(s!); // receives plaintext

For .resx-backed lookups, Nebula supplies a ResourceManager whose resource set decrypts values as they are read, so GetString and GetObject still hand your code plaintext:

// Your code — unchanged. The injected ResourceManager decrypts on read.
var rm = Strings.ResourceManager;       // Nebula-supplied, decrypting
string msg = rm.GetString("Error_LicenseVoid")!; // plaintext at the call site

The mechanics that make this work are the same framework extension points localization already uses — a resource set is constructed per resource stream, and the reader behind it is yours to supply. Nebula slots a decrypting reader into that pipeline. Because satellite (localized) assemblies are just assemblies with their own .resources streams, the identical treatment covers them: each localized .resources is encrypted and resolved the same way, so your culture-specific strings are protected too, not only the neutral set.

Run the resource reader against the protected assembly now and the difference is stark:

using var reader = new ResourceReader("MyApp.Strings.resources");
foreach (DictionaryEntry entry in reader)
    Console.WriteLine($"{entry.Key} = {entry.Value}");

// __nebula_enc_0 = System.Byte[]   (opaque ciphertext; no names, no plaintext)
// __nebula_enc_1 = System.Byte[]

The names are gone, the values are ciphertext, and the only thing that can turn them back into strings is the obfuscated code path inside your assembly — which is exactly where you already spent effort making recovery expensive.

What to take away

Code protection protects code. Resources are data, and they sit in a separate region of the assembly that renaming, control-flow obfuscation, string encryption, and virtualization never visit — which is why a resource viewer can read your embedded SQL, key material, and error text out of a fully obfuscated DLL in one click. Treat resources as a protection target in their own right: encrypt the blob at build time, derive the key through the same protected code that guards your methods so there is no static key to grep for, and decrypt transparently through the GetManifestResourceStream hook and a decrypting ResourceManager so your call sites never change. Nothing shipped to a client is unbreakable, resources included — but you can remove the trivial disclosure and force any recovery through the obfuscated code, where it costs the attacker real work. If strings in your code are the other half of this problem, pair it with string encryption in .NET; for the broader picture of what obfuscation does and does not cover, see obfuscation beyond renaming.

Try Nebula.NET

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