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
|
# The twelve TA types this card accepts that we had never audited
`LOAD_TA` accepts 16 TA components on this Navi 31 (`AUDIT-WINDOWS-DRIVER-CORPUS.md`).
Four were already in scope (ASD, HDCP, DTM, RAS). These are the other twelve.
All were triaged with `fw/ta_triage.py`, which reproduces the layout established on HDCP
in idatui and is **validated against a known TA first** — on `hdcp_23.0.0.82` it returns
entry `0x4042a0`, session table `0x407000`, guard `0x404334`, exactly the values
`GUARD-INIT-CHAIN.md` recorded from IDA.
*Gotcha worth recording: the entry trampoline is **ARM**, while the rest of the image is
Thumb. Decoding it as Thumb yields fluent-looking garbage (`vhadd.s8 d0, d0, d4` …), not
an obvious failure.*
## Structural triage (newest accepted build of each)
| TA | version | size | invoke @ | command surface |
|---|---|---|---|---|
| **GCP** | 22.0.4.50 | 0x13000 | 0x404f20 | real, large |
| **MERIT** | 24.0.4.42 | 0x15000 | 0x401ce8 | real, jump table |
| **OPM** | 25.0.4.48 | 0x19000 | 0x402ae4 | real; ids incl. 0xa9/0xb1/0xb5/0xe1/0xe5 |
| **POWERPLAY** | 34.0.0.20 | 0xa000 | 0x40271c | real, jump table |
| **XGMI** | 32.0.0.20 | 0x6000 | 0x402a1c | real |
| **FP** | 19.0.0.8 | 0x7000 | 0x4025b4 | real, jump table |
| **AUC** | 16.0.0.9 | 0x6000 | 0x401198 | real, two jump tables |
| **WSPE** | 30.0.4.27 | 0x4000 | 0x40240c | real |
| **RAP** | 7.0.2.19 | 0x3000 | 0x4010e4 | real, two jump tables |
| **OC** | 50.0.0.1 | 0x3000 | 0x4013b0 | real |
| **CSU** | 42.0.0.6 | 0x2000 | 0x4010b0 | real (small) |
| **TMZ** | 29.0.0.4 | 0x2000 | 0x4010c0 | **STUB — see below** |
Every one is **class `TA`**, not `DR`. That matters: ASD is `DR` (driver class) and the
PSP refuses to `INVOKE_CMD` it, which is the mechanism that made several ASD defects
unreachable (`FINDING-ASD-IS-DRIVER-CLASS-NOT-INVOKABLE.md`). These twelve carry the
class that *is* invokable, so unlike ASD they are candidate host-facing command surfaces.
Every one also declares a stack guard at header `+0x20`.
## TMZ is an empty stub
`AMD TMZ Application` (Trusted Memory Zone) is the most alarming name in the set and the
least interesting image. Opened in idatui: **6 functions, all 2-4 bytes**, and every one
of the five TEE entry points returns 0:
```
Create 0x4010ba -> return 0 OpenSession 0x4010c4 -> return 0
Destroy 0x4010be -> (empty) Invoke 0x4010c0 -> return 0
CloseSession 0x4010b8 -> (empty)
```
Both shipped builds (29.0.0.3 and 29.0.0.4) are stubs. It is a placeholder that
authenticates and loads but does nothing — no attack surface, and worth saying plainly so
nobody chases the name.
## RAP — first pass
`AMD RAP Application` is the **Register Access Policy** validation TA; it matches
amdgpu's `amdgpu_rap.c` (`TA_CMD_RAP__VALIDATE_L0 = 1`). 13 functions.
`rap_invoke` (`0x4010e4`) is a thin wrapper — `memset(params+6, 24)`, call the dispatcher,
then write back `params[2] = cmd | 0x80000000` and `params[3] = ret`. The dispatcher
`rap_dispatch` (`0x401134`) runs `rap_precheck` first (short-circuits with result 2), then:
* `cmd == 8` — special-cased: selects one of four `{range, policy}` pairs from
`off_401334` according to flag bytes, then calls `rap_validate_range`; on success and
a further flag, repeats with selector 4.
* otherwise — `switch (cmd)` over the `off_4016F4` table, cases 1..6.
## Where the surface actually is
Sizes grow steadily across the driver train — OPM `0x19000 -> 0x21000`, MERIT
`0x15000 -> 0x1c000`, GCP `0x13000 -> 0x16000` — so these are actively developed, not
legacy. OPM, MERIT and GCP are the three big ones and are the obvious next targets.
**Not yet established:** whether the host can actually `INVOKE_CMD` these on this part.
The class magic says they are the invokable kind and `LOAD_TA` accepts them, but that is
an inference from the image plus the load result, not a measurement. The ASD story is the
cautionary precedent — a TA that loads fine and still refuses every invoke. That test is
a small hardware experiment and should be done before any of this is called attack
surface.
Artifacts: `fw/ta_triage.py`, ELFs in `psp_out/versions_win/elf/`.
================================================================================
# INVOKE probe [HW]: all twelve are host-invokable command surfaces
The open question was whether the host can actually `INVOKE_CMD` these, since class magic
plus a successful `LOAD_TA` is only an inference and ASD is the precedent for a TA that
loads cleanly and refuses every invoke.
## Calibrating what the status means (this is what made the first attempt wrong)
The first run used the *first* TA in `psp_13_0_0_ta.bin` as its positive control, got
`0x4`, and stopped. That TA is **ASD** — driver class, the one that refuses everything.
Sweeping command ids across all four bundle TAs gives the vocabulary:
| bundle TA | cmd 0 | 1 | 2 | 0x0e | 0x0f |
|---|---|---|---|---|---|
| #1 ASD (`0x2f000`) | 4 | 4 | 4 | 4 | 4 |
| #2 HDCP | ffff0000 | **0** | **0** | **0** | **0** |
| #3 DTM | ffff0000 | ffff0000 | **0** | ffff0000 | ffff0000 |
| #4 RAS | **0** | **0** | **0** | ffff000a | ffff000a |
So `0x4` = the PSP will not dispatch to this TA at all; `0xffff0000` = dispatched, TA
rejected the id; `0x0` = dispatched and accepted. **A single command id cannot decide
invokability** — HDCP itself returns an error for id 0. The probe therefore tries several
ids and calls a TA invokable if *any* id returns something other than `0x4`.
## Result — 12/12 invokable
```
POS bundle#1 (ASD) refused (0x4 on every id) <- negative control, reproduces the known behaviour
POS bundle#8 INVOKABLE 0:0xffff0000 1:0x0 2:0x0
AUC INVOKABLE 0:0xffff0000 1:0x0 2:0x0
CSU INVOKABLE 0:0x0 1:0x0 2:0x0
FP INVOKABLE 0:0xffff0000 1:0x0 2:0x0
GCP INVOKABLE 0:0xffff0000 1:0x0 2:0x0
MERIT INVOKABLE 0:0x0 1:0x0 2:0x0
OC INVOKABLE 0:0x0 1:0x0 2:0x0
OPM INVOKABLE 0:0x0 1:0x0 2:0x0
RAP INVOKABLE 0:0xffff000a 1:0x0 2:0x0
TMZ INVOKABLE 0:0x0 1:0x0 2:0x0
WSPE INVOKABLE 0:0x0 1:0x0 2:0x0
XGMI INVOKABLE 0:0x0 1:0x0 2:0x0
POWERPLAY INVOKABLE 0:0x0 1:0x0 2:0x0 (see load quirk)
```
**TMZ answering `0x0` to every id is a clean static-to-hardware cross-check**: its
decompiled invoke entry is `return 0`, and that is exactly what the silicon reports.
## POWERPLAY needs a prior TA load in the same boot
POWERPLAY first reported `LOAD failed 0xffff0000`, contradicting the earlier sweep which
accepted it. Isolating it:
```
clean boot, POWERPLAY as the FIRST load -> 0xffff0000 (even with the sweep's exact buffers)
after any prior successful TA load -> 0x0, sid=1
after that prior TA is unloaded again -> 0x0, sid=0
```
So it is not the container, the command buffer size, or load ordering within a sweep: the
PSP refuses POWERPLAY as the very first `LOAD_TA` after reset and accepts it once the TA
subsystem has serviced one load. The earlier sweep only ever saw it succeed because a
positive control was always loaded first. Recorded as a quirk of this part, and as the
reason a "first load after reset" harness can produce a false rejection.
## Standing
Twelve TA types that this project never audited are **loadable and invokable on stock
firmware, from the ordinary host path, with genuine signed AMD images** — against the
four previously in scope. Eleven of them contain real dispatchers (TMZ is a stub).
That is the attack surface statement the previous section deliberately withheld; it is
now measured rather than inferred.
What is still *not* claimed: that any specific command does anything interesting. The
probe used ids 0-2 only and does not touch parameter buffers beyond a zeroed page.
OPM, MERIT and GCP remain the obvious deep-dive targets.
Artifact: `hw/invoke_newtas_probe.py`.
|