An introduction to reverse engineering .NET AOT applications

The article explains methods for recognizing and reverse-engineering .NET Ahead-of-Time (AOT) compiled binaries, including heuristics to detect them and a workflow to recover library symbols and type information using IDA Pro, FLIRT signatures, and a debugger. It demonstrates building a multi-function AOT test binary, creating FLIRT signatures (idb2pat -> sigmake -> .sig), and using runtime hooks to recover type names during debugging. #DuckTail #DotNetAOT

Keypoints

  • .NET AOT removes MSIL and compiles to native code, making reverse-engineering start from assembly instead of high-level IL.
  • Distinctive AOT PE traits: a single export named DotNetRuntimeDebugHeader and a section called .managed; .NET runtime version string can be located by regex to identify the compile commit.
  • To produce AOT test samples: Visual Studio 2022 + “Desktop development with C++” component and .NET SDK (>=7); enable native AOT publish or set <PublishAot>true</PublishAot> in .csproj, publish to folder, choose target CPU.
  • IDA observations: AOT binaries resemble C++ code, use standard x64 calling conv (RCX,RDX,R8,R9), lack prebuilt runtime signatures, and contain strings in an uninitialized “hydrated” section requiring runtime inspection.
  • Signature workflow: compile a large AOT binary with many library functions (to avoid trimming), extract patterns with idb2pat.py, convert .pat to .sig with sigmake.exe, resolve collisions in the .exc file, then load the .sig in IDA to recover thousands of library function names.
  • Recover type information by debugging: intercept S_P_CoreLib_System_Type__GetTypeFromEETypePtr or Object__GetType with ECX pointing to a MethodTable, then follow the return through NativeFormatRuntimeNamedTypeInfo__ToString to obtain type names.

MITRE Techniques

  • [T1136] Create Account – Used when “a new account was being created on the analysis machine” [‘a new account was being created on the analysis machine’]
  • [T1021.001] Remote Services: Remote Desktop Protocol – Observed as “followed by an RDP connection from an operator” [‘followed by an RDP connection from an operator’]
  • [T1105] Ingress Tool Transfer – Operator activity included downloading additional tools: “operator who downloaded additional tools” [‘operator who downloaded additional tools’]
  • [T1539] Steal Web Session Cookie – Data theft included cookie theft: “stole cookies” [‘stole cookies’]

Indicators of Compromise

  • [File hash] BotGetKeyChromium / ResetMainBot samples – 9ba3b2ce74d60e09…0f63f, 1e082ed9733b033a…0a263f, and 6 more hashes

A practical workflow for analyzing .NET AOT binaries starts with automated detection and sample preparation: look for PE files with a single export named DotNetRuntimeDebugHeader and a .managed section, and search for the embedded .NET version string matching the format like “8.0.0+5535e31a7123…”. To build reproducible test cases, use Visual Studio 2022 with the “Desktop development with C++” workload and a .NET SDK (>=7), enable native AOT publishing (or add <PublishAot>true</PublishAot>), publish self-contained to a folder for the desired CPU target, and produce an AOT binary + PDB to obtain symbols. Note that AOT executables are large and include many library functions; string data may be placed in a “hydrated” section that appears uninitialized until runtime, so runtime inspection is required for string recovery.

To recover library symbols for IDA, compile a deliberately exhaustive AOT test binary that references many runtime functions (to prevent trimming), extract patterns from the IDA database using idb2pat.py (FLARE), and convert the .pat file to an IDA .sig with sigmake.exe. Resolve signature collisions by editing the generated .exc file to select the correct entries, rerun sigmake, then place the final .sig into $IDAUSR/sig/pc and load it via File → Load File → FLIRT Signature File; this typically yields thousands of recovered function names and dramatically improves navigation and decompilation clarity.

With runtime functions identified, use a debugger to recover types and higher-level semantics: intercept calls to S_P_CoreLib_System_Type__GetTypeFromEETypePtr (or Object__GetType), set ECX to the MethodTable pointer of interest, and wait for NativeFormatRuntimeNamedTypeInfo__ToString to return a heap pointer containing the readable type name. Also inspect calls to allocator functions (e.g., RhpNewFast) to map MethodTable/EETYPE headers and vtable-like function pointers back to managed types. This combination of FLIRT signatures + targeted debugging lets you reconstruct much of the original program logic despite the absence of MSIL in AOT builds.

Read more: https://harfanglab.io/en/insidethelab/reverse-engineering-ida-pro-aot-net/