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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
# 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.
================================================================================
## PUSH FURTHER: full op EXECUTES, but pointer-buffer I/O does NOT marshal back
Goal: run a full transform (not just AllocateOperation) and read its output. Identified op `0x35` =
**`TEE_GenerateRandom`** (`sub_E1E184 -> sub_E04F38(out_ptr=msg[2], len=msg[3])`) -- self-contained,
no context/key, writes `len` random bytes to the output buffer. The perfect clean test.
### Hardware result (op 0x35, output buffer at window `0x410500`, len 32)
* `svc(0x35)` return = **`0x0`** (success -- the RNG executed to completion).
* But the output buffer, **pre-filled with `0xAA`, came back UNCHANGED** (`aaaaaaaa...`), i.e. the
random bytes did NOT land in our GCP-window buffer.
### Why: the confused-deputy only round-trips the INLINE 124-byte message
* sOS's `svc 0xf2` service path **bounces the request message** into SYS_DRV space; `sub_E087E4`
works on a 124-byte local copy `v8` and copies it back to the caller message on return. That is why
INLINE results round-trip and are readable -- e.g. `TEE_AllocateOperation`'s handle at `msg[14]`.
* A crypto op that writes to a POINTER buffer (`msg[2]` etc.) has the handler dereference that client
VA **in SYS_DRV's address context**. GCP's SRAM window (`0x400000-0x800000`) is not mapped there
(only the message is bounced), so the write hits SYS_DRV's own `0x410500`, never GCP's. The RNG
succeeded (status 0) writing into SYS_DRV-side memory we cannot read.
* The crypto service's buffer model assumes **shared-DRAM** client buffers (host/amdgpu ring RPC),
but the type-5 `svc 0xf2` gate **confines our buffers to GCP's SRAM window** -- which SYS_DRV cannot
access for pointer I/O. So the two are incompatible from the GCP confused-deputy.
### Net (honest)
From a ROP'd GCP TA we **can execute** any crypto operation (RNG confirmed running, status 0), and we
can **read INLINE results** (the `AllocateOperation` handle `0x10`/`0x11`, state persistence). We
**cannot** read POINTER-buffer outputs (RNG/digest/cipher data) because the confused-deputy marshals
only the inline message, and GCP-window pointer buffers are not reachable by the SYS_DRV handler. A
full data-transform with a readable output buffer would require either (a) a crypto op that returns
its result inline in the message, or (b) defeating the cross-domain buffer bounce (placing the buffer
where the SYS_DRV handler actually dereferences it) -- neither available as-is. This is the accurate
boundary of the GCP->SYS_DRV crypto surface: inline-result ops work end-to-end; buffer-I/O ops
execute but their data does not cross back.
================================================================================
## Reading crypto OUTPUT back via GCP shared memory -- principle OK, RNG demo blocked
Follow-up to the retraction: since the crypto buffer write lands in GCP's own shared memory, pointing
an op's output at GCP memory CLEAR of the ROP chain should let us read the engine output back.
* **Principle validated** by `TEE_AllocateOperation` (op 0x01): it returns the context handle `0x10`
inline, and we read it back from GCP memory via the message copy-back. That is a confirmed crypto
output read-back.
* **RNG (op 0x35) data-buffer demo: blocked.** Pointed op 0x35 output at `0x410500` (clear of the
chain), len 4 and 32, dumped `0x410500`. Result: `status = 0xffff000f` (crypto dispatcher
buffer-gate reject, `dword_E1DF48`) -- the RNG never wrote. Decisive control: a `create` baseline
(writes nothing to `0x410500`) dumped the SAME bytes `bb18bb63 aa3069a5 ...` as the RNG runs, across
3 boots.
* **=> the "identical random-looking output" is STALE GCP memory at `0x410500`, not RNG output.** The
seeded/deterministic-RNG hypothesis does not apply -- the RNG did not run. (`0x410500` reads fine
from GCP context, so it is GCP-mapped; but `SVC 0xB5` rejects it as an op-0x35 OUTPUT buffer -- the
earlier single-op `ptr_sweep` accepted `0x410500` for op 0x35 len 4 at status 0, so the gate's
accept/reject is harness/len/state-sensitive and not yet pinned.)
Open: to demo a data-crypto output read-back, resolve why `SVC 0xB5` rejects the op-0x35 output buffer
here (window/direction/len), or use an op whose output buffer the gate accepts, then dump it and
compare against a no-write baseline to prove it is engine output (not stale).
================================================================================
## CONCLUSION: confused-deputy crypto DATA-output read-back is non-deterministic (abandoned)
Tried to pin the op-0x35 (RNG) output-buffer accept rule so we could land a confirmed RNG write and
test the seeded/deterministic-RNG hypothesis. Result across many boots:
| run series | op | ptr | len | verdict (all boots) |
|-----------------------|------|------------|-----|-----------------------------|
| rnglen | 0x35 | 0x410500 | 4 | REJECT 0xffff000f |
| acceptsweep | 0x35 | 0x300..0x1000 (incl 0x410380) | 4 | ACCEPT status 0 (all) |
| rngdet | 0x35 | 0x410380 | 16 | REJECT 0xffff000f (4/4) |
| rngdet4 | 0x35 | 0x410380 | 4 | REJECT 0xffff000f (5/5) |
* The **identical** request (op 0x35, ptr=0x410380, len=4, dump=same) was ACCEPTED in the acceptsweep
boot-series and REJECTED in the rngdet4 boot-series. Same opcode/ptr/len/harness, opposite verdict.
* => `SVC 0xB5`'s validation of a confused-deputy output buffer is **run-to-run non-deterministic**
(whole boot-series correlate: a session accepts all, or rejects all). Most likely the recorded
confused-deputy window (`tcb[24]/[25]`, set by the type-5 gate `sub_22B4` from the SP/msg state at
the FE0A hijack instant) varies with boot stack/heap layout; when it happens to cover the buffer the
op writes, else it rejects.
* **Consequence:** we cannot reliably get a crypto DATA op (RNG/cipher/digest) to write a readable
buffer, so the **seeded-RNG determinism question is unanswerable via this path** -- we never obtained
a single confirmed RNG buffer write to examine. The earlier "identical output" was already disproven
as RNG output (stale GCP memory; the create baseline showed the same bytes with no write).
### What stands (reliable)
* Only crypto output we can read back deterministically is an **inline result**: `TEE_AllocateOperation`
(op 0x01) returns handle `0x10` in the message, round-tripped via the copy-back. That validates the
read-back principle but is not an engine-value oracle.
* The confused-deputy still gives a reliable **GCP->SYS_DRV DoS** (an op with a GCP-mapped but
SYS_DRV-unmapped VA data-aborts the SYS_DRV handler) -- unchanged.
Dropping the crypto-output-oracle line of work: the validation nondeterminism makes it an unreliable
oracle, not an escalation.
|