mirror of
https://github.com/radareorg/r2unity.git
synced 2026-11-07 06:36:00 -10:00
Update README
This commit is contained in:
1 parent
6ddb8d8ab4
commit
9d75f4015e
3 files changed
+646
-55
No files matched your search
+10
@@ -0,0 +1,10 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Build artifacts
|
||||
*.o
|
||||
/r2unity
|
||||
|
||||
# Third-party reference checkouts (radare2, il2cpp dumper, etc.)
|
||||
/third_party/
|
||||
|
||||
@@ -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>.app/<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/<arch>/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:
|
||||
|
||||
```
|
||||
<apk-root>/
|
||||
├── 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/
|
||||
└── <AppName>.app/
|
||||
├── <AppName> ← 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 `<AppName>.app/Data/…`, **not** under the `.framework/` directory.
|
||||
|
||||
### Windows / desktop builds
|
||||
|
||||
```
|
||||
<BuildRoot>/
|
||||
├── GameAssembly.dll ← the IL2CPP binary (Windows)
|
||||
└── <AppName>_Data/
|
||||
└── il2cpp_data/
|
||||
└── Metadata/
|
||||
└── global-metadata.dat
|
||||
```
|
||||
|
||||
macOS standalone builds use `GameAssembly.dylib` inside the `.app` bundle, with metadata at `<App>.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/<arch>/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).
|
||||
+519
@@ -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 `<executable> <global-metadata.dat>`.
|
||||
|
||||
### 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.<image>.<Namespace.Class.Method(N)>
|
||||
'@0x1a2b3c'CCu Method: [<image>] <attrs> <Namespace.Class.Method(N)>
|
||||
```
|
||||
|
||||
- `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.
|
||||
Reference in new issue
Block a user