A ConfuserEx2 deobfuscator with support for anti tamper, compressor, constants, control flow, and resource recovery.
Enhanced fork of UnconfuserEx with improved support for modern ConfuserEx2 variants, anti-tamper recovery, compressor removal, control-flow restoration, and resource reconstruction.
https://github.com/user-attachments/assets/de2c7fd9-6736-4f39-83c0-3c25aa9c1f24
If u ever played with malware samples, some of them are obfuscated with ConfuserEx, and ive tried some public deobfuscators against the latest but it just wouldnt work, so ive decided to fork a public one which did work against that latest ver and just mod it to my needs.
This repo is a fork of MadMin3r/UnconfuserEx. Credit goes to him as well. The original project did the hard part of making a focused ConfuserEx2 deobfuscator that could actually remove real protections.
That is what this project does. Runs a list of removers in a fixed order, rewrites method bodies/resources/metadata where it can, and writes a new assembly back out. The important part is the order. Compressor and anti tamper have to happen early because the rest of the module might not even be real IL yet.
The upstream version was already useful, but a few cases kept showing up.
One was the LZMA path. Some samples hand you bytes that look like the constants/resources payload, but the LZMA properties are nonsense. If you pass that straight into the decoder, you get stupid dictionary sizes and eventually exceptions like array dimensions exceeding the supported range. So this version checks the properties, caps the dictionary size, caps the uncompressed size, and bails before the decoder allocates something ridiculous.
LZMA properties => CE FD 62 5F 9F
Invalid LZMA properties byte 0xCE or unreasonable dictionary size
Constants had another dumb but real problem. A lot of the resolver code wants the ID sitting in front of the getter call to be an ldc.i4. Sometimes it is not one instruction anymore. It is a tiny arithmetic expression.
ldc.i4 0x1234
ldc.i4 0x55
xor
call string <const getter>(int32)
The original fork sees xor, calls GetLdcI4Value(), and dies because xor is obviously not an integer load. This version walks back over the small arithmetic sequence, emulates the stack, collapses it back into one ldc.i4, and then lets the normal resolver keep going.
So instead of treating this as a totally different constants protection, it becomes this:
ldc.i4 0x1261
call string <const getter>(int32)
Then the existing normal/x86 constant resolver path can do its job.
Control flow is where it was meh
The switch remover can handle the normal ConfuserEx switch dispatcher shape. It walks blocks, recovers the next target, deletes dead blocks, and emits a sane method body. But there are samples where only part of the method is understood. If you mutate half a method and then discover it is still obfuscated, the output is worse than useless because now you have broken IL and no clean way to reason about what happened.
So this version snapshots the method body before touching it:
instructions
exception handlers
If deobfuscation throws, or if the method still looks obfuscated afterwards, the original body gets restored. The log can still say "this method was not solved", but the assembly does not get silently corrupted just because one method had a weird dispatcher.
Jump/trampoline control flow got its own pass too. Some methods are not just switch dispatchers. They are small branch trampolines chained together until the real block is reached. Those now get detected and folded instead of being ignored by the switch only path.
The compressor remover is the part that has to run before everyone else.
ConfuserEx compressor stubs usually keep the real assembly compressed, boot a tiny loader, decompress the payload, and load it at runtime.
The remover finds the loader shape, extracts the embedded payload, decompresses it, and swaps the module over to the real assembly. Normal and compact compressor layouts are both handled.
[+] Compressor detected
[+] Extracted compressed module payload
[+] Decompressed real module
[+] Continuing pipeline on unpacked assembly
Anti-tamper has two paths now.
Normal/dynamic anti tamper decrypts method bodies from protected sections and writes the restored bodies back into the module. JIT anti tamper is more annoying because the bodies are meant to be materialised when the runtime asks for them.
The rough shape is:
find init
extract keys
find encrypted JIT body section
derive per method key
read body
write CilBody back
This is still pattern based. If the stub changed enough, it will miss, obv.
Resources are handled similarly to constants: find the encrypted resource blob, recover the key/decryptor shape, decrypt, decompress if needed, then put the resources back where normal .NET tooling expects them.
There is also an optional embedded PE rebuild path. Some protected samples carry a managed PE inside a resource. With rebuild enabled, the remover tries to parse and rewrite that payload too instead of leaving a deobfuscated outer assembly with an untouched inner one.
UnConfuserEx.exe sample.exe sample.clean.exe --rebuild-embedded-pe
Use that when you know the sample hides another managed assembly inside resources. If the payload is not a managed PE, the rebuild path should leave it alone.
Build it:
dotnet build .\UnConfuserEx.sln -c Release
Run it:
.\UnConfuserEx\bin\Release\net9.0\UnConfuserEx.exe .\protected.exe
Or give it an explicit output path:
.\UnConfuserEx\bin\Release\net9.0\UnConfuserEx.exe .\protected.exe .\protected.clean.exe
If you do not give an output path, it writes one next to the input with -deobfuscated added to the name.
For embedded managed payloads:
.\UnConfuserEx\bin\Release\net9.0\UnConfuserEx.exe .\protected.exe .\protected.clean.exe --rebuild-embedded-pe
This is the current support list. It does not mean every possible ConfuserEx fork works. It means these are the shapes the pipeline knows how to look for.
There is probably more hidden in the code that i forgot to list xD.
The useful logs are the ones that tell you which stage failed, not just that the output did not run.