Skip to content

Commit 4a52bc9

Browse files
authored
Initial support for non-heuristics using r2 bin parsers
* Keep the simple bin parsers for fast carving * Configurable pointers via config vars * Use RGetOpt
1 parent e4f63ae commit 4a52bc9

18 files changed

Lines changed: 2146 additions & 1232 deletions

Makefile

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,22 @@ BIN_PLUGIN = src/r2/bin_r2unity.$(SOEXT)
1717
PLUGINS = $(CORE_PLUGIN) $(BIN_PLUGIN)
1818
CC = gcc
1919

20-
CFLAGS = -Wall -Wextra -g -I. $(shell pkg-config --cflags r_util 2>/dev/null || echo "")
21-
LDFLAGS = $(shell pkg-config --libs r_util 2>/dev/null || echo "")
20+
CFLAGS = -Wall -Wextra -g -I. $(shell pkg-config --cflags r_util r_bin 2>/dev/null || echo "")
21+
LDFLAGS = $(shell pkg-config --libs r_util r_bin 2>/dev/null || echo "")
2222

2323
# r_core plugin flags (full radare2)
24-
CORE_PLUGIN_CFLAGS = -Wall -Wextra -g -fPIC $(shell pkg-config --cflags r_core 2>/dev/null || echo "")
25-
CORE_PLUGIN_LDFLAGS = $(shell pkg-config --libs r_core 2>/dev/null || echo "")
24+
CORE_PLUGIN_CFLAGS = -Wall -Wextra -g -fPIC $(shell pkg-config --cflags r_core r_bin 2>/dev/null || echo "")
25+
CORE_PLUGIN_LDFLAGS = $(shell pkg-config --libs r_core r_bin 2>/dev/null || echo "")
2626

2727
# r_bin plugin flags
2828
BIN_PLUGIN_CFLAGS = -Wall -Wextra -g -fPIC $(shell pkg-config --cflags r_bin 2>/dev/null || echo "")
2929
BIN_PLUGIN_LDFLAGS = $(shell pkg-config --libs r_bin 2>/dev/null || echo "")
3030

3131
R2_USER_PLUGINS = $(shell r2 -H R2_USER_PLUGINS 2>/dev/null)
3232

