Add Dart AOT depth (reference + 3 scripts); fix encoding rule and add P26

New reference:
- dart-aot.md: pinning the Dart version, building a matching decompiler, the object-pool
  model and reference indexes, register/boolean conventions, the three assembly signatures
  that identify most business logic, the locating workflow, patch strategy, and the traps
  specific to this layer (including ambiguous identifiers that resolve to protocol fields).

New scripts (parameterized, no machine-specific paths):
- dart_pprefs.py: object-pool offset -> code-site index, decoded arithmetically. Avoids the
  full-capstone pass that costs minutes and 1-2 GB and reads as a hang.
- dart_pool_strings.py: framed literal recovery with file offsets. Defaults to one-byte
  because automatic two-byte (UTF-16) detection is unreliable; --find searches both
  encodings and is the reliable way to locate a known literal.
- dart_disasm.py: annotated windowed disassembly (pool + boolean annotations) and a B/BL
  caller index that needs no disassembler.

Fixes and integration:
- framework-runtimes.md: point the Dart depth at dart-aot.md, and make the string-encoding
  rule precise (ASCII is one-byte/UTF-8; CJK is UTF-16LE) instead of leaving it as 'try both'.
- pitfalls.md: P26 -- self-built tooling that fails in a way that reads as a target finding
  (silent arithmetic, helper shadowing a stdlib module, measuring against an incomplete
  reference, and a knob that does not move the metric).
- SKILL.md / README.md: index the new reference and scripts.

