Author SHA1 Message Date
pancake afb7e4368c Use r2-6.2.6 in the ci 2026-11-06 14:00:36 +01:00
Abhi c2f219c95a meson: guard the class-import test on the file's existence
test/test_import.py is referenced but not committed on all branches,
which makes meson configure abort on Windows with
ERROR: File test/test_import.py does not exist. Wire the test only
when the file is present.
2026-10-18 11:45:15 +05:30
Abhi 4a35664331 Fix bindist triplet naming on runners with older r2pm
CI's bindist job pins radare2 6.2.2, whose r2pm does not provide
R2PM_OS/ARCH/BITS yet, so the unix builds produced
r2unity-6.2.2---.zip. Fall back to uname (darwin/linux, x86/arm)
like the radare2-pm bindist recipes expect.
2026-10-18 11:18:47 +05:30
pancake 31cb6bce07 Improved companion paths detection 2026-10-08 00:54:55 +02:00
pancake ae094c666a Apply interop with -AAA 2026-10-08 00:47:31 +02:00
pancake ccaa476cac Add more -A levels for analysis info 2026-10-08 00:44:32 +02:00
pancake 0bcdb6b8aa Add r2unity-A to import class names 2026-10-08 00:37:42 +02:00
pancake 212085e7fb Fix method offsets, map native code and silence class imports 2026-10-08 00:34:02 +02:00
pancake aa3c6c6254 Register pkgname 2026-10-06 16:55:45 +02:00
pancake 8d46d02b38 Release 1.2.8 2026-10-06 00:37:15 +02:00
pancake a0c7354c37 Fix ELF fast parser bounds checks 2026-10-04 23:50:24 +02:00
dependabot[bot] 67471a35d3 Bump softprops/action-gh-release from 3.0.0 to 3.0.3 2026-10-04 23:18:56 +02:00
dependabot[bot] 12620d1557 Bump actions/setup-python from 6 to 7 2026-10-04 23:18:41 +02:00
dependabot[bot] 06a21356e7 Bump actions/checkout from 6 to 7 2026-10-04 23:18:26 +02:00
pancake 26d3db86c3 Use sdb_num_setf instead char[]+snprintf+sdb_num_set 2026-09-30 12:09:51 +02:00
pancake 6681f1af93 Pin r2v to 6.2.2 2026-09-30 11:54:43 +02:00
pancake 08876f2aa3 Binary dists optimized for r2pm -b 2026-09-30 11:02:37 +02:00
pancake 60267a76e8 Release 1.2.6 2026-09-07 14:17:29 +02:00
pancake 57cda9236e Add typed IL2CPP method metadata 2026-08-25 16:13:22 +02:00
pancake 80701514cd Add support for Windows builds in the ci 2026-08-08 12:51:04 +02:00
pancake 6afe8d65d2 Release 1.2.4 2026-08-08 12:34:00 +02:00
pancake 45dc3b8243 Simpler fix #7 for the nongnu echo - thanks @stygian-ash 2026-08-08 12:34:00 +02:00
pancake 57da6163f6 Fix build for ABIVERSION >= 125 (r2-6.2.0) - thanks @stygian-ash 2026-08-08 12:33:51 +02:00
pancake 1c7b79308a Use r2's gha v3 2026-07-17 16:47:43 +02:00
pancake 2d2299c1c7 Add support for sidercar .resS dumpers 2026-07-17 00:37:31 +02:00
pancake b468bcd592 Add support for resource packs 2026-07-16 21:49:29 +02:00
pancake 474d01e9e5 Add support fr BGDatabases resource files 2026-07-16 21:25:31 +02:00
pancake c9a64a8635 Initial suppot for SerializedFile v22 container 2026-07-16 21:02:30 +02:00
pancake a50a18325c Document all resource formats used in Unity 2026-07-16 20:37:59 +02:00
pancake 540e6ffb16 Release 1.2.2 2026-07-06 19:35:51 +02:00
pancake 2b0320c64f Fix reverse wrapper validation and ELF cleanup 2026-06-26 09:24:45 +02:00
pancake 51701f2fb9 Add r2flags to the native CodeRegistration tables 2026-06-25 02:08:59 +02:00
pancake 5bac14c42e More informative in the native_tables in json 2026-06-25 01:46:49 +02:00
pancake 7420757a62 Improve fuzzy codegenModule symbol resolution 2026-06-25 01:11:04 +02:00
pancake 71f2189d84 Cleanup the CodeRegistration symbol resolution logic 2026-06-25 00:44:51 +02:00
pancake 7e447c8613 Release 1.2.0 2026-06-23 16:43:01 +02:00
pancake 6768e22008 Sanitize image names in emitted r2 commands 2026-06-10 16:58:44 +02:00
pancake 50a7fcc804 Documentation cleanup 2026-06-08 12:32:03 +02:00
pancake 4a52bc9db3 Initial support for non-heuristics using r2 bin parsers
* Keep the simple bin parsers for fast carving
* Configurable pointers via config vars
* Use RGetOpt
2026-06-08 11:42:44 +02:00
pancake e4f63aef69 Release 1.1.0 2026-06-03 09:54:56 +02:00
pancake ca5867eb5c Register class inheritance and fields using r2 6.1.6 commands 2026-06-02 20:23:21 +02:00
pancake d9fae9a6f0 Fix build with r2abi>=104 for rvec strings 2026-06-02 19:03:11 +02:00
pancake 095d602186 Dump class info with -c flag 2026-06-02 18:52:43 +02:00
34 changed files with 9003 additions and 1886 deletions

No files matched your search

