Understanding ConfuserEx2: .NET Obfuscation and Deobfuscation Techniques

Author
Oleg Sergeev
Understanding ConfuserEx2: .NET Obfuscation and Deobfuscation Techniques

ConfuserEx2 is a powerful open-source .NET binary obfuscator that offers a range of features from string encryption to anti-tampering and packing mechanisms. While its primary goal is to protect .NET applications from reverse-engineering, understanding its internals can be extremely valuable for security researchers, penetration testers, and developers looking to safeguard their code or analyze third-party obfuscated assemblies. 

In this article, we explore how to set up ConfuserEx2 and apply its obfuscation techniques as well as walk through the deobfuscation and unpacking process with a variety of tools. By doing so, we’ll shed light on some of the most effective techniques to understand, debug, and restore .NET assemblies obfuscated with ConfuserEx2.

Tools

Below are all the essential tools and resources referenced in this guide:

Environment setup

For demonstration purposes, we have a simple .NET Framework 4.8 console application written in C#. This sample application encrypts and decrypts a string using AES:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public class AESCrypto {
  private static readonly byte[] Key = Encoding.UTF8.GetBytes("0123456789ABCDEF0123456789ABCDEF");
  private static readonly byte[] IV = Encoding.UTF8.GetBytes("ABCDEF0123456789");

  public static string Encrypt(string plainText) {
    if (string.IsNullOrEmpty(plainText))
      throw new ArgumentNullException(nameof(plainText));
    using(Aes aes = Aes.Create()) {
      aes.Key = Key;
      aes.IV = IV;
      using(MemoryStream memoryStream = new MemoryStream()) {
        using(CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateEncryptor(), CryptoStreamMode.Write)) {
          using(StreamWriter writer = new StreamWriter(cryptoStream)) {
            writer.Write(plainText);
          }
        }
        return Convert.ToBase64String(memoryStream.ToArray());
      }
    }
  }

  public static string Decrypt(string encryptedText) {
    if (string.IsNullOrEmpty(encryptedText))
      throw new ArgumentNullException(nameof(encryptedText));
    using(Aes aes = Aes.Create()) {
      aes.Key = Key;
      aes.IV = IV;
      using(MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(encryptedText))) {
        using(CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Read)) {
          using(StreamReader reader = new StreamReader(cryptoStream)) {
            return reader.ReadToEnd();
          }
        }
      }
    }
  }

  public static void Main() {
    string secret = "This is a secret message.";
    Console.WriteLine("Original: " + secret);
    string encrypted = Encrypt(secret);
    Console.WriteLine("Encrypted: " + encrypted);
    string decrypted = Decrypt(encrypted);
    Console.WriteLine("Decrypted: " + decrypted);
  }
}

To keep things straightforward, ensure you have Visual Studio (or another IDE) and the .NET Framework 4.8 installed if you want to compile and run this locally.

Obfuscating with ConfuserEx2

1. Open ConfuserEx2-GUI
Launch the ConfuserEx2 user interface.

2. Project Tab

  • Click the “Project” tab.
  • Select the original test application (AESCrypto.exe or similar) as the input assembly.
  • Set an output directory for the obfuscated version of the binary.

ConfuserEx2

3. Settings Tab

  • Under “Preset,” choose the “Maximum” obfuscation level for a stronger protection.
  • Also select the “Packer” option (if desired) to compress and pack your assembly.

.NET Obfuscation

4. Protect! Tab

  • Click “Protect!” to start the obfuscation process.
  • Verify that the obfuscation completed successfully, ensuring there are no errors in the log.

the obfuscation process

5. Compare in dnSpy

  • Open dnSpy.
  • Load the original .NET binary to see the unobfuscated code structure.
  • Load the newly obfuscated binary to compare how classes and methods look post-obfuscation.

.NET binary

ConfuserEx2 deobfuscation guide

As can be seen, the binary is successfully obfuscated with no ability to read the instructions and several deobfuscation steps need to be performed in order to restore the original code.

ConfuserEx2 deobfuscation guide

Unpacking and Anti-Tamper Protection Removal

ConfuserEx2 employs multiple layers of protection, including, but not limited to decompression, decryption of functions and constants at runtime. These mechanisms ensure that any attempt to tamper with the assembly, such as stepping through it in a debugger or modifying original IL instructions, is detected or thwarted. Additionally, portions of the assembly may be encrypted, with decryption only occurring at runtime. Understanding these protections is vital for reversing the obfuscation process, as once the integrity checks pass and the assembly is decrypted in memory, the original code becomes accessible for analysis.

ConfuserEx2-protected .NET applications begin their execution from the <Module> class’s .cctor initializer. The first function call inside this initializer typically removes anti-tamper protection and exposes the unpacking logic in memory. To extract the decompiled version of the unpacking wrapper module, place a breakpoint after that first function call:

ConfuserEx2-protected .NET applications

Note that ConfuserEx includes anti-debugging functionality, but dnSpy has built-in techniques to bypass common anti-debugging checks. Ensure all relevant options are enabled in Debug -> Options… -> Debugger -> Prevent code from detecting the debugger.

