aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/VULN-ASD-SRM-V9-WALKER-OOB-READ.md
blob: 826bae42d9e2ee5713df3ef682b229533e61277f (plain) (blame)
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
# ASD SRM **v9** revocation walker — out-of-bounds read (walk length decoupled from stored length)

> **Not the authoritative document for this topic.** See **`VULN-ASD-SRM-WALKER-OOB-READ.md`**,
> and `INDEX.md` §3 for why. Kept for its detail and history.


**Function:** `asd_srm_sub_12DB0` @ `0xD12DB0` (asd2.elf)
**Status:** **[EMU] proven by execution** (real ASD code under Unicorn)
**Reachability:** host-reachable code path, but **content-gated by the SRM signature** (see "Honest reachability")
**Novelty:** this function had **never been analysed** in any prior document.

---

## The defect

The walker fetches the stored SRM length, tests it for **nonzero only**, then
**discards it** and takes the record count from an unvalidated blob field:

```asm
0xd12db0  PUSH  {R4-R6,LR}
0xd12db2  CBZ   R0, done            ; a1 (KSV) null check
0xd12db6  BL    asd_srm_sub_10B30   ; fetch stored SRM length
0xd12dba  CBZ   R0, done            ; <-- length used ONLY as a nonzero test
0xd12dbc  LDR   R0, =asd_srm_buf    ; <-- the length in R0 is DISCARDED here
0xd12dbe  ADD.W R5, R0, #0xC        ; read ptr = &asd_srm_buf + 12
0xd12dc2  LDRH  R0, [R0,#8]         ; count field read from the BLOB
0xd12dc4  LSRS  R6, R0, #6          ; count = buf[8..9] >> 6   -> 0..1023
loop:
0xd12dc6  CBZ   R6, done
0xd12dc8  MOV   R0, R5              ; compare record at buf+12+5k
0xd12dce  BL    memcmp_be           ; reads the record
0xd12dd2  CBZ   R0, found           ; returns 23 (0x17) on match
0xd12dd4  SUBS  R6, #1
0xd12dd6  ADDS  R5, #5              ; advance one 5-byte record, UNCHECKED
0xd12dd8  B     loop
```

**The walk length is never compared against the stored length.** `count` comes
from blob bytes `[8..9]` and is capped only by the `>>6` shift, so it reaches
1023 records = 5115 bytes starting at offset 12, i.e. up to offset **5126**.

`asd_srm_buf` @ `0xD1F038` is exactly **5120** bytes (next symbol
`asd_srm_stored_len` @ `0xD20438`). So the walk runs past the end of the buffer,
and — regardless of how few bytes were actually stored — treats the entire
buffer as revocation records.

This is a **different root cause** from the v8 walker
(`VULN-ASD-SRM-WALKER-OOB-READ.md`): that one bounds the record offset but not
the access extent; this one ignores the stored length entirely.

## Emulation proof (`poc_srm_v9_walker_oob.py`)

Real `asd_srm_sub_12DB0` bytes executed under Unicorn with `asd_srm_buf` at its
true address and every read instrumented. `memcmp_be` is deliberately **not**
stubbed — the offending reads happen inside it. Blob: a **minimal 48-byte**
store (the smallest the 0x308 path accepts) with `count = 0xFFFF >> 6 = 1023`.

```
stored SRM   : 48 bytes (minimal legal store), count field = 1023
walk completed, r0 = 0
total reads in buffer region : 1024
reads BEYOND stored 48 bytes : 1015
reads PAST buffer end (5120) : 1
max byte offset touched      : 5122
  -> 3 bytes PAST the end of asd_srm_buf
  -> 5075 bytes read that were NEVER STORED
```

Note the measured overread (3 bytes) is smaller than the arithmetic maximum
(7 bytes) because `memcmp_be` short-circuits on the first mismatching word. An
attacker who matches the leading bytes extends the read to the full `+5126`.

## Honest reachability — content is signature-gated

The call path itself is host-reachable:

    host INVOKE_CMD -> HDCP cmd 3/4 (hdcp1_first/second_part_authentication)
      -> disp_srm_query (SVC 0xF2, ASD opcode 0x302)
      -> asd_srm_query -> asd_srm_sub_13174 -> (buf[0]>>4 == 9) -> asd_srm_sub_12DB0

**But the blob content cannot be planted without a valid signature.** Both
writers of `asd_srm_buf` were checked:

| writer | gate |
|---|---|
| `asd_srm_store` (opcode `0x308`) | `asd_srm_header_check(...) == 0` required before `memcpy_aligned(asd_srm_buf, ...)` |
| `asd_srm_op_301` (opcode `0x301`) | same `asd_srm_header_check(...) == 0` |

`asd_srm_header_check` dispatches to `asd_srm_verify_t8` / `asd_srm_verify_t9`,
which perform an RSA-3072 verify against a public key at `unk_D25BF0`. It accepts
only `0` or `0xFFFF000A`, and the fail-open path was previously tested on
hardware and **held**.

So this is a **latent** memory-safety defect: real, execution-proven, and
triggered by a field an attacker controls in principle — but only reachable with
an SRM that passes signature verification. At boot `asd_srm_buf` is BSS zero, so
`count == 0` and the walk is inert.

**This also corrects the previous finding.** `DISCLOSURE-SUMMARY.md` §1.7 (the v8
walker) claimed the blob is planted "via the already host-reachable `0x308`
store". That reachability claim was **overstated** — the `0x308` *store* is
signature-gated; only the `0x308` OOB *read* (§1.3) is unauthenticated. §1.7 has
been corrected accordingly.

## Impact

* Out-of-bounds read of up to 7 bytes past `asd_srm_buf` (into
  `asd_srm_stored_len` and following globals).
* Treats up to 5075 bytes of never-stored buffer content as revocation records,
  so a short SRM causes stale data to drive HDCP revocation decisions
  (integrity of the revocation check, not just memory safety).
* Weak 1-bit oracle: a match returns 23 (`0x17`) to the caller.
* Not control-flow: the region is pointer-free.

## Fix

Bound the walk by the stored length, e.g. reject when
`12 + 5*count > stored_len`, or clamp `count = min(count, (stored_len - 12) / 5)`.