"""Host-side model of the PSP's pre-auth IFWI parser. WARNING -- THIS MODEL DIVERGES FROM THE FIRMWARE. DO NOT CRAFT WITH IT. Differentially tested against the real parser under Unicorn (`psp_emu/harness/tsu_parser_diff.py`, `fuzz_ifwi_parser.py`): over 400 mutations it disagrees with the firmware on **84**, in both directions: firmware FAULTS, model rejects 42 model is safer, hides the bug firmware ACCEPTS, model rejects 34 model is stricter firmware REJECTS, model accepts 8 model is too lenient -- the dangerous direction for crafting The stricter class comes from `fw_tail_scan`: this model raises on the first slot's failure, while the firmware marks that slot invalid and carries on. An attempted fix (make per-slot failure non-fatal) was MEASURED AND MADE THINGS WORSE -- agreement fell 316 -> 305 and "model accepts, firmware rejects" jumped 8 -> 53 -- so it was reverted rather than shipped. The real semantics, read from `fw_tail_scan` (0xE0F950): an error register accumulates across both slots and is never cleared, so a slot-0 failure leaves it set even when slot 1 parses cleanly -- and `rom_parse_directory` may still succeed on the valid slot regardless. Modelling that faithfully needs the whole interaction, not a one-line change. Until someone does that, use the firmware itself via the harnesses above. 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(' 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()