# Hunt for a READ primitive in HDCP (to leak `__stack_chk_guard`) **Status: BOTH SWEEPS COMPLETE — NEGATIVE.** All 17 variable-length copy sites triaged (bounded), and the remaining axis — **response marshalling** — has now also been swept. No read primitive found in HDCP. ## Why this is the right target The HDCP stack overflow (`subcmd12` -> `v58`) is canary-blocked, and the canary is **per-TA-load TRNG** (`bl_load_ta_image` `0x1665A`: `SVC 0xA2` -> `STR [R2,R11]`), so cross-TA leaks and brute force are both dead (see `DISTINCT-WRITE-HUNT.md`). But the guard lives at a **known fixed address, `0x404334`**. Any HDCP-reachable out-of-bounds *read* that can be pointed there defeats the canary outright — no write primitive needed. Prior sweeps looked for writes; read-side bugs demonstrably exist in this codebase (ASD `0x308` was reclassified from a write to a **source over-read**, `DFAR=0xd5b000`, `DFSR 0x37 WnR=0`), so the class is real. ## Method Enumerated every `memcpy` call site in HDCP whose length register `R2` is **not** an immediate — a fixed-size copy cannot over-read. 15 sites: | function | site | R2 source | |---|---|---| | `buffer_copy_bounded` | `0x401872` | `MOV R2, R4` | | `hdcp1_second_part_authentication` | `0x401E22` | `MOV R2, R4` | | `hdcp2_rx_AKE_Send_H_prime` | `0x4026F8`, `0x402704` | stack (`var_160`, `var_15C`) | | `hdcp2_rx_RepeaterAuth_ReceiverID_List` | `0x402B4C`, `0x402BCC`, `0x402CAC`, `0x402E68` | `5*N` — the KNOWN rx_ids writes | | `hdcp2_rx_RepeaterAuth_ReceiverID_List` | `0x402CDE`, `0x402D72` | `MOV R2, R7` / `R8` | | `hdcp2_rx_repeater_ack_verify` | `0x402EDE` | `MOV R2, R6` | | `hdcp2_crypto_op_setup` | `0x403560` | `MOV R2, R5` | | `hdcp2_tx_RepeaterAuth_Stream_Manage` | `0x403B40` | `UXTB R2, R1` | | `hdcp2_tx_SKE_Send_Eks` | `0x403D00` | `MOV R2, R7` | ## Triage results — all 17 sites, all bounded Scanned both copy helpers (`memcpy` `0x401058` and `memcpy_aligned`) for call sites whose length register is **not** an immediate; a fixed-size copy cannot over-read. 17 sites total. | site | verdict | |---|---| | `rxlist` `5*N` x4 (`0x402B4C/BCC/CAC/E68`) | the known `rx_ids` OOB **writes**, not reads | | `rxlist 0x402CDE` | `memcpy(stack, sess+0x55, 5*count)` — the known **stack overflow** | | `rxlist 0x402D72` | `len = count`, source is the stack buffer just filled with `5*count` bytes -> source always >= length. Bounded | | `repeater_ack_verify 0x402EDE` | `len = sess[0x106]`, whose **only writer** sets it to `2*v15 <= 20`, and `sess+0xF2` holds 20 bytes. Bounded | | `tx_Stream_Manage 0x403B40` | guarded by `if (v21 <= 0xA)`; `2*v15 <= 20` == `sizeof(v20)` (bzero'd `0x14`). Bounded | | `tx_SKE_Send_Eks 0x403D00` | `len` is the output length of `hdcp2_aes_ctr_keystream`, requested as `0x20`. Bounded | | `rx_AKE_Send_H_prime 0x4026F8/0x402704` | lengths are BYREF crypto outputs, path gated on `a3 == 32`. Bounded | | `hdcp1_second_part_authentication 0x401E22` | see below — bounded, but only just | | `buffer_copy_bounded 0x401872` | dst is a local stack buffer, tail bzero'd to `0x200`; caller-validated | | `crypto_op_setup 0x403560` | preceded by `CMP R5,R0 / BLS` bound check | ### The one that looked wrong, and wasn't `hdcp1_second_part_authentication` skips its `bzero` for large lengths (`if ((5*v7) >> 7 > 4) v14 = 0;`) while still running the `memcpy` — the classic shape of a missed bound. It is nonetheless safe, by a margin of 5 bytes: * check: `a3 >> 2 <= 0x9E` -> `a3 <= 635` * use: `v7 = a3 / 5` -> `v7 <= 127` -> `5*v7 <= 635` * buffer: `&v20` at `sp+0x18`, next object `v22` at `sp+0x298` -> exactly **640** bytes `635 < 640`. The `MOVHI R1,#0` is defensive code for a case the bound makes unreachable. Worth recording precisely because the disassembly looks like a bug and the safety depends on `>>2` and `/5` happening to agree. ## What this does NOT cover The sweep keyed on **copy primitives**. A leak built from field-by-field marshalling — a handler writing session/stack words into the response buffer one `STR` at a time — would not appear in it at all. That is a different search (enumerate writes into the invoke response buffer and check their sources) and is the remaining axis. Given the canary sits at a fixed known address, a leak of any *stack* word from the `subcmd12` frame would also suffice, not just `0x404334`. ## Superseded notes * **`5*N` sites** — the already-documented `rx_ids` OOB **writes**, not reads. * **`hdcp2_rx_AKE_Send_H_prime`** (`0x4026EE`-`0x402704`) — structurally the most interesting: two chained copies **from stack buffers into session memory**, second offset by the first length (`memcpy(sess+0x112, stack, len1)` then `memcpy(sess+0x112+len1, stack, len2)`). Session data is host-readable, so oversized lengths would drag stack contents out to the host. **But** `len1`/`len2` are `v21`/`v22`, BYREF outputs of a crypto call, and the path is gated on `a3 == 32`. Not obviously attacker-controlled. *Needs the crypto call's output-length bound checked before this can be ruled in or out.* * **`hdcp2_tx_SKE_Send_Eks`** (`0x403D00`) — `memcpy(stackbuf, stackbuf, R7)` where `R7 = [SP,#var_198]`, set by `hdcp2_aes_ctr_keystream`. Same shape: length is a crypto output. Untriaged. ## Not yet examined `buffer_copy_bounded`, `hdcp1_second_part_authentication`, `hdcp2_rx_RepeaterAuth_ReceiverID_List` sites `0x402CDE`/`0x402D72`, `hdcp2_rx_repeater_ack_verify`, `hdcp2_crypto_op_setup`, `hdcp2_tx_RepeaterAuth_Stream_Manage`. Also not done: the **`ta_memcpy`** variant (this pass keyed on `memcpy` `0x401058` only — HDCP has a second copy helper used by the subcmd12 paths), and any non-`memcpy` read-back (field-by-field response marshalling into the shared buffer, which would not appear in a copy-primitive scan at all). ## Next step For each remaining candidate, answer one question: **can the host influence the length or the source offset, and does the destination reach host-readable memory (shared buffer or session)?** Only sites answering yes to both are leak candidates. `hdcp2_rx_AKE_Send_H_prime` is the leading one and hinges entirely on whether the crypto output length is bounded. -------------------------------------------------------------------------------- ## Marshalling sweep — COMPLETE, NEGATIVE for a canary leak The copy-primitive sweep above filtered on **non-immediate** lengths. That was the wrong filter for this bug class: the classic marshalling leak is a **fixed-size** copy out of a **partially-initialised stack buffer**, which that filter excluded by construction. Re-ran keyed on **SP-relative source** instead — 13 sites. ### Result: the codebase is disciplined Every fixed-size stack->output copy is preceded by a `bzero_core` covering the source buffer: | site | source | zeroed by | |---|---|---| | `tx_AKE_No_Stored_km 0x4036F2` | `v14[128]` | `bzero_core(v14, 0x80u)` | | `tx_LC_Init 0x4038BE` | `v27[32]` | `bzero_core(v27, 0x20u)` | | `hmac_sha256_verify 0x4040A2` | `v17[32]` | `bzero_core(v17, 0x20u)` | | `tx_SKE_Send_Eks 0x403D7A/E14` | `v43[32]` | `bzero_core(v43, 0x20u)` | | `crypto_op_setup 0x403560` | `v13[64]` | `bzero_core(v13, 0x40u)` | | `tx_Stream_Manage` | `v20[20]` | `bzero_core(v20, 0x14u)` | `tx_AKE_No_Stored_km` is additionally gated: the copy only runs when `hdcp2_kdf_sha256` returns success, so a short KDF write still leaves zeros. ### One real (but useless) find: 2-byte uninitialised disclosure `hdcp1_create_session` copies **5 bytes out of 4-byte initialised objects**: ```c int v16; // [sp+8h] BYREF int v18; // [sp+10h] BYREF v18 = 0; v16 = 0; memcpy((_BYTE *)(a2 + 12), &v18, 5u); // sp+0x10..0x14 -> 1 byte past v18 memcpy((_BYTE *)(a2 + 26), &v16, 5u); // sp+0x8..0xC -> 1 byte past v16 ``` HDCP1 KSVs are 5 bytes (40-bit), so the copy length is correct for the protocol and the declarations are simply 1 byte short. Each call leaks **one byte** of the adjacent stack slot into session memory, which is host-readable. **Not useful for the canary**: 1 byte at a time, from two fixed slots (`sp+0x14`, `sp+0xC`) in `hdcp1_create_session`'s frame — not the `subcmd12` frame, and not `__stack_chk_guard`. Recorded for completeness, not as a lead. ## Overall verdict **No read primitive capable of leaking `__stack_chk_guard` exists in HDCP's reachable copy or marshalling surface.** Both sweeps (variable-length and fixed-length/SP-source) are now complete and negative. The HDCP stack overflow remains canary-blocked, and the canary remains per-load TRNG. Remaining unexplored, in descending order of promise: 1. **Field-by-field `STR` marshalling** — handlers writing session/stack words into the response one store at a time. Not covered by either sweep (both keyed on copy helpers). This is now the only untouched axis in HDCP. 2. Widening the target: any leak of a stack word from the **`subcmd12` frame specifically** would suffice, since the canary is copied into every frame. -------------------------------------------------------------------------------- ## Marshalling sweep — COMPLETE, NEGATIVE The axis this document previously left open ("a handler writing session/stack words into the response buffer one `STR` at a time") has now been swept. `TA_InvokeCommandEntryPoint` passes the response buffer as `v5 + 5136`. Nine handlers receive it: | cmd | handler | verdict | |---|---|---| | 1 | `hdcp1_create_session` | clean — every marshalled field is a callee out-param | | 3 | `hdcp1_first_part_authentication` | no stack data reaches the response | | 4 | `hdcp1_second_part_authentication` | no stack data reaches the response | | 7 | `hdcp1_get_encryption_status` | writes only `0`/`1` | | 0xC | `hdcp2_get_encryption_status` | writes only `0`/`1`/`2` | | 0xE | `hdcp2_create_session` | writes the session index only | | 0xF | `hdcp2_process_auth_message_v2` | all marshalled locals pre-initialised; TX writes bounded | | 0x12 | `hdcp_set_protection` | forwards `a2`/`a2+4` to ASD `0x308` | | 0x13 | `hdcp_get_srm_status` | status code only | ### The one that looked like a hit, and wasn't `hdcp1_create_session` marshals two locals that Hex-Rays shows as **never assigned**: ```c char v14; // [sp+6h] <- no initialiser anywhere in the pseudocode char v15; // [sp+7h] ... *(_BYTE *)(a2 + 17) = v15; *(_BYTE *)(a2 + 31) = v14; ``` That is exactly the ASD `0x202` shape. It is **not** a bug. The disassembly shows both are out-parameters: ```asm 0x40193c ADD.W R1, SP, #var_25 ; a2 -> &v15 0x401940 ADD.W R2, SP, #var_26 ; a3 -> &v14 0x401944 BL disp_hdcp1_read_caps ; proto int(int,int,int) ``` Hex-Rays renders the `disp_hdcp1_*` RPC stubs as **zero-argument**, so the writes are invisible in pseudocode and the locals appear dead. The marshalling is also gated on those calls returning 0, i.e. only reached on the path where they were written. Same for `read_bksv` (`var_C`/`var_14`) and `setup` (`var_1C`/`var_24`). **Method note:** check `idc.get_type()` on every callee before concluding a local is unwritten. A wrong/absent prototype is the single most common source of false positives in this codebase — this was the fifth such instance in the project. ### Where that leaves the canary Both HDCP read axes are now exhausted. The remaining known uninit-disclosure is ASD `0x202` (`VULN-ASD-202-CANARY-LEAK.md`), which leaks **ASD's** `__stack_chk_guard`, not HDCP's, and is not host-reachable. The only other demonstrated PSP-memory disclosure is the TSU flash primitive, which costs an 896 KB erase+rewrite per 1 KB leaked and needs `mapped_base` to aim.