Skip to content

felipejfc/XamarinDumper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Xamarin AOT Dumper

A static binary analysis tool that maps managed .NET methods to their native addresses in Xamarin/MAUI AOT-compiled binaries. Supports both iOS and Android. Useful for reverse engineering Xamarin apps in disassemblers like IDA Pro, Ghidra, and Binary Ninja.

How It Works

iOS

The tool parses a Xamarin iOS .app bundle containing:

  • A Mach-O binary (ARM64, including FAT/universal binaries)
  • .dll files with .NET metadata (type/method definitions)
  • .aotdata.arm64 files (AOT compilation data)

It auto-detects the MonoAotFileInfo struct layout embedded in the binary, resolves the ARM64 BL/B call table to find native code addresses for each managed method, and cross-references them with .NET metadata from the DLL files.

Android

The tool processes an extracted APK's native library directory containing:

  • libaot-*.dll.so files (one ELF shared object per assembly with AOT code)
  • .dll files with .NET metadata (standalone or inside libassemblies.*.blob.so XABA blobs)

Each .so exports a mono_aot_file_info symbol pointing to the AOT struct. Pointer fields are resolved via R_AARCH64_RELATIVE relocations in .rela.dyn. DLLs packed in XABA blobs are automatically extracted and LZ4-decompressed.

Output

The output is a JSON file mapping every managed method to its native address, plus optional Ghidra/IDA annotation scripts and an IL2CppDumper-style dump.cs with full type definitions.

Supported AOT Versions

Covers all major Xamarin.iOS, Xamarin.Android, and .NET MAUI releases:

Version(s) Runtime Era
156–162 Mono 6.0 / 6.4 Xamarin.iOS 13.x
170–172 Mono 6.8 / 6.12 Xamarin.iOS 14.x–15.x
176 Xamarin fork Xamarin.iOS custom builds
180–185 .NET 6 / 7 / 8 .NET MAUI (iOS + Android)
186–187+ .NET 9 / 10 .NET MAUI (iOS + Android)

Unknown versions in the 150–250 range are detected heuristically via struct field interpolation.

Dependencies

  • Python 3.6+
  • dnfile — .NET PE file parser

Install dependencies:

pip3 install dnfile

Usage

iOS — Basic dump

python3 xamarin_aot_dumper.py \
    --app "/path/to/MyApp.app" \
    --out output/dump.json

iOS — Generate Ghidra and IDA scripts

python3 xamarin_aot_dumper.py \
    --app "/path/to/MyApp.app" \
    --out output/dump.json \
    --ghidra output/ghidra_annotate.py \
    --ida output/ida_annotate.py

iOS — Specify binary manually

python3 xamarin_aot_dumper.py \
    --app "/path/to/MyApp.app" \
    --binary "/path/to/binary" \
    --out output/dump.json

Android — Basic dump

Extract the APK first (e.g., unzip MyApp.apk -d extracted/), then point to the lib/arm64-v8a/ directory:

python3 xamarin_aot_dumper.py \
    --android "extracted/lib/arm64-v8a/" \
    --out output/dump.json

The tool needs libaot-*.dll.so files and matching .dll files. If standalone DLLs aren't present, it automatically extracts them from libassemblies.*.blob.so XABA blobs (LZ4-compressed).

Android — Full output with dump.cs

python3 xamarin_aot_dumper.py \
    --android "extracted/lib/arm64-v8a/" \
    --out output/dump.json \
    --dump-cs output/dump.cs \
    --ghidra output/ghidra_annotate.py

Filter specific assemblies

python3 xamarin_aot_dumper.py \
    --app "/path/to/MyApp.app" \
    --out output/dump.json \
    --assemblies Common.NetStandard,Snap.iOS.Common

Generate dump.cs (IL2CppDumper-style)

python3 xamarin_aot_dumper.py \
    --app "/path/to/MyApp.app" \
    --out output/dump.json \
    --dump-cs output/dump.cs

CLI Options

Flag Description
--app Path to iOS .app bundle directory (mutually exclusive with --android)
--android Path to directory with libaot-*.dll.so + .dll files (mutually exclusive with --app)
--out (required) Output JSON file path
--ghidra Generate a Ghidra Python script that renames functions from the JSON
--ida Generate an IDA Python script that renames functions from the JSON
--dump-cs Generate an IL2CppDumper-style C# header file with full type definitions
--assemblies Comma-separated list of assembly names to process (default: all)
--binary Override auto-detection of the main Mach-O binary (iOS only)

