Rustemsoft

Stop Exposing Secrets in IL: Hide API Keys & Connection Strings in .NET Assemblies

Hands-on protection strategy · Opaquer-style string hardening · ~14 min read
Bottom line: Protect string literals by replacing their plaintext presence in your managed binary. The practical playbook has two levels: encrypt the strings inside the assembly for convenience, or move the real values into a native compiled DLL so .NET decompilers can’t even “see” the secrets.

Why string literals are a high-value target

Most teams think of obfuscation as a “code problem.” But in .NET, secrets often show up before any meaningful logic executes-inside the assembly itself.

Decompilers reconstruct near-source output from the managed module, and string literals appear as plain, searchable text inside the metadata. That means an attacker can jump straight to interesting locations without understanding your architecture.

Example of what makes this dangerous: if your assembly contains an API base URL or a database connection string with a username/password, the attacker can copy it and attempt direct use (often without patching any code).

What the attacker does (in practice)

  1. Open your .dll or .exe in ILSpy/dnSpy/dotPeek.
  2. Search for recognizable fragments like "api.mycompany.com" or "prod-sql01".
  3. Read the surrounding declarations (or copy the secrets directly).

Once a single “anchor string” is found (e.g., an error message or a license verification URL), reverse engineering becomes a guided scavenger hunt.

Two escalating solutions for string protection

You can harden string literals by increasing the difficulty of locating them in the managed artifact. The two-step approach looks like this:

Selection principle: if exposing a string would directly enable abuse (credentials, API endpoints, license validation servers), prefer native DLL storage. If the string is mostly structural or informational, in-assembly encryption can be enough.

A minimal example of “plaintext string risk”

Consider a configuration class that includes an API endpoint, a connection string, a license server URL, and error text. When compiled without string hardening, those constants land in the assembly as readable literals.

Plain literals in managed IL
public class AppConfiguration
{
    public const string ApiUrl = "https://api.mycompany.com/v2/data";

    public const string ConnectionString =
        "Server=prod-sql01.internal;Database=Customers;User Id=appuser;Password=Xk9mP2qR;";

    public const string LicenseServer = "https://license.mycompany.com/verify";

    public const string ErrorMessage = "License validation failed: server returned status 403";
}

In a reverse-engineering workflow, that means you don’t need to understand the program to get the values-you only need to search.

Configuration workflow (how to apply both options)

1) Load your target assembly

Start by opening the obfuscation tool and loading the assembly you want to protect. Both .dll and .exe inputs are supported.

2) Inspect the strings you plan to protect

Use the Strings view to locate specific literals inside the assembly-classes, methods, fields, and the exact string values associated with them.

3) Choose what gets encrypted

You can mark sensitive strings in multiple granularities:

It’s usually better to protect only what matters: API URLs, connection strings, license endpoints, and error messages that disclose internal behavior.

4) Decide where the “real secret” lives

This is the core trade-off:

Option A - Encrypt in the managed assembly

The tool replaces string literals with encrypted ciphertext and injects a runtime decryption routine to restore values when needed.

Option B - Store strings in a native DLL (recommended for secrets)

The tool removes the sensitive strings from the managed binary, generates a native machine-code DLL, and rewires your .NET code to fetch strings through P/Invoke at runtime.

How to verify the result with ILSpy

In-assembly encryption: strings become ciphertext

After protection, searching your original values should no longer return readable strings in ILSpy. You’ll see encoded byte arrays/ciphertext placeholders instead.

public class AppConfiguration
{
    public const string ApiUrl = "\u0018\u0043\u00A7\u0021\u009B...";

    public const string ConnectionString = "\u000E\u0072\u00C4\u0038...";

    public const string LicenseServer = "\u00A1\u0056\u008D\u00F2...";

    public const string ErrorMessage = "\u003F\u0099\u00B1\u0004...";
}

Native DLL storage: strings disappear from managed code

