Deobfuscation & Technical Analysis of ConfuserEx 2

Deobfuscation & Technical Analysis of ConfuserEx 2

Introduction


ConfuserEx is still one of the most used .NET Obfuscator today. Since the original ConfuserEx has been discontinued, many different versions have been released, and we can encounter different techniques. In this article, we will analyze a version called ConfuserEx 2.

Technical Analysis of ConfuserEx2


I downloaded the compiled version of the project from its repo, set ConfuserEx to maximum, and created the test obfuscator application.


Packer also activated.

First Glance

using System;
using System.Runtime.CompilerServices;
​
[module: SuppressIldasm]
[module: ConfusedBy("Confuser.Core 1.6.0+447341964f")]

After dragging and dropping the target application to dnSpy, click on the module section and see the added Attribute value.

Since a normal application will contain an EntryPoint, I go directly to the EntryPoint.


Here we can see that the function structure is broken due to Anti-Tamper protection. Anti-Tamper is a protection that encrypts and decrypts code structures at runtime to protect application integrity.

In order for the application to run correctly, the first thing it will do in the code executed before EntryPoint (Module Constructor .cctor) will be to decrypt the code structures.

Of course, the next step after decrypting the code structures is to unpack and run the actual application at runtime.

Removing Anti-Tamper & Unpacking


I will go to the point where the code will run, namely .cctor, and put a breakpoint on the second line. This will decrypt the encrypted function structures, and I will dump the clean file.

Since we need the unpacked version of the application, after removing the anti-tamper, we will use the “Open Module from Memory” button in dnSpy to dump the module named “koi” by putting a breakpoint right after the unpack process.

After putting the breakpoint on the second line, we start debugging with the Debug button (or F5 key). After the breakpoint is triggered, we open the file from the Modules tab with the “Open Module from Memory” option and go to the entry point.

Immediately after unpacking, we go to the entry point of the file opened on the left side to get the dump and breakpoint the gchandle.Free() line and continue.

Now we can dump the incoming koi file from the Modules tab.

When you try to go to the entry point of the file after getting the dump of the file, you will see that there is no such point.

The function that contains the STAThread attribute and is in the `internal static class` is the entry point, and we need to edit the entry point of the file accordingly.

Let’s save it like this, and now it works!

Renaming, CFlow Deobfuscating


First of all, I should say that this project is not much different from the original ConfuserEx project. Just use de4dot-cex (for rename) and ConfuserEx Unpacker 2 (proxy ref cleaning and cflow deobfuscating).

ConfuserEx Unpacker 2 & de4dot:

Writing String Fixer


First, let’s look at what the situation looks like:

