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.
213 lines
8.3 KiB
Python
213 lines
8.3 KiB
Python
#!/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())
|