mirror of
https://github.com/radareorg/r2unity.git
synced 2026-11-07 06:36:00 -10:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afb7e4368c | ||
|
|
c2f219c95a | ||
|
|
4a35664331 | ||
|
|
31cb6bce07 | ||
|
|
ae094c666a | ||
|
|
ccaa476cac | ||
|
|
0bcdb6b8aa | ||
|
|
212085e7fb | ||
|
|
aa3c6c6254 | ||
|
|
8d46d02b38 | ||
|
|
a0c7354c37 | ||
|
|
67471a35d3 | ||
|
|
12620d1557 | ||
|
|
06a21356e7 | ||
|
|
26d3db86c3 | ||
|
|
6681f1af93 | ||
|
|
08876f2aa3 | ||
|
|
60267a76e8 | ||
|
|
57cda9236e | ||
|
|
80701514cd | ||
|
|
6afe8d65d2 | ||
|
|
45dc3b8243 | ||
|
|
57da6163f6 | ||
|
|
1c7b79308a | ||
|
|
2d2299c1c7 | ||
|
|
b468bcd592 | ||
|
|
474d01e9e5 | ||
|
|
c9a64a8635 | ||
|
|
a50a18325c | ||
|
|
540e6ffb16 | ||
|
|
2b0320c64f | ||
|
|
51701f2fb9 | ||
|
|
5bac14c42e | ||
|
|
7420757a62 | ||
|
|
71f2189d84 | ||
|
|
7e447c8613 | ||
|
|
6768e22008 | ||
|
|
50a7fcc804 | ||
|
|
4a52bc9db3 | ||
|
|
e4f63aef69 | ||
|
|
ca5867eb5c | ||
|
|
d9fae9a6f0 | ||
|
|
095d602186 |
No files matched your search
+137
-11
@@ -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
|
||||
@@ -22,3 +22,6 @@
|
||||
|
||||
# Third-party reference checkouts (radare2, il2cpp dumper, etc.)
|
||||
/third_party/
|
||||
|
||||
# r2pm binary packages
|
||||
/r2unity-*.zip
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+4
-20
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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, §ion_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;
|
||||
}
|
||||
@@ -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, §ion_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;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -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, §ion_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
@@ -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
@@ -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
|
||||
? ¶meters[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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+870
-84
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user