aboutsummaryrefslogtreecommitdiffstats
path: root/fw/verify_gcp_params_nonrop_discriminator.py
diff options
context:
space:
mode:
authoruser <user@clank>2026-10-09 16:11:31 +0200
committeruser <user@clank>2026-10-09 16:11:31 +0200
commitbc43149d914325e15dc0490dee9d982e21287623 (patch)
treea632cf70de742d5a60d01c9c0d806bf5b9257538 /fw/verify_gcp_params_nonrop_discriminator.py
parentGCP->ASD 0x403: ASD-internal chain PROVEN end-to-end in emulator (real handlers) (diff)
downloadamd-psp-re-main.tar.gz
amd-psp-re-main.tar.xz
amd-psp-re-main.zip
Record PSP security investigation artifacts and probesHEADmain
Diffstat (limited to 'fw/verify_gcp_params_nonrop_discriminator.py')
-rw-r--r--fw/verify_gcp_params_nonrop_discriminator.py109
1 files changed, 109 insertions, 0 deletions
diff --git a/fw/verify_gcp_params_nonrop_discriminator.py b/fw/verify_gcp_params_nonrop_discriminator.py
new file mode 100644
index 0000000..a40b41d
--- /dev/null
+++ b/fw/verify_gcp_params_nonrop_discriminator.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+"""Offline-only FE0A comparator discriminator for two proposed GCP params VAs.
+
+This DOES NOT measure a live command-buffer VA and MUST NOT be treated as
+permission to retry a hardware ROP chain. FE0A comparator REJECT (and
+accepted diagnostic selector 11) call an ASD→SYS_DRV fixed-fabric-write
+event path; this is NOT a read-only card probe.
+No device requests or keys are used.
+"""
+from __future__ import annotations
+
+import struct
+from pathlib import Path
+
+from capstone import Cs, CS_ARCH_ARM, CS_MODE_THUMB
+
+ELF = Path(__file__).resolve().parents[1] / 'psp_out/versions_win/elf_all/gcp_22.0.0.77_7c426e7f.elf'
+OLD_BASE = 0x04024000
+SHIFTED_BASE = 0x04035000 # fault-compatible HYPOTHESIS, not measured
+CODE_VA = 0x00404A74 # signed Thumb function prologue
+SECOND_CODE_VA = 0x00404A78 # unique follow-up word in signed ELF LOAD
+ZERO_VA = 0x00415A74 # zero bytes inside signed ELF LOAD
+COUNTER = 0x40000000
+
+
+def load_bytes(image: bytes, va: int, size: int) -> bytes:
+ assert image[:6] == b'\x7fELF\x01\x01'
+ phoff = struct.unpack_from('<I', image, 28)[0]
+ entsize, count = struct.unpack_from('<HH', image, 42)
+ for i in range(count):
+ typ, off, base, _, filesz, memsz, flags, _ = struct.unpack_from('<8I', image, phoff + entsize * i)
+ if typ == 1 and flags & 1 and base <= va and va + size <= base + filesz:
+ assert va + size <= base + memsz
+ return image[off + va - base:off + va - base + size]
+ raise ValueError(f'{va:#x} is not four initialized bytes in an executable LOAD')
+
+
+def main() -> None:
+ image = ELF.read_bytes()
+ thumb = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
+ # FE0A pre-selector comparator: R1=params+0x10, R2=*(params+0x824),
+ # ADD R1,R2 then LDRB [R1,#4]! => target=params+0x14+R2.
+ for va, mnemonic in ((0x40483A, 'ldr.w'), (0x40484C, 'ldm.w'),
+ (0x40486E, 'add'), (0x404870, 'ldrb.w'),
+ (0x404886, 'ldrd.w'), (0x40488A, 'subs'),
+ (0x404890, 'bhs')):
+ ins = next(thumb.disasm(load_bytes(image, va, 4), va, count=1))
+ assert ins.address == va and ins.mnemonic.split('.')[0] == mnemonic.split('.')[0], (hex(va), ins.mnemonic)
+ # FE0A REJECT is not passive: GCP invokes its signed error emitter 3x.
+ # That wrapper's ASD 0x208 → SYS_DRV 0x1079 path can request a fixed
+ # fabric write at 0x03B30600; no card-side endpoint effect is known.
+ for va, mnemonic in ((0x40490A, 'ldr'), (0x404912, 'blx'),
+ (0x404918, 'blx'), (0x404924, 'blx')):
+ ins = next(thumb.disasm(load_bytes(image, va, 4), va, count=1))
+ assert ins.address == va and ins.mnemonic == mnemonic, (hex(va), ins.mnemonic)
+ prologue = next(thumb.disasm(load_bytes(image, CODE_VA, 4), CODE_VA, count=1))
+ assert prologue.mnemonic == 'push'
+ code_word = struct.unpack('<I', load_bytes(image, CODE_VA, 4))[0]
+ zero_word = struct.unpack('<I', load_bytes(image, ZERO_VA, 4))[0]
+ second = struct.unpack('<I', load_bytes(image, SECOND_CODE_VA, 4))[0]
+ assert code_word == 0x460DB5B0 and second == 0x4604A121 and zero_word == 0
+ assert image.count(struct.pack('<I', second)) == 1, 'second word must identify one ELF position'
+ assert load_bytes(image, ZERO_VA + 4, 4) == bytes(4)
+ assert zero_word < COUNTER < code_word
+ disp = (CODE_VA - OLD_BASE - 0x14) & 0xffffffff
+ assert (OLD_BASE + 0x14 + disp) & 0xffffffff == CODE_VA
+ assert (SHIFTED_BASE + 0x14 + disp) & 0xffffffff == ZERO_VA
+ assert (OLD_BASE + 0x14 + disp + 4) & 0xffffffff == SECOND_CODE_VA
+ assert (SHIFTED_BASE + 0x14 + disp + 4) & 0xffffffff == ZERO_VA + 4
+ # Correct a crucial missing prerequisite: ordinary input_len=0x1000
+ # fails FE0A's signed 0x4048F4..FC bounds check BEFORE the comparator.
+ # Selectors 11..13 jump directly to diagnostic status 0x241 instead of
+ # entering FE0A's potentially huge memcpy on comparator ACCEPT. Even
+ # then, a live probe can fault for any third params-VA hypothesis.
+ input_len = 0xFFFFFFFF
+ assert disp + 4 <= input_len and not disp + 4 <= 0x1000
+ for va, mnemonic in ((0x4048F4, 'ldr'), (0x4048FA, 'cmp'),
+ (0x404938, 'b'), (0x4049D8, 'ldr')):
+ ins = next(thumb.disasm(load_bytes(image, va, 4), va, count=1))
+ assert ins.address == va and ins.mnemonic.split('.')[0] == mnemonic, (hex(va), ins.mnemonic)
+ print(f'signed .77 ELF: both targets are initialized mapped LOAD bytes; '
+ f'old={CODE_VA:#x} ({prologue.mnemonic}), shifted={ZERO_VA:#x} (zero)')
+ print(f'FE0A displacement={disp:#x}; counter={COUNTER:#x}, FE09 seed would be {COUNTER + 1:#x}')
+ print(f'adjacent unique ELF dword={second:#x} at {SECOND_CODE_VA:#x}; '
+ f'follow-up displacement={(disp+4)&0xffffffff:#x}')
+ print('A declared input_len=0xffffffff is REQUIRED to reach the comparator; '
+ 'selector 11 avoids FE0A accepted-branch memcpy.')
+ print('If the signed FE07/08/09 gates and counter hold: '
+ 'old params base -> ACCEPT status 0x241; shifted base -> REJECT 0x100.')
+ # GCP .77 sub_4062B4 puts E3, caller-code 3 and the passed 16-bit
+ # status into ASD 0x208 msg[1]. ASD .163 reorders the three parts and
+ # SYS_DRV 0x1079 filters only top bytes EE/EF (signed reports cited below).
+ forwarded = []
+ for status in (0xF000, 0, 0x100):
+ v = (status << 16) | (3 << 8) | 0xE3
+ packed = ((v & 0xff) << 24) | (((v >> 8) & 0xff) << 16) | ((v >> 16) & 0xffff)
+ assert packed >> 24 == 0xE3 and packed >> 24 not in (0xEE, 0xEF)
+ forwarded.append(packed)
+ print('CAUTION: FE0A REJECT can request three SYS_DRV 0x1079 fixed-fabric writes '
+ 'at 0x03B30600, words '+', '.join(hex(v) for v in forwarded))
+ print('ACCEPT with selector 11 also emits three error RPCs (final status 0x241); '
+ 'both paths may touch the same fixed endpoint.')
+ print('This is NOT a passive live read; the private gate and target effect are unmeasured.')
+ print('This offline script alone cannot measure other bases or the live destination mapping; '
+ 'bounded non-ROP hardware observations are documented separately, not ROP authorization.')
+
+
+if __name__ == '__main__':
+ main()