mirror of
https://github.com/radareorg/r2flutter.git
synced 2026-11-07 06:26:00 -10:00
Locate Dart instruction images without symbols on stripped Mach-O
The four kDart*Snapshot* symbols are external/global and load-bearing (the engine resolves them at runtime), so they survive strip; but obfuscators/repackagers can remove them, leaving only the two data snapshots findable by magic scan (instruction images carry no magic) and iso_instr=0, which disables IT name recovery. Add locate_instr_images_structural(): vm_instr = start of the first executable section; read the Dart Image header (word0=ImageSize) and chain iso_instr = align_up(vm_instr + ImageSize, 64). Every step is cross-checked (plausible headers; the isolate image must end where the first data image begins), so an unknown/gapped layout degrades to 0 (no IT names) rather than junk addresses. No-op when symbols already resolved the instructions. Verified against symbol-derived ground truth: Runner (0x4000/0xe700, full 7671 IT entries recovered) and AuthPass (0x5a80/0xfdc0); the gapped macOS hello.aot safely falls back to 0. Symbol-present bins unchanged; suite green (95 db/cmd, 3 extras, 35 custom). Adds scripts/strip_macho_symbols.py (zeroes LC_SYMTAB + dyld export trie, since strip cannot) and test/db/cmd/discovery-stripped-ios which builds the fixture from the committed Runner App and asserts structural recovery. Remaining follow-ups tracked in COMMIT2.md. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
cd6b655c0c
commit
986e2cb42a
4 files changed
+160
-77
No files matched your search
-77
@@ -1,77 +0,0 @@
|
||||
# COMMIT 2 — Locate Dart instruction images without symbols (stripped Mach-O)
|
||||
|
||||
Follow-up to commit 1 (`Harden Dart snapshot discovery...`). Commit 1 removed the
|
||||
junk-`iso_instr` risk and hardened `find_snapshots`, but on a genuinely
|
||||
symbol-less binary it deliberately leaves `vm_instr`/`iso_instr = 0` (instruction
|
||||
images carry no magic, so the scan cannot find them). This file records what
|
||||
commit 2 must do and everything we learned so we don't re-derive it.
|
||||
|
||||
## Goal
|
||||
When the four `kDart*Snapshot*` symbols are absent, still recover `vm_instr` and
|
||||
`iso_instr` structurally so InstructionTable-based name recovery works (it is
|
||||
gated behind `if (!ctx->iso_instr) return 0;` in `dart_pool_parse.c`).
|
||||
|
||||
## Key findings from the commit-1 investigation (don't redo these)
|
||||
- **Only data snapshots carry the magic `0xf5f5dcdc`.** Instruction images do
|
||||
not, so magic-scanning can only ever locate `vm_data` / `iso_data`. Confirmed:
|
||||
`test/bins/ios/Runner.app/.../App` has exactly 2 magic hits (both data).
|
||||
- **Layout (Runner.app, ground truth from the symbol table):**
|
||||
- `_kDartVmSnapshotInstructions` @ `0x4000` — start of `__TEXT.__text` (`r-x`)
|
||||
- `_kDartIsolateSnapshotInstructions` @ `0xe700` — inside `__TEXT.__text`
|
||||
- `_kDartVmSnapshotData` @ `0x1b0900` — start of `__TEXT.__const`
|
||||
- `_kDartIsolateSnapshotData` @ `0x1b9600` — inside `__TEXT.__const`
|
||||
So instruction images live consecutively at the start of the first executable
|
||||
section: `[VmInstructions][IsolateInstructions]`.
|
||||
- **The Dart snapshot symbols are load-bearing and survive `strip`.** They are
|
||||
external/global (the Flutter engine looks them up at runtime), so `llvm-strip`,
|
||||
`llvm-strip -x`, and `strip` all leave them in place (`strip` even returns rc=1).
|
||||
Consequence: a *functioning* Flutter app almost always keeps these symbols, so
|
||||
the symbol path already works for normal apps. The symbol-less case is an
|
||||
**edge case** (obfuscators, repackagers, hand-stripped LC_SYMTAB), which is why
|
||||
this is a lower-frequency follow-up, not urgent.
|
||||
|
||||
## Proposed approach
|
||||
1. `vm_instr` = start (`vaddr`) of the first executable (`perm & R_PERM_X`) section
|
||||
that is not a segment. On the samples this is `__TEXT.__text`.
|
||||
2. Read the Dart **instructions-image header** at `vm_instr` to get the VM
|
||||
instructions payload length, then `iso_instr = vm_instr + align_up(len, alignment)`.
|
||||
- Need to nail the instructions-image header format (`Image::kHeaderSize` /
|
||||
the leading length/size field) from `third_party/dart-sdk/runtime/vm/image_snapshot.{h,cc}`.
|
||||
Cross-check the computed `iso_instr` against the ground-truth `0x4000`→`0xe700`
|
||||
gap for Runner (i.e. VM-instructions length ≈ `0xe700 - 0x4000 = 0xa700`).
|
||||
- Fallback if the header can't be parsed: leave `iso_instr = 0` (current safe
|
||||
behavior) rather than guess.
|
||||
3. Only run this when `vm_instr`/`iso_instr` are still 0 after symbols + data scan,
|
||||
so it never overrides symbol-derived values.
|
||||
|
||||
## Test / fixture strategy (the hard part)
|
||||
- `strip`/`llvm-strip` do NOT remove the global Dart symbols, so we cannot make a
|
||||
fixture with the CLI strip tools. Options, in order of preference:
|
||||
1. Obtain a **real** obfuscated/stripped Flutter iOS `App` where the symbols are
|
||||
actually gone (best signal). **TODO: collect more iOS Flutter binaries** —
|
||||
especially release/obfuscated ones — into `test/bins/ios/` for coverage.
|
||||
2. Craft a fixture by rewriting a copy of an existing `App` to drop the four
|
||||
symbols from `LC_SYMTAB` (e.g. an r2 script that zeroes/removes the nlist
|
||||
entries), keeping the snapshot bytes intact.
|
||||
- Regression assertion: on the symbol-less fixture, `-H -j` yields
|
||||
`vm_data`/`iso_data` from the scan AND `vm_instr=0x4000` / `iso_instr=0xe700`
|
||||
from structural location, and `-i`/`-f` recover IT entries/names.
|
||||
- Also add a scan-path assertion for commit 1 itself once a fixture exists:
|
||||
data found, `instr` cleanly 0 (no junk), no crash.
|
||||
|
||||
## Also deferred here (small, separate robustness items from the audit §3.1)
|
||||
- **Single-data-snapshot aliasing** (`pick_vm_iso_by_size`, count==1 sets
|
||||
`vm_data == iso_data`). Visible on the synthetic bin (`vm_data==iso_data==4`).
|
||||
The `sbom-synthetic` golden currently *encodes* this (`"vm_data":4,"iso_data":4`).
|
||||
Fixing it (assign the lone snapshot to `iso_data`, leave `vm_data=0`) requires
|
||||
updating that golden — do it alongside commit 2 and decide the correct semantics.
|
||||
- Raw-file fallback scan (`find_snapshots`, the `r_io_size` branch) conflates
|
||||
physical offsets with IO addresses when a bin is loaded with maps but has
|
||||
zero/unnamed sections; only fully correct under `r2 -n`. Low priority.
|
||||
|
||||
## Files likely touched
|
||||
- `src/lib/dart_pool_discovery.c` — structural instruction-image location.
|
||||
- possibly a small helper to read the instructions-image header (new, or in
|
||||
`dart_pool_snapshot.c`).
|
||||
- `test/bins/ios/...` — new stripped/obfuscated fixture(s).
|
||||
- `test/db/cmd/` — new discovery test(s).
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write a copy of a Mach-O with its symbol table and dyld export trie removed.
|
||||
|
||||
Used to build symbol-less test fixtures: the four kDart*Snapshot* symbols are
|
||||
external/global and load-bearing (the Flutter engine looks them up at runtime),
|
||||
so strip(1)/llvm-strip cannot remove them. This neutralises LC_SYMTAB (nsyms=0),
|
||||
LC_DYLD_INFO[_ONLY] (export_off/size=0) and LC_DYLD_EXPORTS_TRIE (datasize=0) in
|
||||
every arm64 slice (or the thin Mach-O), which makes r2 report no symbols while
|
||||
leaving the snapshot bytes intact. This exercises r2flutter's symbol-less
|
||||
discovery path (structural instruction-image location).
|
||||
|
||||
Usage: strip_macho_symbols.py <input> <output>
|
||||
"""
|
||||
import struct, shutil, sys
|
||||
|
||||
CPU_ARM64 = 0x0100000C
|
||||
|
||||
|
||||
def patch_macho(d, base):
|
||||
is64 = struct.unpack_from('<I', d, base)[0] == 0xfeedfacf
|
||||
ncmds = struct.unpack_from('<I', d, base + 16)[0]
|
||||
off = base + (32 if is64 else 28)
|
||||
acted = False
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from('<II', d, off)
|
||||
c = cmd & ~0x80000000
|
||||
if cmd == 0x2: # LC_SYMTAB -> nsyms = 0
|
||||
struct.pack_into('<I', d, off + 12, 0)
|
||||
acted = True
|
||||
elif c == 0x22: # LC_DYLD_INFO[_ONLY] -> export_off/size = 0
|
||||
struct.pack_into('<II', d, off + 40, 0, 0)
|
||||
acted = True
|
||||
elif c == 0x33: # LC_DYLD_EXPORTS_TRIE -> datasize = 0
|
||||
struct.pack_into('<I', d, off + 12, 0)
|
||||
acted = True
|
||||
off += cmdsize
|
||||
return acted
|
||||
|
||||
|
||||
def main(src, dst):
|
||||
shutil.copy(src, dst)
|
||||
d = bytearray(open(dst, 'rb').read())
|
||||
magic = struct.unpack_from('>I', d, 0)[0]
|
||||
ok = False
|
||||
if magic in (0xcafebabe, 0xcafebabf): # fat
|
||||
b64 = magic == 0xcafebabf
|
||||
nfat = struct.unpack_from('>I', d, 4)[0]
|
||||
ao = 8
|
||||
for _ in range(nfat):
|
||||
if b64:
|
||||
ct, _cs, offset, _sz, _al, _res = struct.unpack_from('>IIQQII', d, ao)
|
||||
ao += 32
|
||||
else:
|
||||
ct, _cs, offset, _sz, _al = struct.unpack_from('>IIIII', d, ao)
|
||||
ao += 20
|
||||
if ct == CPU_ARM64:
|
||||
ok = patch_macho(d, offset) or ok
|
||||
else:
|
||||
ok = patch_macho(d, 0)
|
||||
if not ok:
|
||||
print("no symbol/export commands found to strip", file=sys.stderr)
|
||||
return 1
|
||||
open(dst, 'wb').write(d)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print(__doc__)
|
||||
sys.exit(2)
|
||||
sys.exit(main(sys.argv[1], sys.argv[2]))
|
||||
@@ -98,6 +98,82 @@ static int collect_snapshot_magics_in_range(DartCtx *ctx, ut64 start, ut64 size,
|
||||
return found_cnt;
|
||||
}
|
||||
|
||||
// A Dart Image starts with a kObjectStartAlignment (64)-byte header whose first
|
||||
// host word is ImageSize (header + payload) and second is
|
||||
// InstructionsSectionOffset. Bare-instructions AOT lays the two instruction
|
||||
// images consecutively at the start of the first executable section:
|
||||
// [VmInstructions][IsolateInstructions], each padded up to this alignment.
|
||||
#define DART_IMAGE_ALIGN 64
|
||||
#define DART_IMAGE_HEADER_MIN 64
|
||||
|
||||
static ut64 dart_align_up(ut64 v, ut64 a) {
|
||||
return (v + (a - 1)) & ~(a - 1);
|
||||
}
|
||||
|
||||
// Read an Image header at addr and return its ImageSize in *size_out, true only
|
||||
// if it is a plausible instructions image: it fits before the first data image
|
||||
// and its InstructionsSection offset (word 1) is a small nonzero aligned value
|
||||
// inside the image.
|
||||
static bool dart_image_at(DartCtx *ctx, ut64 addr, ut64 data_lo, ut64 *size_out) {
|
||||
ut8 hdr[16];
|
||||
if (!read_mem (ctx, addr, hdr, sizeof (hdr))) {
|
||||
return false;
|
||||
}
|
||||
ut64 size = r_read_le64 (hdr), isoff = r_read_le64 (hdr + 8);
|
||||
*size_out = size;
|
||||
return size >= DART_IMAGE_HEADER_MIN && addr + size <= data_lo &&
|
||||
isoff >= 8 && (isoff & 7) == 0 && isoff < size && isoff < 0x10000;
|
||||
}
|
||||
|
||||
// When the four kDart* symbols are stripped, the magic scan can only find the
|
||||
// data snapshots (instruction images carry no magic). Recover vm_instr/iso_instr
|
||||
// structurally: vm_instr is the start of the first executable section, and the
|
||||
// isolate instructions image follows the VM one and ends where the first data
|
||||
// image begins. Every step is cross-checked; if anything is off we leave the
|
||||
// addresses at 0, so a wrong/unknown layout degrades to "no IT names" rather
|
||||
// than junk addresses. No-op when symbols already resolved the instructions.
|
||||
static void locate_instr_images_structural(DartCtx *ctx) {
|
||||
if (!ctx || !ctx->core || !ctx->core->bin || ctx->vm_instr || ctx->iso_instr) {
|
||||
return;
|
||||
}
|
||||
ut64 data_lo = ctx->vm_data && ctx->iso_data
|
||||
? R_MIN (ctx->vm_data, ctx->iso_data)
|
||||
: (ctx->vm_data? ctx->vm_data: ctx->iso_data);
|
||||
if (!data_lo) {
|
||||
return;
|
||||
}
|
||||
RVecRBinSection *sections = r_bin_get_sections_vec (ctx->core->bin);
|
||||
if (!sections) {
|
||||
return;
|
||||
}
|
||||
ut64 vm_instr = 0, sz1 = 0, sz2 = 0;
|
||||
RBinSection *sec;
|
||||
R_VEC_FOREACH (sections, sec) {
|
||||
if (sec && !sec->is_segment && sec->vaddr && sec->vsize && (sec->perm & R_PERM_X)) {
|
||||
vm_instr = rebase_bin_addr (ctx, sec->vaddr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!vm_instr || vm_instr >= data_lo || !dart_image_at (ctx, vm_instr, data_lo, &sz1)) {
|
||||
return;
|
||||
}
|
||||
ut64 iso_instr = dart_align_up (vm_instr + sz1, DART_IMAGE_ALIGN);
|
||||
if (iso_instr <= vm_instr || iso_instr >= data_lo || !dart_image_at (ctx, iso_instr, data_lo, &sz2)) {
|
||||
return;
|
||||
}
|
||||
// The isolate instructions image is the last text image: it must end right
|
||||
// where the first data image begins (within a page of alignment slack).
|
||||
ut64 iso_end = dart_align_up (iso_instr + sz2, DART_IMAGE_ALIGN);
|
||||
if (iso_end > data_lo || data_lo - iso_end > 0x4000) {
|
||||
return;
|
||||
}
|
||||
ctx->vm_instr = vm_instr;
|
||||
ctx->iso_instr = iso_instr;
|
||||
if (ctx->verbose > 0) {
|
||||
fprintf (stderr, "[r2flutter] located instruction images structurally: vm_instr=0x%" PFMT64x " iso_instr=0x%" PFMT64x "\n", (ut64)vm_instr, (ut64)iso_instr);
|
||||
}
|
||||
}
|
||||
|
||||
int find_snapshots(DartCtx *ctx) {
|
||||
if (!ctx || !ctx->core) {
|
||||
return -1;
|
||||
@@ -212,6 +288,9 @@ int find_snapshots(DartCtx *ctx) {
|
||||
ctx->iso_data = scan_iso_data;
|
||||
}
|
||||
}
|
||||
// Symbols are absent (we reached the scan): try to recover the
|
||||
// instruction images from the section layout so IT name recovery works.
|
||||
locate_instr_images_structural (ctx);
|
||||
if (ctx->vm_data || ctx->iso_data || ctx->vm_instr || ctx->iso_instr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
NAME=discovery-stripped-ios-structural-instr
|
||||
FILE=-
|
||||
CMDS=<<EOF
|
||||
!!sh -c 'A=bins/ios/Runner.app/Frameworks/App.framework/App; T=$(mktemp); python3 ../scripts/strip_macho_symbols.py "$A" "$T" >/dev/null 2>&1 && printf "kdart=%s\n" "$(r2 -qc "is~kDart" "$T" 2>/dev/null | grep -c kDart)" && ../bin/r2flutter -q -H "$T" 2>/dev/null | grep -oE "vm_instr=\S+ iso_data=\S+ iso_instr=\S+"; rm -f "$T"'
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
kdart=0
|
||||
vm_instr=0x4000 iso_data=0x1b9600 iso_instr=0xe700
|
||||
EOF
|
||||
RUN
|
||||
Reference in new issue
Block a user