33-
LIB_SRCS = $(wildcard src/lib/*.c)
33+
LIB_SRCS = $(wildcard src/lib/*.c) $(wildcard src/lib/bin/*.c)
3434
LIB_OBJS = $(LIB_SRCS:.c=.o)
35+
LEGACY_OBJS = src/lib/elf.o src/lib/macho.o src/lib/pe.o src/lib/native.o
3536
CLI_SRCS = src/main.c
3637
CLI_OBJS = $(CLI_SRCS:.c=.o)
3738
OBJS = $(CLI_OBJS) $(LIB_OBJS)
@@ -81,7 +82,7 @@ $(BIN_PLUGIN): src/r2/bin_r2unity.c $(LIB_SRCS)
8182
$(CC) $(CFLAGS) -c -o $@ $<
8283

8384
clean:
84-
rm -f $(EXEC) $(OBJS) $(PLUGINS) $(CONFIG_H)
85+
rm -f $(EXEC) $(OBJS) $(LEGACY_OBJS) $(PLUGINS) $(CONFIG_H)
8586

8687

8788
.PHONY: all clean plugin install-plugin uninstall-plugin fmt

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ IL2CPP binary, and exposes managed metadata for reverse engineering.
1616
Linux, and flat fixture layouts.
1717
- Recovers managed images, assemblies, types, methods, method flags, and `ldstr`
1818
string literals.
19-
- Finds method-pointer tables heuristically in ELF, Mach-O, and PE binaries.
19+
- Resolves method pointers through r_bin symbols/CodeRegistration, with
20+
r_bin or simple ELF/Mach-O/PE section-scan fallback for stripped binaries.
2021
- Lists P/Invoke and v29+ reverse-P/Invoke metadata, and emits CycloneDX 1.5
2122
SBOMs for managed assemblies.
2223
- Provides both a core r2 command plugin and an `r_bin` plugin for direct
@@ -67,6 +68,9 @@ The normal inputs are the native IL2CPP binary and the matching
6768
# recover method flags/comments as r2 commands
6869
./r2unity -f /path/to/GameAssembly.dll /path/to/global-metadata.dat > methods.r2
6970

71+
# override a known native registration symbol address
72+
./r2unity -f -O g_CodeRegistration=0x1234 /path/to/GameAssembly.dll /path/to/global-metadata.dat
73+
7074
# list managed strings, interop metadata, or managed-assembly SBOM data
7175
./r2unity -z /path/to/global-metadata.dat
7276
./r2unity -P -j /path/to/GameAssembly.dll /path/to/global-metadata.dat
@@ -100,8 +104,8 @@ classes, imports, libraries, and header fields.
100104
## Current Limits
101105

102106
- v24.0 metadata, v36/v37 metadata, and WebAssembly are not supported.
103-
- Method-pointer recovery is heuristic; manual `-a` / `-c` pointer reads are
104-
not implemented yet.
107+
- Method-pointer recovery needs CodeRegistration symbols/addresses or the
108+
section-scan fallback; manual `-a` pointer reads are not implemented yet.
105109
- P/Invoke and reverse-P/Invoke output is metadata-first and does not fully
106110
recover native wrapper addresses or every `DllImportAttribute` detail.
107111
- SBOM output covers managed assemblies only, not native dependencies or file

doc/datvsbin.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# IL2CPP Metadata vs Native Binary
2+
3+
Unity IL2CPP builds split managed-code information across two files:
4+
5+
- `global-metadata.dat`
6+
- the native IL2CPP binary (`GameAssembly.dll`, `GameAssembly.so`,
7+
`GameAssembly.dylib`, `libil2cpp.so`, `UnityFramework`, etc.)
8+
9+
They must be read together to recover named native symbols and runtime
10+
layouts.
11+
12+
## `global-metadata.dat`
13+
14+
`global-metadata.dat` is the managed/logical metadata blob. It is
15+
platform-independent for a given IL2CPP run: it stores byte offsets and
16+
dense table indices, not native pointers.
17+
18+
Typical contents:
19+
20+
- metadata magic, version, and table-of-contents header
21+
- assemblies and images
22+
- type definitions for classes, structs, enums, and interfaces
23+
- method definitions: names, signatures, parameters, return types,
24+
tokens, and method indices
25+
- field definitions
26+
- properties and events
27+
- nested-type, interface, and vtable index tables
28+
- generic containers, generic parameters, and method specs
29+
- custom-attribute and default-value blobs, depending on metadata
30+
version
31+
- identifier strings and managed `ldstr` literal payloads
32+
33+
It does not contain:
34+
35+
- native method bodies
36+
- method RVAs or function addresses
37+
- final field offsets
38+
- final type sizes
39+
- native vtables, invokers, wrappers, or trampolines
40+
- Unity asset, scene, prefab, or AssetBundle data
41+
42+
In short, the `.dat` file says what managed things exist and how they
43+
are named, typed, and indexed.
44+
45+
## Native IL2CPP binary
46+
47+
The native binary is the physical/runtime side of the same build. It
48+
contains the compiled machine code and the registration structures that
49+
tie the generated code back to metadata indices.
50+
51+
Important native-side data:
52+
53+
- compiled method bodies
54+
- `Il2CppCodeRegistration`
55+
- `Il2CppMetadataRegistration`
56+
- `methodPointers`
57+
- `genericMethodPointers`
58+
- `reversePInvokeWrappers`
59+
- `invokerPointers`
60+
- `codeGenModules`
61+
- `fieldOffsets`
62+
- `typeDefinitionsSizes`
63+
- native type and generic-instantiation tables
64+
- runtime helper code, wrappers, trampolines, and runtime strings
65+
66+
These tables provide the addresses and runtime layouts that
67+
`global-metadata.dat` intentionally does not carry.
68+
69+
## Mapping model
70+
71+
The two files are correlated by indices:
72+
73+
```text
74+
global-metadata.dat native IL2CPP binary
75+
------------------- --------------------
76+
MethodDefinition.methodIndex --> CodeRegistration / CodeGenModule methodPointers[index]
77+
TypeDefinition index --> MetadataRegistration.fieldOffsets[type]
78+
Type/generic indices --> MetadataRegistration.types and generic tables
79+
```
80+
81+
For r2unity this means:
82+
83+
- parsing only `global-metadata.dat` can recover names, signatures,
84+
tokens, string literals, and table relationships
85+
- resolving those names to native addresses requires locating
86+
`Il2CppCodeRegistration` in the binary; r2unity accepts an explicit
87+
address, an r2 flag/r_bin symbol, or the r_bin/simple-parser
88+
section-scan fallback
89+
- recovering field offsets and final type sizes requires
90+
`Il2CppMetadataRegistration`
91+
- a complete symbol map needs both files from the same build
92+
93+
Today r2unity takes managed structure from `.dat` and native addresses
94+
from the binary:
95+
96+
- `.dat`: image/type/method rows, method indices, names, tokens,
97+
strings, assemblies, P/Invoke metadata, and reverse-P/Invoke
98+
attribute metadata
99+
- binary: executable address ranges, native symbols/flags for
100+
`g_CodeRegistration` and `g_MetadataRegistration`, and method pointer
101+
tables reached from CodeRegistration or the r_bin/simple-parser
102+
fallback scan
103+
104+
The current method-address path only needs `g_CodeRegistration`.
105+
`g_MetadataRegistration` is tracked as the companion anchor for native
106+
layout work such as field offsets and type sizes.

doc/future.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -535,22 +535,23 @@ are always zero; a defensive parser bounds-checks and skips.
535535

536536
## 3. Native binary — pointer arrays we don't follow
537537

538-
`src/lib/elf.c` and `src/lib/macho.c` currently locate exactly one
539-
array: `CodeRegistration.methodPointers` (or its v24.2+ per-image
540-
equivalent, if the heuristic happens to land on the right
541-
`Il2CppCodeGenModule`). The registration structures actually expose
542-
many more pointer arrays, each with its own metadata table partner.
543-
Each entry below is a `{ ulong count; ulong ptr; }` pair inside the
544-
registration (see §3 of `doc/r2unity.md` for the full struct).
538+
`src/lib/bin/native.c` currently recovers method pointers from
539+
`Il2CppCodeRegistration` when the registration anchor is available,
540+
and falls back to a generic section scan backed by r_bin or the simple
541+
ELF/Mach-O/PE parsers for stripped binaries.
542+
The registration structures expose many more pointer arrays, each
543+
with its own metadata table partner. Each entry below is a
544+
`{ ulong count; ulong ptr; }` pair inside the registration (see §3 of
545+
`doc/r2unity.md` for the full struct).
545546

546547
### 3.1 `methodPointers` (≥v24.1 global; v≥24.2 per-module)
547548

548549
What r2unity extracts today. From v24.2 onwards this field is on
549550
`Il2CppCodeGenModule`, **not** `CodeRegistration`, and there is one
550-
module per image. If r2unity finds a single `{count, ptr}` on a
551-
v24.2+ binary it is only extracting **one image's** methods.
552-
Walking `codeGenModules[]` and enumerating each module is mandatory
553-
for full coverage.
551+
module per image. The structural path walks `codeGenModules[]` and
552+
maps modules back to `.dat` image rows; the fallback section scan may
553+
still find only one image's table because it does not know the
554+
registration structure.
554555

555556
### 3.2 `invokerPointers`
556557

doc/r2unity.md

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,13 +1010,13 @@ File-by-file map:
10101010
type/method/image/assembly/referenced-assembly decoders, P/Invoke
10111011
and reverse-P/Invoke enumerators, endian-safe LE readers
10121012
(`RD_LE32`, `RD_LE16`).
1013-
- `src/lib/elf.c` — ELF32/64 loader, dynamic-table walk, relative
1014-
relocation application (`DT_REL`, `DT_RELA`, `DT_RELR`),
1015-
method-pointer-array heuristic.
1016-
- `src/lib/macho.c` — Mach-O 64 loader (thin + FAT first-ARM64),
1017-
`LC_SEGMENT_64` walk, method-pointer-array heuristic.
1018-
- `src/lib/pe.c` — PE32/PE32+ loader, section walk, method-pointer-
1019-
array heuristic.
1013+
- `src/lib/bin/native.c` — shared native-binary view,
1014+
CodeRegistration/MetadataRegistration anchor resolution, structural
1015+
CodeRegistration parsing, RBin adapter, and the generic section-scan
1016+
fallback.
1017+
- `src/lib/bin/elf.c`, `src/lib/bin/macho.c`, `src/lib/bin/pe.c` —
1018+
simple file-backed format parsers used when the RBin path cannot
1019+
recover method pointers.
10201020
- `src/main.c` — CLI entry point and output emitters.
10211021
10221022
Every row decoder reads via `r_read_le32`/`r_read_le16` (LE on all
@@ -1027,45 +1027,41 @@ retained for the two string pools.
10271027
## 6. Native-binary scanning, in one picture
10281028
10291029
```text
1030-
ELF/Mach-O/PE image on disk
1030+
Native IL2CPP image opened by r_bin or a simple ELF/Mach-O/PE mapper
10311031
1032-
├─ load & parse segments/sections
1032+
├─ use r_bin sections/symbols/relocs when available
1033+
│ or simple file-backed sections for fallback
10331034
│ ↓
1034-
segments { vaddr/vmaddr, size, perms, file mapping }
1035+
sections { vaddr, size, perms }
10351036
│ ↓
10361037
│ [text_lo, text_hi) (executable union)
10371038
1038-
├─ ELF only: apply DT_REL / DT_RELA / DT_RELR relative fixups
1039-
│ so data-segment pointer arrays match the runtime
1040-
│ state (addends resolved, RELR bitmap expanded).
1039+
├─ resolve g_CodeRegistration / g_MetadataRegistration
1040+
│ order: CLI -O / r2 eval vars / r2 flags / r_bin symbols
10411041
1042-
├─ scan each writable/data segment:
1043-
pass 1: {count32, pad32, ptr} tuple
1044-
(CodeRegistration-shaped anchor pair)
1045-
pass 2: {count32, ptr} generic
1042+
├─ parse Il2CppCodeRegistration:
1043+
v24.2+: match codeGenModules[] to metadata images and
1044+
copy each module's methodPointers[]
1045+
older: recover the global methodPointers[] pair
10461046
1047-
└─ accept if a sample of entries at *ptr[] lands in text,
1048-
either already (post-relocations) or after + base_vaddr
1049-
(raw RVA case). Emit absolute VAs, one per method index.
1047+
└─ fallback when forced or unresolved:
1048+
scan non-executable data/readable sections for {count, ptr}
1049+
pairs whose table entries land in executable code.
1050+
Emit absolute VAs, one per method index.
10501051
```
10511052

1052-
The heuristic is deliberately weaker than a structural
1053-
`Il2CppCodeRegistration` match, but it works on every supported
1054-
target and doesn't need symbol tables. It does, however, lock onto
1055-
**one** `{count, ptr}` array, which on v24.2+ means one image's
1056-
methods, not all of them (§3.1 / §3.7). A proper structural match
1057-
that walks `codeGenModules[]` is on the roadmap.
1053+
The structural path is preferred because it follows Unity's native
1054+
registration structures instead of guessing which `{count, ptr}` pair
1055+
is the method-pointer table. The fallback remains useful for stripped
1056+
binaries or builds where the registration symbols cannot be resolved.
1057+
The simple ELF/Mach-O/PE parsers do not reimplement full symbol-table
1058+
parsing; they use explicit registration addresses when provided and
1059+
otherwise feed their sections into the fallback scanner.
10581060

1059-
For ELF the relocation pass matters because the Android linker
1060-
produces method-pointer arrays almost entirely as
1061-
`R_AARCH64_RELATIVE` (type 1027) entries. Without applying them,
1062-
the raw array on disk is a run of zeros. Packed Android relocations
1063-
(`DT_ANDROID_RELA`, `DT_ANDROID_RELR`) are not handled yet and
1064-
cause the same "empty array" symptom on Play Store builds.
1065-
1066-
For Mach-O and PE the linker has already materialised concrete
1067-
values; no explicit relocation pass is required for the tables
1068-
r2unity currently scans.
1061+
Relocation handling is delegated to r_bin (`r_bin_patch_relocs`) on
1062+
the RBin path. The simple ELF parser also applies the common relative
1063+
REL/RELA/RELR forms so stripped Android/Linux inputs still have a
1064+
lightweight fallback.
10691065

10701066
## 7. Data we can extract today vs. data we do not
10711067

@@ -1083,7 +1079,7 @@ Already extracted by r2unity (library + CLI):
10831079
| referenced assemblies | flat int32 array |
10841080
| P/Invoke marker methods | `-P` enumeration |
10851081
| reverse-P/Invoke on v29+ | `-R` enumeration via BLOB |
1086-
| method-pointer VA (global) | ELF/Mach-O/PE heuristic |
1082+
| method-pointer VA | CodeRegistration parse + r_bin/simple-parser section-scan fallback |
10871083

10881084
Data present on disk / in the binary but **not yet consumed**:
10891085

@@ -1109,14 +1105,14 @@ Data present on disk / in the binary but **not yet consumed**:
11091105
metadata load in compiled code (§2.12).
11101106
- `fieldMarshaledSizes`, `unresolvedVirtualCall*`, WinRT tables,
11111107
`exportedTypeDefinitions`, RGCTX tables (§2.14–2.18).
1112-
- Native-side `CodeRegistration` and `MetadataRegistration` walk
1108+
- Native-side registration data beyond method pointers
11131109
`invokerPointers`, `customAttributeGenerators`,
11141110
`reversePInvokeWrappers`, `genericMethodPointers`,
1115-
`interopData`, `codeGenModules[]`, `types`, `fieldOffsets`,
1116-
`typeDefinitionsSizes`, `metadataUsages` (§3).
1117-
- Richer native scanning: per-module `methodPointers` on v24.2+,
1118-
packed Android relocations, Mach-O FAT multi-slice,
1119-
chained-fixups, PE import table.
1111+
`interopData`, `types`, `fieldOffsets`, `typeDefinitionsSizes`,
1112+
`metadataUsages` (§3).
1113+
- Richer native support: packed Android relocations and other loader
1114+
details not yet handled by r_bin for a given target, Mach-O FAT
1115+
multi-slice selection, chained-fixups, PE import table.
11201116

11211117
## 8. validation corpus
11221118

meson.build

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,28 @@ add_project_arguments(
1818
)
1919

2020
r_util_dep = dependency('r_util')
21+
r_bin_dep = dependency('r_bin')
2122

2223
conf = configuration_data()
2324
conf.set_quoted('R2UNITY_VERSION', meson.project_version())
2425
configure_file(output: 'r2unity_config.h', configuration: conf)
2526

2627
lib_inc = include_directories('src/lib')
2728
lib_sources = files(
28-
'src/lib/elf.c',
2929
'src/lib/lib.c',
30-
'src/lib/macho.c',
30+
'src/lib/bin/elf.c',
31+
'src/lib/bin/macho.c',
32+
'src/lib/bin/native.c',
33+
'src/lib/bin/pe.c',
3134
'src/lib/paths.c',
32-
'src/lib/pe.c',
3335
'src/lib/sbom.c',
3436
)
3537

3638
r2unity = executable(
3739
'r2unity',
3840
files('src/main.c') + lib_sources,
3941
include_directories: lib_inc,
40-
dependencies: r_util_dep,
42+
dependencies: [r_util_dep, r_bin_dep],
4143
install: true,
4244
)
4345

@@ -46,7 +48,6 @@ test('r2unity-version', r2unity, args: ['-v'])
4648

4749
plugins_opt = get_option('plugins')
4850
r_core_dep = dependency('r_core', required: plugins_opt)
49-
r_bin_dep = dependency('r_bin', required: plugins_opt)
5051
build_plugins = plugins_opt.enabled() or (
5152
plugins_opt.auto() and r_core_dep.found() and r_bin_dep.found()
5253
)
@@ -72,7 +73,7 @@ if build_plugins
7273
'core_r2unity',
7374
files('src/r2/core_r2unity.c') + lib_sources,
7475
include_directories: lib_inc,
75-
dependencies: r_core_dep,
76+
dependencies: [r_core_dep, r_bin_dep],
7677
name_prefix: '',
7778
install: true,
7879
install_dir: r2_plugindir,
@@ -82,7 +83,7 @@ if build_plugins
8283
'bin_r2unity',
8384
files('src/r2/bin_r2unity.c') + lib_sources,
8485
include_directories: lib_inc,
85-
dependencies: r_bin_dep,
86+
dependencies: [r_bin_dep],
8687
name_prefix: '',
8788
install: true,
8889
install_dir: r2_plugindir,

0 commit comments

Comments
 (0)