DEV Community

Cover image for Protecting .NET String Values by Hiding Them in Machine Code DLLs
Rustemsoft LLC
Rustemsoft LLC

Posted on

Protecting .NET String Values by Hiding Them in Machine Code DLLs

Introduction

Every .NET developer should understand just how critical source code protection is for preventing intellectual property loss. Whether you're shipping a commercial library, a licensed desktop application, or a SaaS client tool, your compiled assemblies are far more exposed than most developers realize. There are numerous legitimate reasons why software vendors cannot distribute their source code openly, competitive advantage, licensing enforcement, contractual obligations, or simply the need to preserve trade secrets.

Among the many elements of a .NET assembly that are vulnerable to inspection, string literals stand out as particularly exposed. They are ubiquitous, human-readable, and tend to concentrate the very clues a reverse engineer needs most. This article explores two progressive levels of string protection available in Opaquer .NET Obfuscator, from in-assembly encryption to storing strings in a native machine-code DLL, and walks through a concrete example of each approach.


Why .NET String Values Are a Security Risk

.NET assemblies (.dll and .exe files) are compiled to Intermediate Language (IL), not native machine code. This makes them straightforward to decompile using tools like ILSpy, dnSpy, or JetBrains dotPeek, often producing C# or VB.NET code that closely resembles the original source.

String literals are the most defenseless members of any .NET class. A hacker attempting to bypass a licensing mechanism will almost certainly start by searching the decompiled output for strings related to license keys, activation servers, error messages, or user account identifiers. Locating those strings quickly narrows down which methods to focus on, significantly reducing the attacker's workload.

Encrypting strings in the assembly binary raises the bar, but as we'll see below, modern decompilers have become sophisticated enough to trace encryption routines at decompilation time and recover the original values. This is why Opaquer takes an additional step: moving strings entirely out of managed code and into a native, unmanaged machine-code DLL.


Step 1, The Sample Application

To illustrate the problem and the solution, let's start with the simplest possible .NET project: a Console Application containing a single "Hello World!" string.

Simple

Create this as a .NET command-line project in Visual Studio:

Creating a new .NET Console Application in Visual Studio


Step 2, Decompiling the Unprotected Assembly

Once the application is compiled, run your preferred .NET decompiler against ConsoleApplication1.dll. The result is immediately revealing:

Decompiler output showing the unprotected

The "Hello World!" string is fully visible, exactly as typed in the original source. Decompilation tools can reconstruct a .NET assembly back into high-level C#, VB.NET, or C++ with remarkable fidelity. This is the baseline risk every .NET application faces without any obfuscation.


Step 3, Basic String Encryption (In-Assembly)

The first layer of protection offered by Opaquer .NET Obfuscator is to encrypt strings and store the encrypted values inside the output assembly itself. Open ConsoleApplication1.dll in Opaquer Obfuscator, navigate to the Strings tab, and select "Keep Strings Inside The Output Assembly" under Where Assembly Strings Are Saved.

Opaquer .NET Obfuscator, Strings tab with

After running the obfuscator, the program still executes correctly and produces the same console output. But look at what changed internally. Decompile the obfuscated assembly again:

Decompiler output after in-assembly string encryption, the string is now stored as an encrypted value in a public variable

The "Hello World!" string has been moved into a public field and encrypted. The encrypted ciphertext is visible in the decompiled code, and a decryption routine is called at runtime to recover the original value.

Limitation of This Approach

This is a meaningful improvement, but it has a significant weakness: the decryption routine is also present in the same managed assembly. Modern decompilers and runtime analysis tools can trace the decryption call, recover the key, and reconstruct the original string. A determined attacker with patience can still break through this protection, it simply takes more effort than reading an unprotected assembly.


Step 4, Strings Stored in a Native Machine-Code DLL (Recommended)

The stronger solution is to extract the sensitive strings entirely from the managed assembly and place them inside a native, unmanaged C++ DLL. Opaquer handles this end-to-end:

  1. It extracts the string values from your .NET source.
  2. It generates C++ source code embedding those strings.
  3. It compiles the C++ code into a native machine-code DLL (appExtension.dll by default).
  4. It rewrites your .NET assembly to call the DLL at runtime via Platform Invoke (P/Invoke).

The result is that the strings never exist as readable literals inside your managed assembly, they live only in native binary code, which is orders of magnitude harder to analyze.

Applying This Protection in Opaquer

