The Problem
String literals are the single most underappreciated attack surface in .NET applications. Unlike compiled code, which requires at least some reverse-engineering effort to understand, string literals sit in the assembly metadata in plain, readable text. Any decompiler-including free tools like ILSpy, dnSpy, or dotPeek-will display them without any analysis required.
This creates a specific and immediate danger. Consider what lives in a typical commercial .NET application:
- API keys and tokens that grant access to external services
- Database connection strings containing server addresses, usernames, and passwords
- License server endpoints that control activation and validation
- Error messages that reveal application structure and internal logic
- Hardcoded encryption keys or salts used for data protection
An attacker doesn't need to understand your architecture to exploit these. They simply search the decompiled output for recognizable patterns. The string "https://api.mycompany.com" tells them where your application phones home. The string "Server=prod-sql01;Database=Customers;User Id=appuser;Password=..." is a complete set of credentials. The string "Invalid license key" points directly to the validation logic that can be patched.
Once an attacker locates a string of interest, they are positioned to either extract the secret directly or use the string as a landmark to find and modify the surrounding code. String encryption removes this first and easiest foothold.
Small C# Example
The following class demonstrates the exact kinds of sensitive string literals that appear in production .NET applications:
C# - Sensitive Stringspublic class AppConfiguration
{
// API endpoint for external service
public const string ApiUrl = "https://api.mycompany.com/v2/data";
// Database connection with credentials
public const string ConnectionString =
"Server=prod-sql01.internal;Database=Customers;User Id=appuser;Password=Xk9mP2qR;";
// License validation endpoint
public const string LicenseServer = "https://license.mycompany.com/verify";
// Error message revealing internal state
public const string ErrorMessage = "License validation failed: server returned status 403";
public void Connect()
{
Console.WriteLine($"Connecting to {ApiUrl}...");
// ... use ConnectionString to open database
}
}
Compile this class, and every one of those values is embedded in the assembly as a searchable, readable literal. The connection string alone would allow an attacker to directly access the production database if the credentials are valid and the server is reachable.
What Happens Without Obfuscation
Step 1: Open the Assembly in ILSpy
Load the compiled .dll or .exe into ILSpy, dnSpy, or any other .NET decompiler. The tool reconstructs the source code with near-perfect accuracy.
Step 2: Search for the String
Use the decompiler's search function (Ctrl+Shift+F in ILSpy) to search for "api.mycompany.com" or "prod-sql01". The search returns an immediate hit.
Step 3: Show Plaintext
The decompiler displays the AppConfiguration class with all string literals intact:
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";
}
Every secret is visible. The attacker now has:
- The database server hostname (
prod-sql01.internal) - Valid credentials (
appuser/Xk9mP2qR) for that database - The exact API endpoint for your external service
- The license validation URL, which can be intercepted or redirected
- A clear landmark (
"License validation failed") to locate the validation logic in the surrounding code
This is not a theoretical risk. It is the default state of every unprotected .NET assembly.
Step-by-Step Opaquer Configuration
Step 1: Load the Assembly
Open Opaquer and load the assembly containing your sensitive strings. Both .dll and .exe files are supported.
Step 2: Navigate to the Strings Tab
The Strings tab-page displays a hierarchical tree of your assembly's structure-classes, methods, fields, and the string literals they contain. Use the left pane to navigate; the right pane provides configuration options for the selected strings.
Step 3: Select Strings for Encryption
You can select strings in three ways:
- Individual strings: Click a specific string literal in the tree and mark it for encryption
- By class: Select an entire class to encrypt all strings within it
- By assembly: Encrypt every string in the assembly
For targeted protection of sensitive values, select only the strings that matter-API URLs, connection strings, license endpoints, and error messages that reveal internal logic. Opaquer also provides a "Do not obfuscate Strings with length less than" threshold to exclude trivial short strings automatically.
Step 4: Choose the String Storage Location
This is the critical decision. Opaquer offers two options under "Where Assembly Strings Are Saved":
Option A - In-Assembly Encryption:
Strings are encrypted and remain within the managed assembly. A decryption routine is injected into the assembly and runs at runtime to restore strings in memory when needed.
Option B - Strings Stored in Separate DLL (Recommended):
Opaquer extracts the selected strings entirely from the managed assembly, generates C++ source code containing the strings, compiles that into a native machine-code DLL (appExtension.dll by default), and rewrites your .NET assembly to retrieve the strings via P/Invoke at runtime.
Step 5: Configure Encryption Settings
If using in-assembly encryption, you can select a cryptographic algorithm and provide a password. The password is encrypted and stored within the assembly, or you can store it in a separate file that must be deployed alongside your assembly.
For native DLL storage, Opaquer handles the encryption and compilation automatically. You can specify the output DLL filename.
Step 6: Apply Obfuscation
Click Obfuscate and save the output. For native DLL storage, appExtension.dll appears in the same output directory as your obfuscated assembly.
Before and After in ILSpy
Before Obfuscation (Unprotected)
ILSpy - Unprotectedpublic 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";
}
After Opaquer (In-Assembly Encryption)
The strings are replaced with encrypted representations. The decompiled output shows encoded byte arrays or ciphertext rather than readable text:
ILSpy - In-Assembly Encryptionpublic 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...";
}
The decryption routine is visible in the assembly, but the strings themselves are not readable.
After Opaquer (Native DLL Storage - Recommended)
The strings are completely absent from the managed assembly. In their place are P/Invoke declarations that call into appExtension.dll:
public class AppConfiguration
{
[DllImport("appExtension.dll", EntryPoint = "GetString0")]
public static extern string GetApiUrl();
[DllImport("appExtension.dll", EntryPoint = "GetString1")]
public static extern string GetConnectionString();
// ... additional P/Invoke declarations
}
Searching ILSpy for "api.mycompany.com" or "prod-sql01" returns zero results. The strings exist only in the native binary appExtension.dll, which cannot be opened by any standard .NET assembly browser. Native code is orders of magnitude harder to analyze than managed IL.
When to Choose Which Approach
Both approaches protect strings, but they serve different needs. Use this table to decide:
| Aspect | In-Assembly Encryption | Native DLL Storage |
|---|---|---|
| Deployment | Single managed assembly, no extra files | Requires shipping appExtension.dll alongside the assembly |
| Strings visible in ILSpy | Encrypted, unreadable | Completely absent from managed assembly |
| Resistance to static analysis | Moderate - decryption routine is visible in IL | High - strings live in native machine code |
| Resistance to runtime analysis | Low to moderate - strings appear in memory after decryption | Moderate - strings still appear in memory, but retrieval is harder to trace |
| Best for | Error messages, UI text, log formats, low-sensitivity strings | API keys, connection strings, license server URLs, encryption salts |
| Deployment complexity | Low | Medium - must include native DLL in installer |
| Recommendation | Standard | Recommended for secrets |
Testing the Obfuscated Application
1. Verify String Protection
Open the obfuscated assembly in ILSpy and search for each sensitive string value. Confirm that:
- With in-assembly encryption: strings appear as unreadable encoded text
- With native DLL storage: strings are completely absent from the managed assembly
2. Functional Test
Run the application and exercise every code path that uses the protected strings. Confirm that API calls succeed, database connections open, and license validation works. The strings should be retrieved transparently at runtime with no behavioral change visible to the user.
3. Verify Native DLL Deployment
If using native DLL storage, confirm that appExtension.dll is present in the same directory as your application's executable. Attempt to run the application without the DLL to confirm that it throws DllNotFoundException as expected-this verifies the dependency is properly established.
4. Check for Missing Dependencies
Use dumpbin /dependents (Windows SDK) or a tool like Dependency Walker to verify that your obfuscated assembly correctly references appExtension.dll. On deployment, verify the DLL is included in your installer or distribution package.
Common Failures and Fixes
Failure: Application Throws DllNotFoundException at Startup
Likely cause: appExtension.dll is not present in the application directory.
Fix: Ensure appExtension.dll is deployed alongside your main assembly. Update your build script, installer, or deployment package to include it. There is no graceful fallback-the application cannot retrieve strings without the native DLL.
Failure: Strings Still Visible After In-Assembly Encryption
Likely cause: The strings were not selected for encryption, or the minimum length threshold excluded them.
Fix: Verify that the specific strings are checked in the Strings tab. If using a length threshold, lower it or select strings individually. Some strings may be optimized by the compiler into constant folding-check for inline usage as well.
Failure: Performance Degradation After String Encryption
Likely cause: Encrypting every string in a large assembly, including frequently-called hot paths.
Fix: Be selective. Encrypt sensitive strings (credentials, keys, URLs) rather than all strings. Opaquer's string selection interface allows precise targeting. For performance-sensitive paths, consider whether the string is truly sensitive before encrypting it.
Failure: Native DLL Architecture Mismatch
Likely cause: The generated appExtension.dll is compiled for the wrong platform (32-bit vs 64-bit) relative to your application.
Fix: Verify that your Opaquer output targets the correct platform. If you ship both 32-bit and 64-bit builds, you may need corresponding native DLLs. Consider making the native DLL architecture match your primary deployment target.
Production Recommendations
Choose the Right Storage for the Right String
Not all strings deserve the same level of protection. Use this decision framework:
| String Type | Recommended Protection | Rationale |
|---|---|---|
| Database connection strings | Native DLL | Credentials must never appear in managed assemblies |
| API keys and tokens | Native DLL | Direct financial/security impact if extracted |
| License server endpoints | Native DLL | Prevents redirecting validation to attacker-controlled servers |
| Error messages | In-Assembly Encryption | Reveals internal logic; less critical than credentials |
| UI text and labels | In-Assembly Encryption or None | Low sensitivity; performance matters |
| Log format strings | None or In-Assembly | Rarely useful to attackers |
The native DLL approach is best reserved for the most sensitive string assets rather than applied indiscriminately.
Understand the Trade-offs
In-Assembly Encryption:
- No additional deployment files
- Decryption routine visible in managed IL
- Moderate resistance to runtime analysis
- Good for disrupting casual inspection
Native DLL Storage:
- Requires shipping
appExtension.dllalongside your assembly - Strings exist only in native binary, invisible to .NET decompilers
- High resistance to static analysis
- Adds a native dependency to your build and deployment pipeline
Plan for Maintenance
String encryption is a build-time operation, not a one-time fix. Add it to your release pipeline:
- Build the assembly in Release configuration
- Run Opaquer with your saved string encryption settings
- Verify that sensitive strings are absent from decompiled output
- Confirm that
appExtension.dllis included in the deployment package - Test the obfuscated application end-to-end
Version your Opaquer configuration alongside your source code so protection settings remain consistent across releases.
Combine with Other Protections
String encryption addresses the most visible attack surface, but it works best in combination with:
- Identifier renaming: eliminates method and class names that guide attackers
- Control flow obfuscation: makes the surrounding logic harder to follow
- Native DLL storage: for the highest-value secrets
For license validation and cryptographic code, consider High-Intensity Control Flow or code virtualization (Enterprise) in addition to string protection.
FAQ
What is the difference between in-assembly string encryption and native DLL storage?
In-assembly encryption keeps strings within the managed assembly but replaces them with encrypted representations. Native DLL storage removes strings from the managed assembly entirely, placing them in a compiled C++ binary. The native DLL approach provides stronger protection because the strings never exist in any form that .NET decompilers can inspect.
Why can't I just encrypt strings with a standard algorithm in my source code?
You can, but the decryption key must also exist somewhere in the assembly, and the decryption routine is visible in managed IL. An attacker can locate the decryption logic and extract the key. Opaquer's approach either hides the decryption in native code (DLL storage) or uses injection techniques that make the decryption routine harder to isolate.
Will string encryption break my application?
No, if configured correctly. Opaquer replaces strings with calls that retrieve the original values at runtime. The application behavior is unchanged. However, if you use string literals in contexts that require compile-time constants (like attribute parameters or switch case labels), those specific strings cannot be encrypted and will remain visible.
Do I need to ship appExtension.dll if I use in-assembly encryption?
No. In-assembly encryption keeps everything in the managed assembly. Only the native DLL storage option requires the additional file.
What happens if appExtension.dll is missing at runtime?
The application will throw DllNotFoundException and fail to start. There is no fallback mechanism-the strings simply cannot be retrieved without the native DLL. Always verify that the DLL is included in your deployment package.
Can I use both in-assembly encryption and native DLL storage in the same assembly?
Yes. You can select some strings for in-assembly encryption and others for native DLL storage. This allows you to protect critical secrets with native storage while keeping less sensitive strings in the managed assembly for simplicity.
Does string encryption affect application performance?
There is a small runtime cost for decryption. In-assembly encryption has minimal overhead. Native DLL storage adds P/Invoke calls, which introduce a small marshaling cost. For most applications, the impact is negligible. If you encrypt strings in performance-critical loops, test the impact and consider whether those specific strings need protection.
What about strings stored in configuration files or resources?
Opaquer's string encryption targets string literals embedded in IL code. Strings in .config files, .resx resources, or external JSON files are not covered. For those, use .NET's built-in configuration encryption (aspnet_regiis -pe for web.config) or move sensitive values to environment variables or a secrets manager.
Can I verify that the strings are truly protected?
Yes. After obfuscation, open the assembly in ILSpy and search for the original string values. With in-assembly encryption, they should appear as unreadable ciphertext. With native DLL storage, they should be completely absent. Also verify that the application still functions correctly with the obfuscated assembly.