#!/usr/bin/env python3 """Cross-build search for a reachable ARM-mode pivot via the LSB-zero return trick. The .77 chain (Addenda 139-145) failed on ONE accident of compiler layout: no call site that can be live at a host-aimed store returns into a 256-byte block whose ARM decode is a usable pivot. Both halves of that are per-build accidents, and LOAD_TA lets us choose the container. So sweep every GCP build and ask the joint question directly: is there a build with a host-aimed STORE in function F, such that some frame live at that store (F itself, or one of F's ancestors) carries a saved LR whose &~0xFF block ARM-decodes to an instruction loading PC (ideally PC and SP) from a base register that is not sp/pc? Bodies in psp_out/versions_win are mapped at 0x400100; the 0x400000 image is 0x100 zero bytes plus the body. The body header gives the code region exactly: u32 at +0x04 = code lo, +0x08 = code hi. Candidate-generating, not proof: register-offset stores are matched by SHAPE, and ARM-mode decodes of Thumb are unintended. Every hit must be hand-verified (the .77 run produced five phantom stores from an ARM veneer's literal pool). Usage: python3 build_sweep_pivot.py [--sled 24] [--builds glob] """ import argparse, bisect, collections, glob, os, struct from capstone import * from capstone.arm import * HERE = os.path.dirname(os.path.abspath(__file__)) VERS = os.path.join(HERE, '..', '..', 'psp_out', 'versions_win') BASE = 0x400000 FLAGS = dict(N=0, Z=1, C=1, V=0) # measured hijack-entry flags, for reference only ST = (ARM_INS_STR, ARM_INS_STRB, ARM_INS_STRH, ARM_INS_STRD) CALLS = (ARM_INS_BL, ARM_INS_BLX) TERM_POP = (ARM_INS_POP,) def cond_taken(cc, f): N, Z, C, V = f['N'], f['Z'], f['C'], f['V'] return {ARM_CC_EQ: Z == 1, ARM_CC_NE: Z == 0, ARM_CC_HS: C == 1, ARM_CC_LO: C == 0, ARM_CC_MI: N == 1, ARM_CC_PL: N == 0, ARM_CC_VS: V == 1, ARM_CC_VC: V == 0, ARM_CC_HI: C == 1 and Z == 0, ARM_CC_LS: C == 0 or Z == 1, ARM_CC_GE: N == V, ARM_CC_LT: N != V, ARM_CC_GT: Z == 0 and N == V, ARM_CC_LE: Z == 1 or N != V, ARM_CC_AL: True, ARM_CC_INVALID: True}.get(cc, True) LDM_IDS = (ARM_INS_LDM, ARM_INS_LDMDA, ARM_INS_LDMDB, ARM_INS_LDMIB, ARM_INS_POP) def pc_sp_source(insn): """-> (writes_pc, writes_sp, base_reg) for an instruction that loads PC and/or SP""" _, w = insn.regs_access() wpc, wsp = ARM_REG_PC in w, ARM_REG_SP in w if not (wpc or wsp): return False, False, None base = None if insn.id in LDM_IDS: ops = insn.operands if ops and ops[0].type == ARM_OP_REG and (ops[0].access & CS_AC_READ): base = ops[0].reg elif insn.id == ARM_INS_POP: base = ARM_REG_SP else: for op in insn.operands: if op.type == ARM_OP_MEM: base = op.mem.base return wpc, wsp, base def load_build(path): body = open(path, 'rb').read() lo, hi = struct.unpack_from(' len(img): break for idx, insn in enumerate(md.disasm(img[off:off + 4 * sled], entry)): wpc, wsp, base = pc_sp_source(insn) if wpc and base not in (None, ARM_REG_SP, ARM_REG_PC): out[entry] = (idx, insn.reg_name(base), wsp, insn.cc, "%s %s" % (insn.mnemonic, insn.op_str)) break if wpc or wsp: break if insn.id in (ARM_INS_B, ARM_INS_BX, ARM_INS_BL, ARM_INS_BLX): break return out def scan_funcs(img, lo, hi): """BL/BLX-imm edges, function starts, and register-offset stores per function""" md = Cs(CS_ARCH_ARM, CS_MODE_THUMB); md.detail = True edges = [] for pc in range(lo, hi, 2): g = list(md.disasm(img[pc - BASE:pc - BASE + 4], pc, count=1)) if g and g[0].id in CALLS and g[0].size == 4: for op in g[0].operands: if op.type == ARM_OP_IMM and lo <= op.imm < hi: edges.append((pc, pc + 4, op.imm)) fs = sorted(set(t for _, _, t in edges)) stores = collections.defaultdict(list) for f in fs: # sequential decode from the prologue to a natural terminator for insn in md.disasm(img[f - BASE:min(f + 0x600, hi) - BASE], f): if insn.id in TERM_POP and 'pc' in insn.op_str: break if insn.id == ARM_INS_BX and 'lr' in insn.op_str: break if insn.id in ST: for op in insn.operands: if op.type == ARM_OP_MEM and op.mem.index != 0: stores[f].append((insn.address, "%s %s" % (insn.mnemonic, insn.op_str))) return edges, fs, stores def main(): ap = argparse.ArgumentParser() ap.add_argument('--sled', type=int, default=24) ap.add_argument('--builds', default='ta_22-0-*.bin') args = ap.parse_args() paths = sorted(glob.glob(os.path.join(VERS, args.builds))) print("sweeping %d builds\n" % len(paths)) grand = [] for path in paths: name = os.path.basename(path) img, lo, hi = load_build(path) pivots = find_pivots(img, lo, hi, args.sled) edges, fs, stores = scan_funcs(img, lo, hi) def encl(s): k = bisect.bisect_right(fs, s) - 1 return fs[k] if k >= 0 else None callers = collections.defaultdict(list) for s, r, t in edges: callers[t].append((encl(s), s, r)) def ancestors(fn, d=0, seen=()): if d > 8 or fn in seen: return set() o = set() for f, s, r in callers.get(fn, ()): o.add((f, s, r)); o |= ancestors(f, d + 1, seen + (fn,)) return o hits = [] for f, sts in stores.items(): live = ancestors(f) for af, s, r in live: blk = r & ~0xFF if blk in pivots: hits.append((f, sts[0], blk, r, pivots[blk])) print("%-28s code[%06x,%06x) pivots=%-3d fns=%-4d store-fns=%-4d HITS=%d" % (name, lo, hi, len(pivots), len(fs), len(stores), len(hits))) for f, st, blk, r, piv in hits[:12]: idx, base, wsp, cc, txt = piv print(" store fn 0x%06x @0x%06x %-24s | live LR 0x%06x -> blk 0x%06x" % (f, st[0], st[1], r, blk)) print(" pivot idx=%-2d base=%-4s sp=%-3s %s" % (idx, base, 'yes' if wsp else 'no', txt)) grand += [(name,) + h for h in hits] print("\n==== %d candidate (build, store-fn, live-LR, pivot) triples ====" % len(grand)) if not grand: print("NONE across all builds: the .77 ceiling is not a build accident.") if __name__ == '__main__': main()