With native DLL storage, the sensitive string values are absent from the managed assembly. ILSpy searches for the original plaintext should return zero hits.

public class AppConfiguration
{
    [DllImport("appExtension.dll", EntryPoint = "GetString0")]
    public static extern string GetApiUrl();

    [DllImport("appExtension.dll", EntryPoint = "GetString1")]
    public static extern string GetConnectionString();

    // ...
}

Choosing the right option (a practical matrix)

Dimension In-assembly encryption Native DLL storage
Deployment files Single managed assembly Requires shipping appExtension.dll
Plaintext presence in IL Not readable, but decryption logic still exists in IL Completely absent from managed IL
Static analysis resistance Moderate High (native machine code)
Runtime visibility Strings appear after decryption Strings still appear in memory, but harder to trace from managed artifacts
Best use cases Error messages, UI text, low-sensitivity strings API keys, connection strings, license server endpoints, encryption salts
Recommended label Standard Secrets-first

A tight test plan before you ship

1) Confirm protection by searching

2) Run full functional paths

Exercise the code paths that rely on the protected strings-API calls, DB connections, license validation-and ensure behavior is unchanged from the user perspective.

3) Validate native DLL deployment

If you used native DLL storage, ensure appExtension.dll is present alongside the application binary. Running without it should fail predictably (e.g., DllNotFoundException), which confirms the dependency is correctly wired.

Common issues (and what they usually mean)

DllNotFoundException at startup

This typically indicates the generated native DLL wasn’t deployed with your app. Fix your installer/build pipeline so the DLL ships into the app directory every time.

Strings still appear after “encryption”

Often the string selection step didn’t include the target literals, or the tool’s “do not obfuscate below length threshold” setting excluded them. Re-check the Strings list and adjust thresholds/select individually.

Unexpected performance impact

If you encrypt too broadly-especially in hot paths-you may see overhead. Be selective: protect secrets and high-impact literals instead of encrypting every string.

Native DLL architecture mismatch

If you ship both 32-bit and 64-bit builds, you need matching native outputs. Ensure your obfuscation targets the same platform as your runtime.

Production guidance (what to protect first)

Use a “risk-based” priority list:

Remember: string hardening is strongest when combined with other layers such as identifier renaming and control flow obfuscation.

FAQ

In-assembly encryption vs native DLL storage-what’s the real difference?

In-assembly encryption keeps secrets inside the managed module but stores them as encrypted values with a decryption routine. Native DLL storage removes the secrets from the managed assembly entirely by placing them into a compiled native DLL, which decompilers for managed code can’t directly read.

Why not just encrypt strings myself in C#?

Any solution still needs a decryption key and routine. If both live inside managed IL, attackers can locate them and extract or reproduce the secret. Native DLL storage disrupts that workflow by keeping the secrets in native machine code instead.

Will this break my app?

When configured correctly, behavior should remain unchanged: strings are retrieved at runtime instead of being stored in plaintext. However, some contexts may require compile-time constants, so those particular strings may remain visible.

If I use in-assembly encryption, do I need extra files?

No-everything stays within the managed assembly. Only native DLL storage requires shipping the native DLL dependency.

What happens if the native DLL is missing?

The app fails at startup (typically via DllNotFoundException) because the strings can’t be retrieved without the DLL. There’s no fallback mechanism.

Can I mix both methods?

Yes. Select some strings for in-assembly encryption and others for native DLL storage to balance simplicity, deployment, and protection strength.

Are config files like .resx and .config covered?

This approach targets string literals embedded in IL. Values stored in .config, .resx, or external JSON aren’t automatically covered. Use appropriate .NET configuration encryption or secrets management for those.

How do I verify it’s truly protected?

Search in ILSpy for the original plaintext values after obfuscation: they should be unreadable ciphertext (in-assembly encryption) or absent from managed IL (native DLL storage). Then run end-to-end tests to confirm runtime behavior.