#!/usr/bin/env python3 """What can each TA ask the PSP kernel / SYS_DRV to do? (`SVC` surface per TA) This is the analysis that decided reachability for ASD: an 89-site scan of `SVC 0xF2` call sites across the four known TAs showed every service opcode is an immediate and no TA forwards a host-controlled opcode. The twelve newly-hittable TAs have never been scanned, and they are host-invokable, so their privilege reach matters. For each body: * every `svc #N` site, grouped by N (0xF2 = service RPC, others = kernel calls); * for `svc 0xF2`, the service id (r0) and opcode, recovered by walking backwards for the immediate that last wrote r0/r1 — and **flagging any site where the opcode is not an immediate**, which is the dangerous shape (a TA forwarding a caller-chosen opcode into SYS_DRV). usage: ta_svc_scan.py [...] """ import argparse import collections import os import re import struct import sys import capstone as cs BASE = 0x400100 MT = cs.Cs(cs.CS_ARCH_ARM, cs.CS_MODE_THUMB) def sweep(b, start=0x401000): out, off = [], start - BASE while off < len(b) - 2: got = None for i in MT.disasm(b[off:off + 4], BASE + off): got = i break if got: out.append(got) off += got.size else: off += 2 return out def last_imm(ins, n, reg, depth=18): """Nearest preceding immediate write to `reg`; None if it came from memory/register.""" for k in range(n - 1, max(0, n - depth), -1): i = ins[k] dst = i.op_str.split(',')[0].strip() if dst != reg: continue if i.mnemonic in ('mov', 'movs', 'movw', 'mov.w') and '#' in i.op_str: try: return int(i.op_str.split('#')[1], 0) except ValueError: return None return None # written from something that is not an immediate return None def stored_opcode(ins, n, depth=26): """The service opcode is not in a register: GUARD-INIT-CHAIN.md shows the ABI is `req[0] = opcode; r0 = service id; r1 = &req; svc 0xF2`. So recover the immediate most recently stored at offset 0 of some buffer before the call.""" imm = {} for k in range(max(0, n - depth), n): i = ins[k] parts = [x.strip() for x in i.op_str.split(',')] if i.mnemonic in ('mov', 'movs', 'movw', 'mov.w') and '#' in i.op_str: try: imm[parts[0]] = int(i.op_str.split('#')[1], 0) except ValueError: pass if i.mnemonic.startswith('str') and len(parts) >= 2: mem = ','.join(parts[1:]) if re.fullmatch(r'\[\w+\]', mem) or re.fullmatch(r'\[\w+, #0\]', mem): if parts[0] in imm: return imm[parts[0]] return None def scan(path): b = open(path, 'rb').read() ins = sweep(b) svcs = collections.Counter() rpc = collections.Counter() nonimm = [] for n, i in enumerate(ins): if i.mnemonic != 'svc': continue try: num = int(i.op_str.strip('#'), 0) except ValueError: continue svcs[num] += 1 if num == 0xF2: svc_id = last_imm(ins, n, 'r0') op = stored_opcode(ins, n) if op is None: nonimm.append(i.address) rpc[(svc_id, op)] += 1 return svcs, rpc, nonimm, len(ins) def main(): ap = argparse.ArgumentParser() ap.add_argument('bodies', nargs='+') a = ap.parse_args() for p in a.bodies: svcs, rpc, nonimm, n = scan(p) print('\n=== %s (%d insns)' % (os.path.basename(p), n)) print(' svc numbers: %s' % ', '.join('%#x x%d' % (k, v) for k, v in sorted(svcs.items()))) if rpc: print(' svc 0xF2 sites: %d' % sum(rpc.values())) for (sid, op), c in sorted(rpc.items(), key=lambda kv: (str(kv[0][0]), str(kv[0][1]))): print(' service=%-6s opcode=%-8s x%d' % (sid if sid is None else hex(sid), op if op is None else hex(op), c)) print(' sites whose OPCODE is not an immediate: %d %s' % (len(nonimm), [hex(x) for x in nonimm[:8]])) return 0 if __name__ == '__main__': raise SystemExit(main())