Validated by running all three scripts against a real Dart AOT sample: the pool-ref index
agrees with an independently built index to 99.2%, and the literal extractor's one-byte
output was verified string-by-string against a decompiler's pool listing.
This commit is contained in:
inliver233
2026-09-19 22:19:55 +08:00
parent 46b0858efb
commit cf636c4263
8 changed files with 970 additions and 4 deletions
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Annotated, windowed disassembly of a Dart AOT snapshot, plus a caller index.
WHY THIS EXISTS
---------------
Two things make Dart AOT code readable at all:
1. **Pool annotations.** A raw `ldr x2, [x27, #0x9d0]` is meaningless; annotated with the
pool entry it becomes `"vipflag"` and the surrounding code explains itself. This tool
resolves those loads and prints what they point at, including how many other sites
reference the same entry.
2. **Windowed disassembly.** Decoding an entire multi-MB `.text` in one pass is how a host
gets pinned at 1-2 GB and the run looks like a hang. Disassemble only the ranges you
care about -- that is what this tool does, and it is not a limitation but the method.
Booleans are annotated too: Dart materialises `true`/`false` as small offsets from the null
register, so `add x0, x22, #0x20` is marked TRUE and `#0x30` is marked FALSE. Those are the
cheapest patch sites in the whole snapshot.
The caller index answers the other half of every question -- "what calls this function".
It is built from B/BL target arithmetic on the raw words, so it needs no disassembler at all
and finishes in seconds.
USAGE
-----
# a window around one address (default: 0x180 before, 0x220 after)
python dart_disasm.py libapp.so --pp pp.txt --refs pp_refs.json 0x26e390
# an explicit range
python dart_disasm.py libapp.so --pp pp.txt --refs pp_refs.json --range 0x64f310 0x64f3e0
# widen / narrow the window
python dart_disasm.py libapp.so --refs pp_refs.json 0x64f1bc --before 0x80 --after 0x40
# caller index
python dart_disasm.py libapp.so --build-index callers.json
python dart_disasm.py libapp.so --index callers.json 0x26e230 0x64f0f0
INPUTS
------
* `--pp` is the decompiler's pool listing (blutter `pp.txt`): lines shaped
`[pp+0x9d0] String: "vipflag"`. Without it, loads are annotated with the raw offset only.
* `--refs` is the pool-offset -> code-site index built by `dart_pprefs.py`.
Requires `capstone` for disassembly; the caller index works without it.
"""
import argparse
import json
import os
import re
import struct
import sys
POOL_REG = 27
BOOL_TRUE, BOOL_FALSE = 0x20, 0x30
PP_LINE = re.compile(r'^\[(pp\+0x[0-9a-f]+)\](.*)$')
def text_section(path):
blob = open(path, 'rb').read()
if blob[:4] != b'\x7fELF':
raise SystemExit('not an ELF file: %s' % path)
if blob[4] != 2:
raise SystemExit('not ELF64: %s' % path)
e_shoff = struct.unpack_from('<Q', blob, 0x28)[0]
e_shentsize = struct.unpack_from('<H', blob, 0x3A)[0]
e_shnum = struct.unpack_from('<H', blob, 0x3C)[0]
e_shstrndx = struct.unpack_from('<H', blob, 0x3E)[0]
def sh(i):
return struct.unpack_from('<IIQQQQ', blob, e_shoff + i * e_shentsize)
_, _, _, _, shstr_off, shstr_size = sh(e_shstrndx)
strs = blob[shstr_off:shstr_off + shstr_size]
for i in range(e_shnum):
name, _t, _f, addr, off, size = sh(i)
end = strs.find(b'\x00', name)
if strs[name:end].decode('ascii', 'replace') == '.text':
return addr, blob[off:off + size]
raise SystemExit('.text not found')
def load_pp(path):
pool = {}
if not path or not os.path.exists(path):
return pool
for line in open(path, encoding='utf-8', errors='replace'):
m = PP_LINE.match(line.rstrip('\n'))
if m:
pool[int(m.group(1)[5:], 16)] = m.group(2).strip()[:120]
return pool
def load_refs(path):
if not path or not os.path.exists(path):
return {}
return {int(k, 16): v for k, v in json.load(open(path, encoding='utf-8')).items()}
def disasm_window(sample, addr, before, after, pp, refs):
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
text_addr, blob = text_section(sample)
end_of_text = text_addr + len(blob)
start = max(text_addr, addr - before)
stop = min(end_of_text, addr + after)
md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
md.skipdata = True # without this, one undecodable byte silently ends the range
md.detail = True # without this, operands are unavailable
print('range 0x%x..0x%x focus=0x%x' % (start, stop, addr))
pending = {}
for ins in md.disasm(blob[start - text_addr:stop - text_addr], start):
if ins.id == 0: # SKIPDATA pseudo-instruction: no operands
print('%x: %s' % (ins.address, ins.mnemonic))
continue
note = ''
ops = ins.operands
rn = ins.reg_name
if (ins.mnemonic == 'add' and len(ops) == 3 and ops[1].type == 1
and rn(ops[1].reg) == 'x27' and ops[2].type == 2
and ops[2].shift.value == 12):
pending[rn(ops[0].reg)] = ops[2].imm << 12
elif (ins.mnemonic == 'add' and len(ops) == 3 and ops[1].type == 1
and rn(ops[1].reg) == 'x22' and ops[2].type == 2):
note = {BOOL_TRUE: ' ; TRUE', BOOL_FALSE: ' ; FALSE'}.get(ops[2].imm, '')
elif ins.mnemonic in ('ldr', 'ldur') and len(ops) == 2 and ops[1].type == 3:
base = rn(ops[1].mem.base) if ops[1].mem.base else ''
off = None
if base == 'x27':
off = ops[1].mem.disp
elif base in pending:
off = pending.pop(base) + ops[1].mem.disp
if off is not None:
text = pp.get(off, '')
sites = refs.get(off, [])
note = ' ; pp+0x%x %s [%d refs]' % (off, text, len(sites))
mark = ' <<<' if abs(ins.address - addr) < 4 else ''
print('%x: %-8s %s%s%s' % (ins.address, ins.mnemonic, ins.op_str, note, mark))
def build_index(sample, out):
"""B/BL target -> call sites, decoded arithmetically (no disassembler needed)."""
text_addr, blob = text_section(sample)
index = {}
words = struct.unpack_from('<%dI' % (len(blob) // 4), blob, 0)
for i, w in enumerate(words):
if w & 0xFC000000 in (0x14000000, 0x94000000):
imm = w & 0x03FFFFFF
if imm & 0x02000000:
imm -= 0x04000000
site = text_addr + i * 4
index.setdefault(site + (imm << 2), []).append(site)
with open(out, 'w', encoding='utf-8') as fh:
json.dump({hex(k): v for k, v in index.items()}, fh)
print('distinct targets : %d' % len(index))
print('call sites : %d' % sum(len(v) for v in index.values()))
print('written %s' % out)
def lookup_index(sample, path, addrs):
index = {int(k, 16): v for k, v in json.load(open(path, encoding='utf-8')).items()}
for spec in addrs:
target = int(spec, 16)
sites = sorted(index.get(target, []))
print('0x%x: %d caller(s)' % (target, len(sites)))
for s in sites[:80]:
print(' 0x%x' % s)
if len(sites) > 80:
print(' ... %d more' % (len(sites) - 80))
def main():
ap = argparse.ArgumentParser(
description=__doc__.split('\n')[0],
formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
ap.add_argument('sample', help='libapp.so')
ap.add_argument('addr', nargs='*', help='hex address(es) to disassemble around')
ap.add_argument('--pp', metavar='PP_TXT', help="decompiler pool listing (blutter pp.txt)")
ap.add_argument('--refs', metavar='REFS_JSON', help='index from dart_pprefs.py')
ap.add_argument('--range', nargs=2, metavar=('START', 'END'))
ap.add_argument('--before', default='0x180')
ap.add_argument('--after', default='0x220')
ap.add_argument('--build-index', metavar='OUT_JSON')
ap.add_argument('--index', metavar='CALLERS_JSON')
a = ap.parse_args()
if a.build_index:
return build_index(a.sample, a.build_index)
if a.index:
if not a.addr:
raise SystemExit('give at least one hex address to look up')
return lookup_index(a.sample, a.index, a.addr)
pp = load_pp(a.pp)
refs = load_refs(a.refs)
if a.range:
start, stop = int(a.range[0], 16), int(a.range[1], 16)
disasm_window(a.sample, start, 0, stop - start, pp, refs)
return 0
if not a.addr:
raise SystemExit('give an address, --range, --build-index, or --index')
for spec in a.addr:
disasm_window(a.sample, int(spec, 16),
int(a.before, 0), int(a.after, 0), pp, refs)
return 0
if __name__ == '__main__':
sys.exit(main())