aboutsummaryrefslogtreecommitdiffstats
path: root/fw/gadgets/build_sweep_pivot.py
blob: bcd0092c658823aa22f0647f0f907cf309ba5c5e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#!/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('<II', body, 4)
    img = b'\x00' * 0x100 + body
    return img, lo, hi


def find_pivots(img, lo, hi, sled):
    """256-aligned ARM-mode entries whose sled reaches a PC load off a non-stack base"""
    md = Cs(CS_ARCH_ARM, CS_MODE_ARM); md.detail = True
    out = {}
    for entry in range(lo, hi, 0x100):
        off = entry - BASE
        if off + 4 * sled > 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()