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:
- ConfuserEx2 (v1.6.0)
Precompiled version available here:
https://github.com/mkaring/ConfuserEx - dnlib.dll
Built from source using the .NET Framework 4.5:
https://github.com/0xd4d/dnlib - Python String Decryptor
A Python script for automating ConfuserEx2’s string decryption logic:
https://github.com/iterasec/ConfuserEx2_Python_String_Decrypt - de4dot-cex
A fork of de4dot for ConfuserEx2’s control flow deobfuscation and function renaming:
https://github.com/ViRb3/de4dot-cex - ProxyCall-Remover
Tool for removing proxy object references:
https://github.com/Kaidoz/ProxyCall-Remover - dnSpy
A .NET decompiler for analyzing and debugging .NET assemblies:
https://github.com/dnSpyEx/dnSpy
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.
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.
4. Protect! Tab
- Click “Protect!” to start the obfuscation process.
- Verify that the obfuscation completed successfully, ensuring there are no errors in the log.
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.
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:
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:
At this point, save the original binary in the Modules tab. By default, ConfuserEx names it “koi”:
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:
Click on the function return value of which is assigned to methodBase:
This function is a proxy for the ResolveMethod call in mscorlib. Place a breakpoint inside ResolveMethod and continue execution:
Examine the local variables where you should see the open MDToken. In this case the value is 0x06000040:
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.
2. Start execution of the koi module and wait for the breakpoint to hit.
3. Save the koi module again in the Modules tab:
Now you can locate the original entry point using the previously discovered MDToken (dnSpy highlights it before the target function):
Right-click on the koi module in the left pane and select Edit Module…:
Select the original entry point function:
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:
After saving, the binary should work as intended:
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….
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:
Which in C# decompiled view looks like the following:
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:
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 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:
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:
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.