secskills
secskills / core / analyzing-dotnet-assemblies

analyzing-dotnet-assemblies

core verified 2026-07-27

Reverse engineer .NET assemblies and executables with dnSpyEx, ILSpy, and de4dot — identifying and unwrapping obfuscators and packers, deobfuscating control flow and string encryption, handling single-file and NativeAOT publishes, and patching IL. Use when a binary is a managed PE, when ILSpy shows mangled names or empty method bodies, or when analyzing .NET malware, a loader, or a Windows application.

$ /plugin install secskills-core

.NET compiles to IL with full metadata, so an unobfuscated assembly decompiles to near-original C#. That makes the interesting question not "how do I read this" but "what is in the way" — a packer, an obfuscator, a runtime loader, or a publish mode that hides the managed code inside a native host.

When to Use

When NOT to Use

extraction, then come back here for the DLLs

Confirm It Is Managed

# Linux
file target.exe                                 # "Mono/.Net assembly"
rg -a -o 'BSJB|mscoree\.dll|System\.Private\.CoreLib' target.exe | head

# Windows
dotnet-tool: ildasm /text target.exe | head
# CLR header presence in the optional header data directory 14 is the real test
What you findPublish modeApproach
Managed PE, references mscorlib/System.Private.CoreLibFramework-dependentDecompile directly
Native PE containing an embedded managed bundleSingle-file publishExtract the bundle first
Native PE, no IL metadata at allNativeAOTNot .NET RE — use analyzing-binaries
.dll with no entry pointLibraryDecompile; find callers elsewhere
# Single-file publish: the managed assemblies are appended as a bundle
# Extract with a bundle extractor, or carve on the manifest header
ilspycmd --list-bundle target.exe 2>/dev/null || binwalk -Me target.exe

Decompile

ilspycmd target.dll -o ./decompiled -p       # -p writes a project structure
# GUI: dnSpyEx (maintained fork), ILSpy, dotPeek
# dnSpyEx also debugs and edits assemblies, which is why it is the default choice

rg -n 'https?://|ConnectionString|ApiKey|password|Convert\.FromBase64String' ./decompiled | head -40

Read in this order: the entry point, then any type named for the product, then anything referencing networking, crypto, or process APIs. Skip generated types (<>c__DisplayClass, <Module>) unless following a specific closure.

Identify the Obfuscator Before Fighting It

Deobfuscation is cheap when you name the tool and expensive when you do not.

detect-it-easy target.exe          # or DIE's CLI: diec
# Look for: ConfuserEx, .NET Reactor, SmartAssembly, Eazfuscator, Babel,
#           Dotfuscator, Agile.NET, Obfuscar
rg -a -o 'ConfuserEx|Reactor|SmartAssembly|Eazfuscator|Babel|DotNetPatcher' target.exe
# de4dot handles the classic obfuscators automatically
de4dot target.exe -o cleaned.exe
de4dot -p cr target.exe            # force a specific profile when detection fails
# ConfuserEx specifically: use a maintained unpacker fork, since de4dot's
# built-in support predates later ConfuserEx versions

What each obfuscation layer looks like, and how to handle it:

LayerAppearanceHandling
Name manglingClass1.method_3, unicode/invisible namesCosmetic — rename as you read; de4dot restores some
String encryption<Module>.Decrypt(12345) everywhereRun the decryptor: de4dot, or invoke the method dynamically
Control flow flatteningGiant switch on a state variablede4dot's CFG cleanup, or read dynamically
Proxy/delegate callsEvery call goes through a helperde4dot devirtualization
Anti-tamper / anti-debugFails under dnSpy debuggerPatch the check, or dump after decryption
Virtualization (Agile.NET, KoiVM)No recognizable IL at allDevirtualizer (OldRod for KoiVM) or dynamic analysis
Native stub wrapping ILManaged code decrypted at runtimeDump from memory — the reliable answer

Dumping from memory is the general escape hatch. Whatever the packer, the CLR must eventually hold real IL to execute it. Let it load, then dump:

# MegaDumper / ExtremeDumper / pe-sieve — dump loaded managed modules from a
# running process, then decompile the dump
pe-sieve /pid <pid> /dmode 3

This is the same "let it unpack itself" pattern as packed native malware; see analyzing-malware for containment before running anything hostile.

Debugging and Patching

# dnSpyEx: set breakpoints in decompiled C#, inspect locals, edit method bodies,
# and save the modified assembly. This is unusually powerful — you can change a
# license check or a validation result and immediately re-run.

Patching workflow: edit the IL or the decompiled C# in dnSpyEx, save the assembly, and re-run. Strong-name signatures break on save — either remove the strong name requirement, or if the app verifies its own hash, hook the check. For a signed assembly the app itself verifies, patching is usually the wrong tool; hook at runtime instead.

.NET Malware Notes

.NET is heavily used for loaders and commodity RATs, and the artifacts are distinctive:

decryption routine, run it offline, and analyze the resulting assembly. This is the single most common .NET malware pattern.

patterns or the GetProcAddress calls that precede them.

VirtualAlloc, CreateRemoteThread, SetWindowsHookEx.

enumerate the resource section.

rg -n 'DllImport|Assembly\.Load|CreateInstance|FromBase64String|RijndaelManaged|AmsiScanBuffer' ./decompiled | head -30
ilspycmd --list-resources target.dll

Extracted second stages go back through this skill; capability and IOC output goes to analyzing-malware, detection content to engineering-detections.

Rationalizations to Reject

weakest layer. The structure, strings, and P/Invokes are intact.

use its specific unpacker, or dump from memory.

for native stubs wrapping managed code.

Find the Assembly.Load and recover what it loads.

it in dnSpyEx and watch the real path.

References