aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/ifwi_parse_model.py
diff options
context:
space:
mode:
authorPeter Geissler <peter@haxx.in>2026-09-22 11:14:57 +0200
committerPeter Geissler <peter@haxx.in>2026-09-22 11:14:57 +0200
commit649992b26c75eb13ace40c6eec499895a69a1132 (patch)
tree09bfb3ac77a3cc3462d9e2972a678da246cefeb4 /psp_out/ifwi_parse_model.py
downloadamd-psp-re-649992b26c75eb13ace40c6eec499895a69a1132.tar.gz
amd-psp-re-649992b26c75eb13ace40c6eec499895a69a1132.tar.xz
amd-psp-re-649992b26c75eb13ace40c6eec499895a69a1132.zip
Initial import: AMD PSP reverse engineering, emulator and vulnerability research
Split out of a tinygrad working tree, where all of this lived untracked. tinygrad remains a dependency for hardware-facing scripts only. Contents: psp_emu/ Unicorn-based PSP emulator that boots the real signed firmware from the ARM reset vector: sOS -> SYS_DRV -> HDCP/DTM/RAS TAs, with a CCP crypto device model faithful enough to run the firmware's own drivers (RSA-PSS/SHA-256 container verification passes 5/5 with 5/5 negatives rejected). A simulated host drives the real mailbox/ring path through to TA_InvokeCommandEntryPoint. psp_out/ Static analysis: six PSP modules fully named (asd 1562, sys_drv 1141, sos 527, ras 119, hdcp 109, dtm 14) plus SMU 1033, a cross-version diffing pipeline over 34+ builds, per-handler annotated analysis and ~125 findings documents. Deliberately not tracked: AMD firmware images, the Hex-Rays decompilation derived from them, and IDA databases. data/MANIFEST.sha256 pins the SHA-256 of all 301 blobs results were derived from, so findings remain traceable to their inputs without redistributing AMD code. 6 MB tracked instead of 315 MB. Verified from the new location: test_smoke 5/5, ccp_selftest 11/11, verify_real_image 5/5 + 5/5 negatives, fuzz_ta_invoke controls passing and host-driven TA invoke working (0.73s per invoke).
Diffstat (limited to 'psp_out/ifwi_parse_model.py')
-rw-r--r--psp_out/ifwi_parse_model.py124
1 files changed, 124 insertions, 0 deletions
diff --git a/psp_out/ifwi_parse_model.py b/psp_out/ifwi_parse_model.py
new file mode 100644
index 0000000..8a3aeba
--- /dev/null
+++ b/psp_out/ifwi_parse_model.py
@@ -0,0 +1,124 @@
+"""Host-side model of the PSP's pre-auth IFWI parser.
+
+Mirrors, step for step, what rom_parse_directory and its callees do in
+sys_drv2.elf. Purpose: validate the model against a KNOWN-GOOD image before
+using it to craft anything. If this cannot parse the real BIOS, the model is
+wrong and nothing built on it can be trusted.
+
+Firmware references:
+ rom_parse_directory 0xE16964
+ fw_tail_scan 0xE0F950
+ param_list_scan 0xE0F8A8
+ ring_entry_validate 0xE0FAEC
+ fw_tail_record_check 0xE0FC6C
+"""
+import struct, sys
+
+M32 = 0xFFFFFFFF
+IFWI_MAGIC = 0x55AA55AA
+PSP_MAGIC = 0x50535024 # '$PSP'
+SGN_MAGIC = 0x4E475324 # '$SGN'
+
+class Fail(Exception): pass
+
+def u32(b, off):
+ if off + 4 > len(b) or off < 0: raise Fail("read out of image at %#x" % off)
+ return struct.unpack_from('<I', b, off)[0]
+
+def fw_tail_scan(img, ctx, trace):
+ """v4 = *(img+20) then *(img+44); bound (v4+16)&M32 <= 0x200000; expect '$PSP'."""
+ for slot, hdr_off in ((0, 20), (1, 44)):
+ v4 = u32(img, hdr_off)
+ trace.append(" fw_tail_scan[%d]: v4 = *(img+%d) = %#x" % (slot, hdr_off, v4))
+ if ((v4 + 16) & M32) > 0x200000:
+ raise Fail("slot %d: bound check failed (v4+16 = %#x)" % (slot, (v4+16)&M32))
+ magic = u32(img, v4)
+ if magic != PSP_MAGIC:
+ raise Fail("slot %d: expected $PSP at %#x, got %#010x" % (slot, v4, magic))
+ count = u32(img, v4 + 8)
+ if count < 2:
+ raise Fail("slot %d: count %d < 2 (-> 0xFFFF000B)" % (slot, count))
+ ctx[slot] = {'dir_off': v4, 'len': 16*count + 16, 'valid': 1, 'count': count}
+ trace.append(" -> $PSP ok, count=%d, len=%#x" % (count, 16*count+16))
+ return ctx
+
+def param_list_scan(img, ptr, count, ctx, trace):
+ """entries of 4 dwords; type 316/317; 64-bit bound; low dword of v9 must == 32."""
+ seen = 0
+ for i in range(count):
+ base = ptr + i*16
+ t = u32(img, base)
+ if t not in (316, 317):
+ continue
+ v9 = u32(img, base+4) | (u32(img, base+12) << 32)
+ size = u32(img, base+8)
+ if not (size + v9 <= 0x200000 and (v9 & M32) == 32):
+ raise Fail("param entry %d (type %d): bound/offset check failed (size=%#x v9=%#x)" % (i, t, size, v9))
+ idx = 0 if t == 316 else 1
+ # a1[2] is the OFFSET stored at ctx+40 and used by ring_entry_validate;
+ # a1[1] is a fixed marker that must equal 32.
+ ctx[idx].update({'p_offset': size, 'p_marker': u32(img, base+4), 'p_valid': 1})
+ trace.append(" param_list_scan: type=%d -> offset=%#x marker=%d" % (t, size, u32(img, base+4)))
+ seen += 1
+ if seen == 2: break
+ if seen != 2:
+ raise Fail("param_list_scan: found %d of 2 required entries (types 316/317)" % seen)
+
+def ring_entry_validate(img, ctx, idx, trace):
+ e = ctx[idx]
+ if not e.get('p_valid'): raise Fail("ring_entry_validate[%d]: slot not marked valid" % idx)
+ v4 = e['p_offset'] # ctx+40 == a1[2]
+ v5 = u32(img, v4 + 16)
+ ln = u32(img, v4 + 24)
+ if ((v5 + ln) & M32) > 0x200000:
+ raise Fail("ring_entry_validate[%d]: (v5+len) bound failed" % idx)
+ e.update({'r_off': v5, 'r_len': ln, 'r_valid': 1})
+ trace.append(" ring_entry_validate[%d]: off=%#x len=%#x" % (idx, v5, ln))
+
+def fw_tail_record_check(img, ctx, trace):
+ """v3 = *(img+4096); bound; expect '$SGN'; walk count records of 16 bytes."""
+ v3 = u32(img, 4096)
+ trace.append(" fw_tail_record_check: v3 = *(img+0x1000) = %#x" % v3)
+ if ((v3 + 16) & M32) > 0x200000: raise Fail("$SGN bound check failed")
+ if (m := u32(img, v3)) != SGN_MAGIC:
+ raise Fail("expected $SGN at %#x, got %#010x" % (v3, m))
+ count = u32(img, v3 + 8)
+ trace.append(" -> $SGN ok, %d records" % count)
+ recs = {}
+ for i in range(1, 8):
+ for j in range(count):
+ base = v3 + 16 + j*16
+ if u32(img, base) == i:
+ recs[i] = tuple(u32(img, base + k*4) for k in range(4))
+ break
+ ctx['records'] = recs
+ trace.append(" record types present: %s" % sorted(recs))
+
+def parse(img):
+ trace = []
+ ctx = {0: {}, 1: {}}
+ if (m := u32(img, 0)) != IFWI_MAGIC:
+ raise Fail("IFWI magic: got %#010x want %#010x" % (m, IFWI_MAGIC))
+ trace.append("rom_parse_directory: IFWI magic ok")
+ fw_tail_scan(img, ctx, trace)
+ slot = next((s for s in (0,1) if ctx[s].get('valid')), None)
+ if slot is None: raise Fail("no valid directory slot")
+ d = ctx[slot]['dir_off']
+ param_list_scan(img, d + 16, u32(img, d + 8), ctx, trace)
+ ring_entry_validate(img, ctx, 0, trace)
+ ring_entry_validate(img, ctx, 1, trace)
+ fw_tail_record_check(img, ctx, trace)
+ return ctx, trace
+
+if __name__ == '__main__':
+ for path in sys.argv[1:]:
+ img = open(path, 'rb').read()
+ print("=== %s (%d bytes) ===" % (path, len(img)))
+ try:
+ ctx, trace = parse(img)
+ for t in trace: print(t)
+ print(" RESULT: PARSE SUCCEEDED -> would reach tsu_write_active_slot")
+ except Fail as e:
+ print(" RESULT: PARSE FAILED -> %s" % e)
+ print(" (returns before the writer; flash untouched)")
+ print()