When to use / When not to use
- Use: after confirming it is a .NET assembly (PE CLI header /
fileoutputs "Mono/.Net assembly"), restore CIL to readable C#; handle .NET obfuscation such as ConfuserEx/SmartAssembly; locate registration code/network/decryption logic - Use: static restoration of malicious .NET samples ([[re-malware]] → [[re-managed]] path)
- Do not use: unmanaged PE (native programs go directly to [[re-binary-core]]; the native part of mixed-mode programs is likewise routed out)
- Do not use: Java ([[re-java]]), scripts ([[re-script-deob]])
- Note: dynamic steps default to sandbox ([[re-analyze/platform-tips]] highest principle); first
file/preliminary survey to confirm before acting
Tool preparation
Refer to [[re-analyze/platform-tips]]—decompilation/deobfuscation are static steps, sandbox-free; dynamic verification goes into the sandbox per the highest principle.
dotnet SDK (prerequisite for running .NET tools)
- Debian/Ubuntu:
apt install dotnet-sdk-8.0(official Microsoft source) - Fedora:
dnf install dotnet-sdk-8.0;Arch:pacman -S dotnet-sdk - macOS:
brew install --cask dotnet-sdk - Windows:
choco install dotnet-sdkor the official installer - Verify:
dotnet --version
ILSpy (cross-platform, GUI + CLI)
- CLI(dotnet tool):
dotnet tool install -g ilspycmd - GUI: GitHub
icsharpcode/ILSpyrelease zip (linux-x64 / win-x64 / osx-x64 all available) - Verify:
ilspycmd --help; the GUI can open assemblies
dnSpy (Windows-only, GitHub release)
- GitHub
dnSpyEx/dnSpyrelease zip → extract and rundnSpy.exe - Windows only (WinForms + .NET Framework); for Linux/WSL use ILSpy (or run dnSpy with Wine)
- Verify: GUI launches and can
File > Openassemblies - Advantage: decompile + edit + debug all in one
de4dot (GitHub release, deobfuscation)
- GitHub
de4dot/de4dotreleasede4dot.exe; Linux/macOS requires mono:apt install mono-devel/dnf install mono-devel;Arch:sudo pacman -S mono;macOSbrew install mono- Only switch to the AUR's
mono-gitwhen an upstream Git snapshot is explicitly needed (it is AUR, not an official repository) - Run:
mono de4dot.exe --help; on Windows directlyde4dot.exe --help
- Verify:
--helpoutputs normally
Single-file bundle extraction (sfextract / built into ILSpy, optional but commonly used)
- Zero extra tools: ILSpy 7.1+ GUI directly
File > Opensingle-file bundles, 7.2+ right-clickExtract package entryto export embedded assemblies - CLI:
dotnet tool install -g sfextract(Droppers/SingleFileExtractor, MIT; note thatdotnet-bundle-extractor/dotnet-bundle-extractthese two package names do not exist on NuGet) - Usage:
sfextract sample.exe -o extracted/(without-oonly lists files in the bundle) - Verify:
sfextract --help
Operating steps
Execute in order, recording evidence at each step (path + sha256, see [[re-triage]]).
-
Identify the .NET assembly:
file sample.exe # 含 "Mono/.Net assembly" 即 .NET strings sample.exe | grep -i mscoree # mscoree.dll 导入是经典标识CLI header confirmation (PE data directory entry 15 = COM Descriptor):
import pefile pe = pefile.PE('sample.exe') dd = pe.OPTIONAL_HEADER.DATA_DIRECTORY[14] # COM Descriptor (CLI header) print(hex(dd.VirtualAddress), hex(dd.Size)) # 非 0 → 托管程序集Note the mixed-mode assemblies (visible to both native + managed) and self-contained single-file (see Pitfall 4). CLI header structure quick reference (tested on .NET 10 output): cb=0x48(72), MajorRuntimeVersion=2, MinorRuntimeVersion=5, MetaData RVA, EntryPointToken (4-byte union, ECMA-335 II.25.3.3: when COMIMAGE_FLAGS_NATIVE_ENTRYPOINT is not set it is a metadata token (MethodDef/File), only when set is it a native code RVA; 0 = no entry point, i.e. a DLL), CorFlags (1=ILONLY, 2=32BITREQUIRED, 0x8=strong name signed (COMIMAGE_FLAGS_STRONGNAMESIGNED), 0x20000=32BITPREFERRED). Metadata root (at the MetaData RVA): signature
BSJB(0x424A5342) + version 1.1 + reserved + version string length + version string ("v4.0.30319" is the typical default of Roslyn/MetadataRootBuilder,not a format invariant—ECMA-335 allows vendor-defined strings, so go by the actual bytes) + flags(2) + stream count(2) + stream headers (offset + size + name, 4-byte aligned). The stream name is the content form:#~(compressed tables, Roslyn default)/#-(uncompressed tables),#Strings(metadata string pool),#US(user strings: C# literals in the code),#GUID、#Blob(signature/constant blob)—the plaintext of a string-encryption sample is either not in#USor is assembled at runtime;#~table contents are the type/method tree in dnSpy/ILSpy. -
dnSpy/ILSpy decompilation browsing:
- ilspycmd single file:
ilspycmd sample.exe(outputs decompiled code to stdout) - ilspycmd generate project:
ilspycmd sample.exe -p -o decompiled/ - ILSpy GUI / dnSpy:
File > Open→ expand the assembly in the left tree → double-click a class/method to see the C# view - dnSpy extra capability: right-click method
Edit Method(after editingFile > Save Modulerecompile), breakpoint debugging
- ilspycmd single file:
-
Locate key logic via strings/APIs:
strings sample.exe | grep -iE 'http|https|api|key|secret|register' strings -e l sample.exe | grep -iE 'http|key|secret' # UTF-16LE:#US 池里 C# 字面量是 UTF-16 grep -rn 'HttpClient\|WebRequest\|Regex\|Convert' decompiled/ # ilspycmd 产物- Search within the GUI: ILSpy/dnSpy
Edit > Find(Ctrl+Shift+F), enter the target string - Find references: dnSpy right-click method/field →
Analyze→Used By(xref to locate call sites) - Target forms: registration code validation (
Maininif (key == ...)), network URLs, decryption keys
- Search within the GUI: ILSpy/dnSpy
-
de4dot deobfuscation (ConfuserEx / SmartAssembly, etc.):
mono de4dot.exe sample.exe # 自动检测混淆器 → 输出 sample-cleaned.exe mono de4dot.exe -p ce sample.exe # 显式指定 ConfuserEx (ce) / SmartAssembly (sa)- First
-ddry run to only see detection:mono de4dot.exe -d sample.exe(the log showsDetected: ConfuserEx) - After deobfuscation, reopen in dnSpy/ILSpy
sample-cleaned.exe: strings should be plaintext, control flow restored - Residual obfuscation (strings still encrypted) → Pitfall 2 manual approach
- First
-
Dynamic side (optional; execute in a sandbox [[re-sandbox]]):
- Frida (native layer):
pip install frida-tools;frida sample.exe -l hook.js, useInterceptorto hook low-level native calls (such as the socket read/write behindHttpClientsocket read/write/SSL_read, the native library where the decryption function resides) to dump plaintext/URLs at runtime—stock frida-tools cannot directly hook .NET managed methods by method name; the managed layer requires a CLR bridge (such as MonarchSolutions/frida-clr) or go through the dotnet-dump / dnSpy debugging below - dotnet-dump (retrieve runtime data in-process):
dotnet tool install -g dotnet-dump dotnet-dump collect -p <pid> # 收集 dump dotnet-dump analyze <dump> # clrstack / dumpheap -type System.String - Windows: dnSpy
Debug > Startbreakpoint debugging (break directly on a method to inspect variables)
- Frida (native layer):
Cross-domain federation
- [[re-managed]]: gateway workflow steps ② (decompilation) and ④ (deobfuscation) always invoke this skill
- [[re-malware]]: .NET malicious sample path (re-malware → re-managed → this skill's static recovery)
- [[re-binary-core]]: foundation — initial survey ([[re-triage]]), mixed-mode native parts
- Dynamic side: [[re-frida]], [[re-sandbox]]; for deep obfuscation turn to [[re-deobfuscate]]; for single-file, unpack first then return to this skill
Common pitfalls and traps
- ConfuserEx string encryption + control flow: symptom — decompilation shows only
smethod_0("密文")calls, if/while turned into switch dispatch; cause — ConfuserEx enables both string encryption and control flow obfuscation by default; countermeasure —de4dot -p ceautomatic recovery; when remnants remain, read plaintext dynamically at the return of the decryption method (inside a sandbox), or recover the decryption loop and use a script to decrypt in bulk - de4dot fails against older versions/newer obfuscators: symptom — log
Unknown obfuscatoror the output is still obfuscated; cause — de4dot maintenance has stalled, and newer obfuscator signatures are not included; countermeasure — switch to a community fork (de4dot-cex / de4dot-modified), or do it manually: locate the string decryption function → analyze the algorithm → replicate in python3 + verify dynamically - dnSpy is Windows-only: symptom — on Linux/WSL, double-clicking dnSpy.exe errors out and it will not run; cause — a WinForms + .NET Framework GUI tool; countermeasure — on Linux/macOS use ILSpy (GUI or
ilspycmd), and if you insist on dnSpy you can run it under Wine - .NET self-contained publish (single-file): symptom —
fileshows an ordinary PE, tools open it and find no managed structures/decompilation is empty; cause — self-contained single-file bundles host + runtime + IL into one native executable; countermeasure — unpack first:sfextract sample.exe -o extracted/(or open/export directly with ILSpy), then decompile the extracted assemblies - Mixed-mode assemblies: symptom — decompilation shows only a few managed classes and the main logic cannot be found; cause — C++/CLI or a wrapper mixes native code with managed; countermeasure — handle the managed side per this skill, and turn the native side over to [[re-binary-core]] ([[re-ghidra]] / [[re-imports]])
- Thin-shell launcher assembly misjudged as export failure (headless batch dnSpy): symptom — a single assembly's decompiled output has very little source (a few lines to a few dozen lines), the main logic cannot be found, and you mistakenly think the export failed; cause — that assembly is just a thin-shell launcher, and the main logic is in accompanying assemblies/libraries (purely managed, unlike the native side in the previous item); countermeasure — do a directory-level batch decompile to get the overall view before judging: on Windows use the dnSpy console (
dotnet dnSpy.Console.dll) to output a solution + one .csproj per assembly + source tree, on Linux the counterpart isilspycmd -p; when batching, explicitly pass the dependency search path (pointing to the directory containing the target), otherwise references to accompanying libraries will not resolve fully; confirm success via the dual channels of "non-zero artifact count + exit code 0" - ReadyToRun (R2R) assemblies: symptom —
filereportsUnknown processor 0xfd1d(PE machine 0xfd1d is the R2R marker), decompilation is normal but when dynamic-side behavior is inconsistent with the IL, suspect a precompiled body; cause — R2R publish precompiles native method bodies outside the metadata (for fast startup), while the IL and CLI header still remain (measured .NET 10 R2R output has DD[14] and complete metadata), and what actually executes is the native body; countermeasure — detection:file's 0xfd1d + within the binaryRTR\0(READYTORUN header magic number, measured in the .text section); Analysis: on the IL side, decompile as usual since the logic is consistent; for performance/environment-related differences, look at the native method body (convert to [[re-binary-core]]) - NativeAOT has no managed structure: Symptom —
filedoes not show Mono/.Net assembly, no CLI header, but the source declares it is a .NET program; Cause — NativeAOT compiles C# directly into a native executable (no CLR, no metadata, no CIL); Countermeasure — analyze as native ([[re-format-pe]]/[[re-format-elf]] + [[re-ghidra]]/[[re-imports]]); Characteristics: statically/dynamically linked native binary + no mscoree import, business strings can still bestrings -e lextracted (.NET strings are UTF-16) - .NET strings are UTF-16LE, default strings cannot catch them: Symptom —
strings sample.exe | grep 'http'has no results but the decompilation contains plaintext URLs; Cause — C# literals are stored in the#USpool as UTF-16LE; Countermeasure —strings -e l(or-el) then search; on the dynamic side, [[re-frida]] hooking the string construction point can also retrieve plaintext (Source: LazyReverse (a0yami), MIT)