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)`.
|