Open ConsoleApplication1.dll in Opaquer Obfuscator again. This time, on the Strings tab, select "Strings Stored in Separate DLL" under Where Assembly Strings Are Saved.

Opaquer .NET Obfuscator, Strings tab with

You can also specify the output DLL filename. The default name is appExtension, which produces appExtension.dll alongside your output assembly.

Specifying the Separate DLL file name in Opaquer, default is

Inspecting the Result

After obfuscation completes, the native-code DLL (appExtension.dll) appears in the same output directory as your .exe. Now decompile the obfuscated ConsoleApplication1.dll one more time:

Decompiler output after DLL-based string protection, the

Result
The "Hello World!" string is completely absent from the managed assembly. In its place is a P/Invoke call that reaches into the native appExtension.dll at runtime to retrieve the value. The DLL itself is an unmanaged binary COM file, it cannot be opened or browsed by any standard .NET assembly inspector.


How the Mechanism Works at Runtime

When the obfuscated ConsoleApplication1.dll runs:

  1. The managed code encounters the P/Invoke declaration pointing to appExtension.dll.
  2. The Windows loader maps the native DLL into the process's memory.
  3. The exported function is called, returning the original string value.
  4. The .NET code continues executing normally, with the user seeing no difference in behavior.

This flow means the string only ever exists in native memory during execution, it never appears in the managed metadata that decompilers inspect.


Comparing the Two Approaches

Feature In-Assembly Encryption Native DLL (Recommended)
Strings visible in decompiler? Partially (as ciphertext) No
Decryption routine exposed? Yes (in managed IL) No (native code)
Requires additional file? No Yes (appExtension.dll)
Resistance to runtime analysis Moderate High
Openable by .NET assembly browsers? Yes No

Practical Considerations and Caveats

Distribution Requirements

The most important operational note when using the DLL approach: you must include appExtension.dll in your product's distribution package, installed in the same directory as your .exe or .dll. If the native DLL is absent at runtime, your application will throw a DllNotFoundException and fail to start. There is no graceful fallback, the strings simply cannot be retrieved without it.

No Protection Is Absolute

It would be misleading to claim that any obfuscation technique provides complete security. If your code runs on the end-user's machine, a sufficiently motivated attacker with enough time and skill can analyze it, whether through native debuggers, memory inspection tools, or hardware-level analysis.

What obfuscation achieves is raising the cost of attack:

  • Casual tool-based decompilation is blocked entirely.
  • Automated string scanning (the most common first step in reverse engineering) finds nothing useful.
  • Manual analysis of native machine code is vastly more time-consuming than reading IL.
  • The larger and more complex the application, the exponentially harder native-code analysis becomes.

For most commercial applications, this level of protection is sufficient to deter all but the most determined adversaries, and those adversaries are rarely targeting your licensing strings specifically.

Managed Code Preference

As a matter of software engineering principle, pure managed code is preferable: it is easier to debug, update, and deploy. Introducing a native dependency adds complexity to your build pipeline, installation process, and potentially to your support burden (e.g., 32-bit vs. 64-bit variants). The native DLL approach is best reserved for the most sensitive string assets, license keys, activation endpoints, proprietary algorithm parameters, rather than applied indiscriminately to all strings.


Conclusion

.NET string literals are an underappreciated attack surface. Because they appear in plain text inside compiled assemblies and tend to cluster around the most sensitive logic in your codebase, they are typically the first thing a reverse engineer examines. Opaquer .NET Obfuscator addresses this in two escalating ways:

  • In-assembly encryption disrupts casual inspection and makes strings illegible to basic decompilers, at the cost of still exposing the decryption routine in managed IL.
  • Native DLL storage removes strings from the managed assembly entirely, placing them in a compiled C++ binary that .NET tooling cannot open or analyze.

Neither technique is a silver bullet, but together they represent a substantial and practical improvement over shipping unprotected assemblies. When applied to a real-world application, particularly a large, architecturally complex one, they make reverse engineering your string-dependent logic a very expensive endeavor indeed.

Note: Always include the Opaquer-generated appExtension.dll in your product's installer alongside your main assembly. The two files must be co-located for the application to function. Without the DLL, the P/Invoke calls will fail at startup and your application will not run.


This article covers Opaquer .NET Obfuscator's string protection features. For full documentation including obfuscation of method names, control flow, and resource encryption, refer to the Opaquer .NET Obfuscator documentation.

Top comments (0)