Output Format

JSON

The JSON output contains both a flat methods array (for script compatibility) and a structured types array with full type hierarchies:

{
  "generatedBy": "xamarin_aot_dumper.py",
  "binary": "/path/to/binary",
  "aotVersion": 185,
  "vmBase": "0x100000000",
  "stats": {
    "total_assemblies": 42,
    "total_methods": 15000,
    "total_compiled": 12000,
    "total_types": 5000
  },
  "types": [
    {
      "assembly": "MyApp",
      "namespace": "MyNamespace",
      "name": "MyClass",
      "fullName": "MyNamespace.MyClass",
      "kind": "class",
      "visibility": "public",
      "modifiers": "sealed",
      "baseType": "System.Object",
      "interfaces": ["System.IDisposable"],
      "genericParams": ["T"],
      "fields": [
        { "name": "_count", "type": "int", "visibility": "private", "isStatic": false, "isReadonly": true, "isConst": false }
      ],
      "properties": [
        { "name": "Count", "type": "int", "hasGetter": true, "hasSetter": false }
      ],
      "events": [
        { "name": "Changed", "type": "System.EventHandler" }
      ],
      "methods": [
        {
          "name": "DoSomething",
          "returnType": "void",
          "parameters": [{ "name": "x", "type": "int" }, { "name": "y", "type": "string" }],
          "visibility": "public",
          "isStatic": false,
          "isVirtual": true,
          "token": "0x06000001",
          "nativeAddress": "0x100abc000",
          "isCompiled": true
        }
      ]
    }
  ],
  "methods": [
    {
      "assembly": "MyApp",
      "type": "MyNamespace.MyClass",
      "method": "DoSomething",
      "returnType": "void",
      "parameters": [{ "name": "x", "type": "int" }, { "name": "y", "type": "string" }],
      "token": "0x06000001",
      "methodIndex": 0,
      "nativeAddress": "0x100abc000",
      "isCompiled": true
    }
  ]
}

dump.cs

The --dump-cs output is modeled after IL2CppDumper's dump.cs format:

// Assembly: MyApp

// Namespace: MyNamespace
public sealed class MyClass : IDisposable // TypeDefIndex: 42
{
    // Fields
    private readonly int _count;

    // Properties
    int Count { get; }

    // Methods
    // RVA: 0x1234 VA: 0x100abc000
    public virtual void DoSomething(int x, string y) { }
}

public enum MyEnum // TypeDefIndex: 43
{
    None = 0,
    First = 1,
    Second = 2,
}

Using Generated Scripts

Ghidra

  1. Open your binary in Ghidra
  2. Open the Script Manager (Window > Script Manager)
  3. Run the generated ghidra_annotate.py script
  4. Adjust DUMP_PATH in the script if needed

IDA Pro

  1. Open your binary in IDA
  2. File > Script File and select the generated ida_annotate.py
  3. Adjust DUMP_PATH in the script if needed

Technical Details

Method Address Resolution

The core of the tool is mapping managed .NET methods to native ARM64 code addresses. This happens in several stages:

iOS (Mach-O)

1. MonoAotFileInfo discovery — The Mono AOT compiler embeds one MonoAotFileInfo struct per assembly in the binary's __DATA segment. Each struct contains pointers to code regions, the method address table, the assembly name, and scalar metadata (number of methods, flags, etc.). The struct layout varies across Mono/Xamarin/.NET versions (new fields get inserted, shifting offsets), so the tool:

  • Finds DLL name strings in the binary's string sections
  • Searches DATA segments for 8-byte pointers to those strings
  • For each pointer hit, tries all known struct layouts to see if the surrounding bytes validate as a MonoAotFileInfo (checking version, call table entry size, nmethods sanity, method_addresses pointer validity)
  • Falls back to heuristic interpolation for unknown versions

All assemblies share a single Mach-O binary, with pointer fields directly embedded as absolute VMAs.

Android (ELF)

1. MonoAotFileInfo via symbol — Each assembly gets its own libaot-AssemblyName.dll.so ELF shared object. The mono_aot_file_info struct is exported as a dynamic symbol in .dynsym, so finding it is trivial — just look up the symbol name.

