From 9d75f4015e709d3adeb63b30d122d54e3dcf7053 Mon Sep 17 00:00:00 2001 From: pancake Date: Thu, 16 Apr 2026 18:20:57 +0200 Subject: [PATCH] Update README --- .gitignore | 10 + README.md | 172 ++++++++++------ doc/r2unity.md | 519 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 646 insertions(+), 55 deletions(-) create mode 100644 .gitignore create mode 100644 doc/r2unity.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aef91e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# macOS +.DS_Store + +# Build artifacts +*.o +/r2unity + +# Third-party reference checkouts (radare2, il2cpp dumper, etc.) +/third_party/ + diff --git a/README.md b/README.md index 371c571..d01ae2b 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,158 @@ # r2unity -A lightweight, dependency-free C tool to parse Unity's `global-metadata.dat` and generate [radare2](https://github.com/radareorg/radare2) scripts for analyzing `libil2cpp.so` binaries. +A lightweight C tool and (upcoming) radare2 core plugin that parses Unity's `global-metadata.dat` and annotates the matching IL2CPP-compiled binary with class, method, and field information. ## Motivation -The Unity engine, when using its IL2CPP backend, compiles C# code into C++ and then into a native library (`libil2cpp.so` on Android/Linux, `GameAssembly.dll` on Windows, etc.). To understand the original code structure, reverse engineers need to parse the `global-metadata.dat` file, which contains all the type information, method names, and pointers into the native library. +Unity with the IL2CPP backend compiles C# to native code. The symbolic information (namespaces, classes, method names, tokens) is stripped from the native binary and kept in a separate file called `global-metadata.dat`. Without it, the binary is a blob of anonymous functions. -While powerful tools like [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) exist, they are often written in higher-level languages (like C#) and produce generic outputs (like C++ headers or JSON files). +**r2unity** reads the `.dat`, locates the method pointer table inside the native binary, and emits radare2 commands that flag functions, add comments, and apply types — directly in your r2 session, with no runtime dependencies. -**r2unity** takes a different approach by focusing on a single goal: **deep integration with radare2**. It is written in plain C for maximum performance, portability, and zero runtime dependencies. The output is a ready-to-use `.r2` script, allowing analysts to immediately apply this metadata within their radare2 sessions. +Inspired by [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) and [lib-global-metadata](https://github.com/axstin/lib-global-metadata), but focused on tight integration with [radare2](https://github.com/radareorg/radare2). -This project is inspired by the functionality of Il2CppDumper and the metadata parsing concepts from projects like [lib-global-metadata](https://github.com/axstin/lib-global-metadata). +## What r2unity needs -## Features +Exactly **two files**: -- **Parses `global-metadata.dat`:** Extracts classes, methods, and their relative addresses. -- **Generates Radare2 Scripts:** Creates `.r2` files to name functions and add comments in your analysis session. -- **Written in Plain C:** No dependencies beyond a standard C compiler (like `gcc` or `clang`). -- **Lightweight and Fast:** Designed for speed and minimal memory footprint. -- **Cross-Platform:** Aims to be easily compilable on Linux, macOS, and Windows. +1. The **IL2CPP native binary** (this is what you open in radare2): + - Android: `libil2cpp.so` + - iOS: `UnityFramework` (a Mach-O inside `UnityFramework.framework`) + - Windows: `GameAssembly.dll` + - macOS: `GameAssembly.dylib` +2. The matching **`global-metadata.dat`**. -## Getting Started +The **main application executable is NOT needed**: +- On iOS, `Payload/.app/` is a ~50–100 KB thin launcher whose only job is to load `UnityFramework.framework`. All IL2CPP code lives in `UnityFramework` (tens of MB). +- On Android, `lib//libmain.so` is a small shim (a few KB). All IL2CPP code lives in `libil2cpp.so`. -### Prerequisites +So you only pass r2unity the Unity binary and the `.dat` — never the launcher. -- A C compiler (e.g., `gcc`, `clang`) -- `make` +## Where to find the files -### Installation +### Android (`.apk` / `.apks` / `.aab`) + +An APK is a zip. Extract (or browse with `r2 apk://…`) and look at: + +``` +/ +├── lib/ +│ ├── arm64-v8a/ +│ │ └── libil2cpp.so ← open this in radare2 +│ └── armeabi-v7a/ +│ └── libil2cpp.so ← or this, for 32-bit +└── assets/ + └── bin/ + └── Data/ + └── Managed/ + └── Metadata/ + └── global-metadata.dat ← pass this to r2unity +``` + +Pick the `libil2cpp.so` for the architecture you care about (typically `arm64-v8a`). + +### iOS (`.ipa`) + +An IPA is also a zip. Inside: + +``` +Payload/ +└── .app/ + ├── ← thin launcher (NOT needed) + ├── Frameworks/ + │ └── UnityFramework.framework/ + │ └── UnityFramework ← open this in radare2 + └── Data/ + └── Managed/ + └── Metadata/ + └── global-metadata.dat ← pass this to r2unity +``` + +Note that on iOS the metadata lives under `.app/Data/…`, **not** under the `.framework/` directory. + +### Windows / desktop builds + +``` +/ +├── GameAssembly.dll ← the IL2CPP binary (Windows) +└── _Data/ + └── il2cpp_data/ + └── Metadata/ + └── global-metadata.dat +``` + +macOS standalone builds use `GameAssembly.dylib` inside the `.app` bundle, with metadata at `.app/Contents/Resources/Data/il2cpp_data/Metadata/global-metadata.dat`. + +## Automatic metadata discovery + +The (planned) radare2 core plugin locates `global-metadata.dat` automatically by walking upward from the directory of the currently loaded binary and probing the well-known relative paths above: + +| Loaded binary | Candidate `global-metadata.dat` paths tried (relative to binary) | +| --- | --- | +| `…/lib//libil2cpp.so` (Android) | `../../assets/bin/Data/Managed/Metadata/global-metadata.dat` | +| `…/UnityFramework.framework/UnityFramework` (iOS) | `../../Data/Managed/Metadata/global-metadata.dat` | +| `…/GameAssembly.dll` (Windows) | `./*_Data/il2cpp_data/Metadata/global-metadata.dat` | +| `…/GameAssembly.dylib` (macOS) | `../Resources/Data/il2cpp_data/Metadata/global-metadata.dat` | +| any | `./global-metadata.dat` (user already placed it next to the binary) | + +If none of these resolve, the plugin asks the user to supply the path explicitly. The CLI always takes the two paths as arguments — no auto-detection — to keep scripted use predictable. + +## Getting started + +### Build ```bash -git clone https://github.com/your-username/r2unity.git +git clone https://github.com/radareorg/r2unity.git cd r2unity make ``` -## Usage +Requires a C compiler and `pkg-config` with radare2 dev headers installed (`r_util`, `r_core`). -The primary goal is to generate a script that radare2 can use to annotate the `libil2cpp.so` binary. +### Usage (CLI) -1. **Extract Files:** Obtain the `global-metadata.dat` file and the main binary (e.g., `libil2cpp.so`) from your target application. +```bash +# Generate an r2 script from the Unity binary + metadata: +./r2unity path/to/libil2cpp.so path/to/global-metadata.dat > symbols.r2 -2. **Run r2unity:** Execute `r2unity` with the metadata file and the base address where the binary is loaded. The output will be an `.r2` script. +# Apply the script when opening the binary in radare2: +r2 -i symbols.r2 path/to/libil2cpp.so +``` - ```bash - ./r2unity /path/to/global-metadata.dat > symbols.r2 - ``` +On iOS, swap `libil2cpp.so` for `…/UnityFramework.framework/UnityFramework`. -3. **Launch Radare2:** Open the binary in radare2, sourcing the generated script with the `-i` flag. +Useful flags: - ```bash - r2 -i symbols.r2 /path/to/libil2cpp.so - ``` +- `-j` — emit a single-line JSON status report (used by checks). +- `-q` — quiet, omit the `#` header comments. +- `-f` — fast path: auto-detect ELF/Mach-O and scan for the method pointer table. +- `-a 0xADDR -c N` — read N method pointers starting at address `0xADDR` inside the binary. +- `-l N` — limit number of emitted entries. +- `-v` — verbose debug output. -Radare2 will execute the script, defining flags (function names) for all the methods found in the metadata. - -### Example Output (`symbols.r2`) - -The generated script will contain a series of radare2 commands: +### Example output ``` -# r2 script generated by r2unity -# Found 50,000+ methods in global-metadata.dat - -f Some.Namespace.ClassName.MethodName @ 0x1a2b3c -CCu Method: Some.Namespace.ClassName.MethodName @ 0x1a2b3c - -f Another.Class.Update @ 0x2b3c4d -CCu Method: Another.Class.Update @ 0x2b3c4d - -f Player.Stats.GetHealth @ 0x3c4d5e -CCu Method: Player.Stats.GetHealth @ 0x3c4d5e - +'@0x1a2b3c'f sym.unity.Assembly-CSharp.dll.Player.Update(0) +'@0x1a2b3c'CCu Method: [Assembly-CSharp.dll] public Player.Update(0) +'@0x2b3c4d'f sym.unity.mscorlib.dll.System.String.Concat(2) ... ``` + + + ## Philosophy -- **Do One Thing Well:** Focus solely on creating `r2` scripts from Unity metadata. -- **Native and Dependency-Free:** Avoid the complexity of runtimes like .NET or Python. A single, static binary is the goal. -- **Integrate, Don't Just Convert:** The output should be directly consumable by the target tool (`radare2`) to provide immediate value. - -## Contributing - -Contributions are welcome! Please feel free to open an issue or submit a pull request. As this is a C project, please adhere to a clean and readable coding style. +- **Do one thing well** — turn Unity metadata into radare2 commands. +- **Native and dependency-free** — plain C, no .NET or Python runtime. +- **Integrate, don't convert** — output is directly consumable by r2, not a detour through JSON or C# headers. ## Acknowledgments -- The [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) project for being the gold standard in Unity IL2CPP reversing. -- The [lib-global-metadata](https://github.com/axstin/lib-global-metadata) project for its clear implementation of metadata parsing. +- [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) — the reference implementation for Unity IL2CPP reversing. +- [lib-global-metadata](https://github.com/axstin/lib-global-metadata) — clean reference for metadata parsing. - The [radare2](https://github.com/radareorg/radare2) community. ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +MIT. See [LICENSE](LICENSE). diff --git a/doc/r2unity.md b/doc/r2unity.md new file mode 100644 index 0000000..2f99837 --- /dev/null +++ b/doc/r2unity.md @@ -0,0 +1,519 @@ +# r2unity — Internal Technical Documentation + +This document describes the internal design of r2unity, how the current +implementation works, what each piece of source code is responsible for, +and what is still missing for full portability across iOS and Android +builds produced by different Unity versions. + +It complements the top level `README.md`, `AGENTS.md`, `INFO.md`, +`INFO2.md` and `RVA.md` notes — those cover the *why* and the *file +formats*; this file covers the *how* and the *gaps*. + +--- + +## 1. High level architecture + +r2unity is split into three deliverables: + +1. **`src/lib/`** — a small static library in C that parses + `global-metadata.dat` and scans a native IL2CPP binary (ELF or + Mach-O) to recover the per-method pointer table. +2. **`src/main.c`** — a CLI entry point that glues the library to a + radare2 script generator. This is what the `Makefile` currently + produces as `./r2unity`. +3. **radare2 core plugin** *(planned, not wired into the build yet)* + — will consume the same library plus `r_core` to register flags, + comments and types directly in the running r2 session, and will + auto-discover `global-metadata.dat` from the loaded binary's path. + +The library links only against `r_util` (via `pkg-config r_util`) — no +`r_bin`, no `r_core`. The ELF and Mach-O parsers in `src/lib/elf.c` and +`src/lib/macho.c` are intentionally self-contained so the CLI stays +small and can be embedded later into the plugin without pulling the +full radare2 core. + +``` + +---------------------+ +-----------------------+ + | global-metadata.dat | | libil2cpp.so / .dylib | + +----------+----------+ +-----------+-----------+ + | | + v v + +---------------------+ +-----------------------+ + | src/lib/lib.c | | src/lib/elf.c | + | - header parsing | | src/lib/macho.c | + | - strings | | - segment walk | + | - type/method tbls |<--------+ - relocation apply | + | - images | | - CodeRegistration / | + +---------+-----------+ | method-ptr scan | + | +-----------+-----------+ + +-------------+-------------------+ + v + +-----------------+ +------------------+ + | src/main.c | | r2 core plugin | + | CLI (-j -q -f) | | (not yet built) | + +--------+--------+ +---------+--------+ + v v + r2 command script r_flag / r_anal / r_meta +``` + +--- + +## 2. `global-metadata.dat` parser (`src/lib/lib.c`, `src/lib/lib.h`) + +### 2.1 Header versions + +Three C-level header structures are declared in `lib.h`: +`Il2CppGlobalMetadataHeader_v24`, `_v27` and `_v29`. They differ by the +trailing fields: + +- **v24** — baseline fields up to `exportedTypeDefinitions`. +- **v27** — adds `rgctxEntries{Offset,Size}`. +- **v29** — adds `rgctxEntriesData{Offset,Size}`. + +They are stored in a `union Il2CppGlobalMetadataHeader` and the selected +variant is picked at parse time: + +```c +if (version < 27) header_size = sizeof (v24); +else if (version < 29) header_size = sizeof (v27); +else header_size = sizeof (v29); // 29, 30, 31 +``` + +Metadata version is read from the second 32-bit word of the file +(the first is the `0xFAB11BAF` magic). Only versions `[24, 31]` are +accepted; anything outside that range makes `r2unity_parse_metadata` +return `NULL`. + +Although three header layouts exist, `R2UnityMetadata` keeps a set of +*normalized* fields that are copied out of whichever variant was +selected: + +```c +meta->stringOffset meta->stringSize +meta->stringLiteralOffset meta->stringLiteralSize +meta->methodsOffset meta->methodsSize +meta->typeDefinitionsOffset meta->typeDefinitionsSize +``` + +The rest of the library only touches these normalized fields, which +keeps version-specific knowledge localized to the parse function. + +### 2.2 String table + +`r2unity_get_string (meta, index)` returns a `const char *` (actually an +independently malloc'd copy — callers `free()` it) for a given string +index. Two notable details: + +- The string table is mapped as an `RBuffer` slice (`meta->strings`) + via `r_buf_new_slice`, so nothing is copied eagerly. +- If `index` happens to point into the middle of a string (which does + happen with some metadata payloads), the reader walks *backwards* + until it hits a null byte, then reads forward until the next null. + This "realign to start of string" heuristic covers malformed + `nameIndex` values seen in the wild. + +There is a second string region — `stringLiteralOffset` — used by +`ldstr` opcodes. It is exposed as `meta->string_literals` but not yet +consumed by the CLI. + +### 2.3 Type / method / image definitions + +Three on-disk record types are parsed manually (not via `memcpy` of a +packed struct) to stay endian- and alignment-safe: + +| Table | Entry size | Decoder | +| --- | ---: | --- | +| `Il2CppTypeDefinition` | 88 bytes | `r2unity_get_type_definitions` | +| `Il2CppMethodDefinition` | 32 bytes | `r2unity_get_method_definitions` | +| `Il2CppImageDefinition` | 40 bytes | `r2unity_get_images` | + +Each row is decoded with `RD_LE32` / `RD_LE16` helpers, so the parser +works identically on LE and BE hosts. The returned arrays are +heap-allocated and owned by the caller. + +The fixed entry sizes assume the *current* layout used by Unity 2019+ +IL2CPP (metadata versions 24…31). This is one of the main portability +risks — see §7. + +### 2.4 Ownership and lifetime + +- `RBuffer *buf` passed to `r2unity_parse_metadata` is retained by the + metadata (not `r_ref`'d itself; the caller must `r_unref` the outer + buffer after freeing the metadata). +- `meta->strings` and `meta->string_literals` are slices — they are + `r_unref`'d by `r2unity_free_metadata`. +- Type/method/image arrays are pulled into plain C arrays so that + downstream iteration does not need to deal with `RBuffer` at all. + +### 2.5 Debug switch + +A single file-static boolean `g_debug`, controlled by +`r2unity_set_debug(bool)`, gates `[r2unity]` / `[r2unity/elf]` / +`[r2unity/macho]` trace lines on `stderr`. The CLI wires `-v` to this. + +--- + +## 3. ELF parser (`src/lib/elf.c`) + +Purpose: open an Android `libil2cpp.so`, resolve a method-pointer +table, and return an array of absolute virtual addresses the size of +`methodsSize / sizeof(Il2CppMethodDefinition)`. + +### 3.1 Loading + +`elf_load` slurps the entire file with `open`/`read`, then walks the +program header table: + +- 32-bit ELF (`EI_CLASS == 1`) and 64-bit ELF (`EI_CLASS == 2`) are + both supported. +- Only little-endian (`EI_DATA == 1`) ELF is accepted. +- Segments of type `PT_LOAD` (1) and `PT_DYNAMIC` (2) are captured into + a fixed 128-entry table; everything else is dropped. +- `base_vaddr` is computed as `min(p_vaddr over PT_LOAD)`. Code + ("text") range is the union of `PT_LOAD` segments that are both + readable and executable (`PF_X | PF_R`). + +### 3.2 Dynamic relocation fixup + +Modern Android NDK toolchains emit `DT_RELA`, `DT_REL`, and/or +`DT_RELR`. The IL2CPP method pointer table is typically emitted as an +array of `R_AARCH64_RELATIVE` (type 1027) or `R_*_RELATIVE` (type 8 / +type 23 on x86_64 / ARM32), so before scanning the data segments the +parser iterates all three relocation sections and patches the in-memory +image: + +- `DT_RELA`: `newv = base_vaddr + r_addend` +- `DT_REL`: `newv = base_vaddr + *loc` +- `DT_RELR` (64-bit only): bitmap-encoded relative relocations, applied + to each location implied by the bitmap. + +Only *relative* relocation types are applied. Symbolic / PLT / GOT +relocations are intentionally ignored — they cannot resolve without a +dynamic linker, and they are not needed for the method pointer arrays. + +The dynamic tag set consumed: + +| Tag | Meaning | +| ---:| --- | +| 7 | `DT_RELA` | +| 8 | `DT_RELASZ` | +| 9 | `DT_RELAENT` | +| 17 | `DT_REL` | +| 18 | `DT_RELSZ` | +| 19 | `DT_RELENT` | +| 35 | `DT_RELRSZ` | +| 36 | `DT_RELR` | +| 37 | `DT_RELRENT` | + +### 3.3 Method-pointer discovery + +Two passes run over every `PF_W`-bearing data segment: + +1. **CodeRegistration-shaped pair** — looks for a `{count32, pad, + pointer}` tuple where `count >= 32` and a sample of 128 entries at + `*pointer[]` is dominated by values that land in the text range. +2. **Generic `{count32, pointer}` record** — less strict, used when the + CodeRegistration shape is not found. Uses a *window* + `expected/2 ≤ count ≤ expected*2` around + `methodsSize/sizeof(Il2CppMethodDefinition)` to avoid locking onto a + different count-prefixed array (string table, type table, etc.). + +The "good" heuristic counts entries that either already fall inside +`[text_lo, text_hi)` or that fall inside `[text_lo, text_hi)` after +adding `base_vaddr`. This covers both pre-relocated images (where the +table already contains absolute VAs) and raw RVAs (where it stores +offsets relative to the module base). Once a candidate array is +accepted, all entries are normalized to absolute VAs in the output. + +At least 8 entries must land in text for the table to be accepted — +this prevents false positives on small, count-prefixed arrays that +happen to alias the heuristic. + +--- + +## 4. Mach-O parser (`src/lib/macho.c`) + +Purpose: same as §3, but for iOS `UnityFramework` and macOS +`GameAssembly.dylib`. + +### 4.1 Loading + +- Accepts thin `MH_MAGIC_64` (`0xFEEDFACF`) directly. +- Accepts FAT (`0xCAFEBABE`, big-endian header) and selects the first + ARM64 slice (`cputype == 0x0100000c`), otherwise the first slice. + **Only one ARM64 slice is dumped** — cross-slice iteration is not + yet implemented. +- Only 64-bit Mach-O is supported (`mh_magic != 0xFEEDFACF` is a hard + error). 32-bit (`0xFEEDFACE`) and the little-endian FAT + `0xBEBAFECA` variant fall through. +- Walks `LC_SEGMENT_64` (`0x19`) commands, capturing `segname`, + `vmaddr`, `vmsize`, `fileoff`, `filesize`, `maxprot` for up to 128 + segments. +- `vm_base = min(vmaddr over segments)`. + +### 4.2 Text range + +`macho_vm_in_text` computes `[text_lo, text_hi)` as the union of +executable segments (`maxprot & VM_PROT_EXECUTE` or segname +`__TEXT`). When no executable segment is detected it falls back to the +union of *all* segments — this is a safety net for unusual Mach-O +layouts but normally isn't triggered. + +### 4.3 Method-pointer discovery + +Same two-pass approach as the ELF scanner: CodeRegistration shape +first, generic `{count, ptr}` second, with identical heuristics. Only +8-byte pointers are considered (`ptrsz = 8`). + +Mach-O does *not* have dynamic relative relocations that need to be +applied manually — the linker already baked values in, modulo the +`vm_base` offset which the discovery loop accounts for. + +--- + +## 5. CLI entry point (`src/main.c`) + +### 5.1 Flag surface + +``` +-q Quiet — omit "# r2 script…" banner +-f Fast path: auto-detect ELF/Mach-O, scan for ptr table +-l N Limit emitted entries to N +-a 0xA Read pointer table starting at VA 0xA +-c N ... for N entries (pair with -a) +-v Verbose / debug +``` + +The positional arguments are ` `. + +### 5.2 Output format (radare2 script) + +For every method the CLI emits two lines using the r2 "temporary +seek" prefix `'@0xADDR'`: + +``` +'@0x1a2b3c'f sym.unity.. +'@0x1a2b3c'CCu Method: [] +``` + +- `f` defines a flag. +- `CCu` sets a *unique* per-address comment. +- `sym.unity.` namespacing keeps Unity symbols separate from whatever + radare2 already detected (e.g. exported symbols). +- `r_name_filter(fullname, -1)` sanitizes the flag name so it is safe + to use in r2 (no spaces, dots, parens). +- Method attributes (`public`, `private`, `static`, `virtual`, etc.) + are computed from `flags` in `build_method_attrs_string` using the + subset of `System.Reflection.MethodAttributes` that is meaningful. + +The assembly/image name is looked up via a `type2img[typeIndex]` map +built once from `Il2CppImageDefinition.typeStart` /`typeCount`. + +### 5.3 `-j` JSON status + +and friends: + +```json +{"ok":true,"version":29,"types":41234,"methods":123456,"has_ptrs":true} +``` + +`has_ptrs` is `true` when *any* non-zero pointer made it into the +array — this is the closest we can get to "did the scan work" without +running the full r2 session. + +### 5.4 Address mapping + +The mapping `method index j → pointer[j + mp_shift]` is kept 1:1 +(`mp_shift = 0`). This matches the upstream invariant that +`g_MethodPointers[i]` corresponds to method definition *i*, with +zeros for abstract/generic/external methods. + +Only pointers with `addr > 0x1000` are emitted — a trivial way to skip +the zero-padded slots. + +--- + + + +- `cli-gmp` — smoke check against a generic `global-metadata.dat`. +- `json-one-line` — validates `-j` emits exactly one JSON line. +- `json-unity` — stricter JSON shape + content check. + + +--- + +## 7. Portability status + +The implementation is working end-to-end on the two main targets +(Android arm64 ELF, iOS arm64 Mach-O), but there are several known +gaps before we can claim broad coverage. + +### 7.1 Metadata versions + +| Version | Header struct | Status | +| ------: | ------------- | ------ | +| 16–23 | — | **Not supported.** Rejected by `r2unity_parse_metadata`. | +| 24 | `v24` | Supported. | +| 27 | `v27` | Supported (RGCTX offsets parsed but unused). | +| 27.1/.2 | `v27` | Assumed compatible — not explicitly validated. | +| 29 | `v29` | Supported. | +| 30, 31 | `v29` | **Treated as v29-compatible.** Any layout change in these versions will silently produce wrong offsets. | + + +**TODO:** decode the `token` fields — they carry the ECMA-335 +metadata token which is useful for cross-referencing with managed +DLLs. + +### 7.2 On-disk entry sizes are hard-coded + +`r2unity_get_type_definitions`, `r2unity_get_method_definitions` and +`r2unity_get_images` use literal byte sizes (88, 32, 40). These match +metadata versions 24–31 *today* but will break the moment Unity adds +or removes a field. Detection should be derived from +`(sectionSize / entryCount)` or from version-gated constants declared +next to the header structs. This is the single biggest source of +silent corruption risk. + +### 7.3 iOS (Mach-O) + +Working: +- ARM64 thin Mach-O (`0xFEEDFACF`). +- ARM64 slice of FAT Mach-O (first ARM64 slice selected). + +Known gaps: +- **No ARM64e slice selection** — `cputype` match only checks + `0x0100000c` (CPU_TYPE_ARM64). ARM64e (with `CPU_SUBTYPE_ARM64E`) is + the same `cputype`, so it works by accident, but we don't expose a + way to pick it explicitly if both slices coexist. +- **No 32-bit ARMv7 Mach-O.** Unity hasn't shipped 32-bit iOS since +- **No chained fixups / ARM64e pointer authentication handling.** + Recent Xcode produces `LC_DYLD_CHAINED_FIXUPS` (`0x34`) instead of + classic rebase opcodes. For the specific tables we scan this is not + a problem because values land in the text range either way, but if + we ever need to follow pointers *inside* the chained-fixup region we + will need to parse the chain. +- **No `LC_DYLD_INFO{,_ONLY}` rebase opcode interpretation.** Same + reasoning as above; not blocking today, will matter if pointer + scanning shifts to resolving `CodeRegistration` more precisely. +- **Entitlements / encrypted segments (`LC_ENCRYPTION_INFO_64`)** — + commercial `.ipa` from the App Store are FairPlay-encrypted until + decrypted on a jailbroken device. r2unity currently has no check for + this; scanning will silently return garbage on encrypted binaries. + We should detect the load command and emit a warning. +- **`UnityFramework.framework` vs `GameAssembly.dylib`** — the parser + treats both the same way. That is correct; just noting it. + +### 7.4 Android (ELF) + +Working: +- 64-bit arm64 (`aarch64`) `libil2cpp.so` with `DT_RELA` and/or + `DT_RELR`. +- 32-bit ELF headers are parsed, but… + +Known gaps: +- **ARMv7 (`armeabi-v7a`) `libil2cpp.so` is parsed but rarely + validated.** The relocation-apply path supports `R_ARM_RELATIVE` + quirk (e.g. `DT_ANDROID_RELA`) will go unnoticed. +- **`DT_ANDROID_REL{A,R}` / packed relocations** (`DT_ANDROID_RELA = + 0x60000010`, `DT_ANDROID_RELASZ = 0x60000011`) are **not handled**. + Play-Store builds compressed with `androidx.sqlite`'s packed-relocs + format will read as "no relocations" and the method pointer array + will look unrelocated. This is the biggest Android gap — a real +- **x86 / x86_64 Android emulator builds** have relocation types + (`R_X86_64_RELATIVE = 8`, `R_386_RELATIVE = 8`) that the matching +- **No symbol version (`DT_VERSYM`/`DT_VERNEED`) handling.** Not + needed for the method pointer scan but would help identify the Unity + version from the `.so` directly. +- The ELF loader slurps the entire file with `read()` — fine for a + ~40 MB `libil2cpp.so`, but not ideal. Should move to `mmap` once + the plugin lands inside r2 and inherits `r_core`'s IO layer. + +### 7.5 Windows / macOS standalone + +Unsupported today: +- `GameAssembly.dll` (Windows PE) — no PE parser. +- `GameAssembly.dylib` (macOS standalone) — technically works via the + Mach-O path, but the metadata auto-discovery relative paths listed + in `README.md` aren't wired into the CLI (only the plugin will use + them). + +**TODO:** add a minimal PE scanner in `src/lib/pe.c`. It only needs to +enumerate sections, pick `.data` / `.rdata`, and reuse the same +`{count, ptr}` heuristic. No relocations need to be applied — PE +`.dll`s are already relocated at link time (image base is concrete). + +### 7.6 Auto-detection & plugin integration + +`src/main.c:-f` detects the file by its first 4 bytes. This is a +minimal heuristic and should be moved, together with the metadata path +discovery described in `README.md`, into the core plugin. Specifically: + +- Plugin registers as an `r_core` plugin and hooks file-open. +- On open, walk upward from the binary path probing the well-known + metadata locations (Android `../../assets/bin/Data/Managed/Metadata/`, + iOS `../../Data/Managed/Metadata/`, Windows `*_Data/il2cpp_data/…`, + macOS `../Resources/Data/il2cpp_data/…`, and a final fallback of + `./global-metadata.dat`). +- Replace the printf'd command script with direct `r_flag_set` / + `r_meta_set` / `r_anal_function_add` calls — faster and avoids + shelling a second r2 process. + +### 7.7 CodeRegistration / MetadataRegistration + +Currently the discovery heuristic locks onto the *method pointer* +array directly. A more robust approach, described in `RVA.md`, would +be to locate `Il2CppCodeRegistration` and `Il2CppMetadataRegistration` +by pattern-matching expected counts (methodPointersCount must equal +`methodsSize/sizeof(Il2CppMethodDefinition)`, etc.) and then pulling +all pointer arrays (`g_MethodPointers`, `g_InvokerPointers`, +`g_FieldOffsetTable`, …) from a single anchor. This would also give +us: + +- Field offsets (not exposed at all today). +- VTables. +- Invoker pointers → better demangling of generic method + instantiations. +- Reverse P/Invoke wrappers. + +**TODO:** implement `CodeRegistration` / `MetadataRegistration` +anchor detection and expose the full symbol set, not just methods. + +### 7.8 Other missing features + +- Generic methods and `MethodSpec` expansion (needs both `.dat` and + the binary's generic instantiation table). +- String literals — `meta->string_literals` is parsed but never + emitted; we could add `Cs` string annotations at the literal VAs. +- Attributes — `attributesInfo` / `attributeTypes` tables are in the + header but not decoded. +- Field offsets — requires `Il2CppMetadataRegistration`. +- Proper `pf.` (print format) struct registration for the header, + type defs and method defs, as suggested in `INFO2.md §4`. +- Windows PE support (see §7.5). +- `ET_EXEC` vs `ET_DYN` handling: we assume PIC shared objects for + ELF. A statically linked IL2CPP executable would break the + `base_vaddr` normalization. + +--- + +## 8. Roadmap summary + +Short-term, in rough priority order: + +1. Handle `DT_ANDROID_RELA` / packed relocations (biggest real-world + Android gap). +2. Replace hard-coded entry sizes with version-gated constants and add + still holds. +3. Land the r2 core plugin (auto metadata discovery + native + `r_flag`/`r_meta` calls). +4. Add PE support for `GameAssembly.dll`. +5. Pattern-match `Il2CppCodeRegistration` so we get field offsets and + invoker pointers, not just method pointers. +6. Detect FairPlay-encrypted `UnityFramework` and bail out cleanly. +7. Emit string-literal annotations and type-definition struct info. + +Longer term: + +- Generic instantiations and `MethodSpec` decoding. +- Symbolicating trampolines / adjustors. + checks.