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.
What the attacker does (in practice)
- Open your
.dllor.exein ILSpy/dnSpy/dotPeek. - Search for recognizable fragments like
"api.mycompany.com"or"prod-sql01". - 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:
- In-assembly encryption: keep the decryption mechanism inside your managed assembly and replace plaintext literals with encrypted representations.
- Native DLL storage: remove the secrets from the managed module entirely. Extract them and compile them into a native C++-backed DLL that decompilers for managed code can’t interpret.
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 ILpublic 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:
- Individual strings for surgical protection
- Entire classes if the class is dedicated to configuration
- Whole assemblies for broad coverage
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
- With in-assembly encryption: verify original strings are no longer readable.
- With native DLL storage: verify the strings are absent from managed IL.
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:
- Database connection strings → native DLL storage
- API keys/tokens → native DLL storage
- License server endpoints → native DLL storage
- Error messages / UI text → in-assembly encryption (or none if acceptable)
- Log format strings → typically lower value; avoid over-protection
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.