aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-10-07 21:24:45 +0200
committeruser <user@clank>2026-10-07 21:24:45 +0200
commitd34bc299958660f4e4a9af2ffcc6aeee4ccf78f2 (patch)
tree898a4a3019f2aa32de4b014750b7523673dc0565
parentread the FE0A parent before testing OOB reach: copy is unclamped, which bound... (diff)
downloadamd-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.md81
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.