if (!(this.textBox_0.Text == <Module>.smethod_6<string>(0x67B8714F)))
{
    MessageBox.Show(<Module>.smethod_5<string>(0x51C1189A)<Module>.smethod_4<string>(-0x103242FF), MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    
}
else
{
    MessageBox.Show(<Module>.smethod_5<string>(0x1753C2A8), <Module>.smethod_3<string>(0x59FCB3D1), MessageBoxButtons.OK, MessageBoxIcon.Asterisk);

}

Removing Invoke Protection


There are many functions like smethod_5 that do the same thing but have different key values inside.

Since there is no Control Flow, we can copy the function inside to our own project, but since I don’t want to deal with the calculations in cctor, I will use the Invoke method.

When you look at a function that returns a string, you will see a control at the beginning.

internal static T smethod_5<T>(int id)
{ 
    if (Assembly.GetExecutingAssembly().Equals(Assembly.GetCallingAssembly())) 
    {         
               // .......   
    }
    
}

It is a control mechanism so that Reversers cannot use Invoke. When you perform Invoke without clearing this, the function will return nothing.

Let’s simply delete the four instructions at the beginning of each function that contain this check.

Writing Fixer with dnlib and Reflection


First of all, after starting a Console project on Visual Studio, I download dnlib with NuGeT and include the necessary libraries.

using System;
using System.IO;
using System.Reflection;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using dnlib.DotNet.Writer;

When I look at the usage of the function that returns a string as IL, I see that the first OpCode value is ldc.i4 and the second value is call.

Since there is no unique ordering, we also need to use the <string> expression in the Operand value of the second line.

9	001E	ldc.i4	0x51C1189A
10	0023	call	!!0 '<Module>'::smethod_5<string>(int32)

Steps we’ll follow:


1. Loading modules using dnlib for manipulation of .NET Application

2. Load Assembly module with Reflection library to be able to use the Invoke function

3. Finding the target part (I will explain on the code)

4. Invoke the Operand part of the target expression with string id and write the value instead.


First of all, I’m writing the SaveAssembly function that I will use at the end of the process:

static void SaveAssembly(ModuleDefMD module, string ext)
{    
    var writerOptions = new NativeModuleWriterOptions(module, true);       
    writerOptions.Logger = DummyLogger.NoThrowInstance;    
    writerOptions.MetadataOptions.Flags = (MetadataFlags.PreserveTypeRefRids MetadataFlags.PreserveTypeDefRids | MetadataFlags.PreserveFieldRids | MetadataFlags.PreserveMethodRids | MetadataFlags.PreserveParamRids | MetadataFlags.PreserveMemberRefRids | MetadataFlags.PreserveStandAloneSigRids | MetadataFlags.PreserveEventRids | MetadataFlags.PreservePropertyRids | MetadataFlags.PreserveTypeSpecRids | MetadataFlags.PreserveMethodSpecRids | MetadataFlags.PreserveStringsOffsets | MetadataFlags.PreserveUSOffsets | MetadataFlags.PreserveBlobOffsets | MetadataFlags.PreserveAll | MetadataFlags.AlwaysCreateGuidHeap | MetadataFlags.PreserveExtraSignatureData | MetadataFlags.KeepOldMaxStack);       module.NativeWrite(Path.GetDirectoryName(module.Location) + @"\" + Path.GetFileNameWithoutExtension(module.Location) + ext + ".exe", writerOptions);
}

Loading Modules

public static Assembly asm;
public static ModuleDefMD module;
static void Main(string[] args)
{
 
    asm = Assembly.LoadFrom(@"path");
    module = StringFixer(ModuleDefMD.Load(@"path"));
    SaveAssembly(module, "_test");
    Console.ReadKey();
}

Let’s write the function `StringFixer()`

static ModuleDefMD StringFixer(ModuleDefMD module)
{
    int num = 0;
    
    foreach (TypeDef type in module.Types)
    {
        if (!type.HasMethods) continue;
        foreach (MethodDef method in type.Methods)
        {
            if (!method.HasBody) continue;
            if (method.Body.Instructions.Count <= 4) continue;
            for (int i = 0; i < method.Body.Instructions.Count; i++)
            {
                Module manifestModule = asm.ManifestModule;
                if (method.Body.Instructions[i].OpCode == OpCodes.Call && 
                    method.Body.Instructions[i].Operand.ToString().Contains("tring>") && 
                    method.Body.Instructions[i].Operand is MethodSpec && 
                    method.Body.Instructions[i - 1].IsLdcI4())
                {
                    MethodSpec methodSpec = method.Body.Instructions[i].Operand as MethodSpec;
                    int ldcI4Value = (int)method.Body.Instructions[i - 1].GetLdcI4Value();
                    string text = (string)manifestModule.ResolveMethod(methodSpec.MDToken.ToInt32()).Invoke(null, new object[] { ldcI4Value });
                    method.Body.Instructions[i].OpCode = OpCodes.Nop;
                    method.Body.Instructions[i - 1].OpCode = OpCodes.Ldstr;
                    method.Body.Instructions[i - 1].Operand = text;
                    num++;
                    
                }
            }
        }
    }
    Console.WriteLine(string.Format("Decrypted {0} strings", num));
    return module;
}

We access the functions in each type and perform a search from the Instruction list.

This search looks for a unique pattern to find encrypted strings.

Then we remove the encrypted part and insert the string data returned from the function.

[Full Version of Code]

Writer: Utku “rhotav” Corbaci