diff options
| author | user <user@clank> | 2026-10-07 21:24:45 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-10-07 21:24:45 +0200 |
| commit | d34bc299958660f4e4a9af2ffcc6aeee4ccf78f2 (patch) | |
| tree | 898a4a3019f2aa32de4b014750b7523673dc0565 | |
| parent | read the FE0A parent before testing OOB reach: copy is unclamped, which bound... (diff) | |
| download | amd-psp-re-d34bc299958660f4e4a9af2ffcc6aeee4ccf78f2.tar.gz amd-psp-re-d34bc299958660f4e4a9af2ffcc6aeee4ccf78f2.tar.xz amd-psp-re-d34bc299958660f4e4a9af2ffcc6aeee4ccf78f2.zip | |
the chosen write can aim at the saved LR that the writing function itself pops, which skips the runaway copy
Addendum A concluded the chosen-content write was only useful below params+0x1950. That missed the
best target: the stack slot holding 0x404c74's own return address.
The ordering is favourable and decides it. The two chosen writes land at 0x404d4e/0x404d5c, the
pop {r4..sl,pc} is at 0x404d16, and the unclamped runaway memcpy is at 0x4049b4 -- downstream of the
pop. So a huge *(params+0x818) is needed only to pass the bound check at 0x404cd4; if the write
redirects the pop, the copy never executes, nothing wedges, and the stack target is outside the
command-buffer mapping so the copy could not have overwritten it anyway.
Frame geometry is clean: 0x404c74 pushes 12 regs with no sub sp and pops 8 after add sp,#0x10, so
the popped PC slot is SP_e-4 and clobbering the sl slot at SP_e-8 is free because the parent only
reads sl past the pop. Saved LR value is 0x40494c.
Absolute address is a prediction, flagged as such: chain 0x4053ec -> 0x40433c -> 0x404828 ->
0x404c74 gives 0x40 bytes of frames above, and anchoring on the one measured SP for this build
(0x41bf70 at the UNLOAD bx ip, below CloseSession 8 + tlApiFree 0x10) puts the PC slot near
0x41bf44. Recorded the three assumptions that could shift it.
Also recorded the trick that makes the shot tolerant: both destinations and both contents are
independently host-chosen, so setting arg10 = arg18 = PC|PC<<32 and aiming at SP_e-8 and SP_e-0x10
paints 16 bytes with the same constant and any of the four slots being the real PC slot still gives a
chosen PC, with the other popped registers harmlessly set to the same value.
| -rw-r--r-- | fw/FINDING-GCP-FE0A-SEL4-CHOSEN-CONTENT-WRITE.md | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/fw/FINDING-GCP-FE0A-SEL4-CHOSEN-CONTENT-WRITE.md b/fw/FINDING-GCP-FE0A-SEL4-CHOSEN-CONTENT-WRITE.md index 2a80d85..01167f2 100644 --- a/fw/FINDING-GCP-FE0A-SEL4-CHOSEN-CONTENT-WRITE.md +++ b/fw/FINDING-GCP-FE0A-SEL4-CHOSEN-CONTENT-WRITE.md @@ -185,3 +185,84 @@ TA code -- is unknown and is the question that decides whether this becomes a on stays an information-disclosure plus bounded write. That is a single measurable experiment (plant a recognisable value, trigger, read the fault PC through the existing hijack oracle) and is the correct next step, but it is NOT the step previously written down. + +## Addendum B: the chosen write can target the saved LR that `0x404c74` itself pops -- which +## sidesteps the unclamped copy entirely + +Addendum A concluded the chosen write is only *useful* below `params+0x1950`, because anything higher +is overwritten by the parent's copy and anything outside the mapping faults. That reasoning missed a +target: **the stack slot holding the return address of the very function performing the write.** + +### The ordering is favourable + + 0x404d4e blx memcpy <- chosen write A lands + 0x404d5c blx memcpy <- chosen write B lands + 0x404d74 bl 0x405584 (logger; does not touch slots above its own frame) + 0x404d16 pop.w {r4,r5,r6,r7,r8,sb,sl,pc} <- CONTROL TRANSFER HAPPENS HERE + ... + 0x4049b4 blx memcpy <- the unclamped runaway NEVER EXECUTES + +So a large `*(params+0x818)` is needed only to pass the sub-handler's bound check at `0x404cd4`. The +copy that would run away is downstream of the `pop`, so if the write redirects the `pop`, the fault +never happens and the TA does not wedge. The write is also *not* in the overwritten region, because +the stack is outside the command-buffer mapping. + +### Frame geometry (static, .77) + + 0x404c74 push.w {r0,r1,r2,r3,r4,r5,r6,r7,r8,sb,sl,lr} ; 12 regs = 0x30, no sub sp + 0x404d14 add sp,#0x10 ; discard homed r0-r3 + 0x404d16 pop.w {r4,r5,r6,r7,r8,sb,sl,pc} ; 0x20 -> balanced + +With `SP_e` = SP at entry to `0x404c74`: + + SP_e-0x04 lr -> POPPED INTO PC (value 0x40494c: bl at 0x404948, next insn b 0x40498c) + SP_e-0x08 sl SP_e-0x0c sb SP_e-0x10 r8 + SP_e-0x14 r7 SP_e-0x18 r6 SP_e-0x1c r5 SP_e-0x20 r4 + SP_e-0x24.. r3,r2,r1,r0 (homed, discarded by the add sp) + +Clobbering `sl` is free: the parent reads it at `0x404996`/`0x40499e`, which is past the `pop`. + +### Absolute address: predicted, needs pinning + +Call chain is `0x4053ec` (invoke entry) -> `0x40433c` (dispatcher) -> `0x404828` (FE0A) -> +`0x404c74`, with prologues `0x10 + 0x10 + 0x20 = 0x40` above `0x404c74`'s own frame. Anchoring on the +one measured SP for this build -- `sp = 0x41bf70` at the UNLOAD `bx ip`, which sits below +`TA_CloseSessionEntryPoint` (`push {r4,lr}` = 8) and `gcp_tlApiFree` (`push {r4,r5,r7,lr}` = 0x10): + + SP(TA entry) ~ 0x41bf70 + 0x18 = 0x41bf88 + SP_e(0x404c74) ~ 0x41bf88 - 0x40 = 0x41bf48 + popped PC slot ~ 0x41bf44 + +Assumptions, each of which could shift this: that the TEE enters the invoke and close entry points +with the same SP; that no function in the chain has a `sub sp` the prologue scan missed; and that +`0x4053ec` is the invoke entry. Add.6 did this same arithmetic for `.50` and hit the slot exactly, so +the method is sound, but the number is a prediction until measured. + +### Making the shot tolerant of the prediction + +Both destinations are independently aimed (`*(params+0x840)`, `*(params+0x838)`) and both contents +are independently chosen (`arg10`, `arg18`). Setting + + arg10 = arg18 = (PC << 32) | PC + +and aiming the two 8-byte writes at `SP_e-0x08` and `SP_e-0x10` paints 16 bytes with the same +constant, so **any** 4-byte slot in `[SP_e-0x10, SP_e)` being the true PC slot still yields the chosen +PC. The other popped registers receive the same constant, which is harmless because control leaves +immediately. This converts a one-slot guess into a 4-slot window per invoke. + +Displacements for `params = 0x04024000`: + + dstA = 0x41bf40 -> *(params+0x840) = 0x41bf40 - 0x04024000 - 0x418 = 0xFC3F7B28 + dstB = 0x41bf38 -> *(params+0x838) = 0xFC3F7B20 + *(params+0x818) >= dst+8, e.g. 0xFFFFFFF0 (only the bound check consumes it) + +### Oracle + +A chosen `PC` of `0xDEADBEE0` is unmapped and even, so a hit produces a prefetch abort in ARM state +with the fault PC equal to the planted value -- the exact discriminator the hijack report already +uses (fault PC == planted value, FS=5 translation for unmapped). A miss instead corrupts a saved +register with the same constant and should fault elsewhere or return normally, which is +distinguishable. + +Status: static only. This is the experiment to run, and it is materially better than Addendum A's +withdrawn step because a hit is a one-invoke arbitrary chosen PC with no carve and no wedge. |