Pointer resolution via relocations — Unlike iOS where pointer fields contain absolute VMAs, Android ELF .so files zero out all pointer fields in the struct. The actual target addresses are stored as addends in R_AARCH64_RELATIVE relocation entries in the .rela.dyn section. The tool builds a {VMA → addend} map from all relocations and uses it to resolve each pointer field: assembly_name, method_addresses, jit_code_start, etc.

VMA-to-file-offset mapping — ELF LOAD program headers define the VMA→file offset mapping (each LOAD segment may have a different offset gap). The tool walks all PT_LOAD entries to translate any VMA to its file position.

DLL sourcing — .NET DLL files can be standalone in the directory or packed inside libassemblies.*.blob.so XABA blobs. XABA blobs are ELF files with a payload section containing: a header, hash index tables, a length-prefixed name table, and LZ4-compressed DLL data (XALZ blocks). The tool auto-extracts DLLs from XABA when standalone files aren't present.

Common (both platforms)

2. BL instruction decoding — The method_addresses table is an array of ARM64 BL (Branch with Link) instructions, one per method. Each 4-byte instruction encodes a 26-bit signed PC-relative offset. The tool decodes (instruction & 0x3FFFFFF) * 4 + instruction_VA to get the target native address. Entries where the target points back to the table start are sentinels meaning "not AOT-compiled."

3. DLL metadata cross-reference — Method indices in the AOT table correspond 1:1 to the MethodDef table rows in the .NET DLL metadata. The tool reads each DLL with dnfile, maps method index → (namespace, type, name, signature), then joins with the resolved native addresses.

Type Information Extraction

Full type metadata is extracted from the .NET DLL files using the ECMA-335 metadata tables:

Type declarations — The TypeDef table provides every type's name, namespace, and flags. Flags determine visibility (public/private/nested variants), kind (class/interface via 0x20 bit), and modifiers (abstract/sealed/static). The base type comes from the Extends coded index column (resolves to a TypeDef or TypeRef row). The tool detects enums (extends System.Enum) and structs (extends System.ValueType) from the base type.

Signature blob parsing — Field types, method signatures, and property types are stored as compact binary blobs in the #Blob heap (ECMA-335 §II.23.2). The SigParser class decodes these byte-by-byte:

  • Compressed unsigned integers (1/2/4 bytes based on high bits)
  • TypeDefOrRef coded indices encoded as (row << 2) | tag where tag 0=TypeDef, 1=TypeRef, 2=TypeSpec
  • Recursive type composition: GENERICINST(CLASS List<1>, STRING)List<string>, SZARRAY(I4)int[], PTR(VOID)void*, BYREF(I4)ref int
  • Generic type variables: VAR(n) for class-level, MVAR(n) for method-level

A type name map is pre-built from all TypeDef and TypeRef rows, keyed by their coded index values, so blob references resolve to full Namespace.TypeName strings.

Fields — Each TypeDef owns a range of Field rows (via FieldList_Index). Per field: flags give visibility/static/readonly/const, and the Signature blob is parsed for the field type.

Methods — Each TypeDef owns a range of MethodDef rows (via MethodList_Index). The Signature blob gives return type and parameter types. Parameter names come from the Param table (linked via ParamList_Index), matched by Sequence number.

PropertiesPropertyMap links each TypeDef to a range in the Property table. Property types come from the Type signature blob. Getter/setter detection uses the MethodSemantics table, which links methods to properties with semantic flags (0x02 = getter, 0x01 = setter).

Interfaces — The InterfaceImpl table stores (TypeDef, Interface) pairs. The Interface column is a TypeDefOrRef coded index resolved to a full name.

Generic parametersGenericParam rows have an Owner (TypeOrMethodDef coded index) and a Name. The owner is dereferenced to determine if it belongs to a type or method.

Enum values — Enum members are fields with the Literal flag. Their values come from the Constant table, which stores a Type byte and a Value blob. The blob is decoded based on the element type (e.g., 0x08 = int → 4-byte little-endian).

Nested types — The NestedClass table gives child→parent TypeDef index pairs. The dump.cs emitter uses this to render nested types inside their enclosing type.

License

MIT

About

Xamarin AOT metadata dumper and annotator to help with reverse engineering iOS and Android apps. Inspired by il2cppdumper

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages