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
|
# Simulating the GCP->SYS_DRV crypto ROP message — our setup is correct; the reject is the SVC 0xB5 window
Operator question: *are we confident our ROP sets everything up correctly? can we simulate the ROP
chain and inspect the input buffers/arguments at call time?*
Answer: **yes, and yes.** Built `psp_emu/harness/gcp_crypto_argcheck.py`, which reconstructs the EXACT
124-byte request our hardware harness (`gcp_rop_crypto.py`) builds for op 0x35 (`TEE_GenerateRandom`)
and runs it through the firmware's own `sub_E087E4 -> sub_E1DC30` crypto dispatcher in the emulator,
with the real `sub_E0DEF4` / `sub_E0AB44` validators unpatched, hooking the dispatcher, the validator,
and the RNG leaf to inspect the arguments at call time.
## Our message is constructed correctly (proven, not inferred)
With the emulator's default (permissive) SVC 0xB5, the real dispatcher sees EXACTLY what we intended:
sub_E1DC30 entry: opcode=0x35 buf0(ptr=<out>,len=0x4/0x10/0x20) buf1(0,0)
sub_E0DEF4(ptr=<out>, len=..., dir=0x2, client=0x2) # validated as an OUTPUT buffer
>> sub_E04F38 REACHED: out=<out> len=... # RNG leaf runs with OUR buffer
out[:N] = <TRNG bytes> # buffer WRITTEN
* opcode routed from `msg[0]`; output ptr/len taken straight from `msg[2]`/`msg[3]`; direction
classified as output (dir 2); RNG leaf reached with our exact `(out,len)`; buffer written.
* **len 4, 16 and 32 all succeed** (rv=0). So the hardware len-sensitivity is NOT a message-format bug.
## The hardware reject (0xFFFF000F) is the SVC 0xB5 window check — reproduced in sim
Re-modelled SVC 0xB5 to return a real `[base,limit]` region with the limit just above the buffer base
(`--window lo,base+8`):
[RNG len=4 ] -> rv=0x0 RNG runs, buffer written (end <= limit)
[RNG len=16] -> rv=0xffff000f sub_E04F38 NOT reached (end > limit)
[RNG len=32] -> rv=0xffff000f sub_E04F38 NOT reached (end > limit)
Exact reproduction of the hardware len-sensitivity (`len4` accepts, `len16/32` reject `0xffff000f`),
confirming the reject is the window bound check `(ptr+len)` vs `[base,limit]`, not our setup.
## The real validation chain (ground truth from sys_drv2 + sos2)
`sub_E0DEF4(ptr, len, dir, client)` (sys_drv2 @ 0xE0DEF4), disassembled:
if dir==1 (input) || client==0 -> return 1 (PASS, no pointer check at all)
if len>0x10000000 || ptr>0x40000000 -> return 0 (FAIL)
SVC 0xB5 (R0=client, R1=SP descriptor buf, R2=token) -> fills [b1,l1,b2,l2], returns R0
if R0!=0 -> return 0 (FAIL)
PASS iff [ptr,ptr+len] ⊆ [b1,l1] OR ⊆ [b2,l2] OR ⊆ [dword_E25710, dword_E25714]
SVC 0xB5 = sos2 svc-body case 100 = `sub_178C` (sos_check_user_ptr, generic `[0x200000,0x10000000]`
+ type-4 window exclusions) then `sub_27F8`, which FILLS the `[b1,l1,b2,l2]` descriptor from the
CLIENT process entry:
type 5 : l1 = 0x800000 ; b1 = (mode==1 ? 0x400000 : proc[6]) ; b2/l2 from proc[v1]
type 2 : [0xE00000, 0xF00000]
type 4/6: [0xD00000, 0xE00000]
So for a **type-5** client with mode==1 the window is the wide `[0x400000, 0x800000]`, which covers
our buffer at any length — but when `b1 = proc[6]` (mode!=1 path) the base is the client's actual
image base, a narrow window. The accept/reject therefore turns on the **client process type and the
mode token** presented at validation time, plus the SYS_DRV global fallback region
`[dword_E25710, dword_E25714]` (runtime values, set at init). That selection is the knob that flips
the verdict per boot — not anything our ROP controls.
Window recorder for the type-5 path, `sub_22B4` (sos2): on the gated svc it writes
`tcb[24] = *(tcb+0x34) - 0x44` (base) and `tcb[25] = tcb[16]` (limit) when
`base>=0x400000 && *(tcb+0x34)<0x800000`.
## Conclusions
1. **Our ROP/message construction is correct** — proven by executing it through the real firmware
dispatcher and watching every argument land where intended and the RNG write happen.
2. The crypto-output `0xFFFF000F` reject is **the SVC 0xB5 client-window validation**, reproduced in
sim, governed by the client process type/mode and the SYS_DRV global region — state we do not set.
3. The emulator cannot reproduce the hardware accept/reject directly because its SVC 0xB5 is
deliberately permissive (`[0,0xFFFFFFFF]`); `--window` lets us model it and reproduce the reject.
Tool: `psp_emu/harness/gcp_crypto_argcheck.py` (`--window lo,hi` to model the SVC 0xB5 region).
================================================================================
## Hardware verdict: no deterministic output placement — SVC 0xB5 client check fails
Pinned the per-boot knob and tested on silicon. SVC 0xB5's return (which gates all of `sub_E0DEF4`
via `if R0!=0 -> FAIL`) is `sub_27F8` -> `sub_1978(client_id)` (sos2):
sub_1978(client): return 0 (OK) iff client < 0x20 AND proc_table[client].flag < 0 (active bit);
else return 37 (FAIL)
So if the client id SYS_DRV presents for our confused-deputy call does not resolve to an ACTIVE
process entry, the validation fails for ANY output buffer/length -- a client/session reject, not a
buffer-placement one. That is buffer-independent: no placement can fix it.
Hardware pass-rate test (op 0x35 len4 @ 0x410380, dumped, fresh ATX per boot):
rnglen / rngdet / rngdet4 / passrate : ~25 boots, ALL reject 0xffff000f, buffer never written
acceptsweep (the one apparent pass, 11/11 status 0) : did NOT reproduce -- its exact config
(0x410380, DUMP_OFF 0x380) now rejects 10/10.
Conclusion: **output-buffer crypto ops reliably reject (0xffff000f) from the GCP confused-deputy
path; there is no deterministic buffer placement that makes them land.** The reject is the SVC 0xB5
client validation (`sub_1978` active-check + the region check), governed by client/session state the
GCP side does not set. The single non-reproducible "pass" window is treated as noise, not a result.
Security-model note (the clean asymmetry): `sub_E0DEF4` returns PASS immediately for `dir==1` (INPUT)
buffers -- SYS_DRV does NOT validate pointers it only READS -- but gates `dir==2` (OUTPUT) buffers
through the client check. So the confused deputy can FEED arbitrary pointers (unvalidated reads) but
cannot reliably RECEIVE into a chosen buffer (validated writes). The crypto-output read-back oracle is
therefore closed; the earlier inline read-back (create handle) remains the only reliable channel.
|