+137 -11
View File
@@ -1,5 +1,8 @@
name: CI
env:
R2V: 6.2.6
on:
push:
branches:
@@ -25,11 +28,13 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install radare2
id: r2
uses: radareorg/github-actions@v2
uses: radareorg/github-actions@v3
with:
version: ${{ env.R2V }}
- name: Build
run: make
@@ -46,11 +51,13 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install radare2
id: r2
uses: radareorg/github-actions@v2
uses: radareorg/github-actions@v3
with:
version: ${{ env.R2V }}
- name: Install deps
run: pip install meson ninja --break-system-packages
@@ -69,11 +76,11 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install radare2 from git
id: r2
uses: radareorg/github-actions@v2
uses: radareorg/github-actions@v3
with:
from-git: true
@@ -89,7 +96,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -98,7 +105,9 @@ jobs:
- name: Install radare2
id: r2
uses: radareorg/github-actions@v2
uses: radareorg/github-actions@v3
with:
version: ${{ env.R2V }}
- name: Build Debian package
run: make -C dist/debian SUDO=
@@ -110,6 +119,107 @@ jobs:
if-no-files-found: error
archive: false
windows:
name: Build Windows package
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: 3.12.x
- name: Setup MSVC
uses: TheMrMilchmann/setup-msvc-dev@v4
with:
arch: x64
- name: Install build dependencies
run: python -m pip install meson ninja
- name: Install radare2
id: r2
uses: radareorg/github-actions@v3
with:
version: ${{ env.R2V }}
- name: Configure
run: meson setup build-windows --buildtype=release -Dplugins=enabled
- name: Build
run: meson compile -C build-windows
- name: Test
run: meson test -C build-windows --print-errorlogs
- name: Package Windows build
id: package
shell: pwsh
run: |
$project = meson introspect build-windows --projectinfo | ConvertFrom-Json
$package = "r2unity-$($project.version)-w64"
$files = @(
"build-windows\r2unity.exe",
"build-windows\core_r2unity.dll",
"build-windows\bin_r2unity.dll"
)
New-Item -ItemType Directory -Path $package | Out-Null
foreach ($file in $files) {
if (-not (Test-Path $file)) {
throw "Missing Windows build output: $file"
}
Copy-Item $file -Destination $package
}
$archive = "$package.zip"
Compress-Archive -Path "$package\*" -DestinationPath $archive -Force
"archive=$archive" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Package r2pm binary
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path bindist\plugins,bindist\bin | Out-Null
Copy-Item build-windows\core_r2unity.dll,build-windows\bin_r2unity.dll bindist\plugins
Copy-Item build-windows\r2unity.exe bindist\bin
Compress-Archive -Path bindist\* -DestinationPath "r2unity-$env:R2V-windows-x86-64.zip" -Force
- name: Upload Windows package artifact
uses: actions/upload-artifact@v7
with:
name: windows-package
path: |
${{ steps.package.outputs.archive }}
r2unity-*-windows-x86-64.zip
if-no-files-found: error
bindist:
name: Build r2pm binary (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install radare2
uses: radareorg/github-actions@v3
with:
version: ${{ env.R2V }}
- name: Package r2pm binary
run: make bindist
- name: Upload r2pm binary artifact
uses: actions/upload-artifact@v7
with:
name: bindist-${{ matrix.os }}
path: r2unity-*.zip
if-no-files-found: error
release:
name: Publish release
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
@@ -117,27 +227,42 @@ jobs:
- build
- meson
- debian
- windows
- bindist
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Download artifacts
- name: Download Debian package artifact
uses: actions/download-artifact@v8
with:
pattern: '*.deb'
path: dist/artifacts
merge-multiple: true
- name: Download Windows package artifact
uses: actions/download-artifact@v8
with:
name: windows-package
path: dist/artifacts
- name: Download r2pm binary artifacts
uses: actions/download-artifact@v8
with:
pattern: bindist-*
path: dist/artifacts
merge-multiple: true
- name: Generate release notes
run: ./release.sh notes > release-notes.md
- name: Create GitHub release
uses: softprops/[email protected].0
uses: softprops/[email protected].3
with:
name: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
@@ -147,3 +272,4 @@ jobs:
fail_on_unmatched_files: true
files: |
dist/artifacts/*.deb
dist/artifacts/*.zip
+3
View File
@@ -22,3 +22,6 @@
# Third-party reference checkouts (radare2, il2cpp dumper, etc.)
/third_party/
# r2pm binary packages
/r2unity-*.zip
+58 -13
View File
@@ -17,12 +17,12 @@ BIN_PLUGIN = src/r2/bin_r2unity.$(SOEXT)
PLUGINS = $(CORE_PLUGIN) $(BIN_PLUGIN)
CC = gcc
CFLAGS = -Wall -Wextra -g -I. $(shell pkg-config --cflags r_util 2>/dev/null || echo "")
LDFLAGS = $(shell pkg-config --libs r_util 2>/dev/null || echo "")
CFLAGS = -Wall -Wextra -g -I. $(shell pkg-config --cflags r_util r_bin 2>/dev/null || echo "")
LDFLAGS = $(shell pkg-config --libs r_util r_bin 2>/dev/null || echo "")
# r_core plugin flags (full radare2)
CORE_PLUGIN_CFLAGS = -Wall -Wextra -g -fPIC $(shell pkg-config --cflags r_core 2>/dev/null || echo "")
CORE_PLUGIN_LDFLAGS = $(shell pkg-config --libs r_core 2>/dev/null || echo "")
CORE_PLUGIN_CFLAGS = -Wall -Wextra -g -fPIC $(shell pkg-config --cflags r_core r_bin 2>/dev/null || echo "")
CORE_PLUGIN_LDFLAGS = $(shell pkg-config --libs r_core r_bin 2>/dev/null || echo "")
# r_bin plugin flags
BIN_PLUGIN_CFLAGS = -Wall -Wextra -g -fPIC $(shell pkg-config --cflags r_bin 2>/dev/null || echo "")
@@ -30,8 +30,12 @@ BIN_PLUGIN_LDFLAGS = $(shell pkg-config --libs r_bin 2>/dev/null || echo "")
R2_USER_PLUGINS = $(shell r2 -H R2_USER_PLUGINS 2>/dev/null)
LIB_SRCS = $(wildcard src/lib/*.c)
BGDATABASE_LIB_SRCS = src/lib/bgdatabase.c
RESOURCE_PACK_LIB_SRCS = src/lib/resource_pack.c
BIN_ONLY_LIB_SRCS = $(BGDATABASE_LIB_SRCS) $(RESOURCE_PACK_LIB_SRCS)
LIB_SRCS = $(filter-out $(BIN_ONLY_LIB_SRCS),$(wildcard src/lib/*.c)) $(wildcard src/lib/bin/*.c)
LIB_OBJS = $(LIB_SRCS:.c=.o)
LEGACY_OBJS = src/lib/elf.o src/lib/macho.o src/lib/pe.o src/lib/native.o
CLI_SRCS = src/main.c
CLI_OBJS = $(CLI_SRCS:.c=.o)
OBJS = $(CLI_OBJS) $(LIB_OBJS)
@@ -40,12 +44,20 @@ R2PM_BINDIR=$(shell r2pm -H R2PM_BINDIR)
all: $(EXEC)
plugin: $(PLUGINS)
r2 plugin: $(PLUGINS)
test: $(EXEC) $(PLUGINS)
python3 test/test_import.py
install-plugin: $(PLUGINS)
@[ -n "$(R2_USER_PLUGINS)" ] || (echo "r2 not found; cannot resolve R2_USER_PLUGINS"; exit 1)
mkdir -p "$(R2_USER_PLUGINS)"
cp -f $(PLUGINS) "$(R2_USER_PLUGINS)/"
# Replace the inode so macOS does not reuse a stale code-signature cache.
@for plugin in $(PLUGINS); do \
name=$${plugin##*/}; \
cp -f "$$plugin" "$(R2_USER_PLUGINS)/.$$name.tmp" && \
mv -f "$(R2_USER_PLUGINS)/.$$name.tmp" "$(R2_USER_PLUGINS)/$$name" || exit 1; \
done
uninstall-plugin:
@[ -n "$(R2_USER_PLUGINS)" ] || (echo "r2 not found; cannot resolve R2_USER_PLUGINS"; exit 1)
@@ -55,18 +67,51 @@ uninstall-plugin:
fmt:
clang-format-radare2 src/**/*.c
user-install: install-plugin
user-install: $(EXEC) install-plugin
mkdir -p "$(R2PM_BINDIR)"
cp -f $(EXEC) "$(R2PM_BINDIR)"
cp -f $(EXEC) "$(R2PM_BINDIR)/.$(EXEC).tmp"
mv -f "$(R2PM_BINDIR)/.$(EXEC).tmp" "$(R2PM_BINDIR)/$(EXEC)"
user-uninstall: uninstall-plugin
rm -f "$(R2PM_BINDIR)/$(EXEC)"
# Binary package for r2pm -bi: plugins/ and bin/ for this r2 version and platform
# Binary package for r2pm -bi: plugins/ and bin/ for this r2 version and platform
# R2PM_OS/ARCH/BITS require a recent r2pm; fall back to uname when missing
R2PM_OS_SH = $(shell r2pm -H R2PM_OS 2>/dev/null)
R2PM_ARCH_SH = $(shell r2pm -H R2PM_ARCH 2>/dev/null)
R2PM_BITS_SH = $(shell r2pm -H R2PM_BITS 2>/dev/null)
ifeq ($(R2PM_OS_SH),)
ifeq ($(shell uname -s),Darwin)
R2PM_OS_SH = darwin
else
R2PM_OS_SH = linux
endif
endif
ifeq ($(R2PM_ARCH_SH),)
ifeq ($(shell uname -m),x86_64)
R2PM_ARCH_SH = x86
else ifneq (,$(filter $(shell uname -m),aarch64 arm64))
R2PM_ARCH_SH = arm
else
R2PM_ARCH_SH = $(shell uname -m)
endif
endif
ifeq ($(R2PM_BITS_SH),)
R2PM_BITS_SH = 64
endif
BINDIST_ZIP = r2unity-$(shell r2 -qv)-$(R2PM_OS_SH)-$(R2PM_ARCH_SH)-$(R2PM_BITS_SH).zip
bindist: $(EXEC) $(PLUGINS)
rm -rf build/bindist $(BINDIST_ZIP)
$(MAKE) user-install R2_USER_PLUGINS=$(CURDIR)/build/bindist/plugins R2PM_BINDIR=$(CURDIR)/build/bindist/bin
cd build/bindist && zip -r $(CURDIR)/$(BINDIST_ZIP) plugins bin
$(EXEC): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
$(CONFIG_H): meson.build
echo '#define R2UNITY_VERSION "$(PROJECT_VERSION)"\n' > $@
echo '#define R2UNITY_VERSION "$(PROJECT_VERSION)"' > $@
$(CLI_OBJS): %.o: %.c $(CONFIG_H)
$(CC) $(CFLAGS) -c -o $@ $<
@@ -74,14 +119,14 @@ $(CLI_OBJS): %.o: %.c $(CONFIG_H)
$(CORE_PLUGIN): src/r2/core_r2unity.c $(LIB_SRCS)
$(CC) $(CORE_PLUGIN_CFLAGS) $(PLUGIN_SHFLAGS) -o $@ $^ $(CORE_PLUGIN_LDFLAGS)
$(BIN_PLUGIN): src/r2/bin_r2unity.c $(LIB_SRCS)
$(BIN_PLUGIN): src/r2/bin_r2unity.c $(LIB_SRCS) $(BIN_ONLY_LIB_SRCS)
$(CC) $(BIN_PLUGIN_CFLAGS) $(PLUGIN_SHFLAGS) -o $@ $^ $(BIN_PLUGIN_LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(EXEC) $(OBJS) $(PLUGINS) $(CONFIG_H)
rm -f $(EXEC) $(OBJS) $(LEGACY_OBJS) $(PLUGINS) $(CONFIG_H)
.PHONY: all clean plugin install-plugin uninstall-plugin fmt
.PHONY: all clean plugin install-plugin uninstall-plugin fmt bindist test
+90 -9
View File
@@ -16,11 +16,20 @@ IL2CPP binary, and exposes managed metadata for reverse engineering.
Linux, and flat fixture layouts.
- Recovers managed images, assemblies, types, methods, method flags, and `ldstr`
string literals.
- Finds method-pointer tables heuristically in ELF, Mach-O, and PE binaries.
- Resolves method pointers through r_bin symbols/CodeRegistration, with
r_bin or simple ELF/Mach-O/PE section-scan fallback for stripped binaries.
- Lists P/Invoke and v29+ reverse-P/Invoke metadata, and emits CycloneDX 1.5
SBOMs for managed assemblies.
- Provides both a core r2 command plugin and an `r_bin` plugin for direct
`global-metadata.dat` inspection.
- Parses IL2CPP `*.dll-resources.dat` packs and exposes named payloads through
radare2's resource listing and extraction APIs.
- Recognizes Unity SerializedFile v22 asset databases and exposes their
headers, object ranges, names, symbols, classes, dependencies, and resources.
- Resolves bounded `Texture2D`/`Mesh` `.resS` and `AudioClip`/`VideoClip`
`.resource` ranges beside loose SerializedFiles for `iU`/`iUx` extraction.
- Recognizes BGDatabase v6 repositories and whole-database saves, including
addon payloads, table regions, fields, symbols, and validated UTF-8 values.
## Build
@@ -47,7 +56,7 @@ r2pm -ci r2unity
```
`make` builds the CLI. `make plugin` builds `core_r2unity` and `bin_r2unity`.
`make user-install` installs both the CLI and plugins for the current user.
`make user-install` installs the CLI and plugins for the current user.
Meson builds the CLI by default, matching `make`. Use `-Dplugins=enabled` to
build the radare2 plugins too, or `-Dr2_plugindir=/path/to/plugins` to override
the plugin install directory.
@@ -67,6 +76,9 @@ The normal inputs are the native IL2CPP binary and the matching
# recover method flags/comments as r2 commands
./r2unity -f /path/to/GameAssembly.dll /path/to/global-metadata.dat > methods.r2
# override a known native registration symbol address
./r2unity -f -O g_CodeRegistration=0x1234 /path/to/GameAssembly.dll /path/to/global-metadata.dat
# list managed strings, interop metadata, or managed-assembly SBOM data
./r2unity -z /path/to/global-metadata.dat
./r2unity -P -j /path/to/GameAssembly.dll /path/to/global-metadata.dat
@@ -81,30 +93,99 @@ After installing the plugins, open a Unity binary in r2 and use:
```text
r2unity? show help
r2unity-A import classes and method flags (same as .r2unity-c*)
r2unity-AA also import method comments, strings, native tables and interop
r2unity-AAA also analyze native references (aar)
r2unity-AAAA also perform native analysis (aaa)
r2unity-D detect and cache companion file paths
r2unity-L open/select and map the IL2CPP native library
r2unity-c[*j] list classes, or emit an import script / JSON
r2unity-i[j] show metadata summary
r2unity-s apply managed method flags/comments
r2unity-s* print the r2 commands instead of applying them
r2unity-z[j] list managed string literals
r2unity-P[*j] list P/Invoke entries
r2unity-R[*j] list reverse-P/Invoke entries
r2unity-z[+j] list managed string literals (+ imports them)
r2unity-P[+*j] list P/Invoke entries (+ imports flags and comments)
r2unity-R[+*j] list reverse-P/Invoke entries (+ imports known wrappers)
r2unity-S emit managed-assembly SBOM text summary
r2unity-Sj emit managed-assembly CycloneDX JSON
```
Use `r2unity-A` (equivalent to `.r2unity-c*`) to import classes and method
flags. The script loads the native companion library automatically, so
`s sym.unity.<class>.<method>` followed by `pd` shows the method's native code.
Methods without a native implementation retain address zero and do not get
a seekable flag.
The analysis levels are cumulative. `r2unity-AA` adds method signatures and
comments, native registration tables, managed strings, P/Invoke method flags
and comments, and known reverse-P/Invoke wrappers. It maps `global-metadata.dat`
read-only at an unused address range named `r2unity.metadata`. Seek to
`str.unity.<index>` to read a literal's actual bytes; these addresses refer to
the mapped metadata, rather than runtime managed string objects. You can also
import strings or interop separately with `r2unity-z+`, `r2unity-P+`, and
`r2unity-R+`.
`r2unity-AAA` performs all of those imports and then runs `aar` to find native
references. `r2unity-AAAA` additionally runs `aaa` for deeper native analysis.
These two levels take longer on large binaries.
Set `r2unity.metadata` and `r2unity.library` manually when auto-detection is not
enough. The `bin_r2unity` plugin also lets radare2/rabin2 treat
enough. iOS detection checks both the app's `Data` directory and
`Frameworks/UnityFramework.framework/Data` for managed metadata.
The `bin_r2unity` plugin also lets radare2/rabin2 treat
`global-metadata.dat` as a binary format, exposing sections, strings, symbols,
classes, imports, libraries, and header fields.
classes, imports, libraries, and header fields. It also recognizes loose Unity
SerializedFile v22 inputs such as `sharedassets*.assets`:
```sh
rabin2 -I sharedassets10.assets
rabin2 -S sharedassets10.assets
rabin2 -s sharedassets10.assets
rabin2 -U sharedassets10.assets
rabin2 -xU sharedassets10.assets
```
Streamed resources are extracted when the referenced `.resS` or `.resource`
file is beside its owning SerializedFile. Missing sidecars remain visible in
`-U` output, but extraction fails safely instead of reading the same offset
from the `.assets` file.
BGDatabase repositories and saves produced by `BGRepo.I.Save()` are handled by
the same `r2unity` bin plugin:
```sh
rabin2 -I SaveFile.dat
rabin2 -S SaveFile.dat
rabin2 -s SaveFile.dat
rabin2 -z SaveFile.dat
```
IL2CPP manifest-resource packs use the same bin plugin:
```sh
rabin2 -U System.Drawing.dll-resources.dat
rabin2 -jU System.Drawing.dll-resources.dat
rabin2 -xU -o extracted System.Drawing.dll-resources.dat
```
## Current Limits
- v24.0 metadata, v36/v37 metadata, and WebAssembly are not supported.
- Method-pointer recovery is heuristic; manual `-a` / `-c` pointer reads are
not implemented yet.
- Method-pointer recovery needs CodeRegistration symbols/addresses or the
section-scan fallback; manual `-a` pointer reads are not implemented yet.
- P/Invoke and reverse-P/Invoke output is metadata-first and does not fully
recover native wrapper addresses or every `DllImportAttribute` detail.
- SBOM output covers managed assemblies only, not native dependencies or file
hashes.
- SerializedFile support currently targets format v22. Object naming and
payload decoding cover the common built-in classes needed by the reference
asset; arbitrary stripped type trees and managed script schemas remain to be
implemented.
- Loose sibling `.resS` and `.resource` extraction is supported for the known
v22 stream layouts. Archive-member paths and other version-specific object
layouts still require UnityFS and additional class fixtures.
- BGDatabase support currently targets the structurally validated v6 layout.
It exposes unknown proprietary field payloads as table sections until more
field types and format versions are recovered from additional fixtures.
Deep technical notes live in `doc/`.
+107
View File
@@ -0,0 +1,107 @@
# IL2CPP Metadata vs Native Binary
Unity IL2CPP builds split managed-code information across two files:
- `global-metadata.dat`
- the native IL2CPP binary (`GameAssembly.dll`, `GameAssembly.so`,
`GameAssembly.dylib`, `libil2cpp.so`, `UnityFramework`, etc.)
They must be read together to recover named native symbols and runtime
layouts.
## `global-metadata.dat`
`global-metadata.dat` is the managed/logical metadata blob. It is
platform-independent for a given IL2CPP run: it stores byte offsets and
dense table indices, not native pointers.
Typical contents:
- metadata magic, version, and table-of-contents header
- assemblies and images
- type definitions for classes, structs, enums, and interfaces
- method definitions: names, signatures, parameters, return types,
tokens, and method indices
- field definitions
- properties and events
- nested-type, interface, and vtable index tables
- generic containers, generic parameters, and method specs
- custom-attribute and default-value blobs, depending on metadata
version
- identifier strings and managed `ldstr` literal payloads
It does not contain:
- native method bodies
- method RVAs or function addresses
- final field offsets
- final type sizes
- native vtables, invokers, wrappers, or trampolines
- Unity asset, scene, prefab, or AssetBundle data
In short, the `.dat` file says what managed things exist and how they
are named, typed, and indexed.
## Native IL2CPP binary
The native binary is the physical/runtime side of the same build. It
contains the compiled machine code and the registration structures that
tie the generated code back to metadata indices.
Important native-side data:
- compiled method bodies
- `Il2CppCodeRegistration`
- `Il2CppMetadataRegistration`
- `methodPointers`
- `genericMethodPointers`
- `reversePInvokeWrappers`
- `invokerPointers`
- `codeGenModules`
- `fieldOffsets`
- `typeDefinitionsSizes`
- native type and generic-instantiation tables
- runtime helper code, wrappers, trampolines, and runtime strings
These tables provide the addresses and runtime layouts that
`global-metadata.dat` intentionally does not carry. `doc/ptrtables.md`
is the detailed native pointer-table reference.
## Mapping model
The two files are correlated by indices:
```text
global-metadata.dat native IL2CPP binary
------------------- --------------------
MethodDefinition.methodIndex --> CodeRegistration / CodeGenModule methodPointers[index]
TypeDefinition index --> MetadataRegistration.fieldOffsets[type]
Type/generic indices --> MetadataRegistration.types and generic tables
```
For r2unity this means:
- parsing only `global-metadata.dat` can recover names, signatures,
tokens, string literals, and table relationships
- resolving those names to native addresses requires locating
`Il2CppCodeRegistration` in the binary; r2unity accepts an explicit
address, an r2 flag/r_bin symbol, or the r_bin/simple-parser
section-scan fallback
- recovering field offsets and final type sizes requires
`Il2CppMetadataRegistration`
- a complete symbol map needs both files from the same build
Today r2unity takes managed structure from `.dat` and native addresses
from the binary:
- `.dat`: image/type/method rows, method indices, names, tokens,
strings, assemblies, P/Invoke metadata, and reverse-P/Invoke
attribute metadata
- binary: executable address ranges, native symbols/flags for
`g_CodeRegistration` and `g_MetadataRegistration`, and method pointer
tables reached from CodeRegistration or the r_bin/simple-parser
fallback scan
The current method-address path only needs `g_CodeRegistration`.
`g_MetadataRegistration` is tracked as the companion anchor for native
layout work such as field offsets and type sizes.
+26 -220
View File
@@ -242,7 +242,7 @@ Size: **12 B** (v≤24.0) / **8 B** (v≥24.1). Sliced by
`typeDef.fieldStart` / `typeDef.field_count`.
Value: names and types for every field. Indirectly unlocks
`MetadataRegistration.fieldOffsets` (§3.12), which is what actually
`MetadataRegistration.fieldOffsets` (`doc/ptrtables.md`), which is what actually
lets us label `ldr X, [x0, #0x18]` as `Player.health`. Without
fields, struct-internal code is unreadable.
@@ -335,7 +335,8 @@ typedef struct { /* Il2CppGenericParameter, 16 B */
Value: reconstruct `Dictionary<TKey, TValue> where TKey :
IComparable<TKey>`. Gate for using `methodSpecs` / `genericInsts`
(§3) to emit real instantiation names like `List<int>::Add`.
(`doc/ptrtables.md`) to emit real instantiation names like
`List<int>::Add`.
### 2.7 `interfaces` + `interfaceOffsets` + `vtableMethods`
@@ -358,7 +359,7 @@ IComparable<TKey>`. Gate for using `methodSpecs` / `genericInsts`
```
`usageType == 3` → concrete method definition,
`usageType == 6` → `methodSpecs` (§3.11) index.
`usageType == 6` → `methodSpecs` (`doc/ptrtables.md`) index.
Value: fully resolved virtual dispatch. Every `callvirt` / indexed
vtable load gets a real method name. `class Foo : IDisposable,
@@ -421,7 +422,8 @@ Cross-reference:
`encodedSourceIndex`'s top three bits encode `Il2CppMetadataUsage ∈
{ Invalid, TypeInfo, Il2CppType, MethodDef, FieldInfo,
StringLiteral, MethodRef }`. `destinationIndex` is the slot in the
native `MetadataRegistration.metadataUsages[]` pointer table (§3.14).
native `MetadataRegistration.metadataUsages[]` pointer table
(`doc/ptrtables.md`).
Value: on v19–v26 builds, the compiled code is full of `ldr X,
[g_MetadataUsages + offset]` loads. These tables plus
@@ -522,9 +524,8 @@ plugins via custom marshalling; safe to ignore for plain RE.
typedef struct { int start; int length; } Il2CppRange;
```
Correlates with
`CodeRegistration.unresolvedVirtualCallPointers` (§3.5 of
`r2unity.md`). Lets us name each unresolved-virtual-call stub with
Correlates with native unresolved-call pointer tables documented in
`doc/ptrtables.md`. Lets us name each unresolved-virtual-call stub with
its parameter signature.
### 2.17 `windowsRuntimeTypeNames` + `windowsRuntimeStrings` +
@@ -533,182 +534,13 @@ its parameter signature.
UWP / HoloLens only. For iOS + Android + desktop builds these sizes
are always zero; a defensive parser bounds-checks and skips.
## 3. Native binary — pointer arrays we don't follow
## 3. Native binary pointer-table reference
`src/lib/elf.c` and `src/lib/macho.c` currently locate exactly one
array: `CodeRegistration.methodPointers` (or its v24.2+ per-image
equivalent, if the heuristic happens to land on the right
`Il2CppCodeGenModule`). The registration structures actually expose
many more pointer arrays, each with its own metadata table partner.
Each entry below is a `{ ulong count; ulong ptr; }` pair inside the
registration (see §3 of `doc/r2unity.md` for the full struct).
### 3.1 `methodPointers` (≥v24.1 global; v≥24.2 per-module)
What r2unity extracts today. From v24.2 onwards this field is on
`Il2CppCodeGenModule`, **not** `CodeRegistration`, and there is one
module per image. If r2unity finds a single `{count, ptr}` on a
v24.2+ binary it is only extracting **one image's** methods.
Walking `codeGenModules[]` and enumerating each module is mandatory
for full coverage.
### 3.2 `invokerPointers`
Reflection-dispatch invokers — one per unique method-signature
shape (return type + param count + blittable-ness). Methods
reference them via `methodDef.invokerIndex` (≤v24.1) or
`Il2CppCodeGenModule.invokerIndices[]` (v≥24.2).
Value: naming invokers doesn't rename methods directly, but it
clusters methods by signature and makes reflection-based hook
points discoverable.
### 3.3 `customAttributeGenerators` (pre-v29)
Per-range `void(void*)` — each one news up the attribute objects
for one range. Given `attributesInfo` (§2.9), a tiny
arch-specific interpreter walking the generator body recovers every
attribute argument (strings, `typeof` references, enums). Required
for pre-v29 reverse-P/Invoke DLL-name recovery.
### 3.4 `reversePInvokeWrappers` (v22+)
Array of native-to-managed thunks for `[MonoPInvokeCallback]`
methods. Every callback the game registers with a C library routes
through these. Naming them `ReversePInvoke_<method>` lights up
native plugin callback surfaces (ads SDKs, analytics, audio,
networking).
### 3.5 `genericMethodPointers` + `genericAdjustorThunks`
Array of function pointers for every generic-method instantiation
present in the binary. Bridged to metadata via
`MetadataRegistration.genericMethodTable` and `methodSpecs`:
```c
foreach (genericMethodTable[k]) {
spec = methodSpecs[k.genericMethodIndex];
addr = genericMethodPointers[k.indices.methodIndex];
/* emit "List<int>::Add @ 0x401234" */
}
```
`genericAdjustorThunks` (v24.5, v27.1+) are struct-vs-class `T`
fixup thunks: `mov x0, [x0, #8]; b real_body` (skip the
`Il2CppObject` header for value-type `T`). Labelling both separates
the thunk from the real body.
Value: enormous on generics-heavy games. Without this, every
instantiation is a distinct unnamed `sub_xxx`.
### 3.6 `unresolvedVirtualCallPointers` (v22..v29); split at v29.1
into `unresolvedInstanceCallPointers` +
`unresolvedStaticCallPointers`
Stubs the runtime patches when a virtual / interface call target
becomes known. 1:1 with the metadata ranges in §2.16.
### 3.7 `interopData` (v23+)
Array of `Il2CppInteropData`, each containing a bundle of
P/Invoke / COM-interop function pointers for one type. See §3.6 of
`doc/r2unity.md` for the struct. Lights up every `[DllImport]`
target wrapper and every COM bridge (CCW/RCW).
### 3.8 `codeGenModules` (v24.2+)
One `Il2CppCodeGenModule` per image. Struct definition in §3.7 of
`doc/r2unity.md`.
Highlights that are particularly useful on obfuscated games:
- `moduleInitializer` — each image's `<Module>..cctor`, the per-
image static-init entry point.
- `staticConstructorTypeIndices` — types whose `.cctor` runs at
load. Prime targets when keys/strings are materialised at
startup.
- `adjustorThunks` + `adjustorThunkCount` — the generic-method
struct-vs-class fixup thunks.
### 3.9 `MetadataRegistration.types` — the `Il2CppType` pool
Every `typeIndex` anywhere in metadata indexes this array. Without
it, we can't resolve any method return type, parameter type, field
type, or interface name that goes beyond a plain `TypeDef`.
`Il2CppExecutor.GetTypeName`
(`third_party/Il2CppDumper/Utils/Il2CppExecutor.cs:61`) walks it
recursively to produce qualified names, decoding the ECMA-335
`ELEMENT_TYPE_*` byte (§II.23.1.16) to branch on:
- primitive (`I1`, `U4`, `R8`, …)
- `VALUETYPE`, `CLASS` (→ `typeDef.name`)
- `SZARRAY`, `ARRAY` (→ rank + element recursion)
- `PTR`, `BYREF` (→ pointee recursion)
- `VAR`, `MVAR` (→ generic-parameter name)
- `GENERICINST` (→ open type + type-argument tuple from
`genericInsts`)
- `FNPTR`, `MODOPT`, `MODREQ`
### 3.10 `MetadataRegistration.genericInsts` + `genericClasses`
Pool of concrete type-argument tuples (`(int)`, `(string, int)`,
`(Player, Dictionary<int, float>)`, …). Every generic-instantiation
reference in the binary indexes into this array.
### 3.11 `MetadataRegistration.methodSpecs` + `genericMethodTable`
See §3.9 of `doc/r2unity.md` for the structs and consumption
pattern. `GetMethodSpecName` reconstructs the full
`Foo<string>.Bar<int>` form.
### 3.12 `MetadataRegistration.fieldOffsets`
- Pre-v22: flat `int32_t[]` indexed by global field index.
- v22+: pointer-of-pointers — one `int32_t *` per type definition,
pointing at that type's field-offset array.
`GetFieldOffsetFromIndex`
(`third_party/Il2CppDumper/Il2Cpp/Il2Cpp.cs:274`) has the full
logic, including the `-8` / `-16` adjustment for non-static
value-type fields (to account for the `Il2CppObject` header:
`MonitorData *` + `Il2CppClass *`; see ECMA-335 §II.25.4 and Unity's
`Il2CppClass.cs`).
Value: the byte-level layout of every class / struct. Once read, we
can label every `ldr/str [this + N]` with a field name and type.
Second only to method pointers in RE value.
### 3.13 `MetadataRegistration.typeDefinitionsSizes`
```c
typedef struct {
uint32_t instance_size; /* sizeof(object) w/ Il2CppObject header */
uint32_t native_size; /* P/Invoke marshal size, 0 if N/A */
uint32_t static_fields_size;
uint32_t thread_static_fields_size;
} Il2CppTypeDefinitionSizes; /* 16 B */
```
Total size of every class / struct. Distinguishes 16-byte vs 24-byte
Vector types. Gives us the element stride for array indexing
(`ldr X, [Xn + Xindex * instance_size]`). `static_fields_size`
lets us label every static-field access once we find the per-class
statics pointer.
### 3.14 `MetadataRegistration.metadataUsages` (v19..v26)
The native side of §2.10. Huge `void *[]`. Each slot is an
`Il2CppClass *`, `Il2CppType *`, `MethodInfo *`, `FieldInfo *`,
managed `string *`, or generic `MethodInfo *` — decoded by
combining with `metadataUsageLists` + `metadataUsagePairs`.
Value: on pre-v27 binaries, labelling this array turns every
indirect metadata load in the disassembly into a readable reference.
From v27+ the mechanism was replaced by per-site inline globals and
this array is gone; labels must instead be derived per-site via
`codeGenModule.staticConstructorTypeIndices` + `moduleInitializer`
and code scanning.
Native registration structures and pointer-table shapes are documented in
`doc/ptrtables.md`. This file keeps only version and row-layout details;
put `Il2CppCodeRegistration`, `Il2CppMetadataRegistration`,
`Il2CppCodeGenModule`, field-offset, generic-method, interop, invoker,
and method-address recovery details in `doc/ptrtables.md`.
## 4. Implications for r2unity
@@ -756,50 +588,23 @@ and code scanning.
`third_party/Il2CppDumper/Il2Cpp/MetadataClass.cs` per wire
version.
2. **Per-version header struct.** v38+ is now represented as the
31-section header. Remaining gaps: distinguish v24.0 from v24.1+
headers so assembly / image entries are decoded correctly on
unresolved-virtual-call pointer arrays.
2. **Per-version header structs.** v38+ is represented as the
31-section header. Keep v24.0/v24.1+ discrimination explicit so
assembly, image, type, and method rows do not silently decode at
the wrong stride.
distinct wire version we claim to support (21, 22, 23, 24, 27,
29, 31, 38, 39). Pull candidates from the Il2CppDumper
issue-tracker corpus if new ones are needed.
3. **Version fixtures.** Keep at least one fixture per distinct wire
version we claim to support (24.1, 27, 29, 31, 38, 39), plus edge
cases for rejected v24.0 and v36/v37.
4. **Fail loudly on truly unknown versions.** v36/v37 are currently
rejected because their header shape is not proven. Keep that
confirm the layout.
behavior until a reliable reference or fixture confirms the layout.
### 4.2 Feature additions (coverage)
### 4.2 Feature additions
Ordered by RE value vs. implementation effort:
1. `parameters` table — small, unlocks real method signatures.
2. `fields` table — small, prerequisite for field offsets.
3. `CodeRegistration` / `MetadataRegistration` structural anchor
detection — unlocks everything in §3.
4. `MetadataRegistration.types` + `fieldOffsets` +
`typeDefinitionsSizes` — class layouts + field labels.
5. `codeGenModules` walk for v24.2+ — currently dropping most
methods on modern Unity.
6. Attributes bundle:
- pre-v29: `attributesInfo` + `attributeTypes` +
`customAttributeGenerators` thunk interpreter
- v29+: `attributeDataRange` + `attributeData` BLOB decoder
7. Generics bundle: `genericContainers` + `genericParameters` +
`methodSpecs` + `genericInsts` + `genericMethodPointers` +
`genericMethodTable`. Recovers `List<int>::Add`-style native
functions.
8. `properties` + `events` — recompose `get_X`/`set_X`/`add_X` /
`remove_X` into clean declarations.
9. `metadataUsageLists/Pairs` + `MetadataRegistration.metadataUsages`
— pre-v27 only, labels every indirect metadata load.
10. `interfaces` + `interfaceOffsets` + `vtableMethods` — full
resolved virtual dispatch tables.
11. `nestedTypes` — correct `Outer.Inner` naming.
12. Richer SBOM: native dependencies (`DT_NEEDED`, `LC_LOAD_DYLIB`,
PE import table), file hashes, ELF build IDs, Mach-O UUIDs, PE
debug GUIDs, SPDX output.
13. Manual `-a`/`-c` pointer read with pointer-width detection.
Implementation tasks live in `doc/todo.md`. Keep this document focused
on version/layout facts and portability constraints.
### 4.3 Architecture-neutral vs architecture-branching code
@@ -829,6 +634,7 @@ Ordered by RE value vs. implementation effort:
- v39 is confirmed by the local Unity 6 reproducer. v38 support is
based on the current Il2CppDumper v39 source layout; keep it
guarded by fixtures as samples become available.
- The local Unity install at
`/Applications/Unity/Hub/Editor/6000.4.5f1` ships IL2CPP sources
that assert `s_GlobalMetadataHeader->version == 39` in
+15 -34
View File
@@ -58,36 +58,18 @@ thunk. r2unity does not do this today.
### 0.3 Native-side registration structures (rich, unused by r2unity)
The IL2CPP runtime exposes interop targets through several fields
on `Il2CppCodeRegistration` and through one per-type struct:
The native table shapes are canonical in `doc/ptrtables.md`. For
interop, the important native records are:
```c
/* Il2CppCodeRegistration (relevant slice) */
ulong reversePInvokeWrapperCount;
ulong reversePInvokeWrappers; /* v22+ */
ulong customAttributeCount;
ulong customAttributeGenerators; /* removed at v29 */
ulong interopDataCount;
ulong interopData; /* v23+ */
- `CodeRegistration.reversePInvokeWrappers`
- pre-v29 `CodeRegistration.customAttributeGenerators`
- v23+ `CodeRegistration.interopData`
- `Il2CppInteropData` entries for per-type marshal/delegate/COM
wrappers
/* Il2CppInteropData — one per type involved in P/Invoke or COM */
typedef struct {
ulong delegatePInvokeWrapperFunction;
ulong pinvokeMarshalToNativeFunction;
ulong pinvokeMarshalFromNativeFunction;
ulong marshalToNativeFunction;
ulong marshalFromNativeFunction;
ulong marshalCleanupFunction;
ulong ccwFunction; /* COM CCW */
ulong guid; /* COM IID ptr */
ulong type; /* -> Il2CppType * */
} Il2CppInteropData;
```
This is where the **real native VAs** live: the actual marshalled
thunk that converts managed arguments to native, calls the
`[DllImport]` target, and unmarshals return values. r2unity does
not read any of this today.
This is where the real native VAs live: the actual marshalled thunk that
converts managed arguments to native, calls the `[DllImport]` target, and
unmarshals return values. r2unity does not read any of this today.
## 1. Managed → native: `[DllImport]` and r2unity's `-P`
@@ -197,7 +179,7 @@ and does not answer:
`Utils/CustomAttributeDataReader.cs` is the reference decoder.
- Locate `CodeRegistration` structurally (§3 of `doc/r2unity.md`),
- Locate `CodeRegistration` structurally (`doc/ptrtables.md`),
walk `customAttributeGenerators[range_index]`, step through the
native thunk body, recover immediate operands that correspond to
`string` ctor arguments. Architecture-specific interpreter work.
@@ -338,7 +320,7 @@ the **native trampoline stubs** that call them. Steps to close the
gap:
1. Structurally locate `Il2CppCodeRegistration` in the binary
(§3 of `doc/r2unity.md`).
(`doc/ptrtables.md`).
2. Read `reversePInvokeWrapperCount` and `reversePInvokeWrappers`.
3. For each Mach-O/ELF/PE image: walk `codeGenModules[]`, read
each module's `reversePInvokeWrapperIndices[]`, and for every
@@ -446,7 +428,6 @@ Once wrapper VAs land, `-r` will switch to real
- Il2CppDumper's `Utils/CustomAttributeDataReader.cs` — v29+ BLOB
decoder, authoritative reference.
<https://github.com/Perfare/Il2CppDumper>
- `doc/r2unity.md` §3 — full `Il2CppCodeRegistration` /
`Il2CppMetadataRegistration` field list.
- `doc/future.md` §2.9 and §3 — attribute table layouts per wire
version and the native-side interop pointer arrays.
- `doc/ptrtables.md` - full `Il2CppCodeRegistration` /
`Il2CppMetadataRegistration` pointer-table reference.
- `doc/future.md` §2.9 - attribute table layouts per wire version.
+788
View File
@@ -0,0 +1,788 @@
# IL2CPP Native Pointer Tables
Unity IL2CPP builds split information across two sides:
- `global-metadata.dat`: logical metadata tables, names, tokens, type
definitions, method definitions, field definitions, generic metadata,
and dense indices.
- The native IL2CPP binary: compiled code, registration structures,
function-pointer tables, layout tables, and runtime helper data.
The metadata file does not contain real code addresses. Native addresses
come from tables reachable from `Il2CppCodeRegistration` and
`Il2CppMetadataRegistration`, then are joined back to metadata by row
indices and per-image/per-type ranges.
## What r2unity Resolves Today
The current native recovery code resolves method bodies only:
- It may find or accept `Il2CppCodeRegistration`.
- It may find or accept `Il2CppMetadataRegistration`, but only reports
and caches that VA today.
- It copies recovered method body addresses into
`R2UnityNativeResult.method_ptrs[]`.
- It reports the source as `symbol`, `override`, or `heuristic`.
The current code does not yet parse the rest of
`Il2CppMetadataRegistration`:
- field offsets
- type sizes
- `Il2CppType` pool
- metadata usages
- generic method tables
- invoker pointers
- reverse-P/Invoke wrappers
- `Il2CppInteropData`
Those tables are documented below because they are the natural next
native-recovery targets.
## Minimal Binary Knowledge Required
The heuristic carver does not need a full semantic binary loader, but it
does need a memory view:
- pointer size: 4 or 8 bytes
- readable/data ranges to scan
- executable text range to validate code pointers
- base VA, for rebasing raw RVAs or low addresses
- a `ptr_at(va)` callback that maps a virtual address to file or memory
bytes
For ELF/Mach-O/PE files, this information usually comes from segments or
sections. With r2 loaded, r2unity asks r_bin for sections, symbols,
relocations, pointer size, and VA mapping. The simple fallback readers
read only enough format data to build the same generic view.
Relocations matter. Many IL2CPP tables are in relocatable data sections,
so raw file bytes can contain unpatched addends instead of final VAs. The
simple ELF path applies common relative relocations before scanning.
## Address Normalization
Native table entries may be absolute VAs or raw values that need the
module base added. r2unity treats a raw code pointer as valid when:
```text
raw is inside executable text
or
base_vaddr + raw is inside executable text
```
Data pointers are accepted when they can be dereferenced through the
current memory view.
This is intentionally conservative: a table candidate must point to
readable data, and sampled entries must point to executable code.
## How Method Indices Come From Metadata
For the supported modern metadata layouts, the method index is the row
ordinal in the decoded `Il2CppMethodDefinition[]` table:
```text
methods[0] -> method index 0
methods[1] -> method index 1
...
methods[n] -> method index n
```
The metadata header gives the method table offset and size. r2unity
decodes each row with a version-gated row width. The array index becomes
the native method-pointer index.
Types give structure over that flat method row array:
```text
Il2CppTypeDefinition.methodStart
Il2CppTypeDefinition.method_count
```
For a type:
```text
for k in 0..method_count-1:
method_index = methodStart + k
method = methods[method_index]
```
Images give a larger grouping over type rows:
```text
Il2CppImageDefinition.typeStart
Il2CppImageDefinition.typeCount
```
For modern per-image codegen modules, r2unity matches a native module
name to a metadata image name, walks that image's type range, and scatters
module-local method pointers into the global `method_ptrs[method_index]`
array.
Older metadata rows carried explicit native-side indices such as
`methodIndex`, `invokerIndex`, `delegateWrapperIndex`, and RGCTX ranges.
Those fields disappeared in the v24.1 row shrink. Modern recovery relies
more heavily on row order plus native codegen-module index arrays.
## Top-Level Native Anchors
The two important native anchors are:
```c
typedef struct {
uintptr_t methodPointersCount; /* pre-v24.2 only */
uintptr_t methodPointers; /* pre-v24.2 only */
uintptr_t reversePInvokeWrapperCount;
uintptr_t reversePInvokeWrappers;
uintptr_t genericMethodPointersCount;
uintptr_t genericMethodPointers;
uintptr_t genericAdjustorThunks; /* version-gated */
uintptr_t invokerPointersCount;
uintptr_t invokerPointers;
uintptr_t customAttributeCount;
uintptr_t customAttributeGenerators; /* removed in v29 */
uintptr_t unresolvedVirtualCallCount;
uintptr_t unresolvedVirtualCallPointers;
uintptr_t interopDataCount;
uintptr_t interopData; /* v23+ */
uintptr_t windowsRuntimeFactoryCount;
uintptr_t windowsRuntimeFactoryTable;
uintptr_t codeGenModulesCount; /* v24.2+ */
uintptr_t codeGenModules; /* v24.2+ */
} Il2CppCodeRegistration;
typedef struct {
intptr_t genericClassesCount;
uintptr_t genericClasses;
intptr_t genericInstsCount;
uintptr_t genericInsts;
intptr_t genericMethodTableCount;
uintptr_t genericMethodTable;
intptr_t typesCount;
uintptr_t types;
intptr_t methodSpecsCount;
uintptr_t methodSpecs;
intptr_t fieldOffsetsCount;
uintptr_t fieldOffsets;
intptr_t typeDefinitionsSizesCount;
uintptr_t typeDefinitionsSizes;
uintptr_t metadataUsagesCount;
uintptr_t metadataUsages;
} Il2CppMetadataRegistration;
```
The real struct shape is version-dependent. Parsers should not hardcode
one final layout across Unity versions. Treat these as the conceptual
field families.
## Table Shape Taxonomy
Not every native table is an array of pointers.
Common shapes:
- `void * []`: flat array of function/data pointers.
- `Struct * []`: array of pointers to structs.
- `Struct []`: array of inline structs.
- `int32_t []`: index array into another native or metadata table.
- `int32_t **`: pointer-of-pointers, usually one subarray per type.
- Count/pointer pairs inside a registration struct:
`{ count, pointer_to_table }`.
The heuristic carver looks only for the count/pointer shape and validates
that the pointed-to table looks like function pointers.
## CodeRegistration Tables
### `methodPointers`
Shape:
```text
void *methodPointers[]
```
Meaning:
- native address of compiled managed method bodies
- slot `i` maps to metadata method row `i` on older/global layouts
- zero entries are normal for abstract methods, interface declarations,
open generics, some extern/P-Invoke methods, and runtime intrinsics
Version behavior:
- pre-v24.2: global table is directly in `Il2CppCodeRegistration`
- v24.2+: global table disappears; method pointers move into each
`Il2CppCodeGenModule`
r2unity behavior:
- resolved today
- if `CodeRegistration` is known, it first tries the codegen-module path
and then the older global method-pointer path
- if no anchor is known, the heuristic scanner may find a single
`{count, ptr}` table, but on v24.2+ that may be only one image's
method table
### `codeGenModules`
Shape:
```text
Il2CppCodeGenModule *codeGenModules[]
```
Meaning:
- one module per metadata image/assembly
- introduced in v24.2
- each module owns per-image method pointer tables and several
per-image index tables
Core fields:
```c
typedef struct {
uintptr_t moduleName; /* char * */
intptr_t methodPointerCount;
uintptr_t methodPointers; /* void * [] */
uintptr_t adjustorThunks; /* void * [], version-gated */
uintptr_t adjustorThunkCount;
uintptr_t invokerIndices; /* int32_t [] per method */
uintptr_t reversePInvokeWrapperCount;
uintptr_t reversePInvokeWrapperIndices; /* int32_t [] */
intptr_t rgctxRangesCount;
uintptr_t rgctxRanges; /* range structs */
intptr_t rgctxsCount;
uintptr_t rgctxs; /* RGCTX structs */
uintptr_t debuggerMetadata;
uintptr_t customAttributeCacheGenerator; /* v27-era */
uintptr_t moduleInitializer; /* v27+ */
uintptr_t staticConstructorTypeIndices; /* v27+ int32_t [] */
uintptr_t metadataRegistration; /* v27+ */
uintptr_t codeRegistration; /* v27+ */
} Il2CppCodeGenModule;
```
Mapping model:
```text
moduleName -> metadata image name
image.typeStart/typeCount -> types belonging to this module
type.methodStart/method_count -> method rows
module.methodPointers[local_index] -> method_ptrs[method_index]
```
### `invokerPointers`
Shape:
```text
void *invokerPointers[]
```
Meaning:
- reflection-dispatch invokers
- not one per managed method
- usually one per method-signature/runtime-call shape
Association:
- old layouts: method row carried an `invokerIndex`
- v24.2+: `Il2CppCodeGenModule.invokerIndices[]` has one index per
module-local method
r2unity behavior:
- not parsed today
### `reversePInvokeWrappers`
Shape:
```text
void *reversePInvokeWrappers[]
```
Meaning:
- native-to-managed trampoline stubs for callbacks
- relevant to `[MonoPInvokeCallback]`, unmanaged callbacks, native SDK
hooks, audio callbacks, analytics callbacks, and similar surfaces
Association:
- old layouts had explicit wrapper index fields in method metadata rows
- v24.2+: `Il2CppCodeGenModule.reversePInvokeWrapperIndices[]` has one
index per method; non-negative entries index the global wrapper table
r2unity behavior:
- metadata-only reverse-P/Invoke enumeration exists
- wrapper VAs are not attached yet
### `genericMethodPointers`
Shape:
```text
void *genericMethodPointers[]
```
Meaning:
- native function bodies for concrete generic method instantiations
- separate from ordinary `methodPointers[]`
Association:
```text
MetadataRegistration.genericMethodTable[k]
-> methodSpecs[entry.methodIndex]
-> genericMethodPointers[entry.indices.methodIndex]
```
Value:
- recovers names for concrete instantiations such as
`List<int>.Add` or `Foo<string>.Bar<float>`
r2unity behavior:
- not parsed today
### `genericAdjustorThunks`
Shape:
```text
void *genericAdjustorThunks[]
```
Meaning:
- small thunks used for generic method calls that need value-type vs
reference-type adjustment
- often skip the `Il2CppObject` header before tail-calling the real body
Version behavior:
- appears in late v24.x / Unity 2019.4-era layouts
- appears again in v27.1+
r2unity behavior:
- not parsed today
### `customAttributeGenerators`
Shape:
```text
void (*customAttributeGenerators[])(void *)
```
Meaning:
- pre-v29 native thunks that construct custom attribute objects
- each thunk encodes constructor arguments in machine code
Version behavior:
- used before v29
- v29 replaces this with inline custom-attribute BLOB data in
`global-metadata.dat`
- v27-era layouts may move cache generators per module
r2unity behavior:
- not parsed/interpreted today
### `unresolvedVirtualCallPointers`
Shape:
```text
void *unresolvedVirtualCallPointers[]
```
Meaning:
- dispatch stubs used when virtual/interface call targets are resolved
dynamically
- metadata has parameter/range tables that can name these stubs
Version behavior:
- v22..v29: one unresolved virtual call pointer table
- v29.1+: split into instance and static unresolved call pointer arrays
r2unity behavior:
- not parsed today
### `interopData`
Shape:
```text
Il2CppInteropData interopData[]
```
This is an array of structs, not a flat pointer array.
Conceptual element:
```c
typedef struct {
uintptr_t delegatePInvokeWrapperFunction;
uintptr_t pinvokeMarshalToNativeFunction;
uintptr_t pinvokeMarshalFromNativeFunction;
uintptr_t marshalToNativeFunction;
uintptr_t marshalFromNativeFunction;
uintptr_t marshalCleanupFunction;
uintptr_t ccwFunction;
uintptr_t guid;
uintptr_t type; /* Il2CppType * */
} Il2CppInteropData;
```
Meaning:
- P/Invoke and COM interop functions for one type
- useful for attaching native wrapper VAs to `[DllImport]` methods and
COM bridge code
Version behavior:
- v23+
r2unity behavior:
- not parsed today
### `windowsRuntimeFactoryTable`
Shape:
- UWP/WinRT-specific table
Meaning:
- Windows Runtime factory data
- usually absent or empty on iOS, Android, Linux, macOS, and normal
desktop builds
r2unity behavior:
- not parsed today
## MetadataRegistration Tables
### `types`
Shape:
```text
Il2CppType pool/table
```
Conceptual element:
```c
typedef struct {
uintptr_t data; /* type def index, nested type ptr, array ptr,
generic class ptr, generic parameter index, etc. */
uint32_t bits; /* attrs, ELEMENT_TYPE, byref, pinned, version bits */
} Il2CppType;
```
Meaning:
- every metadata `typeIndex` resolves through this table
- method return types, parameter types, field types, interface entries,
generic constraints, and many other metadata records point here
Value:
- required for full type names beyond simple type definitions:
arrays, pointers, byrefs, generic instances, generic parameters,
modifiers, function pointers
r2unity behavior:
- not parsed today
- current names are limited by what can be reconstructed directly from
decoded metadata rows
### `genericInsts` and `genericClasses`
Shape:
- generic instantiation tables and pointer/struct pools
Meaning:
- concrete type argument tuples such as `(int)` or `(string, int)`
- instantiated generic classes
Association:
- consumed by `Il2CppType` decoding and generic method name recovery
r2unity behavior:
- not parsed today
### `methodSpecs`
Shape:
```c
typedef struct {
int32_t methodDefinitionIndex;
int32_t classIndexIndex;
int32_t methodIndexIndex;
} Il2CppMethodSpec;
```
Meaning:
- describes a concrete generic method instantiation
- points back to the open generic method definition and the class/method
generic argument tuples
r2unity behavior:
- not parsed today
### `genericMethodTable`
Shape:
```c
typedef struct {
int32_t methodIndex; /* -> methodSpecs */
struct {
int32_t methodIndex; /* -> genericMethodPointers */
int32_t invokerIndex; /* -> invokerPointers */
int32_t adjustorThunk; /* version-gated */
} indices;
} Il2CppGenericMethodFunctionsDefinitions;
```
Meaning:
- bridge from a generic method spec to the concrete native function
pointer table
Association:
```text
genericMethodTable[k].methodIndex
-> methodSpecs[]
-> open method + generic args
genericMethodTable[k].indices.methodIndex
-> CodeRegistration.genericMethodPointers[]
-> function VA
```
r2unity behavior:
- not parsed today
### `fieldOffsets`
Shape:
```text
pre-v22: int32_t fieldOffsets[]
v22+: int32_t *fieldOffsets[]
```
Meaning:
- final runtime field offsets
- this is native-side layout data, not present in `global-metadata.dat`
Association:
- pre-v22: indexed by global field index
- v22+: one offset array per type definition
Notes:
- value-type instance fields need object-header adjustment in some
contexts
- once decoded, this table lets analysis label `[this + offset]` memory
accesses with field names and types
r2unity behavior:
- not parsed today
### `typeDefinitionsSizes`
Shape:
```c
typedef struct {
uint32_t instance_size;
uint32_t native_size;
uint32_t static_fields_size;
uint32_t thread_static_fields_size;
} Il2CppTypeDefinitionSizes;
```
Meaning:
- runtime class/struct sizes
- array element stride
- marshalled native size
- static and thread-static storage sizes
r2unity behavior:
- not parsed today
### `metadataUsages`
Shape:
```text
void *metadataUsages[]
```
Meaning:
- native side of old metadata usage lists/pairs
- each slot is a runtime metadata object pointer, such as:
`Il2CppClass *`, `Il2CppType *`, `MethodInfo *`, `FieldInfo *`,
managed string object, or generic `MethodInfo *`
Version behavior:
- v19..v26
- v27+ replaces this model with per-site inline globals and
module-initializer setup patterns
r2unity behavior:
- not parsed today
## Heuristic Carver
The fallback section scanner searches readable/data sections for:
```text
u32 count
padding if 64-bit
ptr table
```
For each candidate pair:
1. `count` must be plausible.
2. `table` must dereference as data.
3. The first up to 128 entries are sampled.
4. Enough entries must be non-zero.
5. Enough entries must normalize to executable text VAs.
6. Accepted entries are copied into `method_ptrs[]`.
This finds flat method-pointer-like tables. It does not understand:
- `Il2CppCodeRegistration` layout
- `Il2CppMetadataRegistration` layout
- `Il2CppCodeGenModule` arrays unless an anchor path reaches them
- generic method table joins
- reverse-P/Invoke wrapper index joins
- field-offset pointer-of-pointer layouts
Because modern Unity stores method pointers per image, a single
heuristically found `{count, ptr}` table can be incomplete.
## Structural CodeRegistration Method Recovery
When `Il2CppCodeRegistration` is known, r2unity uses a stronger path.
First it searches near the registration VA for a count/pointer pair where
the count equals metadata image count:
```text
count == image_count
ptr == codeGenModules[]
```
Then for every module:
```text
moduleName
methodPointerCount
methodPointers
```
The module name is matched to a metadata image name. After that:
```text
image.typeStart/typeCount
-> type rows
type.methodStart/method_count
-> method rows
module.methodPointers[local]
-> method_ptrs[global_method_index]
```
If the module path fails, r2unity tries the older global
`methodPointersCount + methodPointers` pair near `CodeRegistration` and
copies it positionally.
## Version-sensitive native layouts
Native pointer-table layouts are version-gated. This document mentions
version constraints only where they affect a specific table shape, such as
pre-v24.2 global method pointers vs. v24.2+ per-image codegen-module
method tables. The canonical Unity-release, metadata wire-version,
sub-version, and row-layout matrix lives in `doc/future.md`.
## Common Pitfalls
- `global-metadata.dat` does not contain method RVAs.
- Method table slot order is not recovered from names; it is row order
and native index arrays.
- One flat method-pointer table is only the old layout. Modern Unity
needs the `codeGenModules[]` walk for full method coverage.
- Null method pointer entries are normal.
- Generic method instantiations are not ordinary method rows. They need
`methodSpecs`, `genericMethodTable`, and `genericMethodPointers`.
- Invokers are not method bodies and are not one per method.
- Reverse-P/Invoke wrapper VAs require wrapper index joins.
- Field offsets are layout data from `MetadataRegistration`, not from
field definitions alone.
- Metadata registration is a companion native anchor, not an automatic
VA association for every metadata row.
- Pointer width and relocation state must be correct before scanning.
## Practical Recovery Order
For broad native-symbol recovery, the most useful order is:
1. Parse `global-metadata.dat` tables and counts.
2. Establish pointer size, base VA, readable ranges, and text range.
3. Resolve or structurally find `Il2CppCodeRegistration`.
4. Resolve or structurally find `Il2CppMetadataRegistration`.
5. Walk `codeGenModules[]` for v24.2+ method pointers.
6. Fall back to global `methodPointers[]` for older layouts.
7. Parse `MetadataRegistration.types`, `fieldOffsets`, and
`typeDefinitionsSizes`.
8. Join `methodSpecs`, `genericMethodTable`, and
`genericMethodPointers`.
9. Join reverse-P/Invoke wrapper indices to
`reversePInvokeWrappers[]`.
10. Decode invoker indices, unresolved call stubs, metadata usages, and
interop data.
This order keeps method naming useful early while preserving the path to
full class layout, generic-instantiation, interop, and dispatch recovery.
+91 -382
View File
@@ -150,9 +150,9 @@ v29+ — the BLOB pool fields at `attributesInfo*` / `attributeTypes*`
the same logical regions are consumed through `R2U_SEC_*` section
ids instead of individual `*Offset` and `*Size` fields.
Every other header entry is declared but unused. §2.11 and
`doc/future.md` enumerate what's still on the floor and why each
would be worth decoding.
Every other header entry is declared but unused. §2.11 describes the
custom-attribute tables that r2unity partially consumes; `doc/todo.md`
tracks the remaining decoders.
### 1.3 Two distinct string mechanisms
@@ -193,8 +193,8 @@ index types in 1, 2, or 4 bytes depending on the target table count.
`Il2CppMethodDefinition.declaringType` is an index into
`typeDefinitions`; `Il2CppTypeDefinition.methodStart` is an index
into `methods`; `Il2CppFieldDefinition.typeIndex` is an index into
the native-side `Il2CppMetadataRegistration.types` pool (§3). No
cross-reference uses a file offset.
the native-side `Il2CppMetadataRegistration.types` pool documented in
`doc/ptrtables.md`. No cross-reference uses a file offset.
Sentinel "none" is all-bits-one at the serialized width:
`0xFF`, `0xFFFF`, or `0xFFFFFFFF`. r2unity expands all of them to
@@ -331,8 +331,8 @@ v24.0 and below carried an additional set of inline indices:
`customAttributeIndex` (before tokens took over), `methodIndex`,
`invokerIndex`, `delegateWrapperIndex`, `rgctxStartIndex`,
`rgctxCount`. Those were removed at v24.1 and replaced by token-based
lookup + per-module RGCTX ranges (§3.8). r2unity does not decode
that layout today.
lookup + per-module RGCTX ranges in `Il2CppCodeGenModule`
(`doc/ptrtables.md`). r2unity does not decode that layout today.
`flags` is an ECMA-335 `MethodAttributes` bitmask, with
`PINVOKE_IMPL = 0x2000` being the one bit r2unity's `-P` uses. The
@@ -355,10 +355,14 @@ int32_t customAttributeIndex; /* removed at v24.1 */
int32_t typeIndex; /* -> MetadataRegistration.types */
```
Not decoded by r2unity today. Without it, every dumped method
degrades to `Method()` with no parameter types or names. Combined
with `parameterDefaultValues` (§2.4) this is what reconstructs real
C# signatures like `Foo(string name, int count = 0)`.
r2unity decodes this table and exposes parameter tokens, names, and
type indices in `r2unity-sj`; it also emits a canonical signature such
as `Namespace.Type.Foo(this;string:name,int:count):void`. Type indices
that refer directly to a type definition are resolved to source names.
Pointer, array, and constructed-generic indices still require the native
`MetadataRegistration.types` table and use `type_index.N` until that
table is available. `parameterDefaultValues` (§2.4) remains necessary
to reconstruct optional values such as `int count = 0`.
### 2.4 Default values: `fieldDefaultValues`, `parameterDefaultValues`,
`fieldAndParameterDefaultValueData`
@@ -394,7 +398,8 @@ uint32_t token; /* 0x04?????? — added at v19 */
```
Without this table, r2unity cannot label any `this`-relative memory
access. Joined with `MetadataRegistration.fieldOffsets` (§3.6), every
access. Joined with `MetadataRegistration.fieldOffsets`
(`doc/ptrtables.md`), every
`ldr X, [x0, #0x18]` in disassembly would become
`ldr X, [this, #Player.health]`. Second only to the method-pointer
table in RE value.
@@ -518,7 +523,7 @@ encodes variance (`in`/`out`) and special constraints
(`class`/`struct`/`new()`) per ECMA-335 §II.23.1.7.
Gate for `where TKey : IComparable<TKey>`-style reconstruction and
for using `methodSpecs` / `genericInsts` (§3.9) to produce real
for using `methodSpecs` / `genericInsts` (`doc/ptrtables.md`) to produce real
instantiation names like `List<int>::Add`.
Not decoded by r2unity today.
@@ -551,8 +556,8 @@ decoded = (version >= 27)
`{ Invalid=0, TypeInfo=1, Il2CppType=2, MethodDef=3, FieldInfo=4,
StringLiteral=5, MethodRef=6 }`.
`usageType == 3` → concrete method-definition index;
`usageType == 6` → index into `methodSpecs` (§3.9).
`usageType == 3` -> concrete method-definition index;
`usageType == 6` -> index into `methodSpecs` (`doc/ptrtables.md`).
Exposing this unlocks full virtual dispatch resolution, real
interface implementation chains, and `find-all-implementers`
@@ -613,7 +618,7 @@ typedef struct { uint32_t destinationIndex; uint32_t encodedSourceIndex; } Il2Cp
`encodedSourceIndex` uses the same `Il2CppMetadataUsage`-tagged
encoding as vtable slots (§2.10). `destinationIndex` is the slot in
the native `MetadataRegistration.metadataUsages[]` pointer table
(§3.10).
(`doc/ptrtables.md`).
On pre-v27 binaries, compiled code loads every metadata pointer via
`ldr X, [g_MetadataUsages + const]`. Labelling the array turns every
@@ -652,7 +657,8 @@ ignore otherwise.
typedef struct { int start; int length; } Il2CppRange;
```
Pairs with `CodeRegistration.unresolvedVirtualCallPointers` (§3.5)
Pairs with `CodeRegistration.unresolvedVirtualCallPointers`
(`doc/ptrtables.md`)
to name each unresolved-virtual-call stub with its parameter
signature.
@@ -688,315 +694,51 @@ a fractional sub-version leaks into the on-disk layout.
Constrained }`. Per-type via `typeDef.rgctxStartIndex/rgctxCount`
(pre-v24.1), per-method similarly; from v24.2 onward RGCTX tables
migrate to `Il2CppCodeGenModule.rgctxs` + `rgctxRanges` on the
native side (§3.7).
native side (`doc/ptrtables.md`).
**RGCTX** = "Runtime Generic Context" — the per-instantiation
lookup table that lets a single compiled generic method body
resolve `typeof(T)`, `default(T)`, and generic-virtual dispatch
sites without recompiling per instantiation.
## 3. The native side: CodeRegistration / MetadataRegistration
## 3. Native side and pointer-table recovery
The other half of the IL2CPP runtime knowledge lives in the binary,
typically in `.rodata` / `.data.rel.ro`. Two top-level anchors
exist; every other array hangs off one of them.
The other half of the IL2CPP runtime knowledge lives in the native
binary, typically in `.rodata`, `.data`, or `.data.rel.ro`. The two
top-level native anchors are `Il2CppCodeRegistration` and
`Il2CppMetadataRegistration`; codegen modules, method pointers,
generic-method pointers, reverse-P/Invoke wrappers, invokers, field
offsets, type sizes, and metadata-usage tables hang from those anchors.
```c
typedef struct { /* Il2CppCodeRegistration (shape varies by version) */
ulong methodPointersCount; /* pre-v24.2 only */
ulong methodPointers; /* pre-v24.2 only */
ulong reversePInvokeWrapperCount;
ulong reversePInvokeWrappers;
ulong genericMethodPointersCount;
ulong genericMethodPointers;
ulong genericAdjustorThunks; /* v24.5 / v27.1+ */
ulong invokerPointersCount;
ulong invokerPointers;
ulong customAttributeCount;
ulong customAttributeGenerators; /* removed at v29 */
ulong unresolvedVirtualCallCount;
ulong unresolvedVirtualCallPointers; /* split at v29.1 */
ulong interopDataCount;
ulong interopData; /* v23+ */
ulong windowsRuntimeFactoryCount;
ulong windowsRuntimeFactoryTable;
ulong codeGenModulesCount; /* v24.2+ */
ulong codeGenModules; /* v24.2+ */
} Il2CppCodeRegistration;
`doc/ptrtables.md` is the canonical reference for native table shapes,
pointer widths, address normalization, method table carving, and
registration recovery. Keep native pointer-table details there instead
of duplicating them here.
typedef struct { /* Il2CppMetadataRegistration */
long genericClassesCount; ulong genericClasses;
long genericInstsCount; ulong genericInsts;
long genericMethodTableCount; ulong genericMethodTable;
long typesCount; ulong types;
long methodSpecsCount; ulong methodSpecs;
long fieldOffsetsCount; ulong fieldOffsets;
long typeDefinitionsSizesCount; ulong typeDefinitionsSizes;
ulong metadataUsagesCount;
ulong metadataUsages;
} Il2CppMetadataRegistration;
```
r2unity currently uses the native side only for method body VAs:
`ulong` means host pointer width — 8 B on arm64, x86-64, Windows
x64, macOS; 4 B on armeabi-v7a, x86, wasm32. Parsers must branch on
`EI_CLASS` for ELF, `MH_MAGIC_64` vs `MH_MAGIC` for Mach-O,
`IMAGE_FILE_MACHINE_*` for PE. r2unity today only walks arm64 /
x86-64 targets robustly; 32-bit paths exist but are less exercised.
- Resolve registration anchors from overrides, r2 flags, or symbols when
available.
- If `Il2CppCodeRegistration` is usable, walk `codeGenModules[]` for
v24.2+ per-image method tables, or fall back to the old global
`methodPointers[]` pair.
- If no anchor is usable, scan readable data sections for plausible
`{count, pointer}` tables whose entries land in executable code.
Finding the anchors without symbols typically relies on structural
matching: `methodPointersCount` or
`codeGenModulesCount * typesCount * methodsCount` must equal known
integers derivable from the `.dat`. Il2CppDumper's
`ExecuteBinarySearch` does this.
The `Il2CppMetadataRegistration` anchor is tracked as a companion VA, but
the native metadata-registration tables are not decoded yet. The current
backlog for that work lives in `doc/todo.md`.
### 3.1 `methodPointers` (pre-v24.2 global; v24.2+ per-module)
## 4. Version handling
Method `i`'s entry at slot `i`. Zeros for abstract, generic-open,
external/P-Invoke, and intrinsic runtime methods.
`doc/future.md` is the canonical wire-version and Unity-version matrix.
It owns the row-size deltas, acceptance-band notes, compact-index rules,
and sub-version caveats.
From v24.2 onward the array moves **inside each `Il2CppCodeGenModule`**
(one module per image). A global `methodPointers` no longer exists.
A scanner that locks onto a single `{count, ptr}` on a v24.2+ binary
only extracts one image's methods. This is the single most common
"why is my dumper missing half the methods" bug on modern Unity.
r2unity's current heuristic finds one `{count, ptr}` and is
therefore vulnerable to this on Unity ≥ 2019.3. Walking
`codeGenModules[]` and enumerating each module is mandatory for
full coverage.
### 3.2 `invokerPointers`
Reflection-dispatch invokers. One per unique
`(returnType, paramCount, blittable-ness)` signature shape, not per
method. Methods reference them via `methodDef.invokerIndex`
(pre-v24.2) or `Il2CppCodeGenModule.invokerIndices[]` (v24.2+).
Naming these clusters methods by signature; very useful for
discovering reflection hook points (`MethodInfo.Invoke` calls route
through here).
### 3.3 `customAttributeGenerators` (pre-v29)
Per-range `void(void*)` function pointer, each one news up the
attribute object(s) for one token range. The generator body is
typically a few dozen instructions: load constructor argument
constants, call the attribute's constructor, store the result. A
tiny arch-specific interpreter can recover every attribute
argument. Gone at v29 (§2.11).
### 3.4 `reversePInvokeWrappers` (v22+)
Array of native-to-managed trampolines for every
`[MonoPInvokeCallback]`-annotated method. Every callback the game
registers with a native library (ad SDK callback, audio driver
callback, analytics pingback) goes through one of these. Naming
them `ReversePInvoke_<method>` lights up the native plugin callback
surface immediately.
### 3.5 `unresolvedVirtualCallPointers` (v22..v29; split at v29.1
into `unresolvedInstanceCallPointers` + `unresolvedStaticCallPointers`)
Stubs the runtime patches when a virtual or interface call target
becomes known at run time. 1:1 with the metadata ranges in §2.15.
### 3.6 `interopData` (v23+)
Array of `Il2CppInteropData` — each element bundles P/Invoke and
COM-interop function pointers **for one type**:
```c
typedef struct {
ulong delegatePInvokeWrapperFunction;
ulong pinvokeMarshalToNativeFunction;
ulong pinvokeMarshalFromNativeFunction;
ulong marshalToNativeFunction;
ulong marshalFromNativeFunction;
ulong marshalCleanupFunction;
ulong ccwFunction; /* COM only */
ulong guid; /* COM IID pointer */
ulong type; /* -> Il2CppType * */
} Il2CppInteropData;
```
Walking this array gives the native VA of every `[DllImport]` target
wrapper and every COM CCW/RCW bridge. This is where real P/Invoke
target recovery lives — r2unity does not read it today.
### 3.7 `codeGenModules` (v24.2+)
One `Il2CppCodeGenModule` per image. Per Il2CppDumper's
`Il2CppClass.cs`:
```c
typedef struct {
ulong moduleName; /* C string */
long methodPointerCount;
ulong methodPointers; /* PER-IMAGE method table */
ulong adjustorThunks; /* v24.5 / v27.1+ */
ulong adjustorThunkCount;
ulong invokerIndices; /* int32[] per method */
ulong reversePInvokeWrapperCount;
ulong reversePInvokeWrapperIndices;
long rgctxRangesCount;
ulong rgctxRanges; /* Il2CppTokenRangePair[] */
long rgctxsCount;
ulong rgctxs; /* Il2CppRGCTXDefinition[] */
ulong debuggerMetadata;
ulong customAttributeCacheGenerator; /* v27 only */
ulong moduleInitializer; /* v27+ <Module>..cctor */
ulong staticConstructorTypeIndices; /* v27+ */
ulong metadataRegistration; /* v27+ per-assembly */
ulong codeRegistaration; /* sic — v27+ */
} Il2CppCodeGenModule;
```
`moduleInitializer` is each image's `<Module>..cctor` — the per-
image static-init entry point.
`staticConstructorTypeIndices` lists types whose `.cctor` runs at
load time. Both are prime targets in obfuscated games where encryption
keys or obfuscated strings are materialised at startup.
**`adjustorThunks`** (v24.5, v27.1+) are the struct-vs-class `T`
fixup thunks for generic methods: `mov x0, [x0, #8]; b real_body`
(skip the `Il2CppObject` header when `T` is a value type). Labelling
them separately from the real body reduces disassembly confusion.
### 3.8 `MetadataRegistration.types` — the `Il2CppType` pool
```c
typedef struct { /* Il2CppType, 16 B */
ulong datapoint; /* union: klassIndex / type* /
array* / genericParamIdx /
generic_class* */
uint bits; /* packed fields:
attrs:16 | type:8 | num_mods:{5|6} | byref:1 | pinned:1
[ | valuetype:1 at v27.2+ ] */
} Il2CppType;
```
Every `typeIndex` used anywhere in the metadata — method
return types, parameter types, field types, interface lists —
resolves into this pool. Without it, the textual form of any type
beyond a simple `TypeDef` (so: arrays, pointers, by-ref, generic
instantiations, generic parameters, `modreq`/`modopt`) cannot be
reconstructed.
`type` field at bits 16..23 is the ECMA-335 `ELEMENT_TYPE_*`
(ECMA-335 §II.23.1.16). Walking it recursively produces qualified
names like `List<Dictionary<int, string>>[]&`.
### 3.9 Generics on the native side
```c
/* genericInsts[]: concrete type-argument tuples
( (int), (string,int), (Player, Dictionary<int,float>), … ) */
typedef struct { /* Il2CppMethodSpec, 12 B */
int methodDefinitionIndex; /* the open generic method */
int classIndexIndex; /* -> genericInsts (-1 if none) */
int methodIndexIndex; /* -> genericInsts (-1 if none) */
} Il2CppMethodSpec;
typedef struct { /* Il2CppGenericMethodFunctionsDefinitions */
int methodIndex; /* -> methodSpecs */
struct {
int methodIndex; /* -> genericMethodPointers */
int invokerIndex;
int adjustorThunk; /* v24.5+ */
} indices;
} Il2CppGenericMethodFunctionsDefinitions;
```
Consumed together with `CodeRegistration.genericMethodPointers`:
```c
foreach (entry in genericMethodTable) {
spec = methodSpecs[entry.methodIndex];
va = genericMethodPointers[entry.indices.methodIndex];
// emit "List<int>::Add @ va"
}
```
Il2CppDumper's `GetMethodSpecName` reconstructs the full
`Foo<string>.Bar<int>` textual form.
### 3.10 Field offsets, type sizes, metadata usages
- `MetadataRegistration.fieldOffsets`:
- pre-v22: flat `int32_t[]` indexed by global field index
- v22+: pointer-of-pointers — one `int32_t *` per type definition,
pointing at that type's field-offset array
- `typeDefinitionsSizes`:
```c
typedef struct {
uint32_t instance_size; /* includes Il2CppObject hdr */
uint32_t native_size; /* P/Invoke marshalled */
uint32_t static_fields_size;
uint32_t thread_static_fields_size;
} Il2CppTypeDefinitionSizes; /* 16 B */
```
`instance_size` is the array element stride:
`ldr X, [Xn + Xindex*instance_size]` in disassembly.
- `metadataUsages`: native side of §2.12, void-pointer table.
On pre-v27 binaries, every metadata-aware load in the code
dereferences a slot here.
ECMA-335 §II.25.4 documents instance layout (the `MonitorData *` +
`Il2CppClass *` header prefix that makes non-static value-type
fields appear at offset `-8`/`-16` from the `Il2CppObject` pointer).
`GetFieldOffsetFromIndex` in Il2CppDumper's `Il2Cpp.cs:274` has the
exact math.
## 4. Wire-version history and per-version layout deltas
This is a condensed changelog; `doc/future.md` has the full
Unity-release → wire-version → sub-version mapping.
- **v16..v20** (Unity 5.0..5.2): pre-public-IL2CPP-shipped era.
Rejected.
- **v21** (Unity 5.3.0..5.3.5): first on-iOS IL2CPP. Rejected.
- **v22** (Unity 5.3.6..5.4): drops delegate-wrapper tables; adds
`unresolvedVirtualCall*` tables. Rejected.
- **v23** (Unity 5.5): adds `interopData` and WinRT tables.
Rejected.
- **v24** (Unity 5.6..2018.x): adds `exportedTypeDefinitions`. v24.1
(2018.3) switches all custom-attribute resolution to token-based
lookup and drops `customAttributeIndex` from every def. v24.2
(2019.1) introduces `Il2CppCodeGenModule` and per-image method
arrays. v24.5 (2019.4.15) adds `genericAdjustorThunks`.
- **v27** (Unity 2020.2..2021.3): `customAttributeGenerators` move
per-image into `codeGenModule.customAttributeCacheGenerator`.
v27.1 re-adds `genericAdjustorThunks`. v27.2 widens the RGCTX
`type`/`data` fields from 32 to 64 bits on disk and repacks
`Il2CppType.valuetype` as a dedicated bit. `MethodInfo` struct
changes on the native side.
- **v29** (Unity 2022.1..2022.3): replaces the custom-attribute
generator thunks with inline BLOB encoding (§2.11). v29.1 splits
`unresolvedVirtualCallPointers` into separate instance/static
arrays.
- **v30**: reserved, never shipped.
- **v31** (Unity 2023.1..2023.x): adds `returnParameterToken` to
`Il2CppMethodDefinition`, aligning with ECMA-335's return-value
param-row model.
- **v32..v35** (gap range in current references): keep the legacy
offset/size header. v32 removes `Il2CppStringLiteral.length` and
`Il2CppTypeDefinition.elementTypeIndex`; v35 introduces compact-
index reader hooks but keeps 4-byte widths in the current
Il2CppDumper source.
- **v36..v37**: no header layout is declared by the v39 reference;
- **v38** (Unity 6 era): switches the header from offset/size pairs
to 31 `{offset,size,count}` section records.
and compacts additional serialized indices, including
`ParameterIndex`.
r2unity's current shipped-version acceptance band is **v24.1..v31**
plus **v38..v39**. It also accepts unobserved v32..v35 with the
legacy offset/size header, the v32 row deltas above, and 4-byte
header shape. Since the on-disk `version` field is `24` for both
v24.0 and v24.1+, the parser probes the image-row stride (32 B on
v24.0, 40 B on v24.1+) and rejects v24.0 explicitly to avoid silent
mis-decoding of TypeDefinition/MethodDefinition/ImageDefinition rows.
r2unity currently accepts shipped wire versions in the v24.1..v31 and
v38..v39 families, rejects v24.0 after probing the row layout, rejects
v36/v37 because their header shape is not proven, and keeps v32..v35
conservative because public references are incomplete.
## 5. How the parser is wired in r2unity
@@ -1010,13 +752,13 @@ File-by-file map:
type/method/image/assembly/referenced-assembly decoders, P/Invoke
and reverse-P/Invoke enumerators, endian-safe LE readers
(`RD_LE32`, `RD_LE16`).
- `src/lib/elf.c` — ELF32/64 loader, dynamic-table walk, relative
relocation application (`DT_REL`, `DT_RELA`, `DT_RELR`),
method-pointer-array heuristic.
- `src/lib/macho.c` — Mach-O 64 loader (thin + FAT first-ARM64),
`LC_SEGMENT_64` walk, method-pointer-array heuristic.
- `src/lib/pe.c` — PE32/PE32+ loader, section walk, method-pointer-
array heuristic.
- `src/lib/bin/native.c` — shared native-binary view,
CodeRegistration/MetadataRegistration anchor resolution, structural
CodeRegistration parsing, RBin adapter, and the generic section-scan
fallback.
- `src/lib/bin/elf.c`, `src/lib/bin/macho.c`, `src/lib/bin/pe.c` —
simple file-backed format parsers used when the RBin path cannot
recover method pointers.
- `src/main.c` — CLI entry point and output emitters.
Every row decoder reads via `r_read_le32`/`r_read_le16` (LE on all
@@ -1027,45 +769,41 @@ retained for the two string pools.
## 6. Native-binary scanning, in one picture
```text
ELF/Mach-O/PE image on disk
Native IL2CPP image opened by r_bin or a simple ELF/Mach-O/PE mapper
│
├─ load & parse segments/sections
├─ use r_bin sections/symbols/relocs when available
│ or simple file-backed sections for fallback
│ ↓
│ segments { vaddr/vmaddr, size, perms, file mapping }
│ sections { vaddr, size, perms }
│ ↓
│ [text_lo, text_hi) (executable union)
│
├─ ELF only: apply DT_REL / DT_RELA / DT_RELR relative fixups
│ so data-segment pointer arrays match the runtime
│ state (addends resolved, RELR bitmap expanded).
├─ resolve g_CodeRegistration / g_MetadataRegistration
│ order: CLI -O / r2 eval vars / r2 flags / r_bin symbols
│
├─ scan each writable/data segment:
│ pass 1: {count32, pad32, ptr} tuple
│ (CodeRegistration-shaped anchor pair)
│ pass 2: {count32, ptr} generic
├─ parse Il2CppCodeRegistration:
│ v24.2+: match codeGenModules[] to metadata images and
│ copy each module's methodPointers[]
│ older: recover the global methodPointers[] pair
│
└─ accept if a sample of entries at *ptr[] lands in text,
either already (post-relocations) or after + base_vaddr
(raw RVA case). Emit absolute VAs, one per method index.
└─ fallback when forced or unresolved:
scan non-executable data/readable sections for {count, ptr}
pairs whose table entries land in executable code.
Emit absolute VAs, one per method index.
```
The heuristic is deliberately weaker than a structural
`Il2CppCodeRegistration` match, but it works on every supported
target and doesn't need symbol tables. It does, however, lock onto
**one** `{count, ptr}` array, which on v24.2+ means one image's
methods, not all of them (§3.1 / §3.7). A proper structural match
that walks `codeGenModules[]` is on the roadmap.
The structural path is preferred because it follows Unity's native
registration structures instead of guessing which `{count, ptr}` pair
is the method-pointer table. The fallback remains useful for stripped
binaries or builds where the registration symbols cannot be resolved.
The simple ELF/Mach-O/PE parsers do not reimplement full symbol-table
parsing; they use explicit registration addresses when provided and
otherwise feed their sections into the fallback scanner.
For ELF the relocation pass matters because the Android linker
produces method-pointer arrays almost entirely as
`R_AARCH64_RELATIVE` (type 1027) entries. Without applying them,
the raw array on disk is a run of zeros. Packed Android relocations
(`DT_ANDROID_RELA`, `DT_ANDROID_RELR`) are not handled yet and
cause the same "empty array" symptom on Play Store builds.
For Mach-O and PE the linker has already materialised concrete
values; no explicit relocation pass is required for the tables
r2unity currently scans.
Relocation handling is delegated to r_bin (`r_bin_patch_relocs`) on
the RBin path. The simple ELF parser also applies the common relative
REL/RELA/RELR forms so stripped Android/Linux inputs still have a
lightweight fallback.
## 7. Data we can extract today vs. data we do not
@@ -1083,42 +821,13 @@ Already extracted by r2unity (library + CLI):
| referenced assemblies | flat int32 array |
| P/Invoke marker methods | `-P` enumeration |
| reverse-P/Invoke on v29+ | `-R` enumeration via BLOB |
| method-pointer VA (global) | ELF/Mach-O/PE heuristic |
| method-pointer VA | CodeRegistration parse + r_bin/simple-parser section-scan fallback |
Data present on disk / in the binary but **not yet consumed**:
Data present on disk or in the binary but not yet consumed is summarized in
`doc/todo.md`. Keep the task list there; this document only records
current parser behavior and the source-file map.
- `parameters` + `parameterDefaultValues` + BLOB heap → real method
signatures with names, types, and default arguments (§2.3–2.4).
- `fields` + `fieldDefaultValues` + BLOB heap → field names, types,
and compile-time constants; prerequisite for labelling
`[this+N]` accesses (§2.5).
- `properties` + `events` → recomposed C# `property` / `event`
declarations (§2.6).
- `genericContainers` + `genericParameters` +
`genericParameterConstraints` → generic-parameter names and
constraints (`where T : IComparable<T>`) (§2.9).
- `interfaces` + `interfaceOffsets` + `vtableMethods` → fully
resolved virtual dispatch and interface-implementation graph
(§2.10).
- `nestedTypes` → correct `Outer.Inner` naming (§2).
- `attributesInfo`/`attributeTypes` (pre-v29) or
`attributeDataRange`/`attributeData` (v29+) → every `[Attribute]`
on every declaration; `[Preserve]` reflection markers;
`[DllImport]` DLL+entry args (§2.11).
- `metadataUsageLists/Pairs` (pre-v27) → labels every indirect
metadata load in compiled code (§2.12).
- `fieldMarshaledSizes`, `unresolvedVirtualCall*`, WinRT tables,
`exportedTypeDefinitions`, RGCTX tables (§2.14–2.18).
- Native-side `CodeRegistration` and `MetadataRegistration` walk →
`invokerPointers`, `customAttributeGenerators`,
`reversePInvokeWrappers`, `genericMethodPointers`,
`interopData`, `codeGenModules[]`, `types`, `fieldOffsets`,
`typeDefinitionsSizes`, `metadataUsages` (§3).
- Richer native scanning: per-module `methodPointers` on v24.2+,
packed Android relocations, Mach-O FAT multi-slice,
chained-fixups, PE import table.
## 8. validation corpus
## 8. Validation corpus
- `json-one-line`, `json-unity` — `-j` shape/content
+1105
View File
File diff suppressed because it is too large. Load diff
+4 -20
View File
@@ -67,7 +67,7 @@ None of this is implemented in r2unity today.
Inferred:
- Unity version range from the metadata wire version (mapping in
§3).
`doc/future.md`).
- IL2CPP runtime version from symbol signatures or strings in
the native binary (e.g. the `il2cpp_init` / `il2cpp_runtime_version`
exports or a compiled-in string constant).
@@ -191,25 +191,9 @@ date, because the fully-flattened metadata does not carry a Unity
version string (the Unity-version string exists as a constant
inside the native binary, typically `"20xx.y.z.*"` or `"6000.y.z"`).
r2unity uses this mapping:
| Wire version | Unity release range |
|--------------|---------------------|
| 21 | 5.3.0 – 5.3.5 |
| 22 | 5.3.6 – 5.4 |
| 23 | 5.5 |
| 24 | 5.6 – 2020.1 |
| 27 | 2020.2 – 2021.3 |
| 29 | 2022.1 – 2022.3 |
| 31 | 2023.1 – 2023.x |
| 38 | Unity 6 era |
| 39 | Unity 6 current |
The parser accepts shipped wire versions 24.1..31 and 38..39, rejects
24.0 and 36..37, and keeps a compatibility path for unobserved
32..35. `doc/future.md` §1.3 has the full version-to-sub-version
matrix with additional rows for specific Unity 2018/2019/2020/2021/
2022 minor releases and the v38/v39 Unity 6 metadata break.
r2unity uses the wire-version mapping documented in `doc/future.md`.
That file owns the Unity-release ranges, sub-version notes, accepted
version set, rejected versions, and v38/v39 Unity 6 metadata break.
## 3. What the shipped `-S` does not do
+2 -2
View File
@@ -294,5 +294,5 @@ text.
<https://blog.unity.com/technology/il2cpp-internals-a-tour-of-generated-code>
- Il2CppDumper's string-literal emitter: `Il2CppDumper/Outputs/*`.
<https://github.com/Perfare/Il2CppDumper>
- `doc/r2unity.md` §1.3 and §3.4 — metadata pool vs literal table
addressing model and the r2unity decode path.
- `doc/r2unity.md` §1.3 - metadata pool vs literal table addressing
model. This document owns the `-z` decode path details.
+182
View File
@@ -0,0 +1,182 @@
# r2unity TODO
This file tracks implementation work that is still open after the
current metadata parser, method-pointer recovery, and pointer-table notes
in `doc/ptrtables.md`.
## Current Baseline
Implemented or partially implemented today:
- Parse supported `global-metadata.dat` headers and core sections.
- Decode type definitions, method definitions, field definitions, images,
assemblies, string literals, and several index tables.
- Emit method/class names from metadata.
- Recover method body VAs through native method-pointer tables when
possible.
- Use symbols or user overrides for `Il2CppCodeRegistration` and
`Il2CppMetadataRegistration` when available.
- Walk `Il2CppCodeGenModule` method tables when a usable
`Il2CppCodeRegistration` anchor is available.
- Fall back to a generic `{count, pointer}` section scan for stripped
binaries.
Still missing:
- Structural native anchor discovery.
- Full `Il2CppMetadataRegistration` parsing.
- Field layout recovery.
- Generic method instantiation recovery.
- Invoker, reverse-P/Invoke wrapper, unresolved-call, and interop-data
native joins.
## Priority Backlog
1. Add structural registration discovery.
Find `Il2CppCodeRegistration` and `Il2CppMetadataRegistration` in
stripped binaries by validating native table shapes against counts
derived from `global-metadata.dat`. This should replace dependence on
symbols, manual overrides, and the weak generic section scan whenever
possible.
2. Harden modern method recovery.
Modern Unity stores methods per image through `codeGenModules[]`.
Make the module walker robust across v24.2+ layouts, validate module
names/counts, handle null entries cleanly, and add more version
samples. A single flat method-pointer table is only correct for older
layouts.
3. Implement manual pointer-table reads.
Wire up `-a` / class emission manual reads so a user can provide a
method-pointer table VA directly. Detect pointer width, validate the
table against text ranges, and report confidence/source in CLI and
plugin output.
4. Parse `Il2CppMetadataRegistration`.
Decode the native-side companion tables:
- `types`
- `genericInsts`
- `genericClasses`
- `methodSpecs`
- `genericMethodTable`
- `fieldOffsets`
- `typeDefinitionsSizes`
- `metadataUsages` for pre-v27 binaries
5. Recover class and struct field layouts.
Join metadata field rows with native `fieldOffsets` and
`typeDefinitionsSizes`. Emit class/struct layouts, field offsets, type
names, static/thread-static sizes, and enough data to label
`[this + offset]` accesses in r2.
6. Recover generic method instantiations.
Join:
```text
methodSpecs
genericMethodTable
genericMethodPointers
genericAdjustorThunks
genericInsts
```
Use this to name concrete generic bodies such as
`List<int>.Add` or `Foo<string>.Bar<float>`.
7. Attach reverse-P/Invoke wrapper VAs.
Decode module `reversePInvokeWrapperIndices[]`, join those indices to
`CodeRegistration.reversePInvokeWrappers[]`, and attach wrapper VAs
to P/Invoke/reverse-P/Invoke output.
8. Decode invoker and unresolved-call tables.
Parse `invokerPointers`, module `invokerIndices[]`, unresolved
virtual call tables, and v29.1+ instance/static unresolved call
tables. Use metadata range/signature information to name dispatch
stubs.
9. Parse `Il2CppInteropData`.
Recover P/Invoke marshal wrappers, delegate wrappers, COM bridges,
GUID/type associations, and native wrapper VAs from `interopData[]`.
10. Decode custom attributes fully.
- v29+: decode inline custom-attribute BLOBs, especially
`DllImportAttribute` DLL and entry-point names.
- pre-v29: recover constructor arguments by interpreting
`customAttributeGenerators` thunks.
11. Finish higher-level metadata tables.
Add or improve decoding for:
- generic method/type substitutions in signatures
- default values for fields and parameters
- properties and events
- nested types
- interfaces and interface offsets
- vtable method tables
- RGCTX ranges and entries
- generic containers, generic parameters, and constraints
12. Expand version and architecture coverage.
Add fixtures and tests for:
- v24.0, v24.1, v24.2, v24.5
- v27, v27.1, v27.2
- v29 and v29.1
- v31
- v38/v39 compact-index metadata
- 32-bit ARM/x86
- relocation-heavy ELF/Mach-O/PE samples
- Mach-O FAT and arm64e/pointer-authentication edge cases
Native walkers must be pointer-width aware and version-gated instead
of using one hardcoded registration layout.
## SBOM Work
- Add SHA-256 hashes for the executable and metadata file.
- Add native dependency extraction:
- ELF `DT_NEEDED`, `DT_SONAME`, build ID, Android notes.
- Mach-O dylib load commands, UUID, build version, code-signature
summary.
- PE imports, CodeView/PDB GUID, timestamp, Rich header.
- Add a CycloneDX `serialNumber`.
- Bring plugin SBOM properties up to parity with CLI SBOM output.
## Plugin and UX Work
- Add plugin commands for field/type browsing once native layout recovery
exposes those records.
- Make plugin symbol application report skipped methods and pointer-source
confidence.
- Keep CLI and plugin JSON schemas aligned.
- Surface partial recovery clearly: global old-style table, per-module
modern table, heuristic single-table hit, manual table, or no native
table.
## Reference Docs
- `doc/ptrtables.md` - native pointer-table inventory and recovery model.
- `doc/r2unity.md` - internal technical reference.
- `doc/future.md` - version/layout matrix and row-layout deltas.
- `doc/datvsbin.md` - metadata-vs-binary split.
- `doc/strings.md` - managed string-literal mode.
- `doc/sbom.md` - managed SBOM behavior and missing native SBOM work.
- `doc/pinvoke.md` - interop enumeration behavior and missing recovery
work.
- `INFO.md` - high-level explanation of what metadata can and cannot
provide alone.
- `INFO2.md` - implementation-oriented metadata parsing guide.
- `RVA.md` - mapping metadata indices to native addresses.
+50 -12
View File
@@ -1,7 +1,7 @@
project(
'r2unity',
'c',
version: '1.0.0',
version: '1.2.9',
default_options: [
'c_std=c99',
'warning_level=2',
@@ -17,7 +17,26 @@ add_project_arguments(
language: 'c',
)
r_util_dep = dependency('r_util')
r2 = find_program('r2', required: true)
r2_cflags = run_command(r2, '-H', 'R2_CFLAGS', check: true).stdout().strip().split()
r2_ldflags = run_command(r2, '-H', 'R2_LDFLAGS', check: true).stdout().strip().split()
r2_libs = run_command(r2, '-H', 'R2_LIBS', check: true).stdout().strip().split()
r2_fallback_dep = declare_dependency(
compile_args: r2_cflags,
link_args: r2_ldflags + r2_libs,
)
# Windows release ZIPs are relocatable and are not registered with pkg-config.
# Fall back to the paths reported by r2 when pkg-config cannot find its SDK.
r_util_dep = dependency('r_util', required: false)
if not r_util_dep.found()
r_util_dep = r2_fallback_dep
endif
r_bin_dep = dependency('r_bin', required: false)
if not r_bin_dep.found()
r_bin_dep = r2_fallback_dep
endif
conf = configuration_data()
conf.set_quoted('R2UNITY_VERSION', meson.project_version())
@@ -25,11 +44,13 @@ configure_file(output: 'r2unity_config.h', configuration: conf)
lib_inc = include_directories('src/lib')
lib_sources = files(
'src/lib/elf.c',
'src/lib/lib.c',
'src/lib/macho.c',
'src/lib/bin/elf.c',
'src/lib/bin/macho.c',
'src/lib/bin/native.c',
'src/lib/bin/pe.c',
'src/lib/paths.c',
'src/lib/pe.c',
'src/lib/serialized_file.c',
'src/lib/sbom.c',
)
@@ -37,7 +58,7 @@ r2unity = executable(
'r2unity',
files('src/main.c') + lib_sources,
include_directories: lib_inc,
dependencies: r_util_dep,
dependencies: [r_util_dep, r_bin_dep],
install: true,
)
@@ -45,8 +66,10 @@ test('r2unity-help', r2unity, args: ['-h'])
test('r2unity-version', r2unity, args: ['-v'])
plugins_opt = get_option('plugins')
r_core_dep = dependency('r_core', required: plugins_opt)
r_bin_dep = dependency('r_bin', required: plugins_opt)
r_core_dep = dependency('r_core', required: false)
if not r_core_dep.found() and not plugins_opt.disabled()
r_core_dep = r2_fallback_dep
endif
build_plugins = plugins_opt.enabled() or (
plugins_opt.auto() and r_core_dep.found() and r_bin_dep.found()
)
@@ -68,11 +91,11 @@ if build_plugins
warning('r2 was not found; installing plugins under ' + r2_plugindir)
endif
shared_library(
core_plugin = shared_library(
'core_r2unity',
files('src/r2/core_r2unity.c') + lib_sources,
include_directories: lib_inc,
dependencies: r_core_dep,
dependencies: [r_core_dep, r_bin_dep],
name_prefix: '',
install: true,
install_dir: r2_plugindir,
@@ -80,11 +103,26 @@ if build_plugins
shared_library(
'bin_r2unity',
files('src/r2/bin_r2unity.c') + lib_sources,
files(
'src/r2/bin_r2unity.c',
'src/lib/bgdatabase.c',
'src/lib/resource_pack.c',
) + lib_sources,
include_directories: lib_inc,
dependencies: r_bin_dep,
dependencies: [r_bin_dep],
name_prefix: '',
install: true,
install_dir: r2_plugindir,
)
# test_import.py is not always present (it is not committed yet on every
# branch); only wire the test when the file exists
fs = import('fs')
python = find_program('python3', required: false)
if python.found() and fs.exists('test' / 'test_import.py')
test('r2unity-class-import', python,
args: [files('test/test_import.py'), core_plugin],
)
endif
endif
+389
View File
@@ -0,0 +1,389 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
#define R_LOG_ORIGIN "r2unity.bgdatabase"
#include "bgdatabase.h"
#include <ctype.h>
#define R2U_BGDB_VERSION 6
#define R2U_BGDB_MAX_ADDONS 1024
#define R2U_BGDB_MAX_TABLES 65536
#define R2U_BGDB_MAX_FIELDS 65536
#define R2U_BGDB_MAX_STRING (1024 * 1024)
#define R2U_BGDB_MAX_STRINGS 1048576
typedef struct {
RBuffer *buf;
ut64 offset;
ut64 limit;
} R2UnityBGDatabaseReader;
static bool reader_has(const R2UnityBGDatabaseReader *r, ut64 size) {
return r && r->offset <= r->limit && size <= r->limit - r->offset;
}
static bool reader_read(R2UnityBGDatabaseReader *r, void *out, ut64 size) {
if (!reader_has (r, size)
|| r_buf_read_at (r->buf, r->offset, out, size) != (st64)size) {
return false;
}
r->offset += size;
return true;
}
static bool reader_skip(R2UnityBGDatabaseReader *r, ut64 size) {
if (!reader_has (r, size)) {
return false;
}
r->offset += size;
return true;
}
static bool reader_u32(R2UnityBGDatabaseReader *r, ut32 *out) {
ut8 raw[4];
if (!reader_read (r, raw, sizeof (raw))) {
return false;
}
*out = r_read_le32 (raw);
return true;
}
static bool reader_string(R2UnityBGDatabaseReader *r, char **out,
ut64 *data_offset, ut32 *string_size) {
ut32 length;
if (!reader_u32 (r, &length) || length > R2U_BGDB_MAX_STRING
|| !reader_has (r, length)) {
return false;
}
char *value = R_NEWS (char, (ut64)length + 1);
if (!value) {
return false;
}
if (length && r_buf_read_at (r->buf, r->offset,
(ut8 *)value, length) != (st64)length) {
free (value);
return false;
}
value[length] = 0;
if (data_offset) {
*data_offset = r->offset;
}
if (string_size) {
*string_size = length;
}
*out = value;
return reader_skip (r, length);
}
static bool addon_type_valid(const char *type) {
if (R_STR_ISEMPTY (type) || !r_str_startswith (type, "BansheeGz.BGDatabase.")) {
return false;
}
for (const ut8 *p = (const ut8 *)type; *p; p++) {
if (*p < 0x20 || *p > 0x7e) {
return false;
}
}
return strstr (type, ", BGDatabase") != NULL;
}
static bool utf8_printable(const ut8 *bytes, ut32 size) {
ut32 i = 0;
bool visible = false;
while (i < size) {
ut32 cp;
ut8 ch = bytes[i++];
if (ch < 0x80) {
if (!ch || (ch < 0x20 && ch != '\n' && ch != '\r' && ch != '\t')
|| ch == 0x7f) {
return false;
}
visible |= !isspace (ch);
continue;
}
ut32 need;
if (ch >= 0xc2 && ch <= 0xdf) {
cp = ch & 0x1f;
need = 1;
} else if (ch >= 0xe0 && ch <= 0xef) {
cp = ch & 0x0f;
need = 2;
} else if (ch >= 0xf0 && ch <= 0xf4) {
cp = ch & 0x07;
need = 3;
} else {
return false;
}
if (need > size - i) {
return false;
}
for (ut32 j = 0; j < need; j++) {
ut8 continuation = bytes[i++];
if ((continuation & 0xc0) != 0x80) {
return false;
}
cp = (cp << 6) | (continuation & 0x3f);
}
if ((need == 2 && cp < 0x800) || (need == 3 && cp < 0x10000)
|| (cp >= 0xd800 && cp <= 0xdfff) || cp > 0x10ffff) {
return false;
}
visible = true;
}
return visible;
}
static bool append_table(R2UnityBGDatabase *db, ut64 offset,
ut64 name_field_offset, ut32 field_count, ut16 name_field_type) {
if (db->table_count >= R2U_BGDB_MAX_TABLES) {
return false;
}
size_t count = db->table_count + 1;
R2UnityBGDatabaseTable *tables = realloc (db->tables,
count * sizeof (*tables));
if (!tables) {
return false;
}
db->tables = tables;
R2UnityBGDatabaseTable *table = &db->tables[db->table_count++];
memset (table, 0, sizeof (*table));
table->offset = offset;
table->name_field_offset = name_field_offset;
table->field_count = field_count;
table->name_field_type = name_field_type;
return true;
}
static bool collect_tables(R2UnityBGDatabase *db, const ut8 *data,
ut32 declared_count) {
ut64 start = db->body_offset + 4;
for (ut64 at = start; at <= db->size && db->size - at >= 8; at++) {
if (r_read_le32 (data + at) != 4) {
continue;
}
if (memcmp (data + at + 4, "name", 4)) {
continue;
}
/* A v6 table starts with its field count, followed by the canonical
* name-field descriptor: u32 kind, u16 type, 16-byte id, string. */
if (at < start + 26) {
continue;
}
ut64 name_field = at - 22;
ut64 table_offset = name_field - 4;
ut32 field_count = r_read_le32 (data + table_offset);
ut32 field_kind = r_read_le32 (data + name_field);
ut16 field_type = r_read_le16 (data + name_field + 4);
const ut8 *id = data + name_field + 6;
if (!field_count || field_count > R2U_BGDB_MAX_FIELDS
|| field_kind > 0x1000 || !field_type) {
continue;
}
bool nonzero = false;
for (size_t i = 0; i < sizeof (id); i++) {
nonzero |= id[i] != 0;
}
if (!nonzero || !append_table (db, table_offset, name_field,
field_count, field_type)) {
if (!nonzero) {
continue;
}
return false;
}
at += 7;
}
if (db->table_count != declared_count) {
return false;
}
for (size_t i = 0; i < db->table_count; i++) {
ut64 end = i + 1 < db->table_count
? db->tables[i + 1].offset: db->size;
if (end <= db->tables[i].offset) {
return false;
}
db->tables[i].size = end - db->tables[i].offset;
}
return true;
}
static bool append_string(R2UnityBGDatabase *db, ut64 prefix_offset,
ut32 size, const ut8 *bytes) {
if (db->string_count >= R2U_BGDB_MAX_STRINGS) {
return false;
}
size_t count = db->string_count + 1;
R2UnityBGDatabaseString *strings = realloc (db->strings,
count * sizeof (*strings));
if (!strings) {
return false;
}
db->strings = strings;
R2UnityBGDatabaseString *string = &db->strings[db->string_count++];
memset (string, 0, sizeof (*string));
string->value = R_NEWS (char, (ut64)size + 1);
if (!string->value) {
db->string_count--;
return false;
}
memcpy (string->value, bytes, size);
string->value[size] = 0;
string->prefix_offset = prefix_offset;
string->offset = prefix_offset + 4;
string->size = size;
return true;
}
static bool collect_strings(R2UnityBGDatabase *db, const ut8 *data) {
for (ut64 at = 0; at <= db->size && db->size - at >= 6;) {
bool opaque = false;
for (size_t i = 0; i < db->addon_count; i++) {
R2UnityBGDatabaseAddon *addon = &db->addons[i];
ut64 end = addon->payload_offset + addon->payload_size;
if (at >= addon->payload_offset && at < end) {
at = end;
opaque = true;
break;
}
}
if (opaque || at > db->size || db->size - at < 6) {
continue;
}
ut32 length = r_read_le32 (data + at);
if (length < 2
|| length > R2U_BGDB_MAX_STRING
|| length > db->size - at - 4) {
at++;
continue;
}
const ut8 *bytes = data + at + 4;
if (!utf8_printable (bytes, length)) {
at++;
continue;
}
bool ok = append_string (db, at, length, bytes);
if (!ok) {
return false;
}
at += 4 + length;
}
return true;
}
static R2UnityBGDatabase *parse_internal(RBuffer *buf, bool strings) {
ut64 size = r_buf_size (buf);
if (size < R2UNITY_BGDB_HEADER_SIZE + 8) {
return NULL;
}
R2UnityBGDatabase *db = R_NEW0 (R2UnityBGDatabase);
db->buf = r_buf_new_slice (buf, 0, size);
if (!db->buf) {
free (db);
return NULL;
}
db->size = size;
R2UnityBGDatabaseReader r = {
.buf = db->buf,
.offset = 0,
.limit = size,
};
ut32 addon_count;
if (!reader_u32 (&r, &db->version) || db->version != R2U_BGDB_VERSION
|| !reader_read (&r, db->repository_id, sizeof (db->repository_id))
|| !reader_u32 (&r, &db->header_value)
|| !reader_u32 (&r, &addon_count)
|| !addon_count || addon_count > R2U_BGDB_MAX_ADDONS) {
goto fail;
}
db->addon_count = addon_count;
db->addons = R_NEWS0 (R2UnityBGDatabaseAddon, addon_count);
if (!db->addons) {
goto fail;
}
bool recognized = false;
for (size_t i = 0; i < db->addon_count; i++) {
R2UnityBGDatabaseAddon *addon = &db->addons[i];
addon->offset = r.offset;
if (!reader_string (&r, &addon->type, &addon->type_offset,
&addon->type_size)) {
goto fail;
}
recognized |= addon_type_valid (addon->type);
ut32 payload_size;
addon->payload_size_offset = r.offset;
if (!reader_u32 (&r, &payload_size)) {
goto fail;
}
addon->payload_offset = r.offset;
addon->payload_size = payload_size;
if (!reader_skip (&r, payload_size)) {
goto fail;
}
}
if (!recognized) {
goto fail;
}
db->body_offset = r.offset;
ut32 table_count;
if (!reader_u32 (&r, &table_count) || !table_count
|| table_count > R2U_BGDB_MAX_TABLES || size > INT_MAX) {
goto fail;
}
int data_size = 0;
ut8 *data = r_buf_read_all (db->buf, &data_size);
bool valid = data && (ut64)data_size == size
&& collect_tables (db, data, table_count)
&& (!strings || collect_strings (db, data));
free (data);
if (!valid) {
goto fail;
}
return db;
fail:
r2unity_bgdatabase_free (db);
return NULL;
}
bool r2unity_bgdatabase_check(RBuffer *buf) {
R_RETURN_VAL_IF_FAIL (buf, false);
R2UnityBGDatabase *db = parse_internal (buf, false);
if (!db) {
return false;
}
r2unity_bgdatabase_free (db);
return true;
}
R2UnityBGDatabase *r2unity_bgdatabase_parse(RBuffer *buf) {
R_RETURN_VAL_IF_FAIL (buf, NULL);
return parse_internal (buf, true);
}
char *r2unity_bgdatabase_id_string(const ut8 id[16]) {
R_RETURN_VAL_IF_FAIL (id, NULL);
char *result = R_NEWS (char, 33);
if (!result) {
return NULL;
}
for (size_t i = 0; i < 16; i++) {
snprintf (result + i * 2, 3, "%02x", id[i]);
}
return result;
}
void r2unity_bgdatabase_free(R2UnityBGDatabase *db) {
if (!db) {
return;
}
for (size_t i = 0; i < db->addon_count; i++) {
free (db->addons[i].type);
}
for (size_t i = 0; i < db->string_count; i++) {
free (db->strings[i].value);
}
free (db->addons);
free (db->tables);
free (db->strings);
r_unref (db->buf);
free (db);
}
+53
View File
@@ -0,0 +1,53 @@
#ifndef R2UNITY_BGDATABASE_H
#define R2UNITY_BGDATABASE_H
#include "lib.h"
#define R2UNITY_BGDB_HEADER_SIZE 0x1c
typedef struct {
ut64 offset;
ut64 type_offset;
ut32 type_size;
char *type;
ut64 payload_size_offset;
ut64 payload_offset;
ut64 payload_size;
} R2UnityBGDatabaseAddon;
typedef struct {
ut64 offset;
ut64 size;
ut64 name_field_offset;
ut32 field_count;
ut16 name_field_type;
} R2UnityBGDatabaseTable;
typedef struct {
ut64 prefix_offset;
ut64 offset;
ut32 size;
char *value;
} R2UnityBGDatabaseString;
typedef struct {
RBuffer *buf;
ut64 size;
ut32 version;
ut8 repository_id[16];
ut32 header_value;
ut64 body_offset;
R2UnityBGDatabaseAddon *addons;
size_t addon_count;
R2UnityBGDatabaseTable *tables;
size_t table_count;
R2UnityBGDatabaseString *strings;
size_t string_count;
} R2UnityBGDatabase;
bool r2unity_bgdatabase_check(RBuffer *buf);
R2UnityBGDatabase *r2unity_bgdatabase_parse(RBuffer *buf);
void r2unity_bgdatabase_free(R2UnityBGDatabase *db);
char *r2unity_bgdatabase_id_string(const ut8 id[16]);
#endif
+301
View File
@@ -0,0 +1,301 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
#define R_LOG_ORIGIN "r2unity.elf"
#include "native_internal.h"
#include <string.h>
typedef struct {
ut64 vaddr;
ut64 memsz;
ut64 offset;
ut64 filesz;
ut32 flags;
ut32 type;
} ElfSeg;
typedef struct {
ut8 *file;
ut64 size;
bool is64;
ElfSeg segs[128];
size_t nsegs;
} ElfImg;
static void elf_write_word(ut8 *p, ut64 v, bool is64) {
if (is64) {
r_write_le64 (p, v);
} else {
r_write_le32 (p, (ut32)v);
}
}
static bool elf_range_in_file(const ElfImg *e, ut64 off, ut64 size) {
return size <= e->size && off <= e->size - size;
}
static bool elf_load(const char *path, ElfImg *e) {
memset (e, 0, sizeof (*e));
size_t size = 0;
e->file = (ut8 *)r_file_slurp (path, &size);
if (!e->file || size < 0x40) {
R_FREE (e->file);
return false;
}
e->size = size;
if (memcmp (e->file, "\x7f""ELF", 4) || e->file[5] != 1) {
R_FREE (e->file);
return false;
}
e->is64 = e->file[4] == 2;
if (e->is64) {
ut64 phoff = r_read_le64 (e->file + 0x20);
ut16 phentsz = r_read_le16 (e->file + 0x36);
ut16 phnum = r_read_le16 (e->file + 0x38);
for (ut16 i = 0; i < phnum && e->nsegs < R_ARRAY_SIZE (e->segs); i++) {
ut64 off = phoff + (ut64)i * phentsz;
if (off + 56 > e->size) {
break;
}
const ut8 *ph = e->file + off;
ut32 type = r_read_le32 (ph);
if (type != 1 && type != 2) {
continue;
}
ElfSeg *s = &e->segs[e->nsegs];
s->type = type;
s->flags = r_read_le32 (ph + 4);
s->offset = r_read_le64 (ph + 8);
s->vaddr = r_read_le64 (ph + 16);
s->filesz = r_read_le64 (ph + 32);
s->memsz = r_read_le64 (ph + 40);
if (s->filesz && elf_range_in_file (e, s->offset, s->filesz)) {
e->nsegs++;
}
}
} else {
ut32 phoff = r_read_le32 (e->file + 0x1c);
ut16 phentsz = r_read_le16 (e->file + 0x2a);
ut16 phnum = r_read_le16 (e->file + 0x2c);
for (ut16 i = 0; i < phnum && e->nsegs < R_ARRAY_SIZE (e->segs); i++) {
ut64 off = (ut64)phoff + (ut64)i * phentsz;
if (off + 32 > e->size) {
break;
}
const ut8 *ph = e->file + off;
ut32 type = r_read_le32 (ph);
if (type != 1 && type != 2) {
continue;
}
ElfSeg *s = &e->segs[e->nsegs];
s->type = type;
s->offset = r_read_le32 (ph + 4);
s->vaddr = r_read_le32 (ph + 8);
s->filesz = r_read_le32 (ph + 16);
s->memsz = r_read_le32 (ph + 20);
s->flags = r_read_le32 (ph + 24);
if (s->filesz && elf_range_in_file (e, s->offset, s->filesz)) {
e->nsegs++;
}
}
}
return true;
}
static void elf_free(ElfImg *e) {
R_FREE (e->file);
}
static const ut8 *elf_ptr_at_size(ElfImg *e, ut64 va, ut64 size) {
for (size_t i = 0; i < e->nsegs; i++) {
const ElfSeg *s = &e->segs[i];
ut64 memsz = s->memsz? s->memsz: s->filesz;
if (va < s->vaddr || va >= s->vaddr + memsz) {
continue;
}
ut64 delta = va - s->vaddr;
if (size <= s->filesz && delta <= s->filesz - size && elf_range_in_file (e, s->offset + delta, size)) {
return e->file + s->offset + delta;
}
return NULL;
}
return NULL;
}
static const ut8 *elf_ptr_at(void *user, ut64 va) {
return elf_ptr_at_size ((ElfImg *)user, va, 8);
}
static void elf_ranges(ElfImg *e, ut64 *base, ut64 *text_lo, ut64 *text_hi) {
*base = UT64_MAX;
*text_lo = UT64_MAX;
*text_hi = 0;
for (size_t i = 0; i < e->nsegs; i++) {
const ElfSeg *s = &e->segs[i];
if (s->vaddr < *base) {
*base = s->vaddr;
}
if ((s->flags & 1) && (s->flags & 4)) {
*text_lo = R_MIN (*text_lo, s->vaddr);
*text_hi = R_MAX (*text_hi, s->vaddr + (s->memsz? s->memsz: s->filesz));
}
}
if (*base == UT64_MAX) {
*base = 0;
}
if (*text_lo == UT64_MAX || *text_hi <= *text_lo) {
*text_lo = *base;
for (size_t i = 0; i < e->nsegs; i++) {
ut64 memsz = e->segs[i].memsz? e->segs[i].memsz: e->segs[i].filesz;
*text_hi = R_MAX (*text_hi, e->segs[i].vaddr + memsz);
}
}
}
static R2UnityNativeSection *elf_sections(ElfImg *e, size_t *count) {
*count = e->nsegs;
R2UnityNativeSection *out = R_NEWS0 (R2UnityNativeSection, e->nsegs);
if (!out) {
*count = 0;
return NULL;
}
for (size_t i = 0; i < e->nsegs; i++) {
const ElfSeg *s = &e->segs[i];
ut64 memsz = s->memsz? s->memsz: s->filesz;
out[i].vaddr = s->vaddr;
out[i].vsize = memsz;
out[i].size = R_MIN (s->filesz, memsz);
out[i].perm = ((s->flags & 4)? R_PERM_R: 0) | ((s->flags & 2)? R_PERM_W: 0) | ((s->flags & 1)? R_PERM_X: 0);
out[i].is_data = (s->flags & 2) && !(s->flags & 1);
}
return out;
}
static void elf_apply_relocs(ElfImg *e, ut64 base) {
ut64 dyn_off = 0, dyn_sz = 0;
for (size_t i = 0; i < e->nsegs; i++) {
if (e->segs[i].type == 2) {
dyn_off = e->segs[i].offset;
dyn_sz = e->segs[i].filesz;
break;
}
}
if (!dyn_off || !dyn_sz || !elf_range_in_file (e, dyn_off, dyn_sz)) {
return;
}
ut64 rela_off = 0, rela_sz = 0, rela_ent = e->is64? 24: 12;
ut64 rel_off = 0, rel_sz = 0, rel_ent = e->is64? 16: 8;
ut64 relr_off = 0, relr_sz = 0, relr_ent = e->is64? 8: 4;
const ut8 *d = e->file + dyn_off;
ut64 dyn_ent = e->is64? 16: 8;
for (ut64 off = 0; off + dyn_ent <= dyn_sz; off += dyn_ent) {
ut64 tag = e->is64? r_read_le64 (d + off): (ut64)r_read_le32 (d + off);
ut64 val = e->is64? r_read_le64 (d + off + 8): (ut64)r_read_le32 (d + off + 4);
if (!tag) {
break;
}
switch (tag) {
case 7: rela_off = val; break;
case 8: rela_sz = val; break;
case 9: rela_ent = val; break;
case 17: rel_off = val; break;
case 18: rel_sz = val; break;
case 19: rel_ent = val; break;
case 35: relr_sz = val; break;
case 36: relr_off = val; break;
case 37: relr_ent = val; break;
default: break;
}
}
const ut64 type_mask = e->is64? 0xffffffffULL: 0xffULL;
for (ut64 i = 0; rela_off && rela_ent == (e->is64? 24: 12) && i + rela_ent <= rela_sz; i += rela_ent) {
const ut8 *rp = elf_ptr_at_size (e, rela_off + i, rela_ent);
if (!rp) {
break;
}
ut64 r_offset = e->is64? r_read_le64 (rp): (ut64)r_read_le32 (rp);
ut64 r_info = e->is64? r_read_le64 (rp + 8): (ut64)r_read_le32 (rp + 4);
ut64 r_addend = e->is64? r_read_le64 (rp + 16): (ut64)r_read_le32 (rp + 8);
ut64 type = r_info & type_mask;
if (type == 8 || type == 23 || type == 1027) {
ut8 *loc = (ut8 *)elf_ptr_at_size (e, r_offset, e->is64? 8: 4);
if (loc) {
elf_write_word (loc, base + r_addend, e->is64);
}
}
}
for (ut64 i = 0; rel_off && rel_ent == (e->is64? 16: 8) && i + rel_ent <= rel_sz; i += rel_ent) {
const ut8 *rp = elf_ptr_at_size (e, rel_off + i, rel_ent);
if (!rp) {
break;
}
ut64 r_offset = e->is64? r_read_le64 (rp): (ut64)r_read_le32 (rp);
ut64 r_info = e->is64? r_read_le64 (rp + 8): (ut64)r_read_le32 (rp + 4);
ut64 type = r_info & type_mask;
if (type == 8 || type == 23 || type == 1027) {
ut8 *loc = (ut8 *)elf_ptr_at_size (e, r_offset, e->is64? 8: 4);
if (loc) {
ut64 addend = e->is64? r_read_le64 (loc): (ut64)r_read_le32 (loc);
elf_write_word (loc, base + addend, e->is64);
}
}
}
if (!e->is64 || !relr_off || !relr_sz || relr_ent != 8) {
return;
}
ut64 curr = 0;
for (ut64 i = 0; i + relr_ent <= relr_sz; i += relr_ent) {
const ut8 *rp = elf_ptr_at_size (e, relr_off + i, relr_ent);
if (!rp) {
break;
}
ut64 r = r_read_le64 (rp);
if (!(r & 1)) {
ut8 *loc = (ut8 *)elf_ptr_at_size (e, r, 8);
if (loc) {
elf_write_word (loc, base + r_read_le64 (loc), true);
}
curr = r + 8;
continue;
}
ut64 bitmap = r >> 1;
for (int bit = 0; bit < 63; bit++) {
if (bitmap & (1ULL << bit)) {
ut8 *loc = (ut8 *)elf_ptr_at_size (e, curr + (ut64)(bit + 1) * 8, 8);
if (loc) {
elf_write_word (loc, base + r_read_le64 (loc), true);
}
}
}
curr += 8 * 63;
}
}
bool r2unity_find_method_pointers_elf(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result) {
if (!meta || !path || !result) {
return false;
}
ElfImg e;
if (!elf_load (path, &e)) {
return false;
}
ut64 base = 0, text_lo = 0, text_hi = 0;
elf_ranges (&e, &base, &text_lo, &text_hi);
elf_apply_relocs (&e, base);
R2UnityNativeView view = {
.user = &e,
.ptr_at = elf_ptr_at,
.ptr_size = e.is64? 8: 4,
.base_vaddr = base,
.text_lo = text_lo,
.text_hi = text_hi
};
size_t section_count = 0;
R2UnityNativeSection *sections = elf_sections (&e, &section_count);
view.sections = sections;
view.section_count = section_count;
bool ok = r2unity_native_resolve (meta, &view, NULL, options, result);
R_FREE (sections);
elf_free (&e);
return ok;
}
+179
View File
@@ -0,0 +1,179 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
#define R_LOG_ORIGIN "r2unity.macho"
#include "native_internal.h"
#include <string.h>
typedef struct {
char name[17];
ut64 vmaddr;
ut64 vmsize;
ut64 fileoff;
ut64 filesize;
ut32 maxprot;
} MachSeg;
typedef struct {
ut8 *file;
ut64 size;
ut64 base;
ut64 vm_base;
MachSeg segs[128];
size_t nsegs;
} MachO;
static bool macho_load(const char *path, MachO *mo) {
memset (mo, 0, sizeof (*mo));
size_t size = 0;
mo->file = (ut8 *)r_file_slurp (path, &size);
if (!mo->file || size < 0x20) {
R_FREE (mo->file);
return false;
}
mo->size = size;
ut32 magic = r_read_be32 (mo->file);
ut64 off = 0;
if (magic == 0xcafebabe || magic == 0xbebafeca) {
ut32 nfat = r_read_be32 (mo->file + 4);
ut32 best = 0;
for (ut32 i = 0; i < nfat && 8 + (ut64)i * 20 + 20 <= mo->size; i++) {
const ut8 *fa = mo->file + 8 + (ut64)i * 20;
if (r_read_be32 (fa) == 0x0100000c) {
best = i;
break;
}
}
const ut8 *fa = mo->file + 8 + (ut64)best * 20;
off = r_read_be32 (fa + 8);
}
if (off + 0x20 > mo->size) {
R_FREE (mo->file);
return false;
}
mo->base = off;
const ut8 *mh = mo->file + off;
if (r_read_le32 (mh) != 0xfeedfacf) {
R_FREE (mo->file);
return false;
}
ut32 ncmds = r_read_le32 (mh + 0x10);
ut64 co = off + 0x20;
for (ut32 i = 0; i < ncmds && co + 8 <= mo->size; i++) {
ut32 cmd = r_read_le32 (mo->file + co);
ut32 cmdsz = r_read_le32 (mo->file + co + 4);
if (!cmdsz || co + cmdsz > mo->size) {
break;
}
if (cmd == 0x19 && cmdsz >= 72 && mo->nsegs < R_ARRAY_SIZE (mo->segs)) {
const ut8 *sp = mo->file + co + 8;
MachSeg *s = &mo->segs[mo->nsegs++];
memcpy (s->name, sp, 16);
s->name[16] = 0;
s->vmaddr = r_read_le64 (sp + 16);
s->vmsize = r_read_le64 (sp + 24);
s->fileoff = r_read_le64 (sp + 32);
s->filesize = r_read_le64 (sp + 40);
s->maxprot = r_read_le32 (sp + 48);
if (!mo->vm_base || s->vmaddr < mo->vm_base) {
mo->vm_base = s->vmaddr;
}
}
co += cmdsz;
}
return mo->nsegs > 0;
}
static void macho_free(MachO *mo) {
R_FREE (mo->file);
}
static const ut8 *macho_ptr_at(void *user, ut64 va) {
MachO *mo = (MachO *)user;
for (size_t i = 0; i < mo->nsegs; i++) {
const MachSeg *s = &mo->segs[i];
ut64 vsize = s->vmsize? s->vmsize: s->filesize;
if (va < s->vmaddr || va >= s->vmaddr + vsize) {
continue;
}
ut64 delta = va - s->vmaddr;
if (delta + 8 <= s->filesize && mo->base + s->fileoff + delta + 8 <= mo->size) {
return mo->file + mo->base + s->fileoff + delta;
}
return NULL;
}
return NULL;
}
static void macho_text_range(MachO *mo, ut64 *lo, ut64 *hi) {
*lo = UT64_MAX;
*hi = 0;
for (size_t i = 0; i < mo->nsegs; i++) {
const MachSeg *s = &mo->segs[i];
if ((s->maxprot & 4) || !strncmp (s->name, "__TEXT", 6)) {
*lo = R_MIN (*lo, s->vmaddr);
*hi = R_MAX (*hi, s->vmaddr + (s->vmsize? s->vmsize: s->filesize));
}
}
if (*lo != UT64_MAX && *hi > *lo) {
return;
}
*lo = UT64_MAX;
*hi = 0;
for (size_t i = 0; i < mo->nsegs; i++) {
*lo = R_MIN (*lo, mo->segs[i].vmaddr);
ut64 vsize = mo->segs[i].vmsize? mo->segs[i].vmsize: mo->segs[i].filesize;
*hi = R_MAX (*hi, mo->segs[i].vmaddr + vsize);
}
if (*lo == UT64_MAX) {
*lo = 0;
}
}
static R2UnityNativeSection *macho_sections(MachO *mo, size_t *count) {
*count = mo->nsegs;
R2UnityNativeSection *out = R_NEWS0 (R2UnityNativeSection, mo->nsegs);
if (!out) {
*count = 0;
return NULL;
}
for (size_t i = 0; i < mo->nsegs; i++) {
const MachSeg *s = &mo->segs[i];
ut64 vsize = s->vmsize? s->vmsize: s->filesize;
out[i].name = s->name;
out[i].vaddr = s->vmaddr;
out[i].vsize = vsize;
out[i].size = R_MIN (s->filesize, vsize);
out[i].perm = ((s->maxprot & 1)? R_PERM_R: 0) | ((s->maxprot & 2)? R_PERM_W: 0) | ((s->maxprot & 4)? R_PERM_X: 0);
out[i].is_data = (s->maxprot & 1) && !(s->maxprot & 4);
}
return out;
}
bool r2unity_find_method_pointers_macho(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result) {
if (!meta || !path || !result) {
return false;
}
MachO mo;
if (!macho_load (path, &mo)) {
return false;
}
ut64 text_lo = 0, text_hi = 0;
macho_text_range (&mo, &text_lo, &text_hi);
R2UnityNativeView view = {
.user = &mo,
.ptr_at = macho_ptr_at,
.ptr_size = 8,
.base_vaddr = mo.vm_base,
.text_lo = text_lo,
.text_hi = text_hi
};
size_t section_count = 0;
R2UnityNativeSection *sections = macho_sections (&mo, &section_count);
view.sections = sections;
view.section_count = section_count;
bool ok = r2unity_native_resolve (meta, &view, NULL, options, result);
R_FREE (sections);
macho_free (&mo);
return ok;
}
+1298
View File
File diff suppressed because it is too large. Load diff
+35
View File
@@ -0,0 +1,35 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
#ifndef R2UNITY_NATIVE_INTERNAL_H
#define R2UNITY_NATIVE_INTERNAL_H
#include "../lib.h"
typedef const ut8 *(*R2UnityNativePtrAt)(void *user, ut64 va);
typedef struct {
void *user;
R2UnityNativePtrAt ptr_at;
int ptr_size;
ut64 base_vaddr;
ut64 text_lo;
ut64 text_hi;
const struct r2unity_native_section_t *sections;
size_t section_count;
} R2UnityNativeView;
typedef struct r2unity_native_section_t {
const char *name;
ut64 vaddr;
ut64 vsize;
ut64 size;
ut32 perm;
bool is_data;
} R2UnityNativeSection;
bool r2unity_native_resolve(R2UnityMetadata *meta, R2UnityNativeView *view, RBinFile *bf, const R2UnityNativeOptions *options, R2UnityNativeResult *result);
bool r2unity_find_method_pointers_elf(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result);
bool r2unity_find_method_pointers_macho(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result);
bool r2unity_find_method_pointers_pe(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result);
#endif
+185
View File
@@ -0,0 +1,185 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
#define R_LOG_ORIGIN "r2unity.pe"
#include "native_internal.h"
#include <string.h>
typedef struct {
char name[9];
ut64 vaddr;
ut64 vsize;
ut64 fileoff;
ut64 filesize;
ut32 chars;
} PeSec;
typedef struct {
ut8 *file;
ut64 size;
bool is64;
ut64 image_base;
PeSec secs[128];
size_t nsecs;
} PeImg;
static bool pe_load(const char *path, PeImg *pe) {
memset (pe, 0, sizeof (*pe));
size_t size = 0;
pe->file = (ut8 *)r_file_slurp (path, &size);
if (!pe->file || size < 0x40) {
R_FREE (pe->file);
return false;
}
pe->size = size;
if (pe->file[0] != 'M' || pe->file[1] != 'Z') {
R_FREE (pe->file);
return false;
}
ut32 e_lfanew = r_read_le32 (pe->file + 0x3c);
if ((ut64)e_lfanew + 24 > pe->size) {
R_FREE (pe->file);
return false;
}
const ut8 *nt = pe->file + e_lfanew;
if (r_read_le32 (nt) != 0x4550) {
R_FREE (pe->file);
return false;
}
ut16 nsec = r_read_le16 (nt + 6);
ut16 optsz = r_read_le16 (nt + 20);
ut64 optoff = (ut64)e_lfanew + 24;
if (optoff + optsz > pe->size) {
R_FREE (pe->file);
return false;
}
const ut8 *opt = pe->file + optoff;
ut16 magic = r_read_le16 (opt);
if (magic == 0x20b) {
pe->is64 = true;
pe->image_base = r_read_le64 (opt + 24);
} else if (magic == 0x10b) {
pe->image_base = r_read_le32 (opt + 28);
} else {
R_FREE (pe->file);
return false;
}
ut64 shoff = optoff + optsz;
for (ut16 i = 0; i < nsec && pe->nsecs < R_ARRAY_SIZE (pe->secs); i++) {
ut64 off = shoff + (ut64)i * 40;
if (off + 40 > pe->size) {
break;
}
const ut8 *sh = pe->file + off;
PeSec *s = &pe->secs[pe->nsecs++];
memcpy (s->name, sh, 8);
s->name[8] = 0;
s->vsize = r_read_le32 (sh + 8);
s->vaddr = pe->image_base + r_read_le32 (sh + 12);
s->filesize = r_read_le32 (sh + 16);
s->fileoff = r_read_le32 (sh + 20);
s->chars = r_read_le32 (sh + 36);
}
return pe->nsecs > 0;
}
static void pe_free(PeImg *pe) {
R_FREE (pe->file);
}
static const ut8 *pe_ptr_at(void *user, ut64 va) {
PeImg *pe = (PeImg *)user;
for (size_t i = 0; i < pe->nsecs; i++) {
const PeSec *s = &pe->secs[i];
ut64 vsize = s->vsize? s->vsize: s->filesize;
if (va < s->vaddr || va >= s->vaddr + vsize) {
continue;
}
ut64 delta = va - s->vaddr;
if (delta + 8 <= s->filesize && s->fileoff + delta + 8 <= pe->size) {
return pe->file + s->fileoff + delta;
}
return NULL;
}
return NULL;
}
static bool pe_is_data_section(const PeSec *s) {
if (s->chars & 0x20000000) {
return false;
}
return !strncmp (s->name, ".data", 5) || !strncmp (s->name, ".rdata", 6) || (s->chars & 0x40);
}
static void pe_text_range(PeImg *pe, ut64 *lo, ut64 *hi) {
*lo = UT64_MAX;
*hi = 0;
for (size_t i = 0; i < pe->nsecs; i++) {
const PeSec *s = &pe->secs[i];
if ((s->chars & 0x20000000) || !strncmp (s->name, ".text", 5)) {
*lo = R_MIN (*lo, s->vaddr);
*hi = R_MAX (*hi, s->vaddr + (s->vsize? s->vsize: s->filesize));
}
}
if (*lo != UT64_MAX && *hi > *lo) {
return;
}
*lo = UT64_MAX;
*hi = 0;
for (size_t i = 0; i < pe->nsecs; i++) {
*lo = R_MIN (*lo, pe->secs[i].vaddr);
ut64 vsize = pe->secs[i].vsize? pe->secs[i].vsize: pe->secs[i].filesize;
*hi = R_MAX (*hi, pe->secs[i].vaddr + vsize);
}
if (*lo == UT64_MAX) {
*lo = pe->image_base;
}
}
static R2UnityNativeSection *pe_sections(PeImg *pe, size_t *count) {
*count = pe->nsecs;
R2UnityNativeSection *out = R_NEWS0 (R2UnityNativeSection, pe->nsecs);
if (!out) {
*count = 0;
return NULL;
}
for (size_t i = 0; i < pe->nsecs; i++) {
const PeSec *s = &pe->secs[i];
ut64 vsize = s->vsize? s->vsize: s->filesize;
out[i].name = s->name;
out[i].vaddr = s->vaddr;
out[i].vsize = vsize;
out[i].size = R_MIN (s->filesize, vsize);
out[i].perm = ((s->chars & 0x40000000)? R_PERM_R: 0) | ((s->chars & 0x80000000)? R_PERM_W: 0) | ((s->chars & 0x20000000)? R_PERM_X: 0);
out[i].is_data = pe_is_data_section (s);
}
return out;
}
bool r2unity_find_method_pointers_pe(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result) {
if (!meta || !path || !result) {
return false;
}
PeImg pe;
if (!pe_load (path, &pe)) {
return false;
}
ut64 text_lo = 0, text_hi = 0;
pe_text_range (&pe, &text_lo, &text_hi);
R2UnityNativeView view = {
.user = &pe,
.ptr_at = pe_ptr_at,
.ptr_size = pe.is64? 8: 4,
.base_vaddr = pe.image_base,
.text_lo = text_lo,
.text_hi = text_hi
};
size_t section_count = 0;
R2UnityNativeSection *sections = pe_sections (&pe, &section_count);
view.sections = sections;
view.section_count = section_count;
bool ok = r2unity_native_resolve (meta, &view, NULL, options, result);
R_FREE (sections);
pe_free (&pe);
return ok;
}
-389
View File
@@ -1,389 +0,0 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
// Fast ELF parser for r2unity
#define R_LOG_ORIGIN "r2unity.elf"
#include "lib.h"
#include <string.h>
static inline void elf_write_word(ut8 *loc, ut64 val, bool is64) {
if (is64) {
r_write_le64 (loc, val);
} else {
r_write_le32 (loc, (ut32)val);
}
}
typedef struct {
ut64 vaddr;
ut64 memsz;
ut64 offset;
ut64 filesz;
ut32 flags;
ut32 p_type;
} ElfSeg;
typedef struct {
ut8 *file;
ut64 filesize;
int is64;
int le;
ElfSeg segs[128];
int nsegs;
} ElfImg;
static bool elf_load(const char *path, ElfImg *e) {
memset (e, 0, sizeof (*e));
size_t sz = 0;
e->file = (ut8 *)r_file_slurp (path, &sz);
if (!e->file || sz == 0) {
R_FREE (e->file);
return false;
}
e->filesize = (ut64)sz;
const ut8 *p = e->file;
if (! (p[0] == 0x7f && p[1] == 'E' && p[2] == 'L' && p[3] == 'F')) {
R_FREE (e->file);
return false;
}
e->is64 = (p[4] == 2);
e->le = (p[5] == 1);
if (!e->le) {
R_FREE (e->file);
return false;
}
if (e->is64) {
ut64 phoff = r_read_le64 (p + 0x20);
ut16 phentsize = r_read_le16 (p + 0x36);
ut16 phnum = r_read_le16 (p + 0x38);
for (ut16 i = 0; i < phnum && e->nsegs < (int) (sizeof (e->segs) / sizeof (e->segs[0])); i++) {
ut64 off = phoff + (ut64)i * phentsize;
if (off + 56 > e->filesize) {
break;
}
const ut8 *ph = p + off;
ut32 p_type = r_read_le32 (ph + 0);
ut32 p_flags = r_read_le32 (ph + 4);
ut64 p_offset = r_read_le64 (ph + 8);
ut64 p_vaddr = r_read_le64 (ph + 16);
ut64 p_filesz = r_read_le64 (ph + 32);
ut64 p_memsz = r_read_le64 (ph + 40);
if (p_type == 1 || p_type == 2) {
ElfSeg *s = &e->segs[e->nsegs++];
s->vaddr = p_vaddr;
s->memsz = p_memsz;
s->offset = p_offset;
s->filesz = p_filesz;
s->flags = p_flags;
s->p_type = p_type;
}
}
} else {
ut32 phoff = r_read_le32 (p + 0x1C);
ut16 phentsize = r_read_le16 (p + 0x2A);
ut16 phnum = r_read_le16 (p + 0x2C);
for (ut16 i = 0; i < phnum && e->nsegs < (int) (sizeof (e->segs) / sizeof (e->segs[0])); i++) {
ut64 off = (ut64)phoff + (ut64)i * phentsize;
if (off + 32 > e->filesize) {
break;
}
const ut8 *ph = p + off;
ut32 p_type = r_read_le32 (ph + 0);
ut32 p_offset = r_read_le32 (ph + 4);
ut32 p_vaddr = r_read_le32 (ph + 8);
ut32 p_filesz = r_read_le32 (ph + 16);
ut32 p_memsz = r_read_le32 (ph + 20);
ut32 p_flags = r_read_le32 (ph + 24);
if (p_type == 1 || p_type == 2) {
ElfSeg *s = &e->segs[e->nsegs++];
s->vaddr = p_vaddr;
s->memsz = p_memsz;
s->offset = p_offset;
s->filesz = p_filesz;
s->flags = p_flags;
s->p_type = p_type;
}
}
}
return true;
}
static void elf_free(ElfImg *e) {
R_FREE (e->file);
}
static inline const ut8 *elf_vm_to_ptr(ElfImg *e, ut64 vaddr) {
for (int i = 0; i < e->nsegs; i++) {
const ElfSeg *s = &e->segs[i];
if (vaddr >= s->vaddr && vaddr < s->vaddr + s->memsz) {
ut64 delta = vaddr - s->vaddr;
if (delta < s->filesz && s->offset + delta < e->filesize) {
return e->file + s->offset + delta;
}
return NULL;
}
}
return NULL;
}
/* Walk a candidate method pointer table at VA `arrptr`. Entries below text_lo
* are retried with base_vaddr added (RVA→VA fallback). On match, copy up to
* method_count entries into candidates and return true. */
static bool elf_probe_table(ElfImg *e, ut64 arrptr, ut32 count, int ptrsz, ut64 base_vaddr, ut64 text_lo, ut64 text_hi, ut32 min_seen, ut32 min_good, size_t method_count, ut64 *candidates) {
ut32 sample = R_MIN ((ut32)128, count);
ut32 good = 0, seen = 0;
for (ut32 k = 0; k < sample; k++) {
const ut8 *pp = elf_vm_to_ptr (e, arrptr + (ut64)k *(ut64)ptrsz);
if (!pp && base_vaddr != UT64_MAX) {
pp = elf_vm_to_ptr (e, base_vaddr + arrptr + (ut64)k *(ut64)ptrsz);
}
if (!pp) {
return false;
}
ut64 val = (ptrsz == 8)? r_read_le64 (pp): (ut64)r_read_le32 (pp);
if (val) {
seen++;
}
if ((val >= text_lo && val < text_hi) || (val && base_vaddr != UT64_MAX && (val + base_vaddr) >= text_lo && (val + base_vaddr) < text_hi)) {
good++;
}
}
if (seen < min_seen || good < min_good) {
return false;
}
memset (candidates, 0, method_count * sizeof (ut64));
size_t tocopy = R_MIN ((size_t)count, method_count);
size_t in_text = 0;
for (size_t m = 0; m < tocopy; m++) {
const ut8 *pp = elf_vm_to_ptr (e, arrptr + (ut64)m *(ut64)ptrsz);
if (!pp && base_vaddr != UT64_MAX) {
pp = elf_vm_to_ptr (e, base_vaddr + arrptr + (ut64)m *(ut64)ptrsz);
}
if (!pp) {
break;
}
ut64 val = (ptrsz == 8)? r_read_le64 (pp): (ut64)r_read_le32 (pp);
ut64 abs = (val >= text_lo && val < text_hi)
? val
: ((val && base_vaddr != UT64_MAX)? val + base_vaddr: val);
if (abs >= text_lo && abs < text_hi) {
candidates[m] = abs;
in_text++;
}
}
return in_text >= 8;
}
R_API bool r2unity_find_method_pointers_elf(R2UnityMetadata *meta, const char *elf_path, ut64 **out_ptrs) {
R_RETURN_VAL_IF_FAIL (meta && elf_path && out_ptrs, false);
*out_ptrs = NULL;
ElfImg e;
if (!elf_load (elf_path, &e)) {
return false;
}
size_t method_count = (ut64)meta->methodsSize / sizeof (Il2CppMethodDefinition);
if (!method_count) {
elf_free (&e);
return false;
}
ut64 base_vaddr = UT64_MAX, text_lo = UT64_MAX, text_hi = 0;
for (int i = 0; i < e.nsegs; i++) {
const ElfSeg *s = &e.segs[i];
if (s->vaddr < base_vaddr) {
base_vaddr = s->vaddr;
}
if ((s->flags & 0x1) && (s->flags & 0x4)) {
if (s->vaddr < text_lo) {
text_lo = s->vaddr;
}
if (s->vaddr + s->memsz > text_hi) {
text_hi = s->vaddr + s->memsz;
}
}
}
if (text_lo == UT64_MAX || text_hi <= text_lo) {
elf_free (&e);
return false;
}
ut64 *candidates = R_NEWS (ut64, method_count);
if (!candidates) {
elf_free (&e);
return false;
}
bool found = false;
int ptrsz = e.is64? 8: 4;
// Apply REL (A)/RELR (relative only)
ut64 dyn_off = 0, dyn_sz = 0;
for (int i = 0; i < e.nsegs; i++) {
const ElfSeg *s = &e.segs[i];
if (s->p_type == 2) {
dyn_off = s->offset;
dyn_sz = s->filesz;
break;
}
}
ut64 rela_off = 0, rela_sz = 0, rela_ent = e.is64? 24: 12;
ut64 rel_off = 0, rel_sz = 0, rel_ent = e.is64? 16: 8;
ut64 relr_off = 0, relr_sz = 0, relr_ent = e.is64? 8: 4;
if (dyn_off && dyn_sz) {
const ut8 *d = e.file + dyn_off;
for (ut64 off = 0; off + (e.is64? 16: 8) <= dyn_sz; off += (e.is64? 16: 8)) {
ut64 tag = e.is64? r_read_le64 (d + off): (ut64)r_read_le32 (d + off);
ut64 val = e.is64? r_read_le64 (d + off + 8): (ut64)r_read_le32 (d + off + 4);
if (tag == 0) {
break;
}
if (tag == 7) {
rela_off = val;
} else if (tag == 8) {
rela_sz = val;
} else if (tag == 9) {
rela_ent = val;
} else if (tag == 17) {
rel_off = val;
} else if (tag == 18) {
rel_sz = val;
} else if (tag == 19) {
rel_ent = val;
} else if (tag == 36) {
relr_off = val;
} else if (tag == 35) {
relr_sz = val;
} else if (tag == 37) {
relr_ent = val;
}
}
}
const ut64 base = (base_vaddr != UT64_MAX)? base_vaddr: 0;
const ut64 type_mask = e.is64? 0xffffffffULL: 0xffULL;
if (rela_off && rela_sz && rela_ent) {
for (ut64 i = 0; i + rela_ent <= rela_sz; i += rela_ent) {
const ut8 *rp = elf_vm_to_ptr (&e, rela_off + i);
if (!rp) {
break;
}
ut64 r_offset = e.is64? r_read_le64 (rp + 0): (ut64)r_read_le32 (rp + 0);
ut64 r_info = e.is64? r_read_le64 (rp + 8): (ut64)r_read_le32 (rp + 4);
ut64 r_addend = e.is64? r_read_le64 (rp + 16): (ut64)r_read_le32 (rp + 8);
ut64 type = r_info & type_mask;
bool is_relative = (type == 8) || (type == 23) || (type == 1027);
if (!is_relative) {
continue;
}
ut8 *loc = (ut8 *)elf_vm_to_ptr (&e, r_offset);
if (loc) {
elf_write_word (loc, base + r_addend, e.is64);
}
}
}
if (rel_off && rel_sz && rel_ent) {
for (ut64 i = 0; i + rel_ent <= rel_sz; i += rel_ent) {
const ut8 *rp = elf_vm_to_ptr (&e, rel_off + i);
if (!rp) {
break;
}
ut64 r_offset = e.is64? r_read_le64 (rp + 0): (ut64)r_read_le32 (rp + 0);
ut64 r_info = e.is64? r_read_le64 (rp + 8): (ut64)r_read_le32 (rp + 4);
ut64 type = r_info & type_mask;
bool is_relative = (type == 8) || (type == 23) || (type == 1027);
if (!is_relative) {
continue;
}
ut8 *loc = (ut8 *)elf_vm_to_ptr (&e, r_offset);
if (!loc) {
continue;
}
ut64 add = e.is64? r_read_le64 (loc): (ut64)r_read_le32 (loc);
elf_write_word (loc, base + add, e.is64);
}
}
if (e.is64 && relr_off && relr_sz && relr_ent) {
ut64 curr = 0;
for (ut64 i = 0; i + relr_ent <= relr_sz; i += relr_ent) {
const ut8 *rp = elf_vm_to_ptr (&e, relr_off + i);
if (!rp) {
break;
}
ut64 R = r_read_le64 (rp);
if ((R & 1ULL) == 0) {
ut64 addr = R;
ut8 *loc = (ut8 *)elf_vm_to_ptr (&e, addr);
if (loc) {
elf_write_word (loc, base + r_read_le64 (loc), true);
}
curr = addr + 8;
} else {
ut64 bitmap = R >> 1;
for (int bit = 0; bit < 63; bit++) {
if (! (bitmap & (1ULL << bit))) {
continue;
}
ut64 addr = curr + (ut64) (bit + 1) * 8ULL;
ut8 *loc = (ut8 *)elf_vm_to_ptr (&e, addr);
if (loc) {
elf_write_word (loc, base + r_read_le64 (loc), true);
}
}
curr += 8ULL * 63ULL;
}
}
}
// Scan for [count][ptr]
/* expected number of entries in a possible method pointer array
* use method_count computed earlier (methodsSize / sizeof (Il2CppMethodDefinition))
*/
ut32 expected = (ut32)R_MAX ((ut64)64, (ut64)method_count);
/* Pass 0: CodeRegistration-shape (pair of count/ptr tuples), loose floor.
* Pass 1: generic {count32, ptr} with expected-count bounds and stricter
* sample-fraction thresholds. */
for (int pass = 0; pass < 2 && !found; pass++) {
const ut32 min_count = (pass == 0)? 32: 64;
const ut64 min_secsize = (pass == 0)
? (ut64) (16 + (ut64)ptrsz * 2)
: (ut64) (8 + ptrsz);
const ut64 min_step = (pass == 0)
? (ut64) (8 + ptrsz) * 3
: (ut64) (8 + ptrsz);
for (int i = 0; i < e.nsegs && !found; i++) {
const ElfSeg *s = &e.segs[i];
bool is_data = (s->flags & 0x1) && ! (s->flags & 0x4);
if (!is_data || s->filesz < min_secsize) {
continue;
}
const ut8 *buf = e.file + s->offset;
for (ut64 off = 0; off + min_step <= s->filesz; off += 4) {
ut32 cnt = r_read_le32 (buf + off);
if (cnt < min_count) {
continue;
}
if (pass == 0) {
/* Require a second plausible count immediately after the ptr. */
ut32 cnt2 = r_read_le32 (buf + off + (ut64) (8 + ptrsz));
if (cnt2 < 16) {
continue;
}
} else if (expected && (cnt > expected * 2 || cnt < expected / 2)) {
continue;
}
ut64 arrptr = (ptrsz == 8)
? r_read_le64 (buf + off + 8)
: (ut64)r_read_le32 (buf + off + 4);
ut32 sample = R_MIN ((ut32)128, cnt);
ut32 min_seen = (pass == 0)? 8: sample / 2;
ut32 min_good = (pass == 0)? 8: (sample * 3) / 4;
if (elf_probe_table (&e, arrptr, cnt, ptrsz, base_vaddr, text_lo, text_hi, min_seen, min_good, method_count, candidates)) {
R_LOG_DEBUG ("[elf] pass=%d arrptr=0x%" PFMT64x " cnt=%u", pass, arrptr, cnt);
found = true;
break;
}
}
}
}
if (!found) {
R_FREE (candidates);
elf_free (&e);
return false;
}
*out_ptrs = candidates;
elf_free (&e);
return true;
}
+170
View File
@@ -295,6 +295,12 @@ static ut64 field_definition_entry_size(R2UnityMetadata *meta) {
return 4 + type_index_size + 4;
}
static ut64 parameter_definition_entry_size(R2UnityMetadata *meta) {
const bool compact_indices = (meta->version >= 35);
const int type_index_size = compact_indices? meta->typeIndexSize: 4;
return 4 + 4 + type_index_size;
}
/* Detect v24.0 vs v24.1+ (both land on disk as version==24).
*
* At v24.1 the ImageDefinition row grew by 8 bytes (added
@@ -507,6 +513,8 @@ R_API ut64 r2unity_metadata_section_entry_size(R2UnityMetadata *meta, R2UMetadat
return method_definition_entry_size (meta);
case R2U_SEC_FIELDS:
return field_definition_entry_size (meta);
case R2U_SEC_PARAMETERS:
return parameter_definition_entry_size (meta);
case R2U_SEC_TYPE_DEFINITIONS:
return type_definition_entry_size (meta);
case R2U_SEC_IMAGES:
@@ -796,6 +804,168 @@ R_API Il2CppMethodDefinition *r2unity_get_method_definitions(R2UnityMetadata *me
return methods;
}
R_API Il2CppFieldDefinition *r2unity_get_field_definitions(R2UnityMetadata *meta, size_t *count) {
R_RETURN_VAL_IF_FAIL (meta && count, NULL);
const bool compact_indices = (meta->version >= 35);
const int type_index_size = compact_indices? meta->typeIndexSize: 4;
const ut64 entry = field_definition_entry_size (meta);
ut8 *buf = read_metadata_table (meta, R2U_SEC_FIELDS, entry, count);
if (!buf) {
return NULL;
}
Il2CppFieldDefinition *fields = R_NEWS (Il2CppFieldDefinition, *count);
if (!fields) {
R_FREE (buf);
return NULL;
}
for (size_t i = 0; i < *count; i++) {
const ut8 *p = buf + i * entry;
fields[i].nameIndex = read_u32p (&p);
fields[i].typeIndex = read_indexp (&p, type_index_size);
fields[i].token = read_u32p (&p);
}
R_FREE (buf);
return fields;
}
R_API Il2CppParameterDefinition *r2unity_get_parameter_definitions(R2UnityMetadata *meta, size_t *count) {
R_RETURN_VAL_IF_FAIL (meta && count, NULL);
const bool compact_indices = (meta->version >= 35);
const int type_index_size = compact_indices? meta->typeIndexSize: 4;
const ut64 entry = parameter_definition_entry_size (meta);
ut8 *buf = read_metadata_table (meta, R2U_SEC_PARAMETERS, entry, count);
if (!buf) {
return NULL;
}
Il2CppParameterDefinition *parameters = R_NEWS (Il2CppParameterDefinition, *count);
if (!parameters) {
R_FREE (buf);
return NULL;
}
for (size_t i = 0; i < *count; i++) {
const ut8 *p = buf + i * entry;
parameters[i].nameIndex = read_u32p (&p);
parameters[i].token = read_u32p (&p);
parameters[i].typeIndex = read_indexp (&p, type_index_size);
}
R_FREE (buf);
return parameters;
}
static const char *csharp_type_alias(const char *name) {
static const struct {
const char *fullname;
const char *alias;
} aliases[] = {
{ "System.Void", "void" },
{ "System.Boolean", "bool" },
{ "System.Byte", "byte" },
{ "System.SByte", "sbyte" },
{ "System.Char", "char" },
{ "System.Decimal", "decimal" },
{ "System.Double", "double" },
{ "System.Single", "float" },
{ "System.Int16", "short" },
{ "System.UInt16", "ushort" },
{ "System.Int32", "int" },
{ "System.UInt32", "uint" },
{ "System.Int64", "long" },
{ "System.UInt64", "ulong" },
{ "System.IntPtr", "nint" },
{ "System.UIntPtr", "nuint" },
{ "System.Object", "object" },
{ "System.String", "string" },
};
for (size_t i = 0; i < R_ARRAY_SIZE (aliases); i++) {
if (!strcmp (name, aliases[i].fullname)) {
return aliases[i].alias;
}
}
return NULL;
}
R_API char *r2unity_type_name_from_index(R2UnityMetadata *meta, const Il2CppTypeDefinition *types, size_t type_count, int32_t type_index, bool fallback) {
R_RETURN_VAL_IF_FAIL (meta, NULL);
if (type_index >= 0) {
for (size_t i = 0; types && i < type_count; i++) {
if (types[i].byvalTypeIndex != type_index) {
continue;
}
char *name = r2unity_type_fullname (meta, &types[i], i, R2U_NAME_FALLBACK_TYPE);
if (name) {
const char *alias = csharp_type_alias (name);
if (alias) {
free (name);
return strdup (alias);
}
}
return name;
}
}
return fallback? r_str_newf ("type_index.%d", type_index): NULL;
}
R_API char *r2unity_method_signature(R2UnityMetadata *meta, const Il2CppMethodDefinition *method, const Il2CppTypeDefinition *owner, size_t owner_index, const Il2CppTypeDefinition *types, size_t type_count, const Il2CppParameterDefinition *parameters, size_t parameter_count, bool fallback) {
R_RETURN_VAL_IF_FAIL (meta && method, NULL);
char *return_type = r2unity_type_name_from_index (meta, types, type_count, method->returnType, fallback);
char *method_name = r2unity_method_fullname (meta, method, owner, owner_index,
fallback? R2U_NAME_FALLBACK_TYPE: 0);
if (!method_name) {
free (return_type);
return NULL;
}
RStrBuf *sb = r_strbuf_new (method_name);
r_strbuf_append (sb, "(");
bool is_instance = !(method->flags & 0x0010);
if (is_instance) {
r_strbuf_append (sb, "this;");
}
for (size_t i = 0; i < method->parameterCount; i++) {
st64 pi = (st64)method->parameterStart + (st64)i;
const Il2CppParameterDefinition *parameter = parameters
&& pi >= 0 && (ut64)pi < parameter_count
? &parameters[pi]: NULL;
char *type_name = parameter
? r2unity_type_name_from_index (meta, types, type_count, parameter->typeIndex, fallback)
: NULL;
char *parameter_name = parameter? r2unity_get_string (meta, parameter->nameIndex): NULL;
r_strbuf_appendf (sb, "%s%s", i? ",": "",
type_name? type_name: "unknown");
if (R_STR_ISNOTEMPTY (parameter_name)) {
r_strbuf_appendf (sb, ":%s", parameter_name);
} else {
r_strbuf_appendf (sb, ":arg%zu", i);
}
free (parameter_name);
free (type_name);
}
r_strbuf_appendf (sb, "):%s", return_type? return_type: "unknown");
free (method_name);
free (return_type);
return r_strbuf_drain (sb);
}
R_API int32_t *r2unity_get_type_index_table(R2UnityMetadata *meta, R2UMetadataSectionId id, size_t *count) {
R_RETURN_VAL_IF_FAIL (meta && count, NULL);
const bool compact_indices = (meta->version >= 35);
const int type_index_size = compact_indices? meta->typeIndexSize: 4;
ut8 *buf = read_metadata_table (meta, id, type_index_size, count);
if (!buf) {
return NULL;
}
int32_t *out = R_NEWS (int32_t, *count);
if (!out) {
R_FREE (buf);
return NULL;
}
for (size_t i = 0; i < *count; i++) {
const ut8 *p = buf + i * type_index_size;
out[i] = read_indexp (&p, type_index_size);
}
R_FREE (buf);
return out;
}
R_API Il2CppImageDefinition *r2unity_get_images(R2UnityMetadata *meta, size_t *count) {
R_RETURN_VAL_IF_FAIL (meta && count, NULL);
*count = 0;
+82 -5
View File
@@ -3,6 +3,11 @@
#include <r_util.h>
#if !defined(R2_BIN_H) && !defined(R2_BIN_DWARF_H)
typedef struct r_bin_t RBin;
typedef struct r_bin_file_t RBinFile;
#endif
#define IL2CPP_MAGIC 0xFAB11BAF
#define R2UNITY_METADATA_BASE_SECTION_COUNT 31
#define R2UNITY_METADATA_SECTION_COUNT 33
@@ -119,6 +124,18 @@ typedef struct {
uint16_t parameterCount;
} Il2CppMethodDefinition;
typedef struct {
uint32_t nameIndex;
int32_t typeIndex;
uint32_t token;
} Il2CppFieldDefinition;
typedef struct {
uint32_t nameIndex;
uint32_t token;
int32_t typeIndex;
} Il2CppParameterDefinition;
typedef struct {
uint32_t nameIndex;
int32_t assemblyIndex;
@@ -161,8 +178,7 @@ typedef struct {
enum {
R2U_INTEROP_PINVOKE = 1, /* managed -> native, [DllImport] */
R2U_INTEROP_REVERSE_PINVOKE = 2, /* native -> managed, [MonoPInvokeCallback] */
R2U_INTEROP_UNMANAGED_ONLY = 3, /* [UnmanagedCallersOnly] (.NET 5+) */
R2U_INTEROP_INTEROP_DATA = 4 /* Il2CppInteropData entry (opaque) */
R2U_INTEROP_UNMANAGED_ONLY = 3 /* [UnmanagedCallersOnly] (.NET 5+) */
};
/* A single P/Invoke or reverse-P/Invoke entry.
@@ -193,6 +209,56 @@ typedef enum {
R2U_SBOM_JSON
} R2UnitySbomFormat;
typedef enum {
R2U_NATIVE_SOURCE_NONE,
R2U_NATIVE_SOURCE_SYMBOL,
R2U_NATIVE_SOURCE_OVERRIDE,
R2U_NATIVE_SOURCE_HEURISTIC
} R2UnityNativeSource;
typedef enum {
R2U_NATIVE_TABLE_REVERSE_PINVOKE_WRAPPERS,
R2U_NATIVE_TABLE_GENERIC_METHOD_POINTERS,
R2U_NATIVE_TABLE_GENERIC_ADJUSTOR_THUNKS,
R2U_NATIVE_TABLE_INVOKER_POINTERS,
R2U_NATIVE_TABLE_UNRESOLVED_VIRTUAL_CALL_POINTERS,
R2U_NATIVE_TABLE_UNRESOLVED_INSTANCE_CALL_POINTERS,
R2U_NATIVE_TABLE_UNRESOLVED_STATIC_CALL_POINTERS,
R2U_NATIVE_TABLE_INTEROP_DATA,
R2U_NATIVE_TABLE_WINDOWS_RUNTIME_FACTORY_TABLE,
R2U_NATIVE_TABLE_COUNT
} R2UnityNativeTableId;
typedef struct {
ut64 va;
ut64 count;
} R2UnityNativeTable;
typedef struct {
const char *name;
ut64 va;
} R2UnitySymbolOverride;
typedef struct {
bool force_heuristic;
ut64 code_registration_va;
ut64 metadata_registration_va;
const R2UnitySymbolOverride *symbols;
size_t symbols_count;
} R2UnityNativeOptions;
typedef struct {
ut64 *method_ptrs; /* owned; free with r2unity_native_result_fini() */
bool has_method_ptrs;
R2UnityNativeSource source;
ut64 code_registration_va;
ut64 metadata_registration_va;
ut64 method_pointers_va;
ut64 code_gen_modules_va;
R2UnityNativeTable tables[R2U_NATIVE_TABLE_COUNT];
int ptr_size;
} R2UnityNativeResult;
R_API R2UnityMetadata *r2unity_parse_metadata(RBuffer *buf);
R_API void r2unity_free_metadata(R2UnityMetadata *meta);
/* Caller owns the returned string and must free() it. NULL on missing/empty. */
@@ -203,6 +269,11 @@ R_API Il2CppStringLiteral *r2unity_get_string_literals(R2UnityMetadata *meta, si
R_API bool r2unity_read_string_literal(R2UnityMetadata *meta, const Il2CppStringLiteral *lit, ut8 **out_bytes, size_t *out_len);
R_API Il2CppTypeDefinition *r2unity_get_type_definitions(R2UnityMetadata *meta, size_t *count);
R_API Il2CppMethodDefinition *r2unity_get_method_definitions(R2UnityMetadata *meta, size_t *count);
R_API Il2CppFieldDefinition *r2unity_get_field_definitions(R2UnityMetadata *meta, size_t *count);
R_API Il2CppParameterDefinition *r2unity_get_parameter_definitions(R2UnityMetadata *meta, size_t *count);
R_API char *r2unity_type_name_from_index(R2UnityMetadata *meta, const Il2CppTypeDefinition *types, size_t type_count, int32_t type_index, bool fallback);
R_API char *r2unity_method_signature(R2UnityMetadata *meta, const Il2CppMethodDefinition *method, const Il2CppTypeDefinition *owner, size_t owner_index, const Il2CppTypeDefinition *types, size_t type_count, const Il2CppParameterDefinition *parameters, size_t parameter_count, bool fallback);
R_API int32_t *r2unity_get_type_index_table(R2UnityMetadata *meta, R2UMetadataSectionId id, size_t *count);
R_API Il2CppImageDefinition *r2unity_get_images(R2UnityMetadata *meta, size_t *count);
R_API int *r2unity_build_type_image_map(const Il2CppImageDefinition *images, size_t image_count, size_t type_count);
R_API int r2unity_image_index_for_method(const int *type2img, size_t type_count, const Il2CppMethodDefinition *m);
@@ -213,6 +284,7 @@ R_API R2UnityInterop *r2unity_enumerate_pinvokes(R2UnityMetadata *meta, size_t *
* Returns methods tagged with [MonoPInvokeCallback] or [UnmanagedCallersOnly].
* Pre-v29 metadata returns NULL (attribute ctor args live in generator stubs). */
R_API R2UnityInterop *r2unity_enumerate_reverse_pinvokes(R2UnityMetadata *meta, size_t *count);
R_API bool r2unity_enrich_reverse_pinvokes_native(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityInterop **items, size_t *count);
R_API void r2unity_free_interop(R2UnityInterop *items, size_t count);
R_API char *r2unity_sbom_tostring(R2UnityMetadata *meta, const char *exe_path, const char *metadata_path, R2UnitySbomFormat format);
R_API const char *r2unity_unity_range_from_wire(int wire);
@@ -221,9 +293,14 @@ R_API bool r2unity_metadata_section(R2UnityMetadata *meta, R2UMetadataSectionId
R_API ut64 r2unity_metadata_section_entry_size(R2UnityMetadata *meta, R2UMetadataSectionId id);
R_API ut64 r2unity_metadata_section_count(R2UnityMetadata *meta, R2UMetadataSectionId id);
R_API ut64 r2unity_metadata_header_size(R2UnityMetadata *meta);
R_API bool r2unity_find_method_pointers_macho(R2UnityMetadata *meta, const char *macho_path, ut64 **out_ptrs);
R_API bool r2unity_find_method_pointers_elf(R2UnityMetadata *meta, const char *elf_path, ut64 **out_ptrs);
R_API bool r2unity_find_method_pointers_pe(R2UnityMetadata *meta, const char *pe_path, ut64 **out_ptrs);
R_API const char *r2unity_native_source_name(R2UnityNativeSource source);
R_API const char *r2unity_native_table_name(R2UnityNativeTableId id);
R_API const char *const *r2unity_native_code_registration_names(void);
R_API const char *const *r2unity_native_metadata_registration_names(void);
R_API void r2unity_native_result_fini(R2UnityNativeResult *result);
R_API bool r2unity_find_method_pointers(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result);
R_API bool r2unity_find_method_pointers_simple(R2UnityMetadata *meta, const char *path, const R2UnityNativeOptions *options, R2UnityNativeResult *result);
R_API bool r2unity_find_method_pointers_rbin(R2UnityMetadata *meta, RBin *bin, RBinFile *bf, const R2UnityNativeOptions *options, R2UnityNativeResult *result);
/* Companion-file discovery.
*
-253
View File
@@ -1,253 +0,0 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
// Fast Mach-O parser for r2unity
#define R_LOG_ORIGIN "r2unity.macho"
#include "lib.h"
#include <string.h>
typedef struct {
char segname[16];
ut64 vmaddr;
ut64 vmsize;
ut64 fileoff;
ut64 filesize;
ut32 maxprot;
} MachSeg;
typedef struct {
ut8 *file;
ut64 vm_base; // lowest VM address among segments (usually __TEXT vmaddr)
ut64 filesize;
ut64 base;
ut32 ncmds;
ut64 cmd_off;
MachSeg segs[128];
int nsegs;
} MachO;
static bool macho_load(const char *path, MachO *mo) {
memset (mo, 0, sizeof (*mo));
size_t sz = 0;
mo->file = (ut8 *)r_file_slurp (path, &sz);
if (!mo->file || sz == 0) {
R_FREE (mo->file);
return false;
}
mo->filesize = (ut64)sz;
ut32 magic = r_read_be32 (mo->file);
ut64 off = 0;
if (magic == 0xcafebabe) {
ut32 nfat = r_read_be32 (mo->file + 4);
ut32 best = 0;
for (ut32 i = 0; i < nfat; i++) {
const ut8 *fa = mo->file + 8 + i * 20;
ut32 cputype = r_read_be32 (fa + 0);
if (cputype == 0x0100000c) {
best = i;
break;
}
}
const ut8 *fa = mo->file + 8 + best * 20;
off = r_read_be32 (fa + 8);
}
mo->base = off;
const ut8 *p = mo->file + off;
ut32 mh_magic = r_read_le32 (p);
if (mh_magic != 0xFEEDFACF) {
R_FREE (mo->file);
return false;
}
ut32 ncmds = r_read_le32 (p + 0x10);
mo->ncmds = ncmds;
mo->cmd_off = off + 0x20;
ut64 co = mo->cmd_off;
for (ut32 i = 0; i < ncmds && (co + 8 <= mo->filesize); i++) {
ut32 cmd = r_read_le32 (mo->file + co);
ut32 cmdsize = r_read_le32 (mo->file + co + 4);
if (cmd == 0x19 && cmdsize >= 72) {
if (mo->nsegs < (int) (sizeof (mo->segs) / sizeof (mo->segs[0]))) {
MachSeg *s = &mo->segs[mo->nsegs++];
const ut8 *sp = mo->file + co + 8;
memcpy (s->segname, sp, 16);
s->vmaddr = r_read_le64 (sp + 16);
s->vmsize = r_read_le64 (sp + 24);
s->fileoff = r_read_le64 (sp + 32);
s->filesize = r_read_le64 (sp + 40);
s->maxprot = r_read_le32 (sp + 48);
if (!mo->vm_base || s->vmaddr < mo->vm_base) {
mo->vm_base = s->vmaddr;
}
}
}
co += cmdsize? cmdsize: 8;
}
return true;
}
static void macho_free(MachO *mo) {
R_FREE (mo->file);
}
static inline bool macho_vm_in_text(MachO *mo, ut64 addr, ut64 *text_lo, ut64 *text_hi) {
ut64 lo = UT64_MAX, hi = 0;
for (int i = 0; i < mo->nsegs; i++) {
MachSeg *s = &mo->segs[i];
bool is_text = (s->maxprot & 0x4) || !strncmp (s->segname, "__TEXT", 6);
if (is_text) {
if (s->vmaddr < lo) {
lo = s->vmaddr;
}
if (s->vmaddr + s->vmsize > hi) {
hi = s->vmaddr + s->vmsize;
}
}
}
// Fallback: if no exec segment detected, use min/max of all segments
if (lo == UT64_MAX || hi <= lo) {
lo = UT64_MAX;
hi = 0;
for (int i = 0; i < mo->nsegs; i++) {
MachSeg *s = &mo->segs[i];
if (s->vmaddr < lo) {
lo = s->vmaddr;
}
if (s->vmaddr + s->vmsize > hi) {
hi = s->vmaddr + s->vmsize;
}
}
}
if (text_lo) {
*text_lo = lo;
}
if (text_hi) {
*text_hi = hi;
}
return (addr >= lo && addr < hi);
}
static inline const ut8 *macho_vm_to_ptr(MachO *mo, ut64 vmaddr) {
for (int i = 0; i < mo->nsegs; i++) {
MachSeg *s = &mo->segs[i];
if (vmaddr >= s->vmaddr && vmaddr < s->vmaddr + s->vmsize) {
ut64 delta = vmaddr - s->vmaddr;
if (delta < s->filesize) {
return mo->file + mo->base + s->fileoff + delta;
}
return NULL;
}
}
return NULL;
}
/* Evaluate the table at VA `arrptr` with `count` 8-byte pointers. Entries
* below text_lo are retried with vm_base added (RVA→VA fallback). */
static bool macho_probe_table(MachO *mo, ut64 arrptr, ut32 count, ut64 text_lo, ut64 text_hi, ut32 min_seen, ut32 min_good, size_t method_count, ut64 *candidates) {
ut32 sample = R_MIN ((ut32)128, count);
ut32 good = 0, seen = 0;
for (ut32 k = 0; k < sample; k++) {
const ut8 *p = macho_vm_to_ptr (mo, arrptr + (ut64)k * 8);
if (!p && mo->vm_base) {
p = macho_vm_to_ptr (mo, mo->vm_base + arrptr + (ut64)k * 8);
}
if (!p) {
return false;
}
ut64 val = r_read_le64 (p);
if (val && val < text_lo && mo->vm_base && (val + mo->vm_base) >= text_lo && (val + mo->vm_base) < text_hi) {
val += mo->vm_base;
}
if (val) {
seen++;
}
if (val >= text_lo && val < text_hi) {
good++;
}
}
if (seen < min_seen || good < min_good) {
return false;
}
memset (candidates, 0, method_count * sizeof (ut64));
size_t tocopy = R_MIN ((size_t)count, method_count);
for (size_t m = 0; m < tocopy; m++) {
const ut8 *p = macho_vm_to_ptr (mo, arrptr + (ut64)m * 8);
if (!p && mo->vm_base) {
p = macho_vm_to_ptr (mo, mo->vm_base + arrptr + (ut64)m * 8);
}
if (!p) {
break;
}
ut64 val = r_read_le64 (p);
if (val && val < text_lo && mo->vm_base && (val + mo->vm_base) >= text_lo && (val + mo->vm_base) < text_hi) {
val += mo->vm_base;
}
if (val >= text_lo && val < text_hi) {
candidates[m] = val;
}
}
return true;
}
R_API bool r2unity_find_method_pointers_macho(R2UnityMetadata *meta, const char *macho_path, ut64 **out_ptrs) {
R_RETURN_VAL_IF_FAIL (meta && macho_path && out_ptrs, false);
*out_ptrs = NULL;
MachO mo;
if (!macho_load (macho_path, &mo)) {
return false;
}
size_t method_count = (ut64)meta->methodsSize / sizeof (Il2CppMethodDefinition);
if (!method_count) {
macho_free (&mo);
return false;
}
ut64 text_lo = 0, text_hi = 0;
macho_vm_in_text (&mo, 0, &text_lo, &text_hi);
ut64 *candidates = R_NEWS (ut64, method_count);
if (!candidates) {
macho_free (&mo);
return false;
}
bool found = false;
int ptrsz = 8;
ut32 expected = (ut32)R_MAX ((ut64)64, (ut64)method_count);
(void)ptrsz; /* Mach-O fast path is 64-bit only. */
/* Pass 0: CodeRegistration-shaped pair {count32, pad, ptr64}.
* Pass 1: generic {count32, ptr64} with expected-count bounds and
* stricter sample-fraction thresholds. */
for (int pass = 0; pass < 2 && !found; pass++) {
const ut32 min_count = (pass == 0)? 32: 64;
for (int i = 0; i < mo.nsegs && !found; i++) {
MachSeg *s = &mo.segs[i];
bool is_data = (s->maxprot & 0x1) && ! (s->maxprot & 0x4);
if (!is_data || s->filesize < 16) {
continue;
}
const ut8 *buf = mo.file + mo.base + s->fileoff;
for (ut64 off = 0; off + 16 <= s->filesize; off += 4) {
ut32 cnt = r_read_le32 (buf + off);
if (cnt < min_count) {
continue;
}
if (pass == 1 && expected && (cnt > expected * 2 || cnt < expected / 2)) {
continue;
}
ut64 arrptr = r_read_le64 (buf + off + 8);
ut32 sample = R_MIN ((ut32)128, cnt);
ut32 min_seen = (pass == 0)? 8: sample / 2;
ut32 min_good = (pass == 0)? 8: (sample * 3) / 4;
if (macho_probe_table (&mo, arrptr, cnt, text_lo, text_hi, min_seen, min_good, method_count, candidates)) {
R_LOG_DEBUG ("[macho] pass=%d arrptr=0x%" PFMT64x " cnt=%u", pass, arrptr, cnt);
found = true;
break;
}
}
}
}
if (!found) {
R_FREE (candidates);
macho_free (&mo);
return false;
}
*out_ptrs = candidates;
macho_free (&mo);
return true;
}
+42 -38
View File
@@ -3,17 +3,8 @@
#define R_LOG_ORIGIN "r2unity.paths"
#include "lib.h"
#include <strings.h>
#include <r_util.h>
static bool str_ieq(const char *a, const char *b) {
return a && b && !strcasecmp (a, b);
}
static char *pjoin(const char *a, const char *b) {
return r_str_newf ("%s/%s", a, b);
}
/* Take ownership of `candidate` if it exists on disk; otherwise free it. */
static bool take_if_exists(char **out, char *candidate) {
if (r_file_exists (candidate)) {
@@ -49,7 +40,7 @@ static const char *il2cpp_basename_for(const char *platform) {
static bool is_il2cpp_basename(const char *base) {
for (size_t i = 0; i < sizeof (il2cpp_map) / sizeof (il2cpp_map[0]); i++) {
if (str_ieq (base, il2cpp_map[i].basename)) {
if (!r_str_casecmp (base, il2cpp_map[i].basename)) {
return true;
}
}
@@ -59,7 +50,7 @@ static bool is_il2cpp_basename(const char *base) {
static char *find_il2cpp_sibling(const char *dir) {
char *found = NULL;
for (size_t i = 0; i < sizeof (il2cpp_map) / sizeof (il2cpp_map[0]); i++) {
if (take_if_exists (&found, pjoin (dir, il2cpp_map[i].basename))) {
if (take_if_exists (&found, r_file_new (dir, il2cpp_map[i].basename, NULL))) {
return found;
}
}
@@ -69,7 +60,7 @@ static char *find_il2cpp_sibling(const char *dir) {
static char *strip_ext_icase(const char *name, const char *ext) {
size_t nlen = strlen (name);
size_t elen = strlen (ext);
if (nlen > elen && !strcasecmp (name + nlen - elen, ext)) {
if (nlen > elen && !r_str_casecmp (name + nlen - elen, ext)) {
return r_str_ndup (name, (int) (nlen - elen));
}
return strdup (name);
@@ -112,7 +103,7 @@ static bool try_macos(R2UnityPaths *p, const char *abs, const char *dir, const c
char *contents = NULL;
if (r_str_endswith (dir, "/Contents/MacOS")) {
contents = r_file_dirname (dir);
} else if (r_str_endswith (dir, "/Contents/Frameworks") && str_ieq (base, "GameAssembly.dylib")) {
} else if (r_str_endswith (dir, "/Contents/Frameworks") && !r_str_casecmp (base, "GameAssembly.dylib")) {
contents = r_file_dirname (dir);
} else {
return false;
@@ -128,7 +119,7 @@ static bool try_macos(R2UnityPaths *p, const char *abs, const char *dir, const c
p->platform = strdup ("macos");
p->metadata = metadata;
p->data_dir = data_dir;
if (str_ieq (base, "GameAssembly.dylib")) {
if (!r_str_casecmp (base, "GameAssembly.dylib")) {
p->il2cpp_binary = strdup (abs);
} else {
take_if_exists (&p->il2cpp_binary, r_file_new (contents, "Frameworks", "GameAssembly.dylib", NULL));
@@ -145,7 +136,7 @@ static char *find_ios_metadata(const char *app_dir) {
};
char *metadata = NULL;
for (int i = 0; metas[i]; i++) {
if (take_if_exists (&metadata, pjoin (app_dir, metas[i]))) {
if (take_if_exists (&metadata, r_file_new (app_dir, metas[i], NULL))) {
return metadata;
}
}
@@ -157,30 +148,38 @@ static bool try_ios(R2UnityPaths *p, const char *abs, const char *dir, const cha
* Flat iOS layout: Game.app/UnityFramework (older builds)
* Extracted app root: Game/Data plus the main Mach-O beside Data
* Metadata: Game.app/Data/Managed/Metadata/global-metadata.dat
* Game.app/Frameworks/UnityFramework.framework/Data/Managed/Metadata/global-metadata.dat
* Game.app/Data/Raw/Managed/Metadata/global-metadata.dat (newer) */
char *app_dir = NULL;
if (r_str_endswith (dir, ".framework") && str_ieq (base, "UnityFramework")) {
if (r_str_endswith (dir, ".framework") && !r_str_casecmp (base, "UnityFramework")) {
char *frameworks = r_file_dirname (dir);
app_dir = r_file_dirname (frameworks);
free (frameworks);
} else if (r_str_endswith (dir, ".app")) {
app_dir = strdup (dir);
} else {
app_dir = strdup (dir);
}
char *metadata = find_ios_metadata (app_dir);
char *framework_dir = r_file_new (app_dir, "Frameworks", "UnityFramework.framework", NULL);
char *metadata = find_ios_metadata (framework_dir);
char *data_dir = metadata? r_file_new (framework_dir, "Data", NULL): NULL;
free (framework_dir);
if (!metadata) {
metadata = find_ios_metadata (app_dir);
if (metadata) {
data_dir = r_file_new (app_dir, "Data", NULL);
}
}
if (!metadata) {
free (app_dir);
return false;
}
p->platform = strdup ("ios");
p->metadata = metadata;
p->data_dir = pjoin (app_dir, "Data");
if (str_ieq (base, "UnityFramework")) {
p->data_dir = data_dir;
if (!r_str_casecmp (base, "UnityFramework")) {
p->il2cpp_binary = strdup (abs);
} else if (!take_if_exists (&p->il2cpp_binary,
r_file_new (app_dir, "Frameworks", "UnityFramework.framework", "UnityFramework", NULL))) {
if (!take_if_exists (&p->il2cpp_binary, pjoin (app_dir, "UnityFramework"))) {
if (!take_if_exists (&p->il2cpp_binary, r_file_new (app_dir, "UnityFramework", NULL))) {
p->il2cpp_binary = strdup (abs);
}
}
@@ -193,8 +192,8 @@ static bool try_winlin_standalone(R2UnityPaths *p, const char *abs, const char *
* Linux: Game (ELF) + GameAssembly.so + Game_Data/il2cpp_data/Metadata/global-metadata.dat
*
* `base` is either the main exe name ("Game"/"Game.exe") or the IL2CPP lib itself. */
const bool is_dll = str_ieq (base, "GameAssembly.dll");
const bool is_so = str_ieq (base, "GameAssembly.so");
const bool is_dll = !r_str_casecmp (base, "GameAssembly.dll");
const bool is_so = !r_str_casecmp (base, "GameAssembly.so");
const bool is_il2cpp = is_dll || is_so;
char *data_dir = NULL;
@@ -221,8 +220,8 @@ static bool try_winlin_standalone(R2UnityPaths *p, const char *abs, const char *
p->il2cpp_binary = strdup (abs);
p->platform = strdup (is_dll? "windows": "linux");
} else {
char *dll = pjoin (dir, "GameAssembly.dll");
char *so = pjoin (dir, "GameAssembly.so");
char *dll = r_file_new (dir, "GameAssembly.dll", NULL);
char *so = r_file_new (dir, "GameAssembly.so", NULL);
if (take_if_exists (&p->il2cpp_binary, dll)) {
p->platform = strdup ("windows");
free (so);
@@ -240,7 +239,7 @@ static bool try_winlin_standalone(R2UnityPaths *p, const char *abs, const char *
static bool try_android_apk(R2UnityPaths *p, const char *abs, const char *dir, const char *base) {
/* Extracted APK: <root>/lib/<abi>/libil2cpp.so
* <root>/assets/bin/Data/Managed/Metadata/global-metadata.dat */
if (!str_ieq (base, "libil2cpp.so")) {
if (r_str_casecmp (base, "libil2cpp.so")) {
return false;
}
char *abi_parent = r_file_dirname (dir);
@@ -255,10 +254,10 @@ static bool try_android_apk(R2UnityPaths *p, const char *abs, const char *dir, c
NULL
};
for (int i = 0; metas[i]; i++) {
if (take_if_exists (&p->metadata, pjoin (root, metas[i]))) {
if (take_if_exists (&p->metadata, r_file_new (root, metas[i], NULL))) {
p->platform = strdup ("android");
p->il2cpp_binary = strdup (abs);
p->data_dir = pjoin (root, "assets/bin/Data");
p->data_dir = r_file_new (root, "assets/bin/Data", NULL);
free (root);
return true;
}
@@ -271,7 +270,7 @@ static bool try_fixture(R2UnityPaths *p, const char *abs, const char *dir, const
/* Flat fixture: main exe + il2cpp binary + global-metadata.dat all in the
* same directory. Used by users dumping files
* manually for quick triage. */
char *metadata = pjoin (dir, "global-metadata.dat");
char *metadata = r_file_new (dir, "global-metadata.dat", NULL);
if (!r_file_exists (metadata)) {
free (metadata);
return false;
@@ -286,7 +285,7 @@ static bool try_fixture(R2UnityPaths *p, const char *abs, const char *dir, const
/* Drill into a macOS .app bundle's Contents/MacOS and return the first regular
* file there; falls back to iOS framework/flat layouts. */
static char *expand_app_bundle(const char *dir) {
char *macos_dir = pjoin (dir, "Contents/MacOS");
char *macos_dir = r_file_new (dir, "Contents/MacOS", NULL);
if (r_file_is_directory (macos_dir)) {
RList *entries = r_sys_dir (macos_dir);
if (entries) {
@@ -296,7 +295,7 @@ static char *expand_app_bundle(const char *dir) {
if (*name == '.') {
continue;
}
char *f = pjoin (macos_dir, name);
char *f = r_file_new (macos_dir, name, NULL);
if (r_file_is_regular (f)) {
r_list_free (entries);
free (macos_dir);
@@ -313,7 +312,7 @@ static char *expand_app_bundle(const char *dir) {
if (take_if_exists (&out, ios_fw)) {
return out;
}
if (take_if_exists (&out, pjoin (dir, "UnityFramework"))) {
if (take_if_exists (&out, r_file_new (dir, "UnityFramework", NULL))) {
return out;
}
return NULL;
@@ -323,6 +322,11 @@ static char *expand_app_bundle(const char *dir) {
* Mach-O, whose basename is project-specific. */
static char *expand_ios_root_dir(const char *dir) {
char *metadata = find_ios_metadata (dir);
if (!metadata) {
char *framework = r_file_new (dir, "Frameworks", "UnityFramework.framework", NULL);
metadata = find_ios_metadata (framework);
free (framework);
}
if (!metadata) {
return NULL;
}
@@ -332,7 +336,7 @@ static char *expand_ios_root_dir(const char *dir) {
if (take_if_exists (&out, r_file_new (dir, "Frameworks", "UnityFramework.framework", "UnityFramework", NULL))) {
return out;
}
if (take_if_exists (&out, pjoin (dir, "UnityFramework"))) {
if (take_if_exists (&out, r_file_new (dir, "UnityFramework", NULL))) {
return out;
}
@@ -350,7 +354,7 @@ static char *expand_ios_root_dir(const char *dir) {
if (!strcmp (name, "Data") || !strcmp (name, "Info.plist") || !strcmp (name, "PkgInfo") || !strcmp (name, "global-metadata.dat") || !strcmp (name, "embedded.mobileprovision")) {
continue;
}
char *f = pjoin (dir, name);
char *f = r_file_new (dir, name, NULL);
if (r_file_is_regular (f)) {
if (r_file_is_executable (f)) {
r_list_free (entries);
@@ -389,7 +393,7 @@ static char *expand_winlin_dir(const char *dir) {
free (stem);
break;
}
if (take_if_exists (&found, pjoin (dir, stem))) {
if (take_if_exists (&found, r_file_new (dir, stem, NULL))) {
free (stem);
break;
}
@@ -401,7 +405,7 @@ static char *expand_winlin_dir(const char *dir) {
/* Android extracted APK: lib/<abi>/libil2cpp.so */
static char *expand_apk_dir(const char *dir) {
char *lib_dir = pjoin (dir, "lib");
char *lib_dir = r_file_new (dir, "lib", NULL);
if (!r_file_is_directory (lib_dir)) {
free (lib_dir);
return NULL;
@@ -454,7 +458,7 @@ static char *expand_dir_input(const char *dir) {
return apk;
}
char *out = NULL;
take_if_exists (&out, pjoin (dir, "global-metadata.dat"));
take_if_exists (&out, r_file_new (dir, "global-metadata.dat", NULL));
return out;
}
-266
View File
@@ -1,266 +0,0 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
// Minimal PE parser for r2unity (GameAssembly.dll on Windows, GameAssembly.dll on UWP).
// PE .dll images are already relocated at link time (preferred image base is
// concrete), so no relocation fixup is required — pointers in .data / .rdata
// are absolute VAs relative to the ImageBase.
#define R_LOG_ORIGIN "r2unity.pe"
#include "lib.h"
#include <string.h>
typedef struct {
char name[9];
ut64 vaddr;
ut64 vsize;
ut64 fileoff;
ut64 filesize;
ut32 chars;
} PeSec;
typedef struct {
ut8 *file;
ut64 filesize;
int is64;
ut64 image_base;
PeSec secs[128];
int nsecs;
} PeImg;
static bool pe_load(const char *path, PeImg *pe) {
memset (pe, 0, sizeof (*pe));
size_t sz = 0;
pe->file = (ut8 *)r_file_slurp (path, &sz);
if (!pe->file || sz < 0x40) {
R_FREE (pe->file);
return false;
}
pe->filesize = (ut64)sz;
if (pe->file[0] != 'M' || pe->file[1] != 'Z') {
R_FREE (pe->file);
return false;
}
ut32 e_lfanew = r_read_le32 (pe->file + 0x3C);
if ((ut64)e_lfanew + 24 > pe->filesize) {
R_FREE (pe->file);
return false;
}
const ut8 *nt = pe->file + e_lfanew;
if (r_read_le32 (nt) != 0x00004550) { /* "PE\0\0" */
R_FREE (pe->file);
return false;
}
/* COFF file header immediately follows the 4-byte PE signature */
ut16 nsec = r_read_le16 (nt + 4 + 2);
ut16 sizeof_opt = r_read_le16 (nt + 4 + 16);
ut64 opt_off = (ut64)e_lfanew + 4 + 20;
if (opt_off + sizeof_opt > pe->filesize) {
R_FREE (pe->file);
return false;
}
const ut8 *opt = pe->file + opt_off;
ut16 mag = r_read_le16 (opt);
if (mag == 0x20b) {
pe->is64 = 1;
pe->image_base = r_read_le64 (opt + 24);
} else if (mag == 0x10b) {
pe->is64 = 0;
pe->image_base = (ut64)r_read_le32 (opt + 28);
} else {
R_FREE (pe->file);
return false;
}
ut64 sh_off = opt_off + sizeof_opt;
for (ut16 i = 0; i < nsec && pe->nsecs < (int) (sizeof (pe->secs) / sizeof (pe->secs[0])); i++) {
ut64 off = sh_off + (ut64)i * 40;
if (off + 40 > pe->filesize) {
break;
}
const ut8 *sh = pe->file + off;
PeSec *s = &pe->secs[pe->nsecs++];
memcpy (s->name, sh, 8);
s->name[8] = 0;
s->vsize = r_read_le32 (sh + 8);
ut32 rva = r_read_le32 (sh + 12);
s->vaddr = pe->image_base + (ut64)rva;
s->filesize = r_read_le32 (sh + 16);
s->fileoff = r_read_le32 (sh + 20);
s->chars = r_read_le32 (sh + 36);
}
return true;
}
static void pe_free(PeImg *pe) {
R_FREE (pe->file);
}
static inline const ut8 *pe_vm_to_ptr(PeImg *pe, ut64 vaddr) {
for (int i = 0; i < pe->nsecs; i++) {
PeSec *s = &pe->secs[i];
if (vaddr >= s->vaddr && vaddr < s->vaddr + s->vsize) {
ut64 delta = vaddr - s->vaddr;
if (delta < s->filesize && s->fileoff + delta < pe->filesize) {
return pe->file + s->fileoff + delta;
}
return NULL;
}
}
return NULL;
}
static inline void pe_text_range(PeImg *pe, ut64 *text_lo, ut64 *text_hi) {
ut64 lo = UT64_MAX, hi = 0;
for (int i = 0; i < pe->nsecs; i++) {
PeSec *s = &pe->secs[i];
/* IMAGE_SCN_MEM_EXECUTE = 0x20000000 */
bool is_text = (s->chars & 0x20000000) || !strncmp (s->name, ".text", 5);
if (is_text) {
if (s->vaddr < lo) {
lo = s->vaddr;
}
if (s->vaddr + s->vsize > hi) {
hi = s->vaddr + s->vsize;
}
}
}
if (lo == UT64_MAX || hi <= lo) {
lo = UT64_MAX;
hi = 0;
for (int i = 0; i < pe->nsecs; i++) {
PeSec *s = &pe->secs[i];
if (s->vaddr < lo) {
lo = s->vaddr;
}
if (s->vaddr + s->vsize > hi) {
hi = s->vaddr + s->vsize;
}
}
}
*text_lo = lo;
*text_hi = hi;
}
static inline bool pe_is_data_section(const PeSec *s) {
/* Skip executable sections; keep initialized-data / .data / .rdata */
if (s->chars & 0x20000000) {
return false;
}
if (!strncmp (s->name, ".data", 5)) {
return true;
}
if (!strncmp (s->name, ".rdata", 6)) {
return true;
}
/* IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 */
return (s->chars & 0x40) != 0;
}
/* Validate the table at VA `arrptr` (`count` pointers): require at least
* `min_seen` non-zero entries and `min_good` entries landing in [text_lo, text_hi)
* within a 128-entry sample. On match, copy the first method_count entries
* into candidates (clamping to count) and return true. */
static bool pe_probe_table(PeImg *pe, ut64 arrptr, ut32 count, int ptrsz, ut64 text_lo, ut64 text_hi, ut32 min_seen, ut32 min_good, size_t method_count, ut64 *candidates) {
ut32 sample = R_MIN ((ut32)128, count);
ut32 good = 0, seen = 0;
for (ut32 k = 0; k < sample; k++) {
const ut8 *p = pe_vm_to_ptr (pe, arrptr + (ut64)k *(ut64)ptrsz);
if (!p) {
return false;
}
ut64 val = (ptrsz == 8)? r_read_le64 (p): (ut64)r_read_le32 (p);
if (val) {
seen++;
}
if (val >= text_lo && val < text_hi) {
good++;
}
}
if (seen < min_seen || good < min_good) {
return false;
}
memset (candidates, 0, method_count * sizeof (ut64));
size_t tocopy = R_MIN ((size_t)count, method_count);
size_t in_text = 0;
for (size_t m = 0; m < tocopy; m++) {
const ut8 *p = pe_vm_to_ptr (pe, arrptr + (ut64)m *(ut64)ptrsz);
if (!p) {
break;
}
ut64 val = (ptrsz == 8)? r_read_le64 (p): (ut64)r_read_le32 (p);
if (val >= text_lo && val < text_hi) {
candidates[m] = val;
in_text++;
}
}
return in_text >= 8;
}
R_API bool r2unity_find_method_pointers_pe(R2UnityMetadata *meta, const char *pe_path, ut64 **out_ptrs) {
R_RETURN_VAL_IF_FAIL (meta && pe_path && out_ptrs, false);
*out_ptrs = NULL;
PeImg pe;
if (!pe_load (pe_path, &pe)) {
return false;
}
size_t method_count = (ut64)meta->methodsSize / sizeof (Il2CppMethodDefinition);
if (!method_count) {
pe_free (&pe);
return false;
}
ut64 text_lo = 0, text_hi = 0;
pe_text_range (&pe, &text_lo, &text_hi);
if (text_lo >= text_hi) {
pe_free (&pe);
return false;
}
ut64 *candidates = R_NEWS (ut64, method_count);
if (!candidates) {
pe_free (&pe);
return false;
}
bool found = false;
int ptrsz = pe.is64? 8: 4;
ut32 expected = (ut32)R_MAX ((ut64)64, (ut64) ((ut64)meta->methodsSize / sizeof (Il2CppMethodDefinition)));
/* Pass 1: CodeRegistration-shaped pair {count32, pad?, ptr}, loose
* thresholds (small absolute floor). Pass 2: generic {count32, ptr}
* fallback with stricter sample-fraction thresholds. */
for (int pass = 0; pass < 2 && !found; pass++) {
const ut32 min_count = (pass == 0)? 32: 64;
for (int i = 0; i < pe.nsecs && !found; i++) {
PeSec *s = &pe.secs[i];
if (!pe_is_data_section (s) || s->filesize < (ut64) (8 + ptrsz)) {
continue;
}
const ut8 *buf = pe.file + s->fileoff;
for (ut64 off = 0; off + (ut64) (8 + ptrsz) <= s->filesize; off += 4) {
ut32 cnt = r_read_le32 (buf + off);
if (cnt < min_count) {
continue;
}
if (pass == 1 && expected && (cnt > expected * 2 || cnt < expected / 2)) {
continue;
}
ut64 arrptr = (ptrsz == 8)
? r_read_le64 (buf + off + 8)
: (ut64)r_read_le32 (buf + off + 4);
ut32 sample = R_MIN ((ut32)128, cnt);
ut32 min_seen = (pass == 0)? 8: sample / 2;
ut32 min_good = (pass == 0)? 8: (sample * 3) / 4;
if (pe_probe_table (&pe, arrptr, cnt, ptrsz, text_lo, text_hi, min_seen, min_good, method_count, candidates)) {
R_LOG_DEBUG ("[pe] pass=%d arrptr=0x%" PFMT64x " cnt=%u", pass, arrptr, cnt);
found = true;
break;
}
}
}
}
if (!found) {
R_FREE (candidates);
pe_free (&pe);
return false;
}
*out_ptrs = candidates;
pe_free (&pe);
return true;
}
+154
View File
@@ -0,0 +1,154 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
#define R_LOG_ORIGIN "r2unity.resource_pack"
#include "resource_pack.h"
#define R2U_RESOURCE_PACK_MAX_ENTRIES 65536
#define R2U_RESOURCE_PACK_MAX_NAME 65536
typedef struct {
RBuffer *buf;
ut64 offset;
ut64 limit;
} R2UnityResourcePackReader;
static bool reader_read(R2UnityResourcePackReader *reader, void *out, ut64 size) {
if (reader->offset > reader->limit || size > reader->limit - reader->offset
|| r_buf_read_at (reader->buf, reader->offset, out, size) != (st64)size) {
return false;
}
reader->offset += size;
return true;
}
static bool reader_u32(R2UnityResourcePackReader *reader, ut32 *value) {
ut8 bytes[4];
if (!reader_read (reader, bytes, sizeof (bytes))) {
return false;
}
*value = r_read_le32 (bytes);
return true;
}
static bool valid_name(const ut8 *bytes, ut32 size) {
ut32 offset = 0;
bool visible = false;
while (offset < size) {
RRune rune;
int length = r_utf8_decode (bytes + offset, size - offset, &rune);
if (length < 1 || (ut32)length > size - offset
|| rune == 0 || (rune < 0x20 && rune != '\t')
|| rune == 0x7f) {
return false;
}
visible |= rune != ' ' && rune != '\t';
offset += length;
}
return visible;
}
static R2UnityResourcePack *parse_internal(RBuffer *buf) {
ut64 size = r_buf_size (buf);
if (size < 8) {
return NULL;
}
R2UnityResourcePack *pack = R_NEW0 (R2UnityResourcePack);
pack->buf = r_buf_new_slice (buf, 0, size);
if (!pack->buf) {
free (pack);
return NULL;
}
pack->size = size;
R2UnityResourcePackReader reader = {
.buf = pack->buf,
.offset = 0,
.limit = size,
};
ut32 count;
if (!reader_u32 (&reader, &pack->records_size)
|| pack->records_size < 4
|| pack->records_size > size - 4
|| !reader_u32 (&reader, &count)
|| !count || count > R2U_RESOURCE_PACK_MAX_ENTRIES) {
goto fail;
}
pack->payload_offset = 4 + pack->records_size;
reader.limit = pack->payload_offset;
pack->entry_count = count;
pack->entries = R_NEWS0 (R2UnityResourcePackEntry, count);
if (!pack->entries) {
goto fail;
}
for (size_t i = 0; i < pack->entry_count; i++) {
R2UnityResourcePackEntry *entry = &pack->entries[i];
ut32 payload_size;
entry->descriptor_offset = reader.offset;
if (!reader_u32 (&reader, &payload_size)) {
goto fail;
}
entry->payload_size = payload_size;
entry->name_size_offset = reader.offset;
if (!reader_u32 (&reader, &entry->name_size)
|| !entry->name_size
|| entry->name_size > R2U_RESOURCE_PACK_MAX_NAME
|| entry->name_size > reader.limit - reader.offset) {
goto fail;
}
entry->name_offset = reader.offset;
entry->name = R_NEWS (char, (ut64)entry->name_size + 1);
if (!entry->name
|| !reader_read (&reader, entry->name, entry->name_size)
|| !valid_name ((const ut8 *)entry->name, entry->name_size)) {
goto fail;
}
entry->name[entry->name_size] = 0;
}
if (reader.offset != pack->payload_offset) {
goto fail;
}
ut64 payload_offset = pack->payload_offset;
for (size_t i = 0; i < pack->entry_count; i++) {
R2UnityResourcePackEntry *entry = &pack->entries[i];
if (payload_offset > size || entry->payload_size > size - payload_offset) {
goto fail;
}
entry->payload_offset = payload_offset;
payload_offset += entry->payload_size;
}
if (payload_offset != size) {
goto fail;
}
return pack;
fail:
r2unity_resource_pack_free (pack);
return NULL;
}
bool r2unity_resource_pack_check(RBuffer *buf) {
R_RETURN_VAL_IF_FAIL (buf, false);
R2UnityResourcePack *pack = parse_internal (buf);
if (!pack) {
return false;
}
r2unity_resource_pack_free (pack);
return true;
}
R2UnityResourcePack *r2unity_resource_pack_parse(RBuffer *buf) {
R_RETURN_VAL_IF_FAIL (buf, NULL);
return parse_internal (buf);
}
void r2unity_resource_pack_free(R2UnityResourcePack *pack) {
if (!pack) {
return;
}
for (size_t i = 0; i < pack->entry_count; i++) {
free (pack->entries[i].name);
}
free (pack->entries);
r_unref (pack->buf);
free (pack);
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef R2UNITY_RESOURCE_PACK_H
#define R2UNITY_RESOURCE_PACK_H
#include "lib.h"
typedef struct {
ut64 descriptor_offset;
ut64 name_size_offset;
ut64 name_offset;
ut32 name_size;
char *name;
ut64 payload_offset;
ut64 payload_size;
} R2UnityResourcePackEntry;
typedef struct {
RBuffer *buf;
ut64 size;
ut32 records_size;
ut64 payload_offset;
R2UnityResourcePackEntry *entries;
size_t entry_count;
} R2UnityResourcePack;
bool r2unity_resource_pack_check(RBuffer *buf);
R2UnityResourcePack *r2unity_resource_pack_parse(RBuffer *buf);
void r2unity_resource_pack_free(R2UnityResourcePack *pack);
#endif
+601
View File
@@ -0,0 +1,601 @@
/* r2unity - MIT - Copyright 2025-2026 - pancake */
#define R_LOG_ORIGIN "r2unity.serialized_file"
#include "serialized_file.h"
#include <ctype.h>
#define R2U_SERIALIZED_VERSION 22
#define R2U_SERIALIZED_HEADER_SIZE 0x30
#define R2U_MAX_TYPES 65536
#define R2U_MAX_OBJECTS 1048576
#define R2U_MAX_REFERENCES 1048576
#define R2U_MAX_STRING (1024 * 1024)
typedef struct {
RBuffer *buf;
ut64 offset;
ut64 limit;
bool big_endian;
} R2UnityReader;
static bool reader_has(const R2UnityReader *r, ut64 size) {
return r && r->offset <= r->limit && size <= r->limit - r->offset;
}
static bool reader_read(R2UnityReader *r, void *out, ut64 size) {
if (!reader_has (r, size) || r_buf_read_at (r->buf, r->offset, out, size) != (st64)size) {
return false;
}
r->offset += size;
return true;
}
static bool reader_skip(R2UnityReader *r, ut64 size) {
if (!reader_has (r, size)) {
return false;
}
r->offset += size;
return true;
}
static bool reader_align(R2UnityReader *r, ut64 alignment) {
ut64 aligned = R_ROUND (r->offset, alignment);
return aligned >= r->offset && reader_skip (r, aligned - r->offset);
}
static bool reader_u8(R2UnityReader *r, ut8 *out) {
return reader_read (r, out, sizeof (*out));
}
static bool reader_u16(R2UnityReader *r, ut16 *out) {
ut8 raw[2];
if (!reader_read (r, raw, sizeof (raw))) {
return false;
}
*out = r->big_endian? r_read_be16 (raw): r_read_le16 (raw);
return true;
}
static bool reader_u32(R2UnityReader *r, ut32 *out) {
ut8 raw[4];
if (!reader_read (r, raw, sizeof (raw))) {
return false;
}
*out = r->big_endian? r_read_be32 (raw): r_read_le32 (raw);
return true;
}
static bool reader_u64(R2UnityReader *r, ut64 *out) {
ut8 raw[8];
if (!reader_read (r, raw, sizeof (raw))) {
return false;
}
*out = r->big_endian? r_read_be64 (raw): r_read_le64 (raw);
return true;
}
static bool reader_cstring(R2UnityReader *r, char **out, ut64 *string_offset) {
ut64 start = r->offset;
ut64 length = 0;
ut8 ch = 0;
while (reader_has (r, 1)) {
if (!reader_u8 (r, &ch)) {
return false;
}
if (!ch) {
char *s = R_NEWS (char, length + 1);
if (!s) {
return false;
}
if (length && r_buf_read_at (r->buf, start, (ut8 *)s, length) != (st64)length) {
free (s);
return false;
}
s[length] = 0;
*out = s;
if (string_offset) {
*string_offset = start;
}
return true;
}
if (++length > R2U_MAX_STRING) {
return false;
}
}
return false;
}
static bool buffer_range(RBuffer *buf, ut64 offset, ut64 size) {
ut64 total = r_buf_size (buf);
return offset <= total && size <= total - offset;
}
static ut32 buffer_u32(const R2UnitySerializedFile *sf, ut64 offset) {
return r_buf_read_ble32_at (sf->buf, offset, sf->big_endian);
}
static ut64 buffer_u64(const R2UnitySerializedFile *sf, ut64 offset) {
return r_buf_read_ble64_at (sf->buf, offset, sf->big_endian);
}
static bool object_string(const R2UnitySerializedFile *sf, const R2UnitySerializedObject *object, ut64 at, char **out, ut64 *string_offset, ut32 *string_size, ut64 *next) {
ut64 end = object->offset + object->size;
if (end < object->offset || at > end || end - at < 4) {
return false;
}
ut32 length = buffer_u32 (sf, at);
ut64 data = at + 4;
if (length > R2U_MAX_STRING || data > end || length > end - data) {
return false;
}
char *s = R_NEWS (char, (ut64)length + 1);
if (!s) {
return false;
}
if (length && r_buf_read_at (sf->buf, data, (ut8 *)s, length) != length) {
free (s);
return false;
}
s[length] = 0;
*out = s;
if (string_offset) {
*string_offset = data;
}
if (string_size) {
*string_size = length;
}
if (next) {
ut64 aligned = R_ROUND (data + length, 4);
if (aligned < data || aligned > end) {
free (s);
*out = NULL;
return false;
}
*next = aligned;
}
return true;
}
static bool skip_type_tree(R2UnityReader *r) {
ut32 node_count;
ut32 string_size;
if (!reader_u32 (r, &node_count) || !reader_u32 (r, &string_size)) {
return false;
}
/* SerializedFile v19+ type-tree nodes include an eight-byte ref hash. */
const ut64 node_size = 32;
if (node_count > R2U_MAX_OBJECTS || node_count > UT64_MAX / node_size) {
return false;
}
return reader_skip (r, (ut64)node_count * node_size)
&& reader_skip (r, string_size);
}
static bool skip_dependencies(R2UnityReader *r) {
ut32 count;
if (!reader_u32 (r, &count) || count > R2U_MAX_TYPES) {
return false;
}
return reader_skip (r, (ut64)count * 4);
}
static bool read_serialized_type(R2UnityReader *r, bool type_tree, bool reference_type, R2UnitySerializedType *out) {
ut64 start = r->offset;
ut32 class_id;
ut8 stripped;
ut16 script_type;
if (!reader_u32 (r, &class_id) || !reader_u8 (r, &stripped) || !reader_u16 (r, &script_type)) {
return false;
}
out->class_id = (st32)class_id;
out->stripped = stripped != 0;
out->script_type_index = (st16)script_type;
if (out->class_id == 114 && !reader_skip (r, 16)) {
return false;
}
if (!reader_skip (r, 16)) {
return false;
}
if (type_tree && !skip_type_tree (r)) {
return false;
}
if (reference_type) {
char *class_name = NULL;
char *name_space = NULL;
char *assembly = NULL;
bool ok = reader_cstring (r, &class_name, NULL)
&& reader_cstring (r, &name_space, NULL)
&& reader_cstring (r, &assembly, NULL);
free (class_name);
free (name_space);
free (assembly);
if (!ok) {
return false;
}
} else if (type_tree && !skip_dependencies (r)) {
return false;
}
out->offset = start;
out->size = r->offset - start;
return true;
}
const char *r2unity_serialized_class_name(st32 class_id) {
switch (class_id) {
case 21:
return "Material";
case 28:
return "Texture2D";
case 43:
return "Mesh";
case 49:
return "TextAsset";
case 74:
return "AnimationClip";
case 83:
return "AudioClip";
case 91:
return "AnimatorController";
case 114:
return "MonoBehaviour";
case 150:
return "PreloadData";
case 328:
return "VideoClip";
default:
return "Object";
}
}
static bool class_has_name(st32 class_id) {
switch (class_id) {
case 21:
case 28:
case 43:
case 49:
case 74:
case 83:
case 91:
case 150:
case 328:
return true;
default:
return false;
}
}
static char *read_sidecar_path(const R2UnitySerializedFile *sf, ut64 offset,
ut32 size) {
char *path = R_NEWS (char, (ut64)size + 1);
if (!path || r_buf_read_at (sf->buf, offset, (ut8 *)path, size) != size) {
free (path);
return NULL;
}
path[size] = 0;
if (memchr (path, 0, size)
|| (!r_str_endswith (path, ".resS")
&& !r_str_endswith (path, ".resource"))) {
free (path);
return NULL;
}
return path;
}
static void parse_stream_info(R2UnitySerializedFile *sf,
R2UnitySerializedObject *object, ut64 after_name) {
ut64 end = object->offset + object->size;
/* Texture2D/Mesh StreamingInfo: u64 offset, u32 size, string path. */
for (ut64 at = after_name; at <= end && end - at >= 16; at += 4) {
ut64 stream_offset = buffer_u64 (sf, at);
ut32 stream_size = buffer_u32 (sf, at + 8);
ut32 path_size = buffer_u32 (sf, at + 12);
ut64 path_offset = at + 16;
if (!stream_size || !path_size || path_size > R2U_MAX_STRING
|| path_offset > end || path_size > end - path_offset) {
continue;
}
ut64 aligned_end = R_ROUND (path_offset + path_size, 4);
if (aligned_end != end) {
continue;
}
char *path = read_sidecar_path (sf, path_offset, path_size);
if (path) {
object->stream_path = path;
object->stream_offset = stream_offset;
object->stream_size = stream_size;
return;
}
}
/* AudioClip/VideoClip resource: string path, u64 offset, u64 size. */
for (ut64 at = after_name; at <= end && end - at >= 20; at += 4) {
ut32 path_size = buffer_u32 (sf, at);
ut64 path_offset = at + 4;
if (!path_size || path_size > R2U_MAX_STRING
|| path_offset > end || path_size > end - path_offset) {
continue;
}
ut64 stream_info = R_ROUND (path_offset + path_size, 4);
if (stream_info < path_offset || stream_info > end
|| end - stream_info < 16) {
continue;
}
ut64 stream_offset = buffer_u64 (sf, stream_info);
ut64 stream_size = buffer_u64 (sf, stream_info + 8);
if (!stream_size) {
continue;
}
char *path = read_sidecar_path (sf, path_offset, path_size);
if (path) {
object->stream_path = path;
object->stream_offset = stream_offset;
object->stream_size = stream_size;
return;
}
}
}
static void parse_texture_info(R2UnitySerializedFile *sf,
R2UnitySerializedObject *object, ut64 after_name) {
ut64 end = object->offset + object->size;
if (after_name <= end && end - after_name >= 28) {
object->width = buffer_u32 (sf, after_name + 8);
object->height = buffer_u32 (sf, after_name + 12);
object->texture_format = (st32)buffer_u32 (sf, after_name + 24);
object->has_texture_info = object->width > 0 && object->height > 0;
}
parse_stream_info (sf, object, after_name);
}
static bool parse_object_details(R2UnitySerializedFile *sf, R2UnitySerializedObject *object) {
ut64 after_name = object->offset;
if (class_has_name (object->class_id)) {
if (!object_string (sf, object, object->offset, &object->name,
&object->name_offset, &object->name_size, &after_name)) {
return false;
}
if (!*object->name) {
R_FREE (object->name);
}
} else if (object->class_id == 114) {
/* v22 MonoBehaviour: GameObject PPtr, enabled+alignment, Script PPtr, name. */
ut64 name_at = object->offset + 28;
if (!object_string (sf, object, name_at, &object->name,
&object->name_offset, &object->name_size, &after_name)) {
return false;
}
if (!*object->name) {
R_FREE (object->name);
}
}
if (object->class_id == 49) {
ut64 end = object->offset + object->size;
if (after_name > end || end - after_name < 4) {
return false;
}
ut32 payload_size = buffer_u32 (sf, after_name);
ut64 payload_offset = after_name + 4;
if (payload_offset > end || payload_size > end - payload_offset) {
return false;
}
object->payload_offset = payload_offset;
object->payload_size = payload_size;
} else if (object->class_id == 28) {
parse_texture_info (sf, object, after_name);
} else if (object->class_id == 43 || object->class_id == 83
|| object->class_id == 328) {
parse_stream_info (sf, object, after_name);
}
return true;
}
bool r2unity_serialized_file_check(RBuffer *buf) {
R_RETURN_VAL_IF_FAIL (buf, false);
ut64 actual_size = r_buf_size (buf);
if (actual_size < R2U_SERIALIZED_HEADER_SIZE) {
return false;
}
ut8 header[R2U_SERIALIZED_HEADER_SIZE];
if (r_buf_read_at (buf, 0, header, sizeof (header)) != sizeof (header)) {
return false;
}
ut32 version = r_read_be32 (header + 8);
if (version != R2U_SERIALIZED_VERSION || header[16] > 1) {
return false;
}
ut32 metadata_size = r_read_be32 (header + 20);
ut64 file_size = r_read_be64 (header + 24);
ut64 data_offset = r_read_be64 (header + 32);
if (file_size != actual_size || metadata_size < 16
|| data_offset < R2U_SERIALIZED_HEADER_SIZE || data_offset > file_size
|| metadata_size > data_offset - R2U_SERIALIZED_HEADER_SIZE) {
return false;
}
/* The metadata begins with a NUL-terminated Unity version string. */
ut8 first;
if (r_buf_read_at (buf, R2U_SERIALIZED_HEADER_SIZE, &first, 1) != 1 || !isdigit (first)) {
return false;
}
ut64 string_limit = R_MIN ((ut64)metadata_size, 64);
bool nul = false;
for (ut64 i = 0; i < string_limit; i++) {
ut8 ch;
if (r_buf_read_at (buf, R2U_SERIALIZED_HEADER_SIZE + i, &ch, 1) != 1) {
return false;
}
if (!ch) {
nul = i > 0;
break;
}
}
return nul;
}
R2UnitySerializedFile *r2unity_serialized_file_parse(RBuffer *buf) {
if (!r2unity_serialized_file_check (buf)) {
return NULL;
}
R2UnitySerializedFile *sf = R_NEW0 (R2UnitySerializedFile);
sf->buf = r_buf_new_slice (buf, 0, r_buf_size (buf));
if (!sf->buf) {
free (sf);
return NULL;
}
sf->version = r_buf_read_be32_at (sf->buf, 8);
sf->big_endian = r_buf_read8_at (sf->buf, 16) != 0;
sf->metadata_size = r_buf_read_be32_at (sf->buf, 20);
sf->file_size = r_buf_read_be64_at (sf->buf, 24);
sf->data_offset = r_buf_read_be64_at (sf->buf, 32);
sf->header_size = R2U_SERIALIZED_HEADER_SIZE;
R2UnityReader r = {
.buf = sf->buf,
.offset = sf->header_size,
.limit = sf->header_size + sf->metadata_size,
.big_endian = sf->big_endian,
};
ut32 platform;
ut8 type_tree;
ut32 type_count;
if (!reader_cstring (&r, &sf->unity_version, NULL)
|| !reader_u32 (&r, &platform)
|| !reader_u8 (&r, &type_tree)
|| !reader_u32 (&r, &type_count)
|| type_count > R2U_MAX_TYPES) {
goto fail;
}
sf->target_platform = (st32)platform;
sf->enable_type_tree = type_tree != 0;
sf->type_count = type_count;
sf->types = R_NEWS0 (R2UnitySerializedType, type_count);
if (type_count && !sf->types) {
goto fail;
}
for (size_t i = 0; i < sf->type_count; i++) {
if (!read_serialized_type (&r, sf->enable_type_tree, false, &sf->types[i])) {
goto fail;
}
}
ut32 object_count;
if (!reader_u32 (&r, &object_count) || object_count > R2U_MAX_OBJECTS) {
goto fail;
}
sf->object_count = object_count;
sf->objects = R_NEWS0 (R2UnitySerializedObject, object_count);
if (object_count && !sf->objects) {
goto fail;
}
for (size_t i = 0; i < sf->object_count; i++) {
R2UnitySerializedObject *object = &sf->objects[i];
ut64 path_id;
ut64 byte_start;
ut32 byte_size;
ut32 type_index;
if (!reader_align (&r, 4)) {
goto fail;
}
object->table_offset = r.offset;
if (!reader_u64 (&r, &path_id) || !reader_u64 (&r, &byte_start)
|| !reader_u32 (&r, &byte_size) || !reader_u32 (&r, &type_index)
|| type_index >= sf->type_count || byte_start > UT64_MAX - sf->data_offset) {
goto fail;
}
object->path_id = (st64)path_id;
object->type_index = type_index;
object->class_id = sf->types[type_index].class_id;
object->offset = sf->data_offset + byte_start;
object->size = byte_size;
if (!buffer_range (sf->buf, object->offset, object->size)) {
goto fail;
}
}
ut32 script_count;
if (!reader_u32 (&r, &script_count) || script_count > R2U_MAX_REFERENCES) {
goto fail;
}
sf->script_count = script_count;
sf->scripts = R_NEWS0 (R2UnitySerializedScript, script_count);
if (script_count && !sf->scripts) {
goto fail;
}
for (size_t i = 0; i < sf->script_count; i++) {
ut32 file_index;
ut64 path_id;
if (!reader_align (&r, 4) || !reader_u32 (&r, &file_index) || !reader_u64 (&r, &path_id)) {
goto fail;
}
sf->scripts[i].file_index = (st32)file_index;
sf->scripts[i].path_id = (st64)path_id;
}
ut32 external_count;
if (!reader_u32 (&r, &external_count) || external_count > R2U_MAX_REFERENCES) {
goto fail;
}
sf->external_count = external_count;
sf->externals = R_NEWS0 (R2UnitySerializedExternal, external_count);
if (external_count && !sf->externals) {
goto fail;
}
for (size_t i = 0; i < sf->external_count; i++) {
R2UnitySerializedExternal *external = &sf->externals[i];
char *empty = NULL;
ut32 type;
if (!reader_cstring (&r, &empty, NULL) || !reader_read (&r, external->guid, sizeof (external->guid))
|| !reader_u32 (&r, &type)
|| !reader_cstring (&r, &external->path, &external->path_offset)) {
free (empty);
goto fail;
}
free (empty);
external->type = (st32)type;
}
ut32 reference_type_count;
if (!reader_u32 (&r, &reference_type_count) || reference_type_count > R2U_MAX_TYPES) {
goto fail;
}
for (ut32 i = 0; i < reference_type_count; i++) {
R2UnitySerializedType type = {0};
if (!read_serialized_type (&r, sf->enable_type_tree, true, &type)) {
goto fail;
}
}
if (!reader_cstring (&r, &sf->user_information, NULL)) {
goto fail;
}
for (size_t i = 0; i < sf->object_count; i++) {
if (!parse_object_details (sf, &sf->objects[i])) {
goto fail;
}
}
return sf;
fail:
r2unity_serialized_file_free (sf);
return NULL;
}
void r2unity_serialized_file_free(R2UnitySerializedFile *sf) {
if (!sf) {
return;
}
for (size_t i = 0; i < sf->object_count; i++) {
free (sf->objects[i].name);
free (sf->objects[i].stream_path);
}
for (size_t i = 0; i < sf->external_count; i++) {
free (sf->externals[i].path);
}
free (sf->unity_version);
free (sf->types);
free (sf->objects);
free (sf->scripts);
free (sf->externals);
free (sf->user_information);
r_unref (sf->buf);
free (sf);
}
+74
View File
@@ -0,0 +1,74 @@
#ifndef R2UNITY_SERIALIZED_FILE_H
#define R2UNITY_SERIALIZED_FILE_H
#include "lib.h"
typedef struct {
st32 class_id;
st16 script_type_index;
ut64 offset;
ut64 size;
bool stripped;
} R2UnitySerializedType;
typedef struct {
st64 path_id;
st32 class_id;
ut32 type_index;
ut64 table_offset;
ut64 offset;
ut64 size;
char *name;
ut64 name_offset;
ut32 name_size;
ut64 payload_offset;
ut64 payload_size;
char *stream_path;
ut64 stream_offset;
ut64 stream_size;
ut32 width;
ut32 height;
st32 texture_format;
bool has_texture_info;
} R2UnitySerializedObject;
typedef struct {
st32 file_index;
st64 path_id;
} R2UnitySerializedScript;
typedef struct {
char *path;
ut64 path_offset;
ut8 guid[16];
st32 type;
} R2UnitySerializedExternal;
typedef struct {
RBuffer *buf;
ut32 version;
ut32 metadata_size;
ut64 file_size;
ut64 data_offset;
ut64 header_size;
bool big_endian;
char *unity_version;
st32 target_platform;
bool enable_type_tree;
R2UnitySerializedType *types;
size_t type_count;
R2UnitySerializedObject *objects;
size_t object_count;
R2UnitySerializedScript *scripts;
size_t script_count;
R2UnitySerializedExternal *externals;
size_t external_count;
char *user_information;
} R2UnitySerializedFile;
bool r2unity_serialized_file_check(RBuffer *buf);
R2UnitySerializedFile *r2unity_serialized_file_parse(RBuffer *buf);
void r2unity_serialized_file_free(R2UnitySerializedFile *sf);
const char *r2unity_serialized_class_name(st32 class_id);
#endif
+632 -89
View File
@@ -5,10 +5,23 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <r2unity_config.h>
#include "lib/lib.h"
typedef struct {
R2UnitySymbolOverride *items;
size_t count;
} CliSymbolOverrides;
typedef struct {
bool as_json;
bool as_r2;
bool fast;
bool quiet;
long limit;
const R2UnityNativeOptions *native;
} CliEmitOptions;
/* System.Reflection.MethodAttributes subset. Returns an owned string
*(possibly empty) describing the visibility/flags for a managed method. */
static char *method_attrs(unsigned flags) {
@@ -60,15 +73,17 @@ static void print_usage(FILE *out, const char *prog_name) {
"\n"
"Options:\n"
" -a 0xADDR Read the method pointer table starting at virtual address 0xADDR\n"
" -c N Read N pointer entries (pair with -a)\n"
" -c Enumerate classes, inheritance, methods, and fields\n"
" -D Detect companion files from the given executable path and exit\n"
" -f Fast path: auto-detect ELF/Mach-O/PE and scan method pointers\n"
" -f Recover native method pointers through symbols/sections\n"
" -H Force section-scan fallback instead of CodeRegistration\n"
" -h Show this help and exit\n"
" -j One-line JSON status, or JSON output with -P/-R/-S\n"
" -j One-line JSON status, or JSON output with -c/-P/-R/-S\n"
" -l N Limit emitted entries to N\n"
" -O N=A Override a native symbol address, e.g. g_CodeRegistration=0x1234\n"
" -P Enumerate P/Invoke (managed -> native) methods\n"
" -q Quiet mode: omit banner and informational comments\n"
" -r Emit r2 script commands (flags + comments); pairs with -P\n"
" -r Emit r2 script commands; pairs with -c/-P/-R\n"
" -R Enumerate reverse-P/Invoke (native -> managed) methods (v29+)\n"
" -S Emit a text SBOM of the managed assemblies\n"
" -V Verbose debug tracing on stderr\n"
@@ -78,6 +93,7 @@ static void print_usage(FILE *out, const char *prog_name) {
"Arguments:\n"
" <executable> Native IL2CPP binary for the target platform\n"
" <global-metadata.dat> Unity IL2CPP metadata file\n"
" -c and -z also accept metadata-only input\n"
"\n"
"Expected files per target platform:\n"
" iOS build: UnityFramework + global-metadata.dat\n"
@@ -93,6 +109,69 @@ static void print_usage(FILE *out, const char *prog_name) {
prog_name);
}
static bool add_symbol_override(CliSymbolOverrides *overrides, const char *arg) {
if (!overrides || !arg) {
return false;
}
const char *eq = strchr (arg, '=');
if (!eq || eq == arg || !eq[1]) {
return false;
}
R2UnitySymbolOverride *items = realloc (overrides->items, (overrides->count + 1) * sizeof (*items));
if (!items) {
return false;
}
overrides->items = items;
char *name = r_str_ndup (arg, (int)(eq - arg));
if (!name) {
return false;
}
overrides->items[overrides->count].name = name;
overrides->items[overrides->count].va = (ut64)strtoull (eq + 1, NULL, 0);
overrides->count++;
return true;
}
static void free_symbol_overrides(CliSymbolOverrides *overrides) {
if (!overrides) {
return;
}
for (size_t i = 0; i < overrides->count; i++) {
free ((char *)overrides->items[i].name);
}
R_FREE (overrides->items);
overrides->count = 0;
}
static void pj_native_result(PJ *pj, const R2UnityNativeResult *native) {
pj_ks (pj, "native_source", native? r2unity_native_source_name (native->source): "none");
pj_kn (pj, "code_registration", native? native->code_registration_va: 0);
pj_kn (pj, "metadata_registration", native? native->metadata_registration_va: 0);
pj_kn (pj, "method_pointers", native? native->method_pointers_va: 0);
pj_kn (pj, "code_gen_modules", native? native->code_gen_modules_va: 0);
pj_ko (pj, "native_tables");
for (R2UnityNativeTableId i = 0; i < R2U_NATIVE_TABLE_COUNT; i++) {
const R2UnityNativeTable *table = native? &native->tables[i]: NULL;
if (!table || !table->va) {
continue;
}
pj_ko (pj, r2unity_native_table_name (i));
pj_kn (pj, "count", table->count);
pj_kn (pj, "va", table->va);
pj_end (pj);
}
pj_end (pj);
}
static void print_native_comment(const R2UnityNativeResult *native) {
printf ("# native_source=%s code_registration=0x%" PFMT64x " metadata_registration=0x%" PFMT64x " method_pointers=0x%" PFMT64x " code_gen_modules=0x%" PFMT64x "\n",
native? r2unity_native_source_name (native->source): "none",
native? native->code_registration_va: 0,
native? native->metadata_registration_va: 0,
native? native->method_pointers_va: 0,
native? native->code_gen_modules_va: 0);
}
static void json_escape(FILE *f, const char *s) {
for (; s && *s; s++) {
unsigned char c = (unsigned char)*s;
@@ -197,8 +276,36 @@ static int interop_cmp(const void *a, const void *b) {
return strcmp (xn, yn);
}
static int emit_pinvokes(R2UnityMetadata *meta, const char *exe_path, bool is_json, bool is_r2, bool quiet) {
(void)exe_path;
static char *interop_flag_name(const char *prefix, const R2UnityInterop *it) {
char *name = it->image_name && *it->image_name
? r_str_newf ("%s.%s.%s", prefix, it->image_name, it->name)
: r_str_newf ("%s.%s", prefix, it->name);
if (name) {
r_name_filter (name, -1);
if (!r_name_check (name)) {
R_FREE (name);
}
}
return name;
}
static void print_interop_wrapper_flag(const char *name, ut64 wrapper_va) {
if (wrapper_va && name) {
printf ("'@0x%" PFMT64x "'f %s\n", wrapper_va, name);
printf ("'@0x%" PFMT64x "'af+ $$ %s\n", wrapper_va, name);
}
}
static void json_interop_wrapper(FILE *f, const R2UnityInterop *it) {
if (it->wrapper_va) {
fprintf (f, ",\"wrapper_va\":\"0x%" PFMT64x "\"", it->wrapper_va);
}
if (it->wrapper_index != UINT32_MAX) {
fprintf (f, ",\"wrapper_index\":%u", it->wrapper_index);
}
}
static int emit_pinvokes(R2UnityMetadata *meta, bool is_json, bool is_r2, bool quiet) {
size_t n = 0;
R2UnityInterop *items = r2unity_enumerate_pinvokes (meta, &n);
if (items && n > 1) {
@@ -241,18 +348,16 @@ static int emit_pinvokes(R2UnityMetadata *meta, const char *exe_path, bool is_js
if (!it->name) {
continue;
}
char buf[1024];
if (it->image_name && *it->image_name) {
snprintf (buf, sizeof (buf), "sym.unity.%s.%s", it->image_name, it->name);
} else {
snprintf (buf, sizeof (buf), "sym.unity.%s", it->name);
char *name = interop_flag_name ("sym.unity", it);
if (!name) {
continue;
}
r_name_filter (buf, -1);
if (it->dll_name) {
printf ("# PInvoke %s -> %s!%s\n", buf, it->dll_name, it->entry_name? it->entry_name: it->name);
printf ("# PInvoke %s -> %s!%s\n", name, it->dll_name, it->entry_name? it->entry_name: it->name);
} else {
printf ("# PInvoke %s -> <unresolved>\n", buf);
printf ("# PInvoke %s -> <unresolved>\n", name);
}
free (name);
}
} else {
if (!quiet) {
@@ -288,10 +393,12 @@ static const char *interop_kind_label(uint8_t kind) {
}
}
static int emit_reverse_pinvokes(R2UnityMetadata *meta, const char *exe_path, bool is_json, bool is_r2, bool quiet) {
(void)exe_path;
static int emit_reverse_pinvokes(R2UnityMetadata *meta, const char *exe_path, const R2UnityNativeOptions *native, bool is_json, bool is_r2, bool quiet) {
size_t n = 0;
R2UnityInterop *items = r2unity_enumerate_reverse_pinvokes (meta, &n);
if (exe_path && *exe_path) {
r2unity_enrich_reverse_pinvokes_native (meta, exe_path, native, &items, &n);
}
if (items && n > 1) {
qsort (items, n, sizeof (R2UnityInterop), interop_cmp);
}
@@ -309,6 +416,7 @@ static int emit_reverse_pinvokes(R2UnityMetadata *meta, const char *exe_path, bo
fprintf (f, "\",\"method\":\"");
json_escape (f, it->name? it->name: "");
fprintf (f, "\",\"token\":\"0x%08x\",\"flags\":\"0x%04x\",\"iflags\":\"0x%04x\",\"attribute\":\"%s\",\"confidence\":%u", it->token, it->flags, it->iflags, interop_kind_label (it->kind), (unsigned)it->confidence);
json_interop_wrapper (f, it);
fputc ('}', f);
}
fprintf (f, "]}\n");
@@ -318,14 +426,13 @@ static int emit_reverse_pinvokes(R2UnityMetadata *meta, const char *exe_path, bo
if (!it->name) {
continue;
}
char buf[1024];
if (it->image_name && *it->image_name) {
snprintf (buf, sizeof (buf), "sym.unity.reverse.%s.%s", it->image_name, it->name);
} else {
snprintf (buf, sizeof (buf), "sym.unity.reverse.%s", it->name);
char *name = interop_flag_name ("sym.unity.reverse", it);
if (!name) {
continue;
}
r_name_filter (buf, -1);
printf ("# ReversePInvoke %s [%s]\n", buf, interop_kind_label (it->kind));
printf ("# ReversePInvoke %s [%s]\n", name, interop_kind_label (it->kind));
print_interop_wrapper_flag (name, it->wrapper_va);
free (name);
}
} else {
if (!quiet) {
@@ -408,31 +515,6 @@ static int emit_detected_paths(const char *input, bool as_json) {
return 0;
}
/* Sniff the exe magic and dispatch to the matching fast finder. */
static bool find_method_pointers_fast(R2UnityMetadata *meta, const char *path, ut64 **out_ptrs) {
ut8 magic[4] = { 0 };
FILE *fp = fopen (path, "rb");
if (fp) {
if (fread (magic, 1, sizeof (magic), fp) != sizeof (magic)) {
memset (magic, 0, sizeof (magic));
}
fclose (fp);
}
if (!memcmp (magic, "\x7f"
"ELF",
4)) {
return r2unity_find_method_pointers_elf (meta, path, out_ptrs);
}
ut32 m = r_read_le32 (magic);
if (m == 0xfeedfacf || m == 0xcffaedfe || m == 0xcafebabe || m == 0xbebafeca) {
return r2unity_find_method_pointers_macho (meta, path, out_ptrs);
}
if (magic[0] == 'M' && magic[1] == 'Z') {
return r2unity_find_method_pointers_pe (meta, path, out_ptrs);
}
return false;
}
/* Emit one method as r2 script commands. Methods without a plausible native
* address (addr <= 0x1000) are skipped entirely and do not count against the
* emit limit. Returns true when output was produced. */
@@ -459,6 +541,9 @@ static bool emit_r2_method(R2UnityMetadata *meta,
r_name_filter (fullname, -1);
char *attrs = method_attrs (m->flags);
char *im = img? r2unity_get_string (meta, img->nameIndex): NULL;
if (im) {
r_name_filter (im, -1);
}
if (im && *im) {
printf ("'@0x%" PFMT64x "'f sym.unity.%s.%s\n", addr, im, fullname);
printf ("'@0x%" PFMT64x "'CCu Method: [%s]%s%s %s\n",
@@ -486,6 +571,395 @@ static bool emit_r2_method(R2UnityMetadata *meta,
return true;
}
static int type_definition_for_type_index(const Il2CppTypeDefinition *types, size_t type_count, int32_t type_index) {
if (type_index < 0) {
return -1;
}
for (size_t i = 0; i < type_count; i++) {
if (types[i].byvalTypeIndex == type_index) {
return (int)i;
}
}
return -1;
}
static char *type_name_from_index(R2UnityMetadata *meta, const Il2CppTypeDefinition *types, size_t type_count, int32_t type_index, bool fallback) {
int idx = type_definition_for_type_index (types, type_count, type_index);
if (idx >= 0) {
return r2unity_type_fullname (meta, &types[idx], (size_t)idx, R2U_NAME_FALLBACK_TYPE);
}
if (fallback && type_index >= 0) {
return r_str_newf ("type_index.%d", type_index);
}
return NULL;
}
static char *field_name_or_fallback(R2UnityMetadata *meta, const Il2CppFieldDefinition *field, size_t index) {
char *name = r2unity_get_string (meta, field->nameIndex);
if (name && *name) {
return name;
}
free (name);
return r_str_newf ("field.%zu", index);
}
static char *method_name_or_fallback(R2UnityMetadata *meta, const Il2CppMethodDefinition *method, size_t index) {
char *name = r2unity_get_string (meta, method->nameIndex);
if (name && *name) {
return name;
}
free (name);
return r_str_newf ("method.%zu", index);
}
static char *r2_ic_name(const char *prefix, const char *name, size_t index) {
char *out = R_STR_ISNOTEMPTY (name)? r_str_newf ("%s", name): r_str_newf ("%s_%zu", prefix, index);
if (out) {
r_name_filter (out, -1);
}
for (char *p = out; p && *p; p++) {
if (*p == '.' || *p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
*p = '_';
}
}
if (R_STR_ISEMPTY (out)) {
R_FREE (out);
return r_str_newf ("%s_%zu", prefix, index);
}
return out;
}
static void pj_hex(PJ *pj, const char *key, ut64 value, int width) {
char hex[64];
snprintf (hex, sizeof (hex), "0x%0*" PFMT64x, width, value);
pj_ks (pj, key, hex);
}
static void pj_string_or_null(PJ *pj, const char *key, const char *value) {
if (value) {
pj_ks (pj, key, value);
} else {
pj_knull (pj, key);
}
}
static void emit_class_text(R2UnityMetadata *meta,
const Il2CppTypeDefinition *types,
size_t type_count,
const Il2CppMethodDefinition *methods,
size_t method_count,
const Il2CppFieldDefinition *fields,
size_t field_count,
const int32_t *interfaces,
size_t interface_count,
size_t type_index) {
const Il2CppTypeDefinition *td = &types[type_index];
char *name = r2unity_type_fullname (meta, td, type_index, R2U_NAME_FALLBACK_TYPE);
char *base = type_name_from_index (meta, types, type_count, td->parentIndex, true);
printf ("class %zu %s", type_index, name? name: "-");
if (base) {
printf (" : %s", base);
}
if (td->interfaces_count > 0 && td->interfacesStart >= 0) {
bool first = true;
for (size_t k = 0; k < td->interfaces_count && (size_t)(td->interfacesStart + k) < interface_count; k++) {
char *iname = type_name_from_index (meta, types, type_count, interfaces[td->interfacesStart + k], true);
if (iname) {
printf ("%s%s", first? " implements ": ", ", iname);
first = false;
}
free (iname);
}
}
printf (" token=0x%08x fields=%u methods=%u\n", td->token, (unsigned)td->field_count, (unsigned)td->method_count);
int fstart = td->fieldStart;
for (int k = 0; fields && k < td->field_count && fstart >= 0 && (size_t)(fstart + k) < field_count; k++) {
size_t fi = (size_t)(fstart + k);
char *fname = field_name_or_fallback (meta, &fields[fi], fi);
char *ftype = type_name_from_index (meta, types, type_count, fields[fi].typeIndex, true);
printf (" field %zu %s %s token=0x%08x type_index=%d\n",
fi,
ftype? ftype: "-",
fname? fname: "-",
fields[fi].token,
fields[fi].typeIndex);
free (ftype);
free (fname);
}
int mstart = td->methodStart;
for (int k = 0; methods && k < td->method_count && mstart >= 0 && (size_t)(mstart + k) < method_count; k++) {
size_t mi = (size_t)(mstart + k);
char *mname = method_name_or_fallback (meta, &methods[mi], mi);
char *attrs = method_attrs (methods[mi].flags);
printf (" method %zu %s%s%s(%u) token=0x%08x flags=0x%04x\n",
mi,
attrs && *attrs? attrs: "",
attrs && *attrs? " ": "",
mname? mname: "-",
(unsigned)methods[mi].parameterCount,
methods[mi].token,
methods[mi].flags);
free (attrs);
free (mname);
}
free (base);
free (name);
}
static void emit_class_json(PJ *pj,
R2UnityMetadata *meta,
const Il2CppTypeDefinition *types,
size_t type_count,
const Il2CppMethodDefinition *methods,
size_t method_count,
const Il2CppFieldDefinition *fields,
size_t field_count,
const int32_t *interfaces,
size_t interface_count,
const ut64 *method_ptrs,
bool has_ptrs,
size_t type_index) {
const Il2CppTypeDefinition *td = &types[type_index];
char *name = r2unity_type_fullname (meta, td, type_index, R2U_NAME_FALLBACK_TYPE);
char *ns = r2unity_get_string (meta, td->namespaceIndex);
char *short_name = r2unity_get_string (meta, td->nameIndex);
char *base = type_name_from_index (meta, types, type_count, td->parentIndex, true);
pj_o (pj);
pj_kn (pj, "index", (ut64)type_index);
pj_string_or_null (pj, "name", name);
pj_string_or_null (pj, "namespace", ns);
pj_string_or_null (pj, "short_name", short_name);
pj_string_or_null (pj, "base", base);
pj_ki (pj, "base_type_index", td->parentIndex);
pj_hex (pj, "flags", td->flags, 8);
pj_hex (pj, "token", td->token, 8);
pj_ka (pj, "interfaces");
if (td->interfaces_count > 0 && td->interfacesStart >= 0) {
for (size_t k = 0; k < td->interfaces_count && (size_t)(td->interfacesStart + k) < interface_count; k++) {
int32_t idx = interfaces[td->interfacesStart + k];
char *iname = type_name_from_index (meta, types, type_count, idx, true);
pj_o (pj);
pj_ki (pj, "type_index", idx);
pj_string_or_null (pj, "name", iname);
pj_end (pj);
free (iname);
}
}
pj_end (pj);
pj_ka (pj, "fields");
int fstart = td->fieldStart;
for (int k = 0; fields && k < td->field_count && fstart >= 0 && (size_t)(fstart + k) < field_count; k++) {
size_t fi = (size_t)(fstart + k);
char *fname = field_name_or_fallback (meta, &fields[fi], fi);
char *ftype = type_name_from_index (meta, types, type_count, fields[fi].typeIndex, true);
pj_o (pj);
pj_kn (pj, "index", (ut64)fi);
pj_string_or_null (pj, "name", fname);
pj_ki (pj, "type_index", fields[fi].typeIndex);
pj_string_or_null (pj, "type", ftype);
pj_hex (pj, "token", fields[fi].token, 8);
pj_end (pj);
free (ftype);
free (fname);
}
pj_end (pj);
pj_ka (pj, "methods");
int mstart = td->methodStart;
for (int k = 0; methods && k < td->method_count && mstart >= 0 && (size_t)(mstart + k) < method_count; k++) {
size_t mi = (size_t)(mstart + k);
char *mname = method_name_or_fallback (meta, &methods[mi], mi);
char *fullname = r2unity_method_fullname (meta, &methods[mi], td, type_index, R2U_NAME_WITH_PARAMS | R2U_NAME_FALLBACK_TYPE);
char *attrs = method_attrs (methods[mi].flags);
ut64 addr = (has_ptrs && method_ptrs)? method_ptrs[mi]: 0;
pj_o (pj);
pj_kn (pj, "index", (ut64)mi);
pj_string_or_null (pj, "name", mname);
pj_string_or_null (pj, "full_name", fullname);
pj_kn (pj, "parameter_count", (ut64)methods[mi].parameterCount);
pj_kn (pj, "addr", addr);
pj_hex (pj, "token", methods[mi].token, 8);
pj_hex (pj, "flags", methods[mi].flags, 4);
pj_hex (pj, "iflags", methods[mi].iflags, 4);
pj_string_or_null (pj, "attrs", (attrs && *attrs)? attrs: NULL);
pj_end (pj);
free (attrs);
free (fullname);
free (mname);
}
pj_end (pj);
pj_end (pj);
free (base);
free (short_name);
free (ns);
free (name);
}
static void emit_class_r2(R2UnityMetadata *meta,
const Il2CppTypeDefinition *types,
size_t type_count,
const Il2CppMethodDefinition *methods,
size_t method_count,
const Il2CppFieldDefinition *fields,
size_t field_count,
const ut64 *method_ptrs,
bool has_ptrs,
size_t type_index,
bool quiet) {
const Il2CppTypeDefinition *td = &types[type_index];
char *name = r2unity_type_fullname (meta, td, type_index, R2U_NAME_FALLBACK_TYPE);
char *r2klass = r2_ic_name ("type", name, type_index);
if (!quiet) {
char *base = type_name_from_index (meta, types, type_count, td->parentIndex, false);
if (base) {
printf ("# class %s : %s\n", name? name: r2klass, base);
} else {
printf ("# class %s\n", name? name: r2klass);
}
free (base);
}
printf ("'@0x0'ic+ %s\n", r2klass);
int mstart = td->methodStart;
for (int k = 0; methods && k < td->method_count && mstart >= 0 && (size_t)(mstart + k) < method_count; k++) {
size_t mi = (size_t)(mstart + k);
ut64 addr = (has_ptrs && method_ptrs)? method_ptrs[mi]: 0;
if (addr <= 0x1000) {
continue;
}
char *mname = method_name_or_fallback (meta, &methods[mi], mi);
char *r2meth = r2_ic_name ("method", mname, mi);
printf ("'@0x%" PFMT64x "'ic+ %s.%s\n", addr, r2klass, r2meth);
free (r2meth);
free (mname);
}
if (!quiet) {
int fstart = td->fieldStart;
for (int k = 0; fields && k < td->field_count && fstart >= 0 && (size_t)(fstart + k) < field_count; k++) {
size_t fi = (size_t)(fstart + k);
char *fname = field_name_or_fallback (meta, &fields[fi], fi);
char *ftype = type_name_from_index (meta, types, type_count, fields[fi].typeIndex, true);
printf ("# field %s.%s %s type_index=%d token=0x%08x\n",
r2klass,
fname? fname: "-",
ftype? ftype: "-",
fields[fi].typeIndex,
fields[fi].token);
free (ftype);
free (fname);
}
}
free (r2klass);
free (name);
}
static int emit_classes(R2UnityMetadata *meta, const char *exe_path, const char *metadata_path, const CliEmitOptions *opts) {
if (opts->as_json && opts->as_r2) {
R_LOG_ERROR ("-j and -r are mutually exclusive with -c");
return 1;
}
if (opts->fast && (!exe_path || !*exe_path)) {
R_LOG_ERROR ("-c -f requires both executable and metadata paths");
return 1;
}
size_t type_count = 0;
Il2CppTypeDefinition *types = r2unity_get_type_definitions (meta, &type_count);
if (!types) {
R_LOG_ERROR ("unable to decode type definitions");
return 1;
}
size_t method_count = 0;
Il2CppMethodDefinition *methods = r2unity_get_method_definitions (meta, &method_count);
size_t field_count = 0;
Il2CppFieldDefinition *fields = r2unity_get_field_definitions (meta, &field_count);
size_t interface_count = 0;
int32_t *interfaces = r2unity_get_type_index_table (meta, R2U_SEC_INTERFACES, &interface_count);
R2UnityNativeResult native_result = { 0 };
ut64 *method_ptrs = NULL;
bool has_ptrs = false;
if (opts->fast) {
has_ptrs = r2unity_find_method_pointers (meta, exe_path, opts->native, &native_result);
method_ptrs = native_result.method_ptrs;
}
size_t max = type_count;
if (opts->limit >= 0 && (size_t)opts->limit < max) {
max = (size_t)opts->limit;
}
if (opts->as_json) {
PJ *pj = pj_new ();
if (!pj) {
R_LOG_ERROR ("unable to allocate JSON builder");
R_FREE (method_ptrs);
R_FREE (interfaces);
R_FREE (fields);
R_FREE (methods);
R_FREE (types);
return 1;
}
pj_o (pj);
pj_kb (pj, "ok", true);
pj_ki (pj, "version", meta->version);
pj_ks (pj, "unity_range", r2unity_unity_range_from_wire (meta->version));
pj_kb (pj, "has_ptrs", has_ptrs);
pj_native_result (pj, &native_result);
pj_kn (pj, "types", (ut64)type_count);
pj_kn (pj, "methods", (ut64)method_count);
pj_kn (pj, "fields", (ut64)field_count);
pj_ka (pj, "classes");
for (size_t i = 0; i < max; i++) {
emit_class_json (pj, meta, types, type_count, methods, method_count, fields, field_count, interfaces, interface_count, method_ptrs, has_ptrs, i);
}
pj_end (pj);
pj_end (pj);
char *out = pj_drain (pj);
if (out) {
puts (out);
free (out);
}
} else if (opts->as_r2) {
if (!opts->quiet) {
printf ("# r2 script generated by r2unity -c\n");
printf ("# Input file: %s\n", metadata_path && *metadata_path? metadata_path: "-");
print_native_comment (&native_result);
if (!has_ptrs) {
printf ("# Method ic+ entries need native addresses; pass -f with an executable to recover them.\n");
}
}
for (size_t i = 0; i < max; i++) {
emit_class_r2 (meta, types, type_count, methods, method_count, fields, field_count, method_ptrs, has_ptrs, i, opts->quiet);
}
} else {
if (!opts->quiet) {
printf ("# classes from %s\n", metadata_path && *metadata_path? metadata_path: "-");
printf ("# wire_version=%d (%s) types=%zu methods=%zu fields=%zu\n",
meta->version,
r2unity_unity_range_from_wire (meta->version),
type_count,
method_count,
field_count);
}
for (size_t i = 0; i < max; i++) {
emit_class_text (meta, types, type_count, methods, method_count, fields, field_count, interfaces, interface_count, i);
}
}
r2unity_native_result_fini (&native_result);
R_FREE (interfaces);
R_FREE (fields);
R_FREE (methods);
R_FREE (types);
return 0;
}
int main(int argc, char *argv[]) {
bool json_one_line = false;
bool r2_script = false;
@@ -493,78 +967,123 @@ int main(int argc, char *argv[]) {
bool debug = false;
long limit = -1;
ut64 gmp_addr = 0;
size_t gmp_count = 0;
bool fast = false;
bool sbom = false;
bool pinvokes = false;
bool reverse_pinvokes = false;
bool string_literals = false;
bool detect_paths = false;
bool classes = false;
R2UnityNativeOptions native_options = { 0 };
CliSymbolOverrides symbol_overrides = { 0 };
int opt;
while ((opt = getopt (argc, argv, "hjrqfVvSPRDzl:a:c:")) != -1) {
RGetopt go;
r_getopt_init (&go, argc, (const char **)argv, "chjrqfHVvSPRDzl:a:O:");
while ((opt = r_getopt_next (&go)) != -1) {
switch (opt) {
case 'c': classes = true; break;
case 'j': json_one_line = true; break;
case 'r': r2_script = true; break;
case 'q': quiet = true; break;
case 'f': fast = true; break;
case 'H':
fast = true;
native_options.force_heuristic = true;
break;
case 'V': debug = true; break;
case 'v':
printf ("r2unity %s\n", R2UNITY_VERSION);
free_symbol_overrides (&symbol_overrides);
return 0;
case 'S': sbom = true; break;
case 'P': pinvokes = true; break;
case 'R': reverse_pinvokes = true; break;
case 'D': detect_paths = true; break;
case 'z': string_literals = true; break;
case 'l': limit = strtol (optarg, NULL, 0); break;
case 'a': gmp_addr = (ut64)strtoull (optarg, NULL, 0); break;
case 'c': gmp_count = (size_t)strtoull (optarg, NULL, 0); break;
case 'l': limit = strtol (go.arg, NULL, 0); break;
case 'a': gmp_addr = (ut64)strtoull (go.arg, NULL, 0); break;
case 'O':
if (!add_symbol_override (&symbol_overrides, go.arg)) {
R_LOG_ERROR ("invalid -O argument; expected name=addr");
free_symbol_overrides (&symbol_overrides);
return 1;
}
fast = true;
break;
case 'h':
print_usage (stdout, argv[0]);
free_symbol_overrides (&symbol_overrides);
return 0;
default:
print_usage (stderr, argv[0]);
free_symbol_overrides (&symbol_overrides);
return 1;
}
}
native_options.symbols = symbol_overrides.items;
native_options.symbols_count = symbol_overrides.count;
if (pinvokes && reverse_pinvokes) {
R_LOG_ERROR ("-P and -R are mutually exclusive");
free_symbol_overrides (&symbol_overrides);
return 1;
}
if (detect_paths) {
if (argc - optind != 1) {
if (argc - go.ind != 1) {
print_usage (stderr, argv[0]);
free_symbol_overrides (&symbol_overrides);
return 1;
}
return emit_detected_paths (argv[optind], json_one_line);
int rc = emit_detected_paths (argv[go.ind], json_one_line);
free_symbol_overrides (&symbol_overrides);
return rc;
}
if (string_literals) {
if (json_one_line || fast || gmp_addr || sbom || pinvokes || reverse_pinvokes) {
R_LOG_ERROR ("-z cannot be combined with -j, -f, -a/-c, -S, -P or -R");
if (classes) {
if (sbom || pinvokes || reverse_pinvokes || string_literals) {
R_LOG_ERROR ("-c cannot be combined with -S, -P, -R or -z");
free_symbol_overrides (&symbol_overrides);
return 1;
}
if (argc - optind != 1 && argc - optind != 2) {
if (argc - go.ind != 1 && argc - go.ind != 2) {
print_usage (stderr, argv[0]);
free_symbol_overrides (&symbol_overrides);
return 1;
}
} else if (argc - optind != 2) {
if (fast && argc - go.ind != 2) {
R_LOG_ERROR ("-c -f requires both executable and metadata paths");
free_symbol_overrides (&symbol_overrides);
return 1;
}
} else if (string_literals) {
if (json_one_line || fast || gmp_addr || sbom || pinvokes || reverse_pinvokes) {
R_LOG_ERROR ("-z cannot be combined with -j, -f, -a, -S, -P or -R");
free_symbol_overrides (&symbol_overrides);
return 1;
}
if (argc - go.ind != 1 && argc - go.ind != 2) {
print_usage (stderr, argv[0]);
free_symbol_overrides (&symbol_overrides);
return 1;
}
} else if (argc - go.ind != 2) {
print_usage (stderr, argv[0]);
free_symbol_overrides (&symbol_overrides);
return 1;
}
const char *exe_path = NULL;
const char *metadata_path = NULL;
if (string_literals && argc - optind == 1) {
if ((string_literals || classes) && argc - go.ind == 1) {
exe_path = "";
metadata_path = argv[optind];
metadata_path = argv[go.ind];
} else {
exe_path = argv[optind];
metadata_path = argv[optind + 1];
exe_path = argv[go.ind];
metadata_path = argv[go.ind + 1];
}
RBuffer *buf = r_buf_new_file (metadata_path, O_RDONLY, 0);
if (!buf) {
perror ("Error opening file");
free_symbol_overrides (&symbol_overrides);
return 1;
}
@@ -575,6 +1094,7 @@ int main(int argc, char *argv[]) {
if (!meta) {
R_LOG_ERROR ("Failed to parse metadata");
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return 1;
}
@@ -589,6 +1109,7 @@ int main(int argc, char *argv[]) {
}
r2unity_free_metadata (meta);
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return rc;
}
@@ -597,13 +1118,31 @@ int main(int argc, char *argv[]) {
R_LOG_ERROR ("-j and -r are mutually exclusive");
r2unity_free_metadata (meta);
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return 1;
}
int rc = reverse_pinvokes
? emit_reverse_pinvokes (meta, exe_path, json_one_line, r2_script, quiet)
: emit_pinvokes (meta, exe_path, json_one_line, r2_script, quiet);
? emit_reverse_pinvokes (meta, exe_path, &native_options, json_one_line, r2_script, quiet)
: emit_pinvokes (meta, json_one_line, r2_script, quiet);
r2unity_free_metadata (meta);
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return rc;
}
if (classes) {
CliEmitOptions emit_opts = {
.as_json = json_one_line,
.as_r2 = r2_script,
.fast = fast,
.quiet = quiet,
.limit = limit,
.native = &native_options
};
int rc = emit_classes (meta, exe_path, metadata_path, &emit_opts);
r2unity_free_metadata (meta);
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return rc;
}
@@ -611,6 +1150,7 @@ int main(int argc, char *argv[]) {
int rc = emit_string_literals (meta, metadata_path, quiet, limit);
r2unity_free_metadata (meta);
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return rc;
}
@@ -620,43 +1160,45 @@ int main(int argc, char *argv[]) {
size_t method_count = 0;
Il2CppMethodDefinition *methods = r2unity_get_method_definitions (meta, &method_count);
R2UnityNativeResult native_result = { 0 };
ut64 *method_ptrs = NULL;
bool has_ptrs = false;
if (gmp_addr) {
R_LOG_WARN ("Manual method-pointer table reading (-a/-c) is not implemented");
(void)gmp_count;
R_LOG_WARN ("Manual method-pointer table reading (-a) is not implemented");
} else if (fast) {
has_ptrs = find_method_pointers_fast (meta, exe_path, &method_ptrs);
}
/* Fast-path may prealloc an all-zero table; upgrade has_ptrs only if any
* non-zero entry survived. */
if (method_ptrs && !has_ptrs) {
for (size_t k = 0; k < method_count; k++) {
if (method_ptrs[k]) {
has_ptrs = true;
break;
}
}
has_ptrs = r2unity_find_method_pointers (meta, exe_path, &native_options, &native_result);
method_ptrs = native_result.method_ptrs;
}
if (json_one_line) {
// Output a single stable JSON line
printf ("{\"ok\":true,\"version\":%d,\"types\":%u,\"methods\":%u,\"has_ptrs\":%s}\n",
meta->version,
(unsigned)type_count,
(unsigned)method_count,
has_ptrs? "true": "false");
R_FREE (method_ptrs);
PJ *pj = pj_new ();
pj_o (pj);
pj_kb (pj, "ok", true);
pj_ki (pj, "version", meta->version);
pj_kn (pj, "types", (ut64)type_count);
pj_kn (pj, "methods", (ut64)method_count);
pj_kb (pj, "has_ptrs", has_ptrs);
pj_native_result (pj, &native_result);
pj_end (pj);
char *out = pj_drain (pj);
if (out) {
puts (out);
free (out);
}
r2unity_native_result_fini (&native_result);
R_FREE (methods);
R_FREE (types);
r2unity_free_metadata (meta);
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return 0;
}
if (!quiet) {
printf ("# r2 script generated by r2unity\n");
printf ("# Input file: %s\n\n", metadata_path);
printf ("# Input file: %s\n", metadata_path);
print_native_comment (&native_result);
printf ("\n");
}
size_t img_count = 0;
@@ -688,7 +1230,7 @@ int main(int argc, char *argv[]) {
}
}
R_FREE (method_ptrs);
r2unity_native_result_fini (&native_result);
R_FREE (methods);
R_FREE (types);
R_FREE (images);
@@ -696,6 +1238,7 @@ int main(int argc, char *argv[]) {
r2unity_free_metadata (meta);
r_unref (buf);
free_symbol_overrides (&symbol_overrides);
return 0;
}
+1251 -59
View File
File diff suppressed because it is too large. Load diff
+870 -84
View File
File diff suppressed because it is too large. Load diff