1. Start the application and, in Debug -> Windows -> Modules, right-click on the target executable and select Open Module from Memory.

2. In the newly opened module, locate the global <Module> class.

3. Place a breakpoint on the gchandle.Free() function call, after which the original binary is fully unpacked in memory:

anti-debugging functionality

At this point, save the original binary in the Modules tab. By default, ConfuserEx names it “koi”:

the original binary

Note: If the gchandle.Free() call is not found, the obfuscated binary may not be using compression. In that case, skip the unpacking step.

Because the “koi” module is executed from memory by a wrapper module, it does not have an entry point. To find the entry point’s MDToken, continue execution until you reach the instruction just before a variable called methodBase is assigned:

the entry point’s MDToken

Click on the function return value of which is assigned to methodBase:a proxy for the ResolveMethod
This function is a proxy for the ResolveMethod call in mscorlib. Place a breakpoint inside ResolveMethod and continue execution:

a breakpoint inside ResolveMethod

Examine the local variables where you should see the open MDToken. In this case the value is 0x06000040:

anti-tamper protection

To remove anti-tamper protection in the original module, open the dumped “koi” module in dnSpy. Then:

1. Place a breakpoint after the first function call in the <Module> class .cctor.

the Modules tab

2. Start execution of the koi module and wait for the breakpoint to hit.

3. Save the koi module again in the Modules tab:

locate the original entry point

Now you can locate the original entry point using the previously discovered MDToken (dnSpy highlights it before the target function):

the koi module

Right-click on the koi module in the left pane and select Edit Module…:

the original entry point function

Select the original entry point function:

preserved binary metadata

Click OK. Now after patching the entry point, save the module via the File -> Save Module… tab with previously preserved binary metadata in the MD Writer Options tab:

protect .NET applications

After saving, the binary should work as intended:

analyze third-party obfuscated assemblies

Optionally, you can replace the first function call in the .cctor class with NOPs:

  • Right-click on the function call and select Edit IL Instructions…
  • Right-click on the highlighted instruction, choose Replace with NOPs, then save the patched module under File -> Save Module….

set up ConfuserEx2

obfuscation techniques

Note: If the obfuscated binary does not use compression, there is no need to patch the entry point. NOP-patching the first function call alone is mandatory.

String Decryption 

ConfuserEx2 uses functions inside the root <Module> class to decrypt originally used strings in the binary. The encrypted strings can be easily identified by the following instruction pattern:

deobfuscation and unpacking process

Which in C# decompiled view looks like the following:

restore .NET assemblies

To automate the decryption process, .NET Reflection can be used to call these decryption functions. However, ConfuserEx2 makes external calls more difficult by verifying the execution context:

A Python script for automating ConfuserEx2’

These checks can be bypassed by replacing the verification instructions with NOPs. Once that’s done, the decryption functions become accessible for external calling in order to patch the binary to load the original, unencrypted strings. A Python automation script implementing this logic is available in the following repository:

https://github.com/iterasec/ConfuserEx2_Python_String_Decrypt

After running the script with:

python .\decryptor.py -l "dnlib.dll path" -f "target binary with no anti-tamper path" -v

you’ll see the decrypted strings in the verbose output, and a new patched binary with decrypted strings will be written to disk:

Cflow Deobfuscation

Cflow Deobfuscation and Proxy Functions Removal

Generally, by this stage, the overall functionality and data flow are clearer. However, due to control flow obfuscation and the use of proxy functions, there may still be significant junk code. To deobfuscate control flow, use this de4dot fork [https://github.com/ViRb3/de4dot-cex] with the following command:

.\de4dot-x64.exe "target binary with decrypted strings path" -p crx

The resulting binary, stripped of cflow obfuscation, looks like the following:

cflow obfuscation

As you can see, proxy functions are still used in place of direct calls to the original methods. To handle this, you can run the ProxyCall-Remover tool [https://github.com/Kaidoz/ProxyCall-Remover]. After doing so, the original functionality appears in near one-to-one relation with the unobfuscated code:

deobfuscating more complex binaries

Occasionally, deobfuscating more complex binaries with both de4dot and ProxyCall-Remover can break functionality. In such cases, additional manual steps or partial automation may be needed. However, for this test binary, the operations described here successfully restored normal function while keeping the code clear and readable. 

Summary 

By following the steps outlined — removing anti-tamper protection, decrypting strings, cleaning up control flow, and eliminating proxy functions — you can thoroughly examine and understand .NET binaries obfuscated with ConfuserEx2. This process provides valuable insight into how modern .NET obfuscation works and how to systematically reverse it. 

If you have further questions about .NET security, reverse engineering, or any other cybersecurity concerns, feel free to reach out to our team at Iterasec. We’re here to help you strengthen your defenses and navigate the ever-evolving cybersecurity landscape.

Contact us

Please tell us what are you looking for and we will happily support you in that.

Fell free to use our contact form or contact us directly.