SuperAPI AI Directory

re-dotnet

General & other

View repository

Updated 10-15 21:35

About this skill

.NET CIL reverse engineering (dnSpy / ILSpy / de4dot)

When to use / When not to use

  • Use: after confirming it is a .NET assembly (PE CLI header / file outputs "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-sdk or the official installer
  • Verify: dotnet --version

ILSpy (cross-platform, GUI + CLI)

  • CLI(dotnet tool): dotnet tool install -g ilspycmd
  • GUI: GitHub icsharpcode/ILSpy release zip (linux-x64 / win-x64 / osx-x64 all available)
  • Verify: ilspycmd --help; the GUI can open assemblies

dnSpy (Windows-only, GitHub release)

  • GitHub dnSpyEx/dnSpy release zip → extract and run dnSpy.exe
  • Windows only (WinForms + .NET Framework); for Linux/WSL use ILSpy (or run dnSpy with Wine)
  • Verify: GUI launches and can File > Open assemblies
  • Advantage: decompile + edit + debug all in one

de4dot (GitHub release, deobfuscation)

  • GitHub de4dot/de4dot release de4dot.exe; Linux/macOS requires mono:
    • apt install mono-devel / dnf install mono-devel;Arch: sudo pacman -S mono;macOS brew 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 directly de4dot.exe --help
  • Verify: --help outputs normally

Single-file bundle extraction (sfextract / built into ILSpy, optional but commonly used)

  • Zero extra tools: ILSpy 7.1+ GUI directly File > Open single-file bundles, 7.2+ right-click Extract package entry to export embedded assemblies
  • CLI: dotnet tool install -g sfextract(Droppers/SingleFileExtractor, MIT; note that dotnet-bundle-extractor/dotnet-bundle-extract these two package names do not exist on NuGet)
  • Usage: sfextract sample.exe -o extracted/(without -o only lists files in the bundle)
  • Verify: sfextract --help

Operating steps

Execute in order, recording evidence at each step (path + sha256, see [[re-triage]]).

  1. 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 #US or is assembled at runtime;#~ table contents are the type/method tree in dnSpy/ILSpy.

  2. 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 editing File > Save Module recompile), breakpoint debugging
  3. 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 (Main in if (key == ...)), network URLs, decryption keys
  4. 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 -d dry run to only see detection: mono de4dot.exe -d sample.exe(the log shows Detected: 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
  5. Dynamic side (optional; execute in a sandbox [[re-sandbox]]):

    • Frida (native layer): pip install frida-tools;frida sample.exe -l hook.js, use Interceptor to hook low-level native calls (such as the socket read/write behind HttpClient socket 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 > Start breakpoint debugging (break directly on a method to inspect variables)

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 ce automatic 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 obfuscator or 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 —file shows 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 is ilspycmd -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 —file reports Unknown 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 binary RTR\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 —file does 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 be strings -e l extracted (.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 #US pool 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)