diff options
| author | Peter Geissler <peter@haxx.in> | 2026-09-22 11:14:57 +0200 |
|---|---|---|
| committer | Peter Geissler <peter@haxx.in> | 2026-09-22 11:14:57 +0200 |
| commit | 649992b26c75eb13ace40c6eec499895a69a1132 (patch) | |
| tree | 09bfb3ac77a3cc3462d9e2972a678da246cefeb4 /psp_out/ifwi_parse_model.py | |
| download | amd-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.py | 124 |
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() |
