diff options
| author | user <user@clank> | 2026-10-09 16:11:31 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-10-09 16:11:31 +0200 |
| commit | bc43149d914325e15dc0490dee9d982e21287623 (patch) | |
| tree | a632cf70de742d5a60d01c9c0d806bf5b9257538 /psp_emu | |
| parent | GCP->ASD 0x403: ASD-internal chain PROVEN end-to-end in emulator (real handlers) (diff) | |
| download | amd-psp-re-main.tar.gz amd-psp-re-main.tar.xz amd-psp-re-main.zip | |
Diffstat (limited to 'psp_emu')
89 files changed, 5121 insertions, 0 deletions
diff --git a/psp_emu/harness/measure_sysdrv_svc78.py b/psp_emu/harness/measure_sysdrv_svc78.py new file mode 100644 index 0000000..5e2cde9 --- /dev/null +++ b/psp_emu/harness/measure_sysdrv_svc78.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Measure SYS_DRV's crypto workspace using the signed sOS SVC 0x78 path. + +Offline full-system emulator: let sOS boot the real SYS_DRV, then observe the +return of SYS_DRV sub_E04A28's SVC 0x78. This reads the base written by sOS to +SYS_DRV's 0xE257D8 and the allocation size written to SYS_DRV's stack. Sample +adjacent CPU-MMU mappings; these are emulator results, not hardware PTEs. + +Run from the repository root: python3 psp_emu/harness/measure_sysdrv_svc78.py +""" +from __future__ import annotations + +import sys + +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_CPSR, UC_ARM_REG_R0, UC_ARM_REG_SP + +import boot_sos_and_tas as boot +from pspemu import armmmu + + +def main() -> int: + comp, _ccp, _exceptions = boot.build() + # The key DB is needed for SYS_DRV initialization; no other TA is needed. + boot.stage_all(comp, slots=boot.SLOTS[:2], verbose=False) + seen: list[tuple[int, int, int, int, tuple[int | None, ...], bool]] = [] + + def after_svc78(uc, _address, _size, _user_data): + sp = uc.reg_read(UC_ARM_REG_SP) + word = lambda va: int.from_bytes(armmmu.read_va(uc, va, 4), 'little') + status, size, base = uc.reg_read(UC_ARM_REG_R0), word(sp), word(0xE257D8) + offsets = (0, 0x1000, size - 1, size, size + 0x1000) + pas = tuple(armmmu.translate_live(uc, base + off) for off in offsets) + print('signed SYS_DRV after SVC 0x78: status=%#x base=%#x size=%#x SP=%#x' + % (status, base, size, sp), flush=True) + for offset, pa in zip(offsets, pas): + print(' VA %#x -> %s' % (base + offset, 'UNMAPPED' if pa is None + else 'PA %#x' % pa), flush=True) + null_pa = armmmu.translate_live(uc, 0) + print(' NULL source VA 0 -> %s' % ('UNMAPPED' if null_pa is None + else 'PA %#x (permission decode below)' % null_pa), + flush=True) + l1base = armmmu.ttbr0(uc) + desc0 = int.from_bytes(uc.mem_read(l1base, 4), 'little') + l2zero = int.from_bytes(uc.mem_read(desc0 & ~0x3FF, 4), 'little') if desc0 & 3 == 1 else 0 + dacr = armmmu.read_cp15(uc, 3, 0, 0, 0) + mode = uc.reg_read(UC_ARM_REG_CPSR) & 0x1F + domain = (desc0 >> 5) & 0xF + ap = ((l2zero >> 9) & 1) << 2 | ((l2zero >> 4) & 3) + domain_mode = (dacr >> (2 * domain)) & 3 + # ARM short-descriptor AP=0b101 means privileged read-only, USER no access. + denied = (mode == 0x10 and domain_mode == 1 and ap == 5) + seen.append((status, size, base, sp, pas, denied)) + print(' VA0 L1=%#x L2=%#x AP=%d domain=%d DACR-domain=%d CPU-mode=%#x user-denied=%s' + % (desc0, l2zero, ap, domain, domain_mode, mode, denied), flush=True) + + # Return from sOS SVC 0x78, just before SYS_DRV checks size >= 0x11FC. + comp.uc.hook_add(UC_HOOK_CODE, after_svc78, begin=0xE04A36, end=0xE04A36) + comp.uc.reg_write(UC_ARM_REG_SP, 0x3F000) + boot.run_system(comp, seconds=30, done=lambda: bool(seen), settle=0, verbose=False) + if (len(seen) != 1 or seen[0][:3] != (0, 0x2000, 0xECC000) + or seen[0][4] != (0x10000, 0x11000, 0x11FFF, None, None) + or not seen[0][5]): + print('Unexpected or missing sOS/SYS_DRV workspace result:', seen, file=sys.stderr) + return 1 + # These PTEs were checked inside the hook, in the right process context. + print('MATCH: two mapped workspace pages; next VA unmapped in this emulator') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/psp_emu/harness/probe_asd163_100_type1_error_record.py b/psp_emu/harness/probe_asd163_100_type1_error_record.py new file mode 100644 index 0000000..ec15226 --- /dev/null +++ b/psp_emu/harness/probe_asd163_100_type1_error_record.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 0x100 type-1 ASIC20 callback: error leaves an active record. +Isolated synthetic descriptor and callback installation; no card/link operation. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE=0xD33510; SLOT=BASE+769*4; STATE=0xD29108 +REQ=0x400040;DESC=0x400100 + +def main(): + asd=Component('asd',image_path=str(SIGNED)) + asd.uc.mem_map(0x400000,0x3000) + prev={p:asd.rd32(p) for p in (SLOT,STATE)} + # A *single signed* ASIC20/25 type-1 thunk, not an ASIC boot state. + asd.wr32(SLOT,0xD10441) + def run(state): + asd.write(BASE,bytes(92));asd.wr32(STATE,state) + asd.write(DESC,bytes(0x40));asd.wr32(DESC,1) + asd.write(REQ,bytes(0x40));asd.wr32(REQ,0x100) + asd.wr32(REQ+4,0);asd.wr32(REQ+8,DESC) + ret=asd.run_function(0xD013B8,(REQ,),thumb=True,count=150_000,timeout=2_000_000) + return ret,tuple(asd.rd32(BASE+n) for n in (4,8,64,68,76)) + try: + fail,rec=run(0) + ok,rec2=run(1) + assert (fail,rec)==(1,(1,1,6,4,5)),(fail,rec) + assert (ok,rec2)==(0,(1,1,6,2,5)),(ok,rec2) + assert not asd.faults,asd.faults + print('signed ASD 0x100: synthetic type-1 descriptor leaves active type-6 record even when startup predicate makes callback return 1; success case state=1; no actual ASIC admission') + finally: + for p,v in prev.items():asd.wr32(p,v) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_101_ownerless_record_zero.py b/psp_emu/harness/probe_asd163_101_ownerless_record_zero.py new file mode 100644 index 0000000..260105a --- /dev/null +++ b/psp_emu/harness/probe_asd163_101_ownerless_record_zero.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""ASD .163 0x101 record-zero ordering with callback effects blocked. + +Signed dispatcher and zeroing leaf run unmodified; two ASIC/resource helper +calls are intercepted to return errors. Synthetic private records and caller +identities: not a live victim or hardware teardown. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +DISPATCH, REQ, TABLE, STRIDE = 0xD013B8, 0x400040, 0xD33510, 92 +CALLBACKS = (0xD0A7A8, 0xD0AEB0) + + +def main() -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1000) + visits: list[int] = [] + + def deny_helper(uc, ea, size, data): + visits.append(ea) + uc.reg_write(UC_ARM_REG_R0, 0x1234) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + + for ea in CALLBACKS: + asd.uc.hook_add(UC_HOOK_CODE, deny_helper, begin=ea, end=ea+1) + victim, control = 5, 6 + marker = bytes((i*3+1)&0xFF for i in range(STRIDE)) + asd.write(TABLE+victim*STRIDE, marker) + asd.write(TABLE+control*STRIDE, marker) + + def invoke(channel: int, caller: int) -> int: + asd.write(REQ, bytes(0x40)) + asd.wr32(REQ, 0x101) + asd.wr32(REQ+4, channel) + ret = asd.run_function(DISPATCH, (REQ, caller), thumb=True, + count=200_000, timeout=3_000_000) + assert not asd.faults, (channel, ret, asd.faults) + return ret + + denied = invoke(32, 2) + assert denied == 5 and not visits + assert asd.read(TABLE+victim*STRIDE, STRIDE) == marker + assert invoke(victim, 2) == 0 + assert visits == list(CALLBACKS), [hex(x) for x in visits] + assert asd.read(TABLE+victim*STRIDE, STRIDE) == bytes(STRIDE) + assert asd.read(TABLE+control*STRIDE, STRIDE) == marker + print('signed 0x101 channel 5: zeroed 92 bytes despite two modeled helper failures') + print('index-32 negative: return 5; adjacent channel 6 unchanged') + # Restore the synthetic record and change only the opaque caller scalar. + asd.write(TABLE+victim*STRIDE, marker) + visits.clear() + assert invoke(victim, 7) == 0 and visits == list(CALLBACKS) + assert asd.read(TABLE+victim*STRIDE, STRIDE) == bytes(STRIDE) + print('different synthetic caller scalar: same signed record-zero effect') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_asd163_10a_10b_dispatch.py b/psp_emu/harness/probe_asd163_10a_10b_dispatch.py new file mode 100644 index 0000000..73291d5 --- /dev/null +++ b/psp_emu/harness/probe_asd163_10a_10b_dispatch.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Synthetic signed ASD .163 dispatch: 0x10A/B are handlers, not aborts. + +The table is zero-initialized in this isolated emulation; this does not measure +live card callbacks or sOS caller-window routing. +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +DISPATCH, REQ = 0xD013B8, 0x400040 + + +def call(asd: Component, op: int, *args: int) -> int: + asd.write(REQ, b'\0' * 0x40) + asd.wr32(REQ, op) + for i, arg in enumerate(args): + asd.wr32(REQ + 4 + 4*i, arg) + status = asd.run_function(DISPATCH, (REQ,), thumb=True, count=30000, timeout=2_000_000) + assert not asd.faults, (hex(op), status, asd.faults) + return status + + +def run() -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) + assert asd.rd32(0xD33510 + 777 * 4) == 0 + assert asd.rd32(0xD33510 + 778 * 4) == 0 + asd.write(0x400080, b'\0' * 12) + for op, args in ((0x10A, (0, 0x400080)), + (0x10B, (0, 0x400080, 0x400084, 0x400088))): + status = call(asd, op, *args) + assert status == 7, (hex(op), status) + print(f'signed 0x{op:03x}: valid caller pointer(s), null callback returns 7') + assert call(asd, 0x10A, 0, 0xD34000) == 2 + assert call(asd, 0x10B, 0, 0xD34000, 0x400084, 0x400088) == 2 + print('negative controls: private pointer rejected with 2 in both leaves') + assert call(asd, 0x106) == 4 + print('invalid-table control: opcode 0x106 returns 4') + + +if __name__ == '__main__': + run() diff --git a/psp_emu/harness/probe_asd163_160_srm_state_bypass.py b/psp_emu/harness/probe_asd163_160_srm_state_bypass.py new file mode 100644 index 0000000..f17ace7 --- /dev/null +++ b/psp_emu/harness/probe_asd163_160_srm_state_bypass.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 0x160 state scalar gates HDCP1/HDCP2 SRM cleanup. + +Synthetic signed 0x18E allocation, planted UNSIGNED SRM, and stubbed ASIC +callback. No signature acceptance, natural victim or card effect. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE, REQ, SRC, MASK, SRM = 0xD33510, 0x400040, 0x401000, 0x402000, 0xD15030 +CHANNEL = 5 + + +def one_case(clear_by_caller: bool, hdcp2: bool) -> tuple[int, int, int]: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) + + def request(op: int, *args: int) -> int: + asd.write(REQ, bytes(0x40)) + asd.wr32(REQ, op) + for i, arg in enumerate(args): + asd.wr32(REQ + 4 + 4*i, arg) + rv = asd.run_function(0xD013B8, (REQ,), thumb=True, + count=100_000, timeout=3_000_000) + assert not asd.faults, (hex(op), rv, asd.faults) + return rv + + bitmap = BASE + (0xDF4 if hdcp2 else 4092) + count = BASE + (0xDF8 if hdcp2 else 4108) + asd.write(SRC, b'ABCDE' + bytes((0xA0 if hdcp2 else 0x280)-5)) + asd.write(MASK, bytes(16)) + assert request(0x1A7 if hdcp2 else 0x18E, CHANNEL, SRC, 1, MASK) == 0 + assert asd.rd32(bitmap) == 1 and asd.read(count,1) == b'\x01' + asd.wr32(BASE+92*CHANNEL+4, 1) # synthetic active record + asd.wr32(BASE+92*CHANNEL+88, 1) # synthetic live-link annotation + if clear_by_caller: + assert request(0x160, CHANNEL, 0) == 0 + assert asd.rd32(BASE+92*CHANNEL+88) == 0 + + # HDCP1 reverses the stored ID, ABCDE -> EDCBA; HDCP2 compares ABCDE. + # This SRM is NOT signed, and its bytes are planted past the 0x301 gate. + asd.write(SRM, bytes(0x1400)) + asd.write(SRM, b'\x91') + asd.write(SRM+6, b'\x01\x88') + asd.write(SRM+8, (1 << 6).to_bytes(2,'little')) + asd.write(SRM+12, b'ABCDE' if hdcp2 else b'EDCBA') + calls: list[int] = [] + def block_callback(uc, ea, size, data): + calls.append(ea) + uc.reg_write(UC_ARM_REG_R0, 0) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + callback = 0xD0AEB0 if hdcp2 else 0xD0A7A8 + asd.uc.hook_add(UC_HOOK_CODE, block_callback, begin=callback, end=callback+1) + rv = asd.run_function(0xD0CB70, (0,), thumb=True, + count=200_000, timeout=4_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + bitmap_value = asd.rd32(bitmap) + count_value = asd.read(count,1)[0] + print('HDCP2' if hdcp2 else 'HDCP1', 'caller cleared?',clear_by_caller, + 'bitmap',bin(bitmap_value),'count',count_value, + 'ASIC callback visits',len(calls)) + return bitmap_value, count_value, len(calls) + + +def main() -> None: + for hdcp2 in (False, True): + good = one_case(False, hdcp2) + bypass = one_case(True, hdcp2) + assert good == (0,0,1), good + assert bypass == (1,1,0), bypass + print('signed 0x160 clears cleanup predicates in both loops; unsigned single-match SRMs then skip revocation') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_asd163_18f_cross_channel.py b/psp_emu/harness/probe_asd163_18f_cross_channel.py new file mode 100644 index 0000000..aee13ea --- /dev/null +++ b/psp_emu/harness/probe_asd163_18f_cross_channel.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 0x18F: caller bitmap releases a different channel's record. + +Synthetic global table and caller window only. No card invocation, sOS caller +window, real channel ownership, or live impact is established here. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE = 0xD33510 +REQ = 0x400040 +BITS = 0x400080 +DISPATCH = 0xD013B8 +COUNT = BASE + 4108 + + +def record(slot: int) -> int: + return BASE + 4109 + 5 * slot + + +def run() -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) # synthetic GCP caller window + asd.write(REQ, b'\0' * 0x40) + asd.wr32(REQ, 0x18F) + asd.wr32(REQ + 4, BITS) + asd.write(BITS, b'\0' * 16) + + # Slot 7 belongs to another channel (5); caller claims it by bit number + # alone. Slot 8 is a control for collateral stores. + asd.wr32(BASE + 4092, 1 << 7) + asd.write(COUNT, b'\x01') + asd.write(record(7), b'ABCDE') + asd.write(record(8), b'VWXYZ') + asd.wr32(BASE + 3580 + 4 * 7, 5) + asd.wr32(BITS, 1 << 7) + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=30000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + assert asd.rd32(BASE + 4092) == 0 + assert asd.read(COUNT, 1) == b'\x00' + assert asd.read(record(7), 5) == b'\0' * 5 + assert asd.rd32(BASE + 3580 + 4 * 7) == 5 # owner field NOT checked/cleared + assert asd.read(record(8), 5) == b'VWXYZ' + print('allocated slot 7: arbitrary caller bitmap clears another channel record, ' + 'frees global bit, decrements count; owner word remains 5') + + # The five-byte zero operation is unconditional even for an unset bit. + asd.write(record(9), b'12345') + asd.wr32(BITS, 1 << 9) + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=30000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + assert asd.rd32(BASE + 4092) == 0 and asd.read(COUNT, 1) == b'\x00' + assert asd.read(record(9), 5) == b'\0' * 5 + assert asd.read(record(8), 5) == b'VWXYZ' + print('unallocated slot 9: caller bitmap clears five bytes despite unset global bit') + + asd.wr32(REQ + 4, 0xD34000) # private, not in synthetic caller window + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=30000, timeout=2_000_000) + assert rv == 2 and not asd.faults, (rv, asd.faults) + assert asd.read(record(8), 5) == b'VWXYZ' + print('negative control: private bitmap pointer rejected with status 2') + + +def connected_allocation() -> None: + """Let the signed 0x18E allocator create the record, then forge its release.""" + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) + input_buf, output_buf = 0x401000, 0x402000 + asd.write(REQ, b'\0' * 0x40) + asd.write(input_buf, b'abcde' + b'\0' * (0x280 - 5)) + asd.write(output_buf, b'\0' * 0x10) + asd.wr32(REQ, 0x18E) + asd.wr32(REQ + 4, 5) # channel index 5 + asd.wr32(REQ + 8, input_buf) + asd.wr32(REQ + 12, 1) + asd.wr32(REQ + 16, output_buf) + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=40000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + mask = asd.rd32(output_buf) + assert mask == 1 and asd.rd32(BASE + 4092) == 1, hex(mask) + assert asd.read(record(0), 5) == b'abcde' + assert asd.rd32(BASE + 3580) == 5 and asd.read(COUNT, 1) == b'\x01' + print('signed 0x18E allocated slot 0, record abcde, owner/channel 5') + + # Free request has only bitmap pointer, not a channel/owner parameter. + asd.write(BITS, mask.to_bytes(4, 'little') + b'\0' * 12) + asd.wr32(REQ, 0x18F) + asd.wr32(REQ + 4, BITS) + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=30000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + assert asd.rd32(BASE + 4092) == 0 and asd.read(COUNT, 1) == b'\x00' + assert asd.read(record(0), 5) == b'\0' * 5 + assert asd.rd32(BASE + 3580) == 5 + print('signed 0x18F freed channel-5 record using caller bitmap alone; owner stale') + + +if __name__ == '__main__': + connected_allocation() + run() diff --git a/psp_emu/harness/probe_asd163_1a1_failed_rng_prefix.py b/psp_emu/harness/probe_asd163_1a1_failed_rng_prefix.py new file mode 100644 index 0000000..c953cab --- /dev/null +++ b/psp_emu/harness/probe_asd163_1a1_failed_rng_prefix.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 ASIC-20 0x1A1 callback prefix only: model no-output RNG failure. +Stops BEFORE ASD's link-table or fabric operations; does not emulate a live session. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_PC,UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +REC=0xD33510; OUT=0x400100; INPUT=0x400200 + +def main(): + asd=Component('asd',image_path=str(SIGNED)) + asd.uc.mem_map(0x400000,0x3000) + asd.write(OUT,b'K'*16);asd.write(INPUT,b'P'*8) + prev=asd.rd32(REC+64);asd.wr32(REC+64,6) # synthetic ASIC-20 type-6 record + hits=[] + def failed(uc,ea,size,_): + hits.append('rpc35') + # Model the already independently verified signed SYS_DRV first-fetch + # failure: no bytes written to the destination, dispatcher reports 0. + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,0xD10BEF) + def copy(uc,ea,size,_): + # Isolated emulator does not handle this Thumb->ARM BLX memcpy; + # copy only the exact registers of the signed call, not later state. + dst,src,n=[uc.reg_read(x) for x in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2)] + assert dst==OUT and n==16,(hex(dst),hex(n)) + asd.write(dst,asd.read(src,n)) + hits.append('intercepted_memcpy') + uc.reg_write(UC_ARM_REG_PC,0xD10BF9) + def stop(uc,ea,size,_): + hits.append('copy_done');uc.emu_stop() + asd.uc.hook_add(UC_HOOK_CODE,failed,begin=0xD10BEA,end=0xD10BEA) + asd.uc.hook_add(UC_HOOK_CODE,copy,begin=0xD10BF4,end=0xD10BF4) + asd.uc.hook_add(UC_HOOK_CODE,stop,begin=0xD10BF8,end=0xD10BF8) + try: + try: + asd.run_function(0xD10BB8,(REC,OUT,INPUT),thumb=True,count=50_000,timeout=2_000_000) + except Exception: + print('hits',hits,'PC',hex(asd.uc.reg_read(UC_ARM_REG_PC)),'R0',hex(asd.uc.reg_read(UC_ARM_REG_R0))) + raise + assert hits==['rpc35','intercepted_memcpy','copy_done'],hits + assert asd.read(OUT,16)==bytes(16),asd.read(OUT,16).hex() + assert asd.read(INPUT,8)==b'P'*8 + print('signed ASD ASIC20 0x1a1 callback prefix: modeled failed RNG plus intercepted memcpy replaced caller 16-byte marker with zero; STOP before global/fabric stores; no live entropy failure') + finally: + asd.wr32(REC+64,prev) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_1a2_last_channel_sweep.py b/psp_emu/harness/probe_asd163_1a2_last_channel_sweep.py new file mode 100644 index 0000000..d578931 --- /dev/null +++ b/psp_emu/harness/probe_asd163_1a2_last_channel_sweep.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 0x1A2: selected group sweep visits 0..30, omits 31. +Synthetic active records only; all ASIC/link callback callsites intercepted. +No real HDCP session, stateful callback, or card request. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE=0xD33510;STRIDE=92;REQ=0x400040 + +def main(): + asd=Component('asd',image_path=str(SIGNED)) + assert Path(asd.image_path).read_bytes()==SIGNED.read_bytes() + asd.uc.mem_map(0x400000,0x1000) + visits=[]; final=[] + for ea in (0xD0AF40,0xD0AF4E,0xD0AF86,0xD0AF94,0xD0AF9C): + def intercept(uc,addr,size,user): + idx=(uc.reg_read(UC_ARM_REG_R0)-BASE)//STRIDE + (visits if addr in (0xD0AF40,0xD0AF4E) else final).append((addr,idx)) + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,(addr+size)|1) + asd.uc.hook_add(UC_HOOK_CODE,intercept,begin=ea,end=ea+1) + def setup(peer): + # Source channel 0, same keyed group for exactly one synthetic peer. + for idx in (0,30,31):asd.write(BASE+idx*STRIDE,bytes(STRIDE)) + for idx in (0,peer): + b=BASE+idx*STRIDE + for off,v in ((4,1),(20,0x1234),(64,5),(84,idx+1)): + asd.wr32(b+off,v) + asd.wr32(BASE+60,0) + def run(peer): + setup(peer);visits.clear();final.clear() + asd.write(REQ,bytes(0x40));asd.wr32(REQ,0x1A2);asd.wr32(REQ+4,0) + try:rv=asd.run_function(0xD013B8,(REQ,),thumb=True,count=70000,timeout=2_000_000) + except Exception: + print('model stop',peer,hex(asd.uc.reg_read(UC_ARM_REG_PC)),visits,final,asd.faults[-2:]) + raise + assert not asd.faults,(peer,rv,asd.faults) + return rv,[i for ea,i in visits if ea==0xD0AF40],list(final) + rv30,seen30,fin30=run(30) + rv31,seen31,fin31=run(31) + assert rv30==rv31==0,(rv30,rv31,seen30,seen31,fin30,fin31) + assert 30 in seen30 and 31 not in seen31,(seen30,seen31) + assert seen31==[0] and fin30 and fin31,(seen30,seen31,fin30,fin31) + print('signed 0x1A2: synthetic matching peer30 reached captured sweep callback; peer31 did not; selected-channel final callbacks reached in both, all callbacks intercepted; no natural HDCP victim') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_1a3_1a4_scalar_setters.py b/psp_emu/harness/probe_asd163_1a3_1a4_scalar_setters.py new file mode 100644 index 0000000..6daa0c7 --- /dev/null +++ b/psp_emu/harness/probe_asd163_1a3_1a4_scalar_setters.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 service-2 0x1A3/0x1A4 selected private scalar stores. + +Synthetic active channel records only. No HDCP victim, ASIC callback, or card +request. Deliberately does not claim cross-client impact from a selected store. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE, REQ, STRIDE = 0xD33510, 0x400040, 92 + + +def main() -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1000) + record = BASE + 5*STRIDE + control = BASE + 6*STRIDE + asd.write(record, bytes(STRIDE)) + asd.write(control, bytes(STRIDE)) + asd.wr32(record+4, 1) + asd.wr32(control+4, 1) + + def call(op: int, channel: int, scalar: int) -> int: + asd.write(REQ, bytes(0x40)) + for i, v in enumerate((op, channel, scalar)): + asd.wr32(REQ+4*i, v) + rv = asd.run_function(0xD013B8, (REQ,), thumb=True, + count=30000, timeout=2_000_000) + assert not asd.faults, (hex(op),rv,asd.faults) + return rv + + for op, off, val in ((0x1A3, 84, 0xDEADBEEF), + (0x1A4, 80, 0xFFFFFFFF)): + assert call(op, 32, val) == 5 + assert asd.rd32(record+off) == 0 + assert call(op, 5, val) == 0 + assert asd.rd32(record+off) == val + assert asd.rd32(control+off) == 0 + asd.wr32(record+4, 0) + assert call(op, 5, 0) == 5 + assert asd.rd32(record+off) == val + asd.wr32(record+4, 1) + print(f'signed 0x{op:x}: active channel 5 +{off} = 0x{val:08x}; invalid/inactive negatives, adjacent unchanged') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_asd163_1a7_1a8_bitmap_release.py b/psp_emu/harness/probe_asd163_1a7_1a8_bitmap_release.py new file mode 100644 index 0000000..8131b40 --- /dev/null +++ b/psp_emu/harness/probe_asd163_1a7_1a8_bitmap_release.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 hidden 0x1A7/0x1A8 allocator/release, synthetic state. + +No hardware state changes, live channel ownership proof, or sOS policy emulation. +""" +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +DISPATCH = 0xD013B8 +BASE = 0xD33510 +REQ, INPUT, OUT, BAD = 0x400040, 0x401000, 0x402000, 0xD33550 +BITMAP = BASE + 3572 +COUNT = BASE + 3576 +OWNER = BASE + 3444 +RECORD = BASE + 3124 + + +def run() -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) + asd.write(REQ, b'\x00' * 0x40) + asd.write(INPUT, b'abcde' + b'\x00' * (0xA0 - 5)) + asd.wr32(OUT, 0) + asd.wr32(REQ, 0x1A7) + asd.wr32(REQ + 4, 5) # synthetic channel index + asd.wr32(REQ + 8, INPUT) + asd.wr32(REQ + 12, 1) # byte count, dispatcher masks to byte + asd.wr32(REQ + 16, OUT) + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=70000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + assert asd.rd32(OUT) == asd.rd32(BITMAP) == 1 + assert asd.read(COUNT, 1) == b'\x01' + assert asd.read(RECORD, 5) == b'abcde' + assert asd.rd32(OWNER) == 5 + print('signed 0x1A7: allocated global bit 0, record abcde, channel owner 5') + + # Release only takes a pointer to a mask; no channel argument accompanies it. + asd.wr32(REQ, 0x1A8) + asd.wr32(REQ + 4, OUT) + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=30000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + assert asd.rd32(OUT) == asd.rd32(BITMAP) == 0 + assert asd.read(COUNT, 1) == b'\x00' + assert asd.rd32(OWNER) == 5 and asd.read(RECORD, 5) == b'abcde' + print('signed 0x1A8: caller mask frees global bit without checking owner; metadata stays') + + asd.wr32(REQ + 4, BAD) # rejected private pointer in synthetic caller window + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=30000, timeout=2_000_000) + assert rv == 2 and not asd.faults, (rv, asd.faults) + assert asd.rd32(BITMAP) == 0 and asd.read(COUNT, 1) == b'\x00' + print('negative control: private bitmap pointer rejected with status 2') + + +if __name__ == '__main__': + run() diff --git a/psp_emu/harness/probe_asd163_1a9_sparse_enumeration.py b/psp_emu/harness/probe_asd163_1a9_sparse_enumeration.py new file mode 100644 index 0000000..bcce031 --- /dev/null +++ b/psp_emu/harness/probe_asd163_1a9_sparse_enumeration.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 0x1A9 enumeration after a normal sparse bitmap release. + +Synthetic ASD state/caller region only; no hardware 0x1A7/0x1A8/0x1A9 calls. +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE, DISPATCH = 0xD33510, 0xD013B8 +REQ, SOURCE, MASK, DEST, COUNT_PTR = 0x400040, 0x401000, 0x402000, 0x403000, 0x403200 + + +def rpc(asd: Component, op: int, *args: int) -> int: + asd.write(REQ, b'\0' * 0x40) + asd.wr32(REQ, op) + for i, arg in enumerate(args): + asd.wr32(REQ + 4 + 4*i, arg) + rv = asd.run_function(DISPATCH, (REQ,), thumb=True, count=70000, timeout=2_000_000) + assert not asd.faults, (op, rv, asd.faults) + return rv + + +def read_list(asd: Component) -> tuple[int, bytes]: + asd.write(DEST, b'\xA5' * 0xA0) + asd.write(COUNT_PTR, b'\xA5') + rv = rpc(asd, 0x1A9, DEST, COUNT_PTR) + assert rv == 0, (rv, asd.faults) + return asd.read(COUNT_PTR, 1)[0], asd.read(DEST, 10) + + +def run() -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) + asd.write(SOURCE, b'abcdeFGHIJ' + b'\0' * (0xA0-10)) + asd.wr32(MASK, 0) + assert rpc(asd, 0x1A7, 5, SOURCE, 2, MASK) == 0 + assert asd.rd32(MASK) == 3 and asd.rd32(BASE+0xDF4) == 3 + assert asd.read(BASE+0xDF8, 1) == b'\x02' + cnt, data = read_list(asd) + assert cnt == 2 and data == b'abcdeFGHIJ', (cnt, data.hex()) + print('control: contiguous slots 0 and 1 enumerate 2 records correctly') + + asd.wr32(MASK, 1) # release first slot; second remains allocated + assert rpc(asd, 0x1A8, MASK) == 0 + assert asd.rd32(BASE+0xDF4) == 2 and asd.read(BASE+0xDF8, 1) == b'\x01' + assert asd.read(BASE+0xC34+5, 5) == b'FGHIJ' + cnt, data = read_list(asd) + assert cnt == 1 and data == b'\xA5' * 10, (cnt, data.hex()) + print('sparse bitmap: status=0, advertised count=1, no record copied; ' + 'live slot-1 record remains FGHIJ') + + +if __name__ == '__main__': + run() diff --git a/psp_emu/harness/probe_asd163_1aa_alloc_cleanup.py b/psp_emu/harness/probe_asd163_1aa_alloc_cleanup.py new file mode 100644 index 0000000..8a88cc6 --- /dev/null +++ b/psp_emu/harness/probe_asd163_1aa_alloc_cleanup.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Connected signed 0x1AA ID-list update -> signed 0x1A7 allocation cleanup. + +Synthetic channel/ID ownership. No accepted SRM, card effect or actual ASIC +teardown: sub_D0AEB0 is stubbed; signed list and bitmap logic execute. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE, REQ, SOURCE, MASK = 0xD33510, 0x400040, 0x401000, 0x402000 +BITMAP, COUNT, LISTCOUNT = BASE+0xDF4, BASE+0xDF8, BASE+3577 +CHANNEL = 6 + + +def scenario(batch: bytes, expected_bitmap: int, expected_listcount: int, + expected_callbacks: int) -> None: + asd=Component('asd',image_path=str(SIGNED)) + asd.uc.mem_map(0x400000,0x1C000) + visits: list[int] = [] + def skip_asic_teardown(uc,ea,size,data): + visits.append(ea) + uc.reg_write(UC_ARM_REG_R0,9) # modeled failure; signed sweep ignores result + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + asd.uc.hook_add(UC_HOOK_CODE,skip_asic_teardown,begin=0xD0AEB0,end=0xD0AEB1) + + def request(op: int, *args: int, caller: int = 2) -> int: + asd.write(REQ,bytes(0x40)) + for i,v in enumerate((op,*args)): + asd.wr32(REQ+4*i,v) + rv=asd.run_function(0xD013B8,(REQ,caller),thumb=True, + count=300_000,timeout=4_000_000) + assert not asd.faults,(hex(op),rv,asd.faults) + return rv + + # One signed HDCP2 allocation, distinct from the seeded ID ABCDE. + asd.write(SOURCE,b'FGHIJ'+bytes(0xA0-5)) + asd.wr32(MASK,0) + assert request(0x1A7,CHANNEL,SOURCE,1,MASK)==0 + assert asd.rd32(BITMAP)==1 and asd.read(COUNT,1)==b'\x01' + assert asd.rd32(BASE+0xD74)==CHANNEL + asd.wr32(BASE+92*CHANNEL+4,1) + asd.wr32(BASE+92*CHANNEL+88,1) + asd.write(SOURCE,b'ABCDE'+bytes(155)) + assert request(0x1AA,SOURCE,1,caller=7)==0 + assert asd.rd32(BITMAP)==1 and not visits + assert asd.read(LISTCOUNT,1)==b'\x01' + asd.write(SOURCE,batch+bytes(160-len(batch))) + assert request(0x1AA,SOURCE,len(batch)//5,caller=7)==0 + assert asd.rd32(BITMAP)==expected_bitmap + assert asd.read(COUNT,1)[0]==expected_bitmap + assert asd.read(LISTCOUNT,1)[0]==expected_listcount + assert len(visits)==expected_callbacks + print(f'batch={batch!r} ID count={expected_listcount} alloc bitmap={expected_bitmap} ' + f'teardown-error stub visits={len(visits)} (synthetic caller 2 allocated, 7 updated)') + + +def main() -> None: + scenario(b'FGHIJ',0,2,1) # single unique ID revokes signed allocation + scenario(b'ABCDEFGHIJ',1,1,0) # duplicate ABCDE suppresses FGHIJ + print('signed allocation remains allocated only when list-update duplicate drops its ID') + + +if __name__=='__main__': + main() diff --git a/psp_emu/harness/probe_asd163_1aa_allocator_blacklist.py b/psp_emu/harness/probe_asd163_1aa_allocator_blacklist.py new file mode 100644 index 0000000..4cbe6e5 --- /dev/null +++ b/psp_emu/harness/probe_asd163_1aa_allocator_blacklist.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""ASD .163 0x1AA duplicate-drop changes signed 0x1A7 ID admission. + +Synthetic ID list; stub final 0x1AA revocation sweep. No accepted SRM, +real HDCP client, ASIC callback, card request, or cross-client effect. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE,REQ,SOURCE,MASK = 0xD33510,0x400040,0x401000,0x402000 +LISTCOUNT,BITMAP,ALLOCCOUNT = BASE+3577,BASE+0xDF4,BASE+0xDF8 + + +def scenario(batch: bytes, expected_status: int, expected_allocs: int) -> None: + asd=Component('asd',image_path=str(SIGNED)) + asd.uc.mem_map(0x400000,0x1C000) + def skip_sweep(uc,ea,size,data): + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + asd.uc.hook_add(UC_HOOK_CODE,skip_sweep,begin=0xD08404,end=0xD08405) + def request(op:int,*args:int) -> int: + asd.write(REQ,bytes(0x40)) + for i,v in enumerate((op,*args)):asd.wr32(REQ+4*i,v) + status=asd.run_function(0xD013B8,(REQ,),thumb=True, + count=160_000,timeout=3_000_000) + assert not asd.faults,(hex(op),status,asd.faults) + return status + def add_ids(data:bytes) -> None: + asd.write(SOURCE,data+bytes(160-len(data))) + assert request(0x1AA,SOURCE,len(data)//5)==0 + + add_ids(b'ABCDE') + add_ids(batch) + assert asd.read(LISTCOUNT,1)[0] == (2 if batch==b'FGHIJ' else 1) + asd.write(SOURCE,b'FGHIJ'+bytes(0xA0-5)) + asd.wr32(MASK,0) + status=request(0x1A7,6,SOURCE,1,MASK) + assert status==expected_status,(status,batch) + assert asd.rd32(BITMAP)==expected_allocs + assert asd.read(ALLOCCOUNT,1)[0]==expected_allocs + print(f'list batch={batch!r}: signed 0x1A7 ID FGHIJ status={status}, ' + f'allocated={expected_allocs}') + + +def main() -> None: + scenario(b'FGHIJ',23,0) # listed token rejected + scenario(b'ABCDEFGHIJ',0,1) # duplicate silently drops FGHIJ, then admitted + print('conditional signed list-update defect propagates to allocation admission') + + +if __name__=='__main__': + main() diff --git a/psp_emu/harness/probe_asd163_1aa_duplicate_tail.py b/psp_emu/harness/probe_asd163_1aa_duplicate_tail.py new file mode 100644 index 0000000..1ef30c3 --- /dev/null +++ b/psp_emu/harness/probe_asd163_1aa_duplicate_tail.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""ASD .163 0x1AA: a pre-existing duplicate suppresses later unique IDs. + +Signed dispatcher/dedup body execute; only final stateful revocation sweep +sub_D08404 is stubbed. Synthetic ASD-private list, no accepted SRM/card run. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE, LIST, COUNT = 0xD33510, 0xD33510+3284, 0xD33510+3577 +REQ, SRC = 0x400040, 0x401000 + + +def scenario(batch: bytes, expect: bytes) -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) + sweeps: list[int] = [] + def skip_sweep(uc, ea, size, data): + sweeps.append(ea) + uc.reg_write(UC_ARM_REG_R0, 0) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + asd.uc.hook_add(UC_HOOK_CODE, skip_sweep, begin=0xD08404, end=0xD08405) + + def request(entries: bytes, count: int, ptr: int = SRC) -> int: + assert len(entries)==count*5 and count<=32 + asd.write(SRC, entries + bytes(160-len(entries))) + asd.write(REQ,bytes(0x40)) + for i,v in enumerate((0x1AA,ptr,count)): + asd.wr32(REQ+4*i,v) + rv=asd.run_function(0xD013B8,(REQ,),thumb=True, + count=200_000,timeout=3_000_000) + assert not asd.faults,(rv,asd.faults) + return rv + + assert request(b'ABCDE',1)==0 + assert asd.read(COUNT,1)==b'\x01' and asd.read(LIST,5)==b'ABCDE' + assert request(batch,len(batch)//5)==0 + observed=asd.read(LIST,len(expect)) + assert asd.read(COUNT,1)[0]==len(expect)//5,(observed,asd.read(COUNT,1)) + assert observed==expect,(observed,expect) + assert len(sweeps)==2,sweeps + before=asd.read(LIST,20) + assert request(b'KLMNO',1,ptr=BASE)==2 # not an admitted caller input + assert asd.read(LIST,20)==before + print(f'input batch={batch!r} retained={observed!r}; sweep stubbed twice') + + +def main() -> None: + scenario(b'FGHIJ',b'ABCDEFGHIJ') # solo unique must append + scenario(b'ABCDEFGHIJ',b'ABCDE') # duplicate first suppresses unique tail + scenario(b'FGHIJABCDEKLMNO',b'ABCDEFGHIJ') # duplicate suppresses later third + scenario(b'FGHIJFGHIJ',b'ABCDEFGHIJFGHIJ') # new duplicate within batch not detected + print('signed 0x1AA: earlier duplicate drops tail; in-batch duplicate is appended twice') + + +if __name__=='__main__': + main() diff --git a/psp_emu/harness/probe_asd163_200_asic20_output.py b/psp_emu/harness/probe_asd163_200_asic20_output.py new file mode 100644 index 0000000..d31f07f --- /dev/null +++ b/psp_emu/harness/probe_asd163_200_asic20_output.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""ASD .163 0x200 signed ASIC-20 output footprint; SYS_DRV RPC modeled. +No live chip ID, real SYS_DRV result, or hardware effect. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE,UC_HOOK_MEM_WRITE +from unicorn.arm_const import UC_ARM_REG_LR,UC_ARM_REG_PC,UC_ARM_REG_R0 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +REQ,OUT,CFG=0x400040,0x401000,0xD397B0 + +def scenario(rpc_status:int,expected_offsets:list[int]): + asd=Component('asd',image_path=str(SIGNED));asd.uc.mem_map(0x400000,0x20000) + asd.wr32(CFG+32,20);asd.write(OUT,bytes([0xA5])*40) + asd.write(REQ,bytes(0x40));asd.wr32(REQ,0x200);asd.wr32(REQ+4,OUT) + hits=[];writes=[] + def model_rpc(uc,ea,size,_): + hits.append(ea) + uc.reg_write(UC_ARM_REG_R0,rpc_status) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + def record(uc,access,addr,size,value,_): + if OUT<=addr<OUT+40:writes.append((addr-OUT,size)) + asd.uc.hook_add(UC_HOOK_CODE,model_rpc,begin=0xD019A4,end=0xD019A5) + asd.uc.hook_add(UC_HOOK_MEM_WRITE,record) + rv=asd.run_function(0xD013B8,(REQ,),thumb=True,count=130_000,timeout=2_000_000) + assert not asd.faults,(rv,asd.faults) + assert rv==0 and hits==[0xD019A4],(rv,hits) + assert sorted(writes)==sorted((o,1 if o==0 else 4) + for o in expected_offsets),writes + assert asd.read(OUT+24,16)==bytes([0xA5])*16 + assert (asd.read(OUT+16,8)==bytes(8))==(rpc_status==0) + print(f'RPC modeled status {rpc_status}; signed 0x200 output stores {writes}; tail untouched') + +def main(): + scenario(1,[0,4,8]) + scenario(0,[0,4,8,16,20]) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_203_dio_config.py b/psp_emu/harness/probe_asd163_203_dio_config.py new file mode 100644 index 0000000..5d554f7 --- /dev/null +++ b/psp_emu/harness/probe_asd163_203_dio_config.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""ASD .163 signed 0x205→0x203 bounded DIO record *read* branch. +Synthetic global records only; intentionally avoids the 0x203 one-bit +callback branch and any card/SVC fabric operation. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +REQ,SRC,BASE=0x400040,0x401000,0xD29080 + +def main(): + asd=Component('asd',image_path=str(SIGNED));asd.uc.mem_map(0x400000,0x20000) + def rpc(op,*args): + asd.write(REQ,bytes(0x40)) + for i,x in enumerate((op,*args)):asd.wr32(REQ+4*i,x) + rv=asd.run_function(0xD013B8,(REQ,),thumb=True,count=100_000,timeout=2_000_000) + assert not asd.faults,(hex(op),rv,asd.faults) + return rv + ownmask,ownpayload=BASE+16,BASE+20 + adjacent=BASE+40 + marker=bytes(range(1,17));asd.write(ownpayload,marker) + asd.write(adjacent,bytes([0xCC])*16) + asd.write(SRC,bytes([0xA5])*16) + assert rpc(0x203,4,SRC,16)==2 # bounded index, no callback + assert asd.read(SRC,16)==bytes([0xA5])*16 + assert rpc(0x205,0,1)==0 and rpc(0x205,0,2)==0 + assert asd.rd32(ownmask)==3 + assert rpc(0x203,0,SRC,16)==0 + assert asd.read(SRC,16)==marker + assert asd.read(ownpayload,16)==marker + assert asd.read(adjacent,16)==bytes([0xCC])*16 + asd.write(SRC,bytes(16)) + assert rpc(0x203,0,SRC,16)==0 and asd.read(SRC,16)==bytes(16) + print('signed 0x205 one-bit setters select 0x203 global→caller copy; ' + 'zero caller checksum skips it, adjacent synthetic record unchanged') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_203_dio_config_write.py b/psp_emu/harness/probe_asd163_203_dio_config_write.py new file mode 100644 index 0000000..7babd03 --- /dev/null +++ b/psp_emu/harness/probe_asd163_203_dio_config_write.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""ASD .163 0x203 one-bit caller->global DIO record copy. +The two post-copy stateful link callbacks are intercepted, not executed; +synthetic record only, no live ASIC, hardware or second-client effect. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_PC,UC_ARM_REG_LR,UC_ARM_REG_R0 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +REQ,SRC,BASE=0x400040,0x401000,0xD29080 + +def main(): + asd=Component('asd',image_path=str(SIGNED));asd.uc.mem_map(0x400000,0x20000) + visits=[] + def skip_stateful(uc,ea,size,_): + visits.append(ea) + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + for ea in (0xD0BC80,0xD0DDDC): + asd.uc.hook_add(UC_HOOK_CODE,skip_stateful,begin=ea,end=ea+1) + def rpc(op,*args): + asd.write(REQ,bytes(0x40)) + for i,x in enumerate((op,*args)):asd.wr32(REQ+4*i,x) + rv=asd.run_function(0xD013B8,(REQ,),thumb=True,count=100_000,timeout=2_000_000) + assert not asd.faults,(hex(op),rv,asd.faults) + return rv + payload,neighbor=BASE+20,BASE+40 + asd.write(payload,bytes([0xAA])*16) + asd.write(neighbor,bytes([0xBB])*16) + marker=bytes(range(1,17));asd.write(SRC,marker) + assert rpc(0x203,0,SRC,16)==2 # no active bit: no write + assert asd.read(payload,16)==bytes([0xAA])*16 and not visits + assert rpc(0x205,0,1)==0 + assert asd.rd32(BASE+16)==1 + assert rpc(0x203,0,SRC,16)==0 + assert asd.read(payload,16)==marker + assert asd.read(SRC,16)==marker and asd.read(neighbor,16)==bytes([0xBB])*16 + assert visits==[0xD0BC80,0xD0DDDC],visits + print('signed 0x205->0x203 copied caller 16 bytes to synthetic global record; ' + 'two later stateful helpers modeled, adjacent record untouched') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_204_asic20_output.py b/psp_emu/harness/probe_asd163_204_asic20_output.py new file mode 100644 index 0000000..7a80368 --- /dev/null +++ b/psp_emu/harness/probe_asd163_204_asic20_output.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 0x204 ASIC-20 bounded-output synthetic callback check. + +Directly seeds the signed callback slot (does not run startup), explicitly +models only SVC 0x7B fixed-register reads. No card or real fabric effect. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE, UC_HOOK_MEM_WRITE +from unicorn.arm_const import UC_ARM_REG_PC, UC_ARM_REG_R1, UC_ARM_REG_R0 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +REQ,OUT,CFG=0x400040,0x401000,0xD397B0 + +def main(): + asd=Component('asd',image_path=str(SIGNED)) + asd.uc.mem_map(0x400000,0x20000) + asd.wr32(CFG+32,20) + asd.wr32(CFG+56,0xD0C5C5) # signed ASIC-20 sub_D0E10C slot + asd.wr32(0xD29048,0) + asd.write(OUT,bytes([0xA5])*32) + asd.write(REQ,bytes(0x40));asd.wr32(REQ,0x204);asd.wr32(REQ+4,OUT) + reads=[];writes=[] + def svc_read(uc,ea,size,_): + assert uc.reg_read(UC_ARM_REG_R1)!=0 + value=(0x1234abcd,0x55556666,0x77778888)[len(reads)] + uc.mem_write(uc.reg_read(UC_ARM_REG_R1),value.to_bytes(4,'little')) + reads.append(ea) + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,ea+3) # skip only 16-bit Thumb SVC + def record_write(uc,access,address,size,value,_): + if OUT<=address<OUT+32:writes.append((address,size)) + for ea in (0xD0C832,0xD0C854,0xD0C862): + asd.uc.hook_add(UC_HOOK_CODE,svc_read,begin=ea,end=ea+1) + asd.uc.hook_add(UC_HOOK_MEM_WRITE,record_write) + result=asd.run_function(0xD013B8,(REQ,),thumb=True,count=150_000,timeout=3_000_000) + assert not asd.faults,(result,asd.faults) + assert result==0 and reads==[0xD0C832,0xD0C854,0xD0C862],(result,reads) + assert writes==[(OUT+i,4) for i in (0,4,8,12)],writes + assert asd.read(OUT+16,16)==bytes([0xA5])*16 + print('signed 0x204 ASIC-20 path: three modeled fixed-register reads, ' + 'four dword caller stores within validated 32 bytes; tail untouched') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_205_206_invalid_side_effect.py b/psp_emu/harness/probe_asd163_205_206_invalid_side_effect.py new file mode 100644 index 0000000..98c182d --- /dev/null +++ b/psp_emu/harness/probe_asd163_205_206_invalid_side_effect.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Signed .163 0x205/0x206 invalid mask error-return state mutation. + +Only synthetic ASD globals and one modeled SVC 0x57; no card, HDCP or +independent hardware security effect. Uses signed dispatcher and leaves. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_PC + +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +REQ,GLOBAL,STATE,DEADLINE=0x400040,0xD29080+16+20,0xD397B0+32,0xD1BA24 + +def scenario(op:int, mask:int, initial_state:int, expected_state:int, + expected_deadline:int, expected_status:int): + asd=Component('asd',image_path=str(SIGNED)) + asd.uc.mem_map(0x400000,0x2000) + svc57=[] + def skip_svc57(uc,ea,size,user_data): + svc57.append(ea) + uc.reg_write(UC_ARM_REG_PC,0xD0B767) # model only SVC; signed STR already ran + asd.uc.hook_add(UC_HOOK_CODE,skip_svc57,begin=0xD0B764,end=0xD0B765) + asd.wr32(STATE,20) # signed sub_D0EB3C uses status-table entry zero + asd.wr32(GLOBAL,initial_state) + asd.wr32(DEADLINE,0x12345678) + asd.write(REQ,bytes(0x40)) + asd.wr32(REQ,op) + asd.wr32(REQ+4,1) + asd.wr32(REQ+8,mask) + status=asd.run_function(0xD013B8,(REQ,),thumb=True, + count=100_000,timeout=3_000_000) + assert not asd.faults,(hex(op),status,asd.faults) + assert status==expected_status,(hex(op),status) + assert asd.rd32(GLOBAL)==expected_state,(hex(op),hex(asd.rd32(GLOBAL))) + assert asd.rd32(DEADLINE)==expected_deadline,(hex(op),hex(asd.rd32(DEADLINE))) + assert len(svc57)==(1 if op==0x205 else 0),(hex(op),svc57) + print(f'op={op:#x} mask={mask:#x} return={status:#x} ' + f'word={asd.rd32(GLOBAL):#x} deadline={asd.rd32(DEADLINE):#x}') + + +def main(): + scenario(0x206,0,0,0,0xffffffff,2) # invalid bit, timeout side effect + scenario(0x206,1,1,0,0xffffffff,0) # valid clear, zero state + scenario(0x206,0,1,1,0x12345678,2) # still-active state: no disable + scenario(0x205,0,0,0,1000,2) # invalid bit, session-op deadline write + scenario(0x205,1,0,1,1000,0) # valid set control + print('signed dispatcher does not gate session/time side effects on bitmask validation') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_207_asic20_output.py b/psp_emu/harness/probe_asd163_207_asic20_output.py new file mode 100644 index 0000000..4da06e4 --- /dev/null +++ b/psp_emu/harness/probe_asd163_207_asic20_output.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 0x207 ASIC-20 callback output width with synthetic +config and source pointer. No card, real private values or fabric access. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +REQ,OUT,SOURCE=0x400040,0x400100,0x400200 +SLOT,PTR=0xD397B0+0x3C,0xD39848 + +def main(): + asd=Component('asd',image_path=str(SIGNED)) + asd.uc.mem_map(0x400000,0x3000) + oldslot=asd.rd32(SLOT);oldptr=asd.rd32(PTR) + def run(out): + asd.write(REQ,bytes(0x40));asd.wr32(REQ,0x207);asd.wr32(REQ+4,out) + return asd.run_function(0xD013B8,(REQ,),thumb=True,count=100_000,timeout=2_000_000) + try: + asd.wr32(SLOT,0xD0C351) # signed ASIC-20 thunk to signed getter + asd.wr32(PTR,SOURCE) + asd.wr32(SOURCE,0x12345678) # synthetic public marker + asd.write(OUT,b'Z'*16) + ok=run(OUT) + assert ok==0 and asd.read(OUT,16)==bytes.fromhex('78 56 34 12')+b'Z'*12,(ok,asd.read(OUT,16).hex()) + bad=run(PTR) + assert bad==2 and asd.rd32(PTR)==SOURCE and asd.rd32(SOURCE)==0x12345678,bad + assert not asd.faults,asd.faults + print('signed ASD 0x207 ASIC20 synthetic callback: validated output received exactly 4 public bytes, +4..+15 intact; ASD-private output pointer rejected; no live pointer value') + finally: + asd.wr32(SLOT,oldslot);asd.wr32(PTR,oldptr) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_208_sysdrv1079_forward.py b/psp_emu/harness/probe_asd163_208_sysdrv1079_forward.py new file mode 100644 index 0000000..16a8d08 --- /dev/null +++ b/psp_emu/harness/probe_asd163_208_sysdrv1079_forward.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Connect signed ASD .163 0x208 event packing to signed SYS_DRV .54 +0x1079, capturing SVC7C. Synthetic SVCF2/sOS caps; no real fabric. +""" +from pathlib import Path +import sys +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component,sysdrv_env +ROOT=Path(__file__).resolve().parents[2] +ASD=ROOT/'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +SYSDRV=ROOT/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +REQ=0x400040 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SYSDRV.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 and sd.rd32(0xE26314)==2 + sd.svc.register(0x76,lambda c,n:int((c.uc.reg_read(UC_ARM_REG_R0)&~0x10)==0)) + writes=[];sent=[] + sd.svc.register(0x7C,lambda c,n:writes.append(tuple(c.uc.reg_read(reg) for reg in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))) or 0) + sr=sd.alloc(0x100) + asd=Component('asd',image_path=str(ASD)) + asd.uc.mem_map(0x400000,0x2000) + def forward(c,n): + service=c.uc.reg_read(UC_ARM_REG_R0) + msg=c.uc.reg_read(UC_ARM_REG_R1) + op=asd.rd32(msg);value=asd.rd32(msg+14*4) + sent.append((service,op,value)) + assert service==1 and op==0x1079,(service,hex(op)) + sd.write(sr,bytes(0x100)) + for i,v in ((0,op),(1,2),(14,value)): + sd.wr32(sr+4*i,v) + return sd.run_function(0xE087E4,(sr,2,0,0),count=200_000,timeout=2_000_000) + asd.svc.register(0xF2,forward) + def run(value): + asd.write(REQ,bytes(0x40));asd.wr32(REQ,0x208);asd.wr32(REQ+4,value) + return asd.run_function(0xD013B8,(REQ,),thumb=True,count=200_000,timeout=2_000_000) + assert run(0x11223344)==0 and sent==[(1,0x1079,0x44331122)] and writes==[(0x3B30600,0x44331122,4)],(sent,writes) + assert run(0x112233EE)==0 and sent[-1]==(1,0x1079,0xEE331122) and len(writes)==1,(sent,writes) + assert not asd.faults and not sd.faults,(asd.faults,sd.faults) + print('signed ASD 0x208 packed 0x11223344 -> signed SYS_DRV 0x1079 requested fixed SVC7C word 0x44331122; low-byte EE input -> SYS_DRV top-byte EE skip; only synthetic relay/capture, no hardware') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_asd163_20b_private_store.py b/psp_emu/harness/probe_asd163_20b_private_store.py new file mode 100644 index 0000000..31f0e5a --- /dev/null +++ b/psp_emu/harness/probe_asd163_20b_private_store.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Isolated signed ASD .163 RPC 0x20B: missing nested-output pointer check. + +Synthetic state and ASD-private mapping; does NOT emulate sOS SVC 0xF2, +GCP ROP, or live card PTE/permissions. No device or secret state touched. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_MEM_WRITE + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +DISPATCH = 0xD013B8 +OUTPUT_HANDLER = 0xD0DC14 +STATUS_GLOBAL = 0xD1BA28 +CALLER_REQ = 0x400040 +SYNTH_PRIVATE = 0xD4AF00 # isolated emulator's ASD RAM, not a measured card address + + +def test_state(value: int) -> None: + asd = Component('asd', image_path=str(SIGNED)) + assert Path(asd.image_path).read_bytes() == SIGNED.read_bytes() + asd.uc.mem_map(0x400000, 0x1C000) # synthetic GCP-sized caller window + asd.wr32(STATUS_GLOBAL, value) # bypass optional state-255 callback + asd.wr32(SYNTH_PRIVATE, 0xA5A5A5A5) + asd.write(CALLER_REQ, b'\x00' * 0x40) + asd.wr32(CALLER_REQ + 0, 0x20B) + asd.wr32(CALLER_REQ + 4, SYNTH_PRIVATE) + writes = [] + + def observe(uc, access, addr, size, val, data): + if addr == SYNTH_PRIVATE: + writes.append((addr, size, val & 0xFFFFFFFF)) + asd.uc.hook_add(UC_HOOK_MEM_WRITE, observe, begin=SYNTH_PRIVATE, + end=SYNTH_PRIVATE + 3) + rv = asd.run_function(DISPATCH, (CALLER_REQ,), thumb=True, + count=30000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + assert writes == [(SYNTH_PRIVATE, 4, value)] and asd.rd32(SYNTH_PRIVATE) == value, writes + print('signed dispatch 0x20B: state', value, 'private store at synthetic ASD RAM:', + 'size=4 value=', value, 'status=', hex(rv)) + + # Negative control: signed 0x207 explicitly checks the same pointer with + # asd_validate_ptr(ptr,4) and rejects this ASD-private destination. + asd.wr32(CALLER_REQ, 0x207) + asd.wr32(SYNTH_PRIVATE, 0xA5A5A5A5) + before = len(writes) + rejected = asd.run_function(DISPATCH, (CALLER_REQ,), thumb=True, + count=30000, timeout=2_000_000) + assert rejected == 2 and not asd.faults and len(writes) == before + assert asd.rd32(SYNTH_PRIVATE) == 0xA5A5A5A5 + print('control opcode 0x207: private pointer rejected', rejected) + + +if __name__ == '__main__': + test_state(0) + test_state(1) diff --git a/psp_emu/harness/probe_asd163_srm_hdcp1_multi_revocation.py b/psp_emu/harness/probe_asd163_srm_hdcp1_multi_revocation.py new file mode 100644 index 0000000..a06b986 --- /dev/null +++ b/psp_emu/harness/probe_asd163_srm_hdcp1_multi_revocation.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Signed ASD .163 HDCP1/0x18E SRM bitmap cleanup, synthetic only. + +Uses signed 0x18E allocator and signed SRM update helper. Plants UNSIGNED v9 +SRM records in synthetic ASD RAM, bypassing signature acceptance. Executes the +signed channel-state helper and stubs only the ASIC callback. No card request. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE, SRM = 0xD33510, 0xD15030 +BITMAP, COUNT = BASE+4092, BASE+4108 # HDCP1 128-slot table, distinct from HDCP2 +0xDF4 +REQ, SOURCE, MASK = 0x400040, 0x401000, 0x402000 + + +def allocate(asd: Component, channel: int, token: bytes) -> None: + assert len(token) == 5 + asd.write(SOURCE, token + b'\0' * (0x280 - 5)) + asd.write(MASK, b'\0' * 16) + asd.write(REQ, b'\0' * 0x40) + for i, v in enumerate((0x18E, channel, SOURCE, 1, MASK)): + asd.wr32(REQ + 4*i, v) + rv = asd.run_function(0xD013B8, (REQ,), thumb=True, + count=60000, timeout=2_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + + +def run_case(slot0_match: bool, expected_bitmap: int, expected_calls: int) -> None: + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) # synthetic caller window, NOT live sOS PTEs + allocate(asd, 5, b'ABCDE') + allocate(asd, 6, b'FGHIJ') + assert asd.rd32(BITMAP) == 3 and asd.read(COUNT, 1) == b'\x02' + assert [asd.rd32(BASE+3580+4*i) for i in (0, 1)] == [5, 6] + for channel in (5, 6): + asd.wr32(BASE+92*channel+4, 1) + asd.wr32(BASE+92*channel+88, 1) # synthetic active link state + + # The signed HDCP1 SRM matcher reverses each stored five-byte record for + # v9 membership: ABCDE -> EDCBA, FGHIJ -> JIHGF. + revoked = (b'EDCBA' if slot0_match else b'') + b'JIHGF' + asd.write(SRM, b'\0' * 0x1400) + asd.write(SRM, b'\x91') + asd.write(SRM+6, b'\x01\x88') + asd.write(SRM+8, (len(revoked)//5 << 6).to_bytes(2, 'little')) + asd.write(SRM+12, revoked) + + states, callbacks = [], [] + asd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, size, data: states.append(ea), + begin=0xD0B438, end=0xD0B439) + def skip_asic(uc, ea, size, data): + callbacks.append(ea) + uc.reg_write(UC_ARM_REG_R0, 0) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + asd.uc.hook_add(UC_HOOK_CODE, skip_asic, begin=0xD0A7A8, end=0xD0A7A9) + + rv = asd.run_function(0xD0CB70, (0,), thumb=True, + count=200000, timeout=4_000_000) + assert rv == 0 and not asd.faults, (rv, asd.faults) + assert len(states) == len(callbacks) == expected_calls, (states, callbacks) + for channel in ((5, 6) if slot0_match else (6,)): + assert asd.rd32(BASE+92*channel+88) == 0 + assert asd.rd32(BITMAP) == expected_bitmap and asd.read(COUNT, 1) == b'\x01', ( + hex(asd.rd32(BITMAP)), asd.read(COUNT, 1).hex()) + print(f'HDCP1 signed helper: {expected_calls} match(es), ' + f'bitmap={expected_bitmap:02b}, count=1, ASIC callback stubbed') + + +def main() -> None: + run_case(False, expected_bitmap=1, expected_calls=1) + run_case(True, expected_bitmap=2, expected_calls=2) + print('single slot-1 match clears bit1; dual match visits both channel ' + 'helpers but leaves bit1 allocated (UNSIGNED planted SRM only)') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_asd163_srm_multi_revocation.py b/psp_emu/harness/probe_asd163_srm_multi_revocation.py new file mode 100644 index 0000000..34251fc --- /dev/null +++ b/psp_emu/harness/probe_asd163_srm_multi_revocation.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Synthetic ASD .163 SRM revocation: two matches, only first bitmap bit cleared. + +Direct signed helper entry with a planted UNSIGNED SRM header. The signed +channel-state helper runs; only the per-channel ASIC/link callback is stubbed +at entry to avoid fabric activity. The signed bitmap cleanup is unmodified. This is +NOT a valid signed-SRM or live-device exploit test. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/versions_win/elf_all/asd_33.0.0.163_18debcc4.elf' +BASE, SRM = 0xD33510, 0xD15030 +BITMAP, COUNT = BASE+0xDF4, BASE+0xDF8 +REQ, SOURCE, MASK = 0x400040, 0x401000, 0x402000 + + +def allocate(asd: Component, owner: int, token: bytes) -> None: + assert len(token) == 5 + asd.write(SOURCE, token + b'\0' * (0xA0 - 5)) + asd.wr32(MASK, 0) + asd.write(REQ, b'\0' * 0x40) + for i, v in enumerate((0x1A7, owner, SOURCE, 1, MASK)): + asd.wr32(REQ + 4*i, v) + status = asd.run_function(0xD013B8, (REQ,), thumb=True, + count=50000, timeout=2_000_000) + assert status == 0 and not asd.faults, (status, asd.faults) + + +def run_case(revoked: bytes, expected_bitmap: int, expected_stubs: int) -> None: + assert len(revoked) in (5, 10) + asd = Component('asd', image_path=str(SIGNED)) + asd.uc.mem_map(0x400000, 0x1C000) + allocate(asd, 5, b'ABCDE') + allocate(asd, 6, b'FGHIJ') + assert asd.rd32(BITMAP) == 3 and asd.read(COUNT, 1) == b'\x02' + assert [asd.rd32(BASE+0xD74+4*i) for i in range(2)] == [5, 6] + for owner in (5, 6): + asd.wr32(BASE + 92*owner + 4, 1) # synthetic active channel + asd.wr32(BASE + 92*owner + 88, 1) # nonzero -> revocation queue + + # Directly plant synthetic SRM v9 records. Signature verification is NOT + # run; this data must never be described as an accepted/valid signed SRM. + asd.write(SRM, b'\0' * 0x1400) + asd.write(SRM, b'\x91') + asd.write(SRM + 6, b'\x01\x88') # declared v9 length 0x188 (>0x186) + asd.write(SRM + 8, (len(revoked)//5 << 6).to_bytes(2, 'little')) + asd.write(SRM + 12, revoked) + + state_visits: list[int] = [] + link_stubs: list[int] = [] + def observe_signed_state_helper(uc, address, size, data): + state_visits.append(address) # execute the actual instruction afterward + def skip_link_callback(uc, address, size, data): + link_stubs.append(address) + uc.reg_write(UC_ARM_REG_R0, 0) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + asd.uc.hook_add(UC_HOOK_CODE, observe_signed_state_helper, + begin=0xD0B438, end=0xD0B439) + asd.uc.hook_add(UC_HOOK_CODE, skip_link_callback, + begin=0xD0AEB0, end=0xD0AEB1) + + status = asd.run_function(0xD0CB70, (0,), thumb=True, + count=180000, timeout=4_000_000) + assert status == 0 and not asd.faults, (status, asd.faults) + assert state_visits.count(0xD0B438) == expected_stubs, state_visits + assert link_stubs.count(0xD0AEB0) == expected_stubs, link_stubs + for owner in ((6,) if expected_stubs == 1 else (5, 6)): + assert asd.rd32(BASE + 92*owner + 88) == 0 # signed state-helper effect + assert asd.rd32(BITMAP) == expected_bitmap and asd.read(COUNT, 1) == b'\x01', ( + hex(asd.rd32(BITMAP)), asd.read(COUNT, 1).hex()) + print(f'signed helper: {expected_stubs} SRM record match(es), ' + f'cleanup bitmap=0b{expected_bitmap:02b} count=1; ' + 'channel-state helper ran, ASIC callback stubbed, no signature checked') + + +def run() -> None: + run_case(b'FGHIJ', expected_bitmap=1, expected_stubs=1) + run_case(b'ABCDEFGHIJ', expected_bitmap=2, expected_stubs=2) + print('controls: allocator installed owners 5/6 both times; ' + 'a single slot-1 revocation clears slot 1, but two revocations ' + 'clear only slot 0') + + +if __name__ == '__main__': + run() diff --git a/psp_emu/harness/probe_asd20b_model_pte.py b/psp_emu/harness/probe_asd20b_model_pte.py new file mode 100644 index 0000000..6012b51 --- /dev/null +++ b/psp_emu/harness/probe_asd20b_model_pte.py @@ -0,0 +1,92 @@ +"""Read-only emulator estimate for the hardware order: LOAD_ASD before LOAD_TA GCP.""" +import sys, struct +sys.path.insert(0,'/home/user/dev/amd-psp-re/psp_emu/harness') +sys.path.insert(0,'/home/user/dev/amd-psp-re/psp_emu') +import gcp_fe08_fullsys as H +from pspemu import armmmu +from unicorn import UC_HOOK_BLOCK +BM, M = H.BM, H.M +c, ccp, irqs, exc = BM.build() +run, _ = M.make_det_runner(c, irqs) +run(6_000_000) +M.install_debug_gpio(c) +box = M.Mailbox(c, irqs) +c.uc.mem_write(BM.RING_PA,b'\x00'*BM.RING_SZ) +rp = bytearray(0x1c) +struct.pack_into('<I', rp,0x10,BM.RING_PA) +struct.pack_into('<I', rp,0x18,BM.RING_SZ) +st,_ = box.submit(2,bytes(rp),timeout=12,runner=run) +assert st==0,st +ring=M.Ring(c,irqs,BM.RING_PA,BM.RING_SZ,BM.CMDBUF_PA,run) +c.uc.mem_write(H.PLATFORM_ID_BYTE,bytes([H.NAVI31_ASIC_ID])) +asd=open(H.ASD_FULL,'rb').read() +base=0x04600000 +for b in (base,base+M.Ring.ALIAS): + try:c.uc.mem_map(b&~0xfff,(len(asd)+0xffff)&~0xfff) + except Exception:pass + c.uc.mem_write(b,asd) +obs=[];count=[0] +def signed_sos_process_l2(uc,label): + # sOS .54 sub_6D84: 0xD00000..0xDFFFFF uses sub_4350(pid), + # whose per-process table lives through pointer at 0x8114. + rd=lambda a:int.from_bytes(uc.mem_read(a,4),'little') + context=rd(0x80D8) + implicit=uc.mem_read(context+75,1)[0] + override=uc.mem_read(context+76,1)[0] + pid=implicit if override==255 else override + proc_table=rd(0x8114) + l2=rd(proc_table+96*pid+92) + print('%s signed-sOS pid=%d implicit=%d override=%d L2=%#x'%(label,pid,implicit,override,l2),flush=True) + for va in (0xD1BA28,0xD29080,0xD39834,0xD4AF00): + pte=rd(l2+4*((va>>12)&255)) if l2 else 0 + print('%s VA %#x proc-PTE %#x PA %#x AP=%d APX=%d'%(label,va,pte,(pte&0xfffff000)|(va&0xfff) if pte&3 else 0,(pte>>4)&3,(pte>>9)&1),flush=True) +def on_asd(uc,a,size,user): + count[0]+=1 + if count[0]==1: + signed_sos_process_l2(uc,'ASD first') + n=armmmu.ttbcr(uc)&7;tt=armmmu.read_cp15(uc,2,0,0,0)&~((1<<(14-n))-1) + for va0 in (0x41061C,0xD1BA28,0xD29080,0xD39834,0xD4AF00): + i1=tt+4*(va0>>20);e1=int.from_bytes(uc.mem_read(i1,4),'little') + if (e1&3)==1: + i2=(e1&0xfffffc00)+4*((va0>>12)&255) + e2=int.from_bytes(uc.mem_read(i2,4),'little') + pa0=(e2&0xfffff000)|(va0&4095) + print('ASD PTE VA %#x L1@%#x=%#x L2@%#x=%#x phys=%#x AP=%d APX=%d'%(va0,i1,e1,i2,e2,pa0,(e2>>4)&3,(e2>>9)&1),flush=True) + else:print('ASD PTE VA %#x L1=%#x type=%d'%(va0,e1,e1&3),flush=True) + if count[0]<=3 or count[0]%1024==0: + va=0xD4AF00 + obs.append((a,armmmu.ttbr0(uc),armmmu.translate_live(uc,va))) +c.uc.hook_add(UC_HOOK_BLOCK,on_asd,begin=0xD00100,end=0xDFFFFF) +ld=bytearray(0x20) +for off,val in ((0,base),(8,len(asd)),(12,H.GW.CMDBUF2_PA+0x10000),(20,0x1000)): + struct.pack_into('<I',ld,off,val) +print('LOAD_ASD first, container',H.ASD_FULL,flush=True) +ring.submit(4,bytes(ld),timeout=30,probe=[]) +BM.drain_until(run,lambda:ring.response()[1]!=0 or ring.response()[0]!=0,slices=4,blocks=2_000_000) +print('ASD blocks',count[0],'sampled VA->PA',[(hex(a),hex(tt),hex(pa) if pa is not None else None) for a,tt,pa in obs][:15],flush=True) +print('post-ASD ring',ring.response(),'service2',c.uc.mem_read(H.SERVICE_TABLE+2,1)[0],flush=True) +print('post-ASD current TTBR0',hex(armmmu.ttbr0(c.uc)),'D4AF00 translation',hex(armmmu.translate_live(c.uc,0xD4AF00) or 0),flush=True) +if not obs or obs[0][2] is None:raise SystemExit(1) +# Now load GCP in the same order as the hardware script and sample the shared MMU map. +cont=H.NT.pick_container('gcp') +img=open(cont,'rb').read() +for b in (H.FT.TA_IMAGE_PA,H.FT.TA_IMAGE_PA+M.Ring.ALIAS): + c.uc.mem_write(b,img) +for b in (H.GW.CMDBUF2_PA,H.GW.CMDBUF2_PA+M.Ring.ALIAS): + try:c.uc.mem_map(b&~0xfff,0x8000) + except Exception:pass +ld=bytearray(0x20) +for off,val in ((0,H.FT.TA_IMAGE_PA),(8,len(img)),(12,H.GW.CMDBUF2_PA),(20,0x8000)): + struct.pack_into('<I',ld,off,val) +gcp=[] +def on_gcp(uc,a,size,user): + if not gcp: + signed_sos_process_l2(uc,'GCP first') + gcp.append((a,armmmu.ttbr0(uc),armmmu.translate_live(uc,0x41061C),armmmu.translate_live(uc,0xD4AF00))) +c.uc.hook_add(UC_HOOK_BLOCK,on_gcp,begin=0x400100,end=0x40FFFF) +ring.submit(1,bytes(ld),timeout=30,probe=[]) +BM.drain_until(run,lambda:bool(gcp) and ring.response()[1]!=0,slices=40,blocks=2_000_000) +print('GCP first block TTBR0, GCP VA->PA, ASD VA->PA:',[(hex(a),hex(tt),hex(pg) if pg is not None else None,hex(pa) if pa is not None else None) for a,tt,pg,pa in gcp],flush=True) +print('post-GCP ring',ring.response(),'service2',c.uc.mem_read(H.SERVICE_TABLE+2,1)[0],flush=True) +print('post-GCP current TTBR0',hex(armmmu.ttbr0(c.uc)),'ASD target PA',hex(armmmu.translate_live(c.uc,0xD4AF00) or 0),flush=True) +if not gcp:raise SystemExit(1) diff --git a/psp_emu/harness/probe_hdcp52_alloc_failure.py b/psp_emu/harness/probe_hdcp52_alloc_failure.py new file mode 100644 index 0000000..6594903 --- /dev/null +++ b/psp_emu/harness/probe_hdcp52_alloc_failure.py @@ -0,0 +1,68 @@ +"""Bounded signed-HDCP .52 allocation-failure trace (no hardware calls). + +The service-1 stub compares failing/successful opcode 0x30 responses, +leaves the out-handle untouched only on failure, and logs the subsequent +opcode 0x33 request. It does *not* emulate the real SYS_DRV fatal path; +that remains signed-code evidence, separately. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from pspemu import Component, consts # noqa: E402 + +SIGNED_HDCP = os.path.abspath(os.path.join( + os.path.dirname(__file__), "..", "..", "psp_out", "versions_win", "elf_all", + "hdcp_23.0.0.52_c6b25da3.elf")) + + +class ControlledCrypto: + def __init__(self, fail_allocate): + self.fail_allocate = fail_allocate + self.calls = [] + + def __call__(self, comp, req): + op = comp.rd32(req) + field38 = comp.rd32(req + 0x38) # type for 0x30, handle for 0x33 + self.calls.append((op, field38)) + if op == 0x30: + if self.fail_allocate: + # No write to req+0x40: matches the signed SYS_DRV failure case. + return 0x80000001 + comp.wr32(req + 0x40, 0x10) + return 0 + if op == 0x33: + return 0x80000001 + if op == 0x31: + return 0 # successful allocation is retired on populate error + raise AssertionError(f"unexpected SYS_DRV opcode {op:#x}") + + +def main(): + for fail, expected_handle in ((True, 0), (False, 0x10)): + comp = Component("hdcp", image_path=SIGNED_HDCP) + crypto = ControlledCrypto(fail_allocate=fail) + comp.set_rpc_services({consts.SERVICE_CRYPTO: crypto}) + src = comp.alloc(0x200) + comp.write(src + 0xC8, b"\x12" * 0x20) + # Four AAPCS args for the signed routine, with R0 non-null. This + # tests its local error path, not host HDCP message-11 reachability. + ret = comp.run_function(0x4042C4, (src, 0x20, 0, 0), thumb=True, + count=50000, timeout=2_000_000) + print("signed_hdcp_image:", os.path.basename(comp.image_path)) + print("fail_allocate:", fail) + print("svc1_calls (opcode, field+0x38):", + [(hex(op), hex(field38)) for op, field38 in crypto.calls]) + print("hdcp_return:", hex(ret)) + print("faults:", comp.faults) + expected = [(0x30, 0xA0000004), (0x33, expected_handle)] + if not fail: + expected.append((0x31, 0x10)) + assert crypto.calls == expected, crypto.calls + assert ret == 0x80000001 and not comp.faults + + +if __name__ == "__main__": + main() diff --git a/psp_emu/harness/probe_hdcp52_connected_zero_handle.py b/psp_emu/harness/probe_hdcp52_connected_zero_handle.py new file mode 100644 index 0000000..8af1352 --- /dev/null +++ b/psp_emu/harness/probe_hdcp52_connected_zero_handle.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""One connected signed-HDCP -> signed-SYS_DRV emulator path. + +Inject ONLY a no-free-slot return at the real SYS_DRV 0x30 allocator +helper (no fabricated out-handle). Forward the resulting real signed HDCP +0x33 request to the real signed SYS_DRV 0x33 handler, stopping before its +fatal routine executes. This is a +bounded two-component Python bridge, NOT full sOS boot, card reachability, +or honest allocation exhaustion. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component, consts # noqa: E402 + +REPO = Path(__file__).resolve().parents[2] +HDCP = REPO / 'psp_out/versions_win/elf_all/hdcp_23.0.0.52_c6b25da3.elf' +SYSDRV = REPO / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +HDCP_CALLER = 0x4042C4 +SYSDRV_ALLOCATE = 0xE1CAD4 +NO_FREE_SLOT = 0xE0243C +SYSDRV_POPULATE = 0xE1EA54 +SYSDRV_FATAL = 0xE0F890 + + +def main() -> None: + sd = Component('sys_drv', image_path=str(SYSDRV)) + hdcp = Component('hdcp', image_path=str(HDCP)) + sd_req = sd.alloc(0x80) + sd_attrs = sd.alloc(0x20) + observed, fatal, no_slot, alloc_returns = [], [], [], [] + + def force_no_slot(uc, ea, size, user): + no_slot.append(ea) + uc.reg_write(UC_ARM_REG_R0, 0xFFFFFFFF) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + + sd.uc.hook_add(UC_HOOK_CODE, force_no_slot, + begin=NO_FREE_SLOT, end=NO_FREE_SLOT) + + def stop_before_fatal(uc, ea, size, user): + fatal.append(ea) + uc.emu_stop() + + sd.uc.hook_add(UC_HOOK_CODE, stop_before_fatal, + begin=SYSDRV_FATAL, end=SYSDRV_FATAL) + + def service_one(caller: Component, request: int) -> int: + opcode, field38 = caller.rd32(request), caller.rd32(request + 0x38) + observed.append((opcode, field38)) + if opcode == 0x30: + assert field38 == 0xA0000004, observed + sd.write(sd_req, caller.read(request, 0x7C)) + rv = sd.run_function(SYSDRV_ALLOCATE, (sd_req + 0x38, 1), + thumb=True, count=30000, timeout=2_000_000) + alloc_returns.append(rv) + assert no_slot == [NO_FREE_SLOT] and rv != 0 and not sd.faults + assert sd.rd32(sd_req + 0x40) == 0 # failed allocator didn't issue handle + return rv + assert opcode == 0x33 and field38 == 0, observed + sd.write(sd_req, caller.read(request, 0x7C)) + sd.run_function(SYSDRV_POPULATE, (sd_req, sd_attrs, 1, 1), + thumb=True, count=20000, timeout=2_000_000) + assert fatal == [SYSDRV_FATAL] and not sd.faults, (fatal, sd.faults) + # Halt HDCP as soon as its second RPC has reached the signed SYS_DRV + # fatal branch. We make no assertion about a later return or cleanup. + caller.uc.emu_stop() + return 0x80000001 + + hdcp.set_rpc_services({consts.SERVICE_CRYPTO: service_one}) + source = hdcp.alloc(0x200) + hdcp.write(source + 0xC8, b'\x12' * 0x20) + hdcp.run_function(HDCP_CALLER, (source, 0x20, 0, 0), + thumb=True, count=50000, timeout=2_000_000) + assert observed == [(0x30, 0xA0000004), (0x33, 0)], observed + assert fatal == [SYSDRV_FATAL] and not hdcp.faults and not sd.faults + print('signed_hdcp:', HDCP.name, 'signed_sysdrv:', SYSDRV.name) + print('bridge requests:', [(hex(op), hex(field)) for op, field in observed]) + print('real SYS_DRV 0x30 handler:', hex(SYSDRV_ALLOCATE), + 'forced no-slot helper:', [hex(x) for x in no_slot], + 'return:', [hex(x) for x in alloc_returns]) + print('real SYS_DRV 0x33 handler:', hex(SYSDRV_POPULATE), + 'fatal entry (stopped):', [hex(x) for x in fatal]) + print('HDCP stopped at second RPC; no host path, honest pool exhaustion, or hardware effect modeled') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_opm51_cmd11_global_flag.py b/psp_emu/harness/probe_opm51_cmd11_global_flag.py new file mode 100644 index 0000000..837df6e --- /dev/null +++ b/psp_emu/harness/probe_opm51_cmd11_global_flag.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Signed OPM .51 cmd 11 global flag write and entry-length controls. +Synthetic host memref. Never invokes this hanging command on a card. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import Component,memmap +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/versions_win/elf_all/opm_25.0.0.51_97f44863.elf' +PARAM,HOST,FLAG=0x500000,0x501000,0x418F84 + +def scenario(size:int,expect:int): + # Synthetic OPM component map, not a measured live loader layout. + def opm_map(): + cm=memmap.ComponentMap('opm',0x400100) + cm.add('image',0x400000,0x20000) + return memmap._common(cm,0x4F0000,0x10000,0x600000,0x20000) + memmap.MAPS['opm']=opm_map + opm=Component('opm',image_path=str(SIGNED)) + opm.uc.mem_map(0x500000,0x10000) + opm.write(PARAM,bytes(0x40));opm.write(HOST,bytes(0x3D80)) + opm.wr32(PARAM,HOST);opm.wr32(PARAM+4,size) + opm.wr32(HOST,11);opm.wr32(HOST+0x10,0x12345678) + opm.wr32(FLAG,0xAABBCCDD) + rv=opm.run_function(0x402AE4,(0,0xF00,7,PARAM),thumb=True, + count=160_000,timeout=2_000_000) + assert not opm.faults,(rv,opm.faults) + assert opm.rd32(FLAG)==expect,(hex(rv),hex(opm.rd32(FLAG))) + print(f'declared size={size:#x} result={rv:#x} global flag={opm.rd32(FLAG):#x}') + +def main(): + scenario(0x3D53,0xAABBCCDD) # signed entry rejects before cmd 11 store + scenario(0x3D54,0x12345678) # signed cmd 11 stores host scalar +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sos54_6c_missing_resource.py b/psp_emu/harness/probe_sos54_6c_missing_resource.py new file mode 100644 index 0000000..ad3b3d0 --- /dev/null +++ b/psp_emu/harness/probe_sos54_6c_missing_resource.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Signed sOS .54 SVC6C leaf with a synthetic absent resource 0. +The prior teardown helper is stubbed to its signed not-found status 43; +page zero is deliberately protected to pinpoint the unchecked dereference. +No hardware or real kernel resource is touched. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE, UC_PROT_NONE +from unicorn.arm_const import UC_ARM_REG_R0, UC_ARM_REG_PC, UC_ARM_REG_LR +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu.component import Component, Fault +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sos_0.49.0.54_3dcb63e8.elf' + +def main(): + sos=Component('sos',image_path=str(SIGNED),config={'randomize_canary':False}) + assert Path(sos.image_path).read_bytes()==SIGNED.read_bytes() + table=sos.alloc(0x120) + sos.wr32(0x8170,table) + sos.write(table,bytes(0x120)) # synthetic resource 0 has a null descriptor pointer + seen=[] + def not_found(uc,ea,size,user): + seen.append(uc.reg_read(UC_ARM_REG_R0)) + uc.reg_write(UC_ARM_REG_R0,43) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sos.uc.hook_add(UC_HOOK_CODE,not_found,begin=0x6918,end=0x6919) + sos.uc.mem_protect(0,0x1000,UC_PROT_NONE) + assert sos.run_function(0x6828,(32,),count=12000,thumb=True)==43 and not seen + try: + sos.run_function(0x6828,(0,),count=12000,timeout=3_000_000,thumb=True) + raise AssertionError('missing resource reached unexpected normal return') + except Fault as fault: + assert (fault.addr,fault.pc)==(0,0x68A2) and seen==[0],(fault,seen) + print('signed sOS .54 sub_6828(0): stubbed prior not-found(43) ignored; synthetic null resource pointer dereferenced at 0x68A2; id32 returned43 without deref; no live resource evidence') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sos54_resource0_model.py b/psp_emu/harness/probe_sos54_resource0_model.py new file mode 100644 index 0000000..33bdd3d --- /dev/null +++ b/psp_emu/harness/probe_sos54_resource0_model.py @@ -0,0 +1,91 @@ +"""Read-only emulator estimate for the hardware order: LOAD_ASD before LOAD_TA GCP.""" +import sys, struct +sys.path.insert(0,'/home/user/dev/amd-psp-re/psp_emu/harness') +sys.path.insert(0,'/home/user/dev/amd-psp-re/psp_emu') +import gcp_fe08_fullsys as H +from pspemu import armmmu +from unicorn import UC_HOOK_BLOCK +BM, M = H.BM, H.M +c, ccp, irqs, exc = BM.build() +run, _ = M.make_det_runner(c, irqs) +run(6_000_000) +M.install_debug_gpio(c) +box = M.Mailbox(c, irqs) +c.uc.mem_write(BM.RING_PA,b'\x00'*BM.RING_SZ) +rp = bytearray(0x1c) +struct.pack_into('<I', rp,0x10,BM.RING_PA) +struct.pack_into('<I', rp,0x18,BM.RING_SZ) +st,_ = box.submit(2,bytes(rp),timeout=12,runner=run) +assert st==0,st +ring=M.Ring(c,irqs,BM.RING_PA,BM.RING_SZ,BM.CMDBUF_PA,run) +def resource_state(label): + rd=lambda a:int.from_bytes(c.uc.mem_read(a,4),'little') + root=rd(0x8170) # signed sOS .54 sub_6870's resource-table pointer + if not root: + print(label,'sOS resource-table pointer is zero',flush=True);return + pa=armmmu.translate_live(c.uc,root) + if pa is None: + print(label,'sOS table root',hex(root),'not translated by generic TTBR0',flush=True);return + try:ptrs=[rd(pa+12+4*i) for i in range(32)] + except Exception as exc: + print(label,'sOS table root VA',hex(root),'PA',hex(pa),'not backed:',type(exc).__name__,flush=True);return + print(label,'sOS table VA',hex(root),'model PA',hex(pa),'count',rd(pa),'assigned slots',sum(bool(x) for x in ptrs), + 'id0 assigned',bool(ptrs[0]),'id31 assigned',bool(ptrs[31]),flush=True) + if ptrs[0]: + desc_pa=armmmu.translate_live(c.uc,ptrs[0]) + if desc_pa is None: + print(label,'id0 descriptor not translated in generic TTBR0',flush=True) + else: + desc=struct.unpack('<6I',c.uc.mem_read(desc_pa,24)) + print(label,'id0 descriptor segment_count',desc[0], + 'descriptor_word1',desc[1],'owner_byte',desc[3]&255, + 'first_segment_len',desc[5],flush=True) +resource_state('after ring init') +c.uc.mem_write(H.PLATFORM_ID_BYTE,bytes([H.NAVI31_ASIC_ID])) +asd=open(H.ASD_FULL,'rb').read() +base=0x04600000 +for b in (base,base+M.Ring.ALIAS): + try:c.uc.mem_map(b&~0xfff,(len(asd)+0xffff)&~0xfff) + except Exception:pass + c.uc.mem_write(b,asd) +obs=[];count=[0] +def on_asd(uc,a,size,user): + count[0]+=1 + if count[0]<=3 or count[0]%1024==0: + va=0xD3355C + obs.append((a,armmmu.ttbr0(uc),armmmu.translate_live(uc,va))) +c.uc.hook_add(UC_HOOK_BLOCK,on_asd,begin=0xD00100,end=0xDFFFFF) +ld=bytearray(0x20) +for off,val in ((0,base),(8,len(asd)),(12,H.GW.CMDBUF2_PA+0x10000),(20,0x1000)): + struct.pack_into('<I',ld,off,val) +print('LOAD_ASD first, container',H.ASD_FULL,flush=True) +ring.submit(4,bytes(ld),timeout=30,probe=[]) +BM.drain_until(run,lambda:ring.response()[1]!=0 or ring.response()[0]!=0,slices=4,blocks=2_000_000) +print('ASD blocks',count[0],'sampled VA->PA',[(hex(a),hex(tt),hex(pa) if pa is not None else None) for a,tt,pa in obs][:15],flush=True) +print('post-ASD ring',ring.response(),'service2',c.uc.mem_read(H.SERVICE_TABLE+2,1)[0],flush=True) +resource_state('post ASD') +print('post-ASD current TTBR0',hex(armmmu.ttbr0(c.uc)),'translation',hex(armmmu.translate_live(c.uc,0xD3355C) or 0),flush=True) +if not obs or obs[0][2] is None:raise SystemExit(1) +# Now load GCP in the same order as the hardware script and sample the shared MMU map. +cont=H.NT.pick_container('gcp') +img=open(cont,'rb').read() +for b in (H.FT.TA_IMAGE_PA,H.FT.TA_IMAGE_PA+M.Ring.ALIAS): + c.uc.mem_write(b,img) +for b in (H.GW.CMDBUF2_PA,H.GW.CMDBUF2_PA+M.Ring.ALIAS): + try:c.uc.mem_map(b&~0xfff,0x8000) + except Exception:pass +ld=bytearray(0x20) +for off,val in ((0,H.FT.TA_IMAGE_PA),(8,len(img)),(12,H.GW.CMDBUF2_PA),(20,0x8000)): + struct.pack_into('<I',ld,off,val) +gcp=[] +def on_gcp(uc,a,size,user): + if not gcp: + gcp.append((a,armmmu.ttbr0(uc),armmmu.translate_live(uc,0x410800),armmmu.translate_live(uc,0xD3355C))) +c.uc.hook_add(UC_HOOK_BLOCK,on_gcp,begin=0x400100,end=0x40FFFF) +ring.submit(1,bytes(ld),timeout=30,probe=[]) +BM.drain_until(run,lambda:bool(gcp) and ring.response()[1]!=0,slices=40,blocks=2_000_000) +print('GCP first block TTBR0, GCP VA->PA, ASD VA->PA:',[(hex(a),hex(tt),hex(pg) if pg is not None else None,hex(pa) if pa is not None else None) for a,tt,pg,pa in gcp],flush=True) +print('post-GCP ring',ring.response(),'service2',c.uc.mem_read(H.SERVICE_TABLE+2,1)[0],flush=True) +resource_state('post GCP') +print('post-GCP current TTBR0',hex(armmmu.ttbr0(c.uc)),'ASD target PA',hex(armmmu.translate_live(c.uc,0xD3355C) or 0),flush=True) +if not gcp:raise SystemExit(1) diff --git a/psp_emu/harness/probe_sysdrv_1001_owner_cleanup.py b/psp_emu/harness/probe_sysdrv_1001_owner_cleanup.py new file mode 100644 index 0000000..ba7ddf3 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1001_owner_cleanup.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x1001: caller-selected identity cleanup; modeled sOS +SVC6C returns error43. Synthetic records and default-empty slot control. +No kernel resource teardown or hardware interaction. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0, UC_ARM_REG_LR, UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +SLOT=0xE57538;MSG=0xE575B8;SMALL=0xE58038 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req=sd.alloc(0x100);calls=[] + def reject_sos(comp,n): + calls.append(comp.uc.reg_read(UC_ARM_REG_R0)) + return 43 # synthetic kernel failure; signed sOS not run + sd.svc.register(0x6C,reject_sos) + snapshot=(sd.read(SLOT,0x80),sd.read(MSG,2*84),sd.read(SMALL,16)) + def run(identity): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x1001),(1,2),(14,identity)): + sd.wr32(req+4*i,v) + return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + try: + empty=sum(sd.read(SLOT+8*i,1)==b'\x00' for i in range(16)) + assert empty==16,empty + assert run(0)==0 and calls==[0]*16,(calls,sd.read(SLOT,0x10).hex()) + assert sd.read(SLOT,0x80)==snapshot[0] + calls.clear() + # Two synthetic identities in three private tables; not real clients. + sd.write(SLOT,bytes.fromhex('07 1e 00 00 33 33 33 33 08 1d 00 00 44 44 44 44')+snapshot[0][16:]) + m7=bytearray(84);m7[32]=1;m7[34]=7 + m8=bytearray(84);m8[32]=1;m8[34]=8 + sd.write(MSG,bytes(m7)+bytes(m8)) + sd.write(SMALL,bytes.fromhex('01 07 00 00 11 11 11 11 01 08 00 00 22 22 22 22')) + fatal=[] + def capture_fatal(uc,ea,size,data): + fatal.append(uc.reg_read(UC_ARM_REG_R0)) + uc.reg_write(UC_ARM_REG_R0,0xDEAD) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,capture_fatal,begin=0xE0F890,end=0xE0F891) + assert sd.run_function(0xE0B148,(16,7,0,0))==MSG and not fatal + assert sd.run_function(0xE0B148,(17,8,0,0))==MSG+84 and not fatal + assert run(7)==0 and calls==[30],calls + assert sd.read(SLOT,8)==bytes(8) and sd.read(SLOT+8,8)==bytes.fromhex('08 1d 00 00 44 44 44 44') + assert sd.read(MSG+32,1)==b'\x00' and sd.read(MSG+34,1)==b'\x00' and sd.read(MSG+84+32,1)==b'\x01' + assert sd.read(SMALL,8)==bytes(8) and sd.read(SMALL+8,8)==bytes.fromhex('01 08 00 00 22 22 22 22') + sd.run_function(0xE0B148,(16,7,0,0)) + assert len(fatal)==1 # captured fatal; the modeled hook returns, not signed normal control flow + assert sd.run_function(0xE0B148,(17,8,0,0))==MSG+84 and len(fatal)==1 + print('signed 0x1001: default empty slots identity0 requested sixteen SVC6C(0) despite modeled error43; synthetic identity7 cleared three tables and its consumer reached captured fatal; identity8 stayed usable; no real victim/kernel call') + finally: + sd.write(SLOT,snapshot[0]);sd.write(MSG,snapshot[1]);sd.write(SMALL,snapshot[2]) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_100b_nested_span.py b/psp_emu/harness/probe_sysdrv_100b_nested_span.py new file mode 100644 index 0000000..97b52f2 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_100b_nested_span.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Signed .54 0x100B reads nested +16/+20/+24 past four-byte declaration. +Artificial scratch-page boundary and sOS window; no hardware effect. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from pspemu.component import Fault +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,nested,dummy=[sd.alloc(n) for n in (0x100,0x400,0x40,0x40)] + sd.write(cfg,bytes(0x400));sd.write(nested,bytes(0x40));sd.write(dummy,bytes(0x40)) + sd.wr32(nested+16,0x12340000);sd.wr32(nested+20,1);sd.wr32(nested+24,0x1000) + seen=[];allocator=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:seen.append(tuple(uc.reg_read(x) for x in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))),begin=0xE086FC,end=0xE086FD) + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:allocator.append(ea),begin=0xE1BBAE,end=0xE1BBAF) + def run(ptr,declared): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x100B),(1,2),(2,ptr),(3,declared),(4,dummy),(5,4), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + try:return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + except Fault as exc:return exc + mapped=run(nested,4) + assert seen and seen[-1]==(1,0x12340000,0x1000),(mapped,seen[-1] if seen else None) + # The next page after scratch is deliberately UNMAPPED in this model. + edge=sd.cmap.scratch+sd.cmap.scratch_size-4 + sd.write(edge,b'ABCD') + def svc_window(comp,n): + p=comp.uc.reg_read(UC_ARM_REG_R1) + for i,v in enumerate((sd.cmap.scratch,sd.cmap.scratch+sd.cmap.scratch_size,0,0)): + comp.wr32(p+4*i,v) + return 0 + sd.svc.register(0xB5,svc_window) + before=len(seen) + fault=run(edge,4) + assert isinstance(fault,Fault) and fault.addr==edge+24 and fault.pc==0xE1BB80,(fault,seen) + assert len(seen)==before + denied=run(edge,28) + assert not isinstance(denied,Fault) and denied==0xFFFF000F and len(seen)==before,(denied,seen) + assert not allocator,allocator + print(f'signed 0x100B: nested +16/+20/+24 read after declared4; synthetic edge fault={fault.addr:#x}, declared28 denied={denied:#x}; no mapped hardware claim') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_100c_slot_release.py b/psp_emu/harness/probe_sysdrv_100c_slot_release.py new file mode 100644 index 0000000..d928cc5 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_100c_slot_release.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x100C: selected synthetic private slot cleared even if +modeled sOS SVC 0x6C rejects the associated resource. No live records. +""" +from pathlib import Path +import sys +from unicorn.arm_const import UC_ARM_REG_R0 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +SLOTS=0xE57538 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req=sd.alloc(0x100);seen=[] + def reject_svc(comp,n): + seen.append(comp.uc.reg_read(UC_ARM_REG_R0)) + return 43 # synthetic failed resource removal; not sOS execution + sd.svc.register(0x6C,reject_svc) + prior=sd.read(SLOTS,0x80) + slot=bytes.fromhex('17 1e 00 00 de c0 ad de') + def run(idx): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x100C),(1,2),(14,idx)): + sd.wr32(req+4*i,v) + return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + try: + sd.write(SLOTS,slot+prior[8:]) + assert run(15)==0 and not seen and sd.read(SLOTS,8)==slot + assert run(32)==0 and not seen and sd.read(SLOTS,8)==slot + selected=run(16) + assert selected==0 and seen==[0x1E] and sd.read(SLOTS,8)==bytes(8),(selected,seen,sd.read(SLOTS,8).hex()) + assert sd.read(SLOTS+8,0x78)==prior[8:] + print('signed 0x100c: synthetic slot16 -> SVC6C(resource30) returned modeled error43 yet descriptor zeroed/status0; 15/32 untouched, neighboring slots preserved') + finally:sd.write(SLOTS,prior) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_100e_100f_ccp_natural.py b/psp_emu/harness/probe_sysdrv_100e_100f_ccp_natural.py new file mode 100644 index 0000000..e0abd56 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_100e_100f_ccp_natural.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x100E/0x100F with modeled CCP, no job hook. + +Synthetic type-5 descriptor, identity SVC 0x79, default-success SVCs and +modeled CCP queue: NOT real sOS PTEs, real card completion or victim impact. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn.arm_const import UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.ccp import CCP # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +BASE, END = 0x400000, 0x800000 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + ccp = CCP(sd, base=0x0CB00000) + assert sd.run_function(0xE05008, (0x0CB00000,)) == 0 + assert sd.run_function(0xE050F0) == 0 + ring = sd.alloc(16*32, align=16*32) + assert sd.run_function(0xE0503C, (0, ring, ring, 16)) == 0 + for addr, value in ((0xE257D0, 0xE50000), (0xE257D4, 0xE51000), + (0xE257D8, ring), (0xE257DC, ring)): + sd.wr32(addr, value) + sd.uc.mem_map(BASE, 0x3000) + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, word in enumerate((BASE, END, 0, 0)): + comp.wr32(out+4*i, word) + return 0 + + sd.svc.register(0xB5, svc_window) + sd.svc.register(0x68, lambda comp, n: 0) + src, dst = BASE+0x2000, BASE+0x1000 + planted = bytes(range(64)) + sd.write(src, planted) + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, bytes(0x400)) + for op, flag in ((0x100E, 0), (0x100F, 0), (0x100F, 1)): + blobs = [] + for declared in (64, 4): + sd.write(dst, b'\xA5'*64) + sd.write(req, bytes(0x100)) + for i, value in ((0, op), (2, dst), (3, 64), (4, src), + (5, declared), (14, flag), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req+4*i, value) + before = len(ccp.executed) + status = sd.run_function(0xE087E4, (req, 2, 0, 0), count=3_000_000, + timeout=6_000_000) + changed = bytes(sd.uc.mem_read(dst, 64)) + recent = ccp.executed[before:] + assert status == 0 and changed == planted, (hex(op), declared, + hex(status), changed[:8], [str(x) for x in recent]) + assert any(d.engine == 5 and d.length == 64 and d.src_lo == src + and d.dst_lo == dst for d in recent), recent + blobs.append(changed) + print(hex(op),'flag',flag,'input declared',declared, + 'signed status',hex(status),'model executed',len(recent), + 'source bytes copied',len(changed)) + assert blobs[0] == blobs[1] + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_100e_100f_second_span.py b/psp_emu/harness/probe_sysdrv_100e_100f_second_span.py new file mode 100644 index 0000000..0168a6b --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_100e_100f_second_span.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x100E/0x100F: second pointer's declared size vs job span. + +Stop at the signed job-submission helper before any modeled crypto/device +operation. SVC 0x79 is synthetic identity translation, 0xB5 supplies a +synthetic type-5 window, and SVC 0x68 is a default-success cache stub. +""" +from __future__ import annotations + +import struct +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE, UC_PROT_NONE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.ccp import CCP, CCPAccessFault, Descriptor # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY, JOB = 0xE087E4, 0xE04F1C +BASE, END = 0x400000, 0x800000 +SENTINEL = 0x8000000A + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + sd.uc.mem_map(BASE, 0x3000) + if not any(lo <= END-4 <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_map(END-0x1000, 0x1000) + if any(lo <= END <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_protect(END, 0x1000, UC_PROT_NONE) + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, word in enumerate((BASE, END, 0, 0)): + comp.wr32(out + 4*i, word) + return 0 + + sd.svc.register(0xB5, svc_window) + sd.svc.register(0x68, lambda comp, n: 0) + captures: list[tuple[int, ...]] = [] + + def stop_job(uc, ea, size, data): + arg = uc.reg_read(UC_ARM_REG_R2) + captures.append(tuple(sd.rd32(arg + 4*i) for i in range(8))) + uc.reg_write(UC_ARM_REG_R0, SENTINEL) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + + sd.uc.hook_add(UC_HOOK_CODE, stop_job, begin=JOB, end=JOB+1) + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, bytes(0x400)) + output, src, edge = BASE+0x1000, BASE+0x2000, END-4 + sd.write(src, b'Y'*64) + sd.write(edge, b'Z'*4) + + def request(opcode: int, ptr: int, declared: int, flag: int = 0) -> int: + sd.write(req, bytes(0x100)) + for i, word in ((0, opcode), (2, output), (3, 64), (4, ptr), + (5, declared), (14, flag), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4*i, word) + return sd.run_function(ENTRY, (req, 2, 0, 0), count=5_000_000, + timeout=4_000_000) + + baseline_desc = edge_desc = None + for opcode, flag in ((0x100E, 0), (0x100F, 0), (0x100F, 1)): + old = len(captures) + normal = request(opcode, src, 64, flag) + assert len(captures) == old+1, (hex(opcode), hex(normal), captures) + expected = captures[-1] + old = len(captures) + short = request(opcode, edge, 4, flag) + assert len(captures) == old+1, (hex(opcode), hex(short), captures) + got = captures[-1] + assert got[1] == 64 and got[2] == edge and got[4] == output, (hex(opcode), got) + if opcode == 0x100E: + baseline_desc, edge_desc = expected, got + old = len(captures) + denied = request(opcode, edge, 64, flag) + assert denied != 0 and len(captures) == old, (hex(opcode), denied) + print(hex(opcode),'flag',flag,'baseline',hex(normal),'short',hex(short), + 'denied',hex(denied),'job words',tuple(hex(x) for x in got[:6]), + 'baseline words',tuple(hex(x) for x in expected[:6])) + + # Only the separate CCP model interprets the signed descriptor here; + # signed SYS_DRV execution stopped before queue submission above. + assert baseline_desc is not None and edge_desc is not None + # Unicorn's host-side mem_read used by the CCP model does not enforce + # UC_PROT_NONE (which CPU execution does). Unmap that next page only + # after the signed-dispatch runs so the isolated model has a real edge. + if any(lo <= END <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_unmap(END, 0x1000) + ccp = CCP(sd) + def desc(words: tuple[int, ...]) -> Descriptor: + return Descriptor(struct.pack('<8I', *words)) + good = desc(baseline_desc) + assert good.engine == 5 and good.src_mem == 2 and good.dst_mem == 2 + assert ccp.execute(good) == 64 + assert bytes(sd.uc.mem_read(output, 64)) == b'Y'*64 + try: + ccp.execute(desc(edge_desc)) + except CCPAccessFault as exc: + assert exc.code == 0x13, exc + print('separate CCP model: 64-byte source at unmapped synthetic edge yields IDMA fault', + hex(exc.code)) + else: + raise AssertionError('modeled CCP unexpectedly read across synthetic boundary') + bounded = list(edge_desc) + bounded[1] = 4 + assert ccp.execute(desc(tuple(bounded))) == 4 + assert bytes(sd.uc.mem_read(output, 4)) == b'Z'*4 + print('separate CCP model: 4-byte source at edge copies 4 bytes') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1010_nested_gate.py b/psp_emu/harness/probe_sysdrv_1010_nested_gate.py new file mode 100644 index 0000000..28d630a --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1010_nested_gate.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1010 short pairs: default emulator stops before nested +68 read. +No artificial crypto session or hardware request. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from pspemu.component import Fault +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,p1,p2,p3,out=[sd.alloc(n) for n in (0x100,0x400,0x100,0x100,0x100,0x100)] + sd.write(cfg,bytes(0x400)) + for ptr in (p1,p2,p3,out):sd.write(ptr,bytes(0x100)) + nested=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:nested.append(ea),begin=0xE09458,end=0xE09459) + sd.write(req,bytes(0x100)) + for i,v in ((0,0x1010),(1,2),(2,p1),(3,4),(4,p2),(5,4),(6,p3),(7,4),(8,out),(9,4), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + try:rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + except Fault as exc: + assert (exc.pc,exc.addr)==(0xE019F8,0x113C) and not nested,(exc.pc,exc.addr,nested) + print(f'signed 0x1010 default emulator: missing shared crypto base fault_pc={exc.pc:#x} FAR={exc.addr:#x}; nested+68 not reached') + else: + print(f'signed 0x1010 default emulator: status={rv:#x} nested+68_reached={bool(nested)}; no crypto bytes printed') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1016_short_input.py b/psp_emu/harness/probe_sysdrv_1016_short_input.py new file mode 100644 index 0000000..ac04274 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1016_short_input.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1016 declared-input-size versus 24+72-byte leaf reads. + +Synthetic caller mapping and kernel SVC returns: this is NOT a live sOS +caller-window, hardware read, accepted firmware configuration, or safe card +probe. In signed sOS .54, the SVC 0x6B mode-zero path returns error 9 before +the leaf copies; default emulator success for this SVC is a KNOWN departure. +The SYS_DRV request validator, dispatcher and copy leaf are unmodified. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY, LEAF, CACHE = 0xE087E4, 0xE19BD8, 0xE7D5D0 +BASE, END = 0x400000, 0x41C000 # synthetic type-5 caller window + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + sd.uc.mem_map(BASE, END-BASE) + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, val in enumerate((BASE, END, 0, 0)): + comp.wr32(out + 4*i, val) + return 0 + + sd.svc.register(0xB5, svc_window) + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + first, normal, edge = BASE+0x1000, BASE+0x2000, END-4 + sd.write(first, b'\x01' + b'\x00' * 23) + second = bytes(0xA0 + i % 0x20 for i in range(72)) + sd.write(normal, second) + sd.write(edge, second[:4]) + visited: list[int] = [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, n, data: visited.append(ea), + begin=LEAF, end=LEAF+1) + + def request(second_ptr: int, declared: int) -> int: + sd.write(req, b'\x00' * 0x100) + for i, word in ((0, 0x1016), (2, first), (3, 4), (4, second_ptr), + (5, declared), (14, BASE+0x3000), (15, 0), (16, 104), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + i*4, word) + return sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + + sd.write(CACHE, b'\xCC' * 96) + before = len(visited) + short_rv = request(normal, 4) + assert len(visited) == before + 1, (hex(short_rv), visited) + copied = sd.read(CACHE, 96) + assert copied[:24] == sd.read(first, 24) + # Leaf intentionally permutes the first two 24-byte chunks from input 2. + assert copied[24:96] == second[24:48] + second[:24] + second[48:72], copied.hex() + print('declared second input 4: reached signed leaf, copied all 72 bytes;', + 'final status', hex(short_rv)) + + # Signed sOS .54 SVC 0x6B mode zero returns 9. Model THAT result and + # confirm the signed SYS_DRV leaf exits before the mismatched reads. + sd.svc.register(0x6B, lambda comp, n: 9) + sd.write(CACHE, b'\xCC' * 96) + before = len(visited) + blocked = request(normal, 4) + assert blocked == 0x80000703 and len(visited) == before + 1 + assert sd.read(CACHE, 96) == b'\xCC' * 96 + print('signed sOS .54 SVC 0x6B result 9: leaf returns', hex(blocked), + 'before any cache copy') + sd.svc.register(0x6B, lambda comp, n: 0) # return to synthetic-success experiment + + sd.write(CACHE, b'\xCC' * 96) + before = len(visited) + rejected = request(edge, 72) + assert rejected != 0 and len(visited) == before + assert sd.read(CACHE, 96) == b'\xCC' * 96 + print('synthetic window-edge declared 72: rejected before leaf, status', hex(rejected)) + + try: + request(edge, 4) + except Fault as exc: + assert len(visited) == before + 1 + assert END <= exc.addr < END+72, (exc, sd.faults) + print('synthetic window-edge declared 4: reached leaf, fault', hex(exc.addr)) + else: + raise AssertionError('edge read unexpectedly completed without a fault') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1017_short_outputs.py b/psp_emu/harness/probe_sysdrv_1017_short_outputs.py new file mode 100644 index 0000000..3d1e849 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1017_short_outputs.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x1017: two four-byte stores despite one-byte declarations. +Private transport state and response are synthetic, and the device call is intercepted. +No card, real device transaction, or cross-allocation overwrite. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + sd.write(0xE26259,b'\x00') # conditional transport branch + cb=sd.alloc(0x100);req=sd.alloc(0x100);out1=sd.alloc(0x20);out2=sd.alloc(0x20) + sd.wr32(0xE26340,cb);sd.wr32(0xE2632C,0x100) + sd.wr32(0xE7D5D0,1);sd.wr32(0xE7D5D4,1) + hits=[] + def transport(uc,ea,size,data): + hits.append((uc.reg_read(UC_ARM_REG_R0),uc.reg_read(UC_ARM_REG_R1))) + sd.wr32(cb+12,0) # v12[1], transport status + sd.wr32(cb+8+6*4,0x11223344) + sd.wr32(cb+8+8*4,0x55667788) + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,transport,begin=0xE1F470,end=0xE1F471) + sd.write(out1,b'A'*32);sd.write(out2,b'B'*32);sd.write(req,bytes(0x100)) + for i,v in ((0,0x1017),(1,2),(2,out1),(3,1),(4,out2),(5,1),(14,0x123),(15,0x456)): + sd.wr32(req+4*i,v) + status=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert status==0 and len(hits)==1,(hex(status),hits) + assert sd.read(out1,8)==bytes.fromhex('44332211')+b'A'*4,sd.read(out1,8) + assert sd.read(out2,8)==bytes.fromhex('88776655')+b'B'*4,sd.read(out2,8) + # The isolated signed init's default mode=1 rejects this path before transport. + sd.write(0xE26259,b'\x01');sd.write(out1,b'A'*32);sd.write(out2,b'B'*32) + rejected=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert rejected==0xFFFF000A and len(hits)==1,(hex(rejected),hits) + assert sd.read(out1,8)==b'A'*8 and sd.read(out2,8)==b'B'*8 + print('signed 0x1017: synthetic mode=0: two declared-one-byte outputs each got four-byte stores; default mode=1 rejects before transport; no card effect') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1018_synthetic_range.py b/psp_emu/harness/probe_sysdrv_1018_synthetic_range.py new file mode 100644 index 0000000..9081763 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1018_synthetic_range.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1018: compare two encodings of one device address. + +Private class row/upper bound are PLANTED; backend is capture-only. +No natural class policy, storage contents, or card reads are established. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_R3,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + assert sd.read(0xE26259,1)==b'\x01' + # Fake root: one class-34 row with base 0x2000; global upper 0x10000. + # The chosen address is not a CPU pointer and no backend is allowed to run. + root=0xE7904C + sd.wr32(root+16,0x10000) + sd.wr32(root+72,1) + sd.wr32(root+76,34) + sd.wr32(root+80,0x2000) + req=sd.alloc(0x100);out=sd.alloc(0x100);seen=[];trace=[] + def log_entry(uc,ea,size,data): + trace.append((hex(ea),tuple(uc.reg_read(x) for x in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_R3)))) + for addr in (0xE0AE90,0xE15234): + sd.uc.hook_add(UC_HOOK_CODE,log_entry,begin=addr,end=addr+1) + def capture(uc,ea,size,data): + seen.append(tuple(uc.reg_read(x) for x in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_R3))) + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,capture,begin=0xE1AAB4,end=0xE1AAB5) + def run(index,offset): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x1018),(1,2),(2,out),(3,4),(14,1),(15,index),(16,offset)): + sd.wr32(req+4*i,v) + return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + a=run(0,0x1000) + b=run(1,0) + blocked=run(0,0xE000) # address=upper; four-byte read exceeds upper + assert a==b==0 and blocked!=0,(hex(a),hex(b),hex(blocked),seen,trace) + assert len(seen)==2 and seen[0][1]==seen[1][1]==0x3000,seen + assert all(x[0]==2 and x[2]==out and x[3]==4 for x in seen),seen + print('signed 0x1018 mode=1: page0+0x1000 and page1+0 target same 0x3000; upper-bound negative stops before captured backend; planted descriptor, no hardware effect') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_101a_count_wrap.py b/psp_emu/harness/probe_sysdrv_101a_count_wrap.py new file mode 100644 index 0000000..8f9e5c5 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_101a_count_wrap.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x101A: capture 32-bit shift of caller block count. +Fixed-image lookup and hardware sink are stubbed; no flash writes. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_R3,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + assert sd.read(0xE26259,1)==b'\x01' + req=sd.alloc(0x100);captures=[];lookups=[] + def jump(uc,value): + uc.reg_write(UC_ARM_REG_R0,value) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + def lookup(uc,ea,size,data): + lookups.append(tuple(uc.reg_read(x) for x in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))) + sd.wr32(uc.reg_read(UC_ARM_REG_R3),0x10000) + jump(uc,0) + def sink(uc,ea,size,data): + captures.append(tuple(uc.reg_read(x) for x in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_R3))) + jump(uc,0x80001004) + sd.uc.hook_add(UC_HOOK_CODE,lookup,begin=0xE0AE90,end=0xE0AE91) + sd.uc.hook_add(UC_HOOK_CODE,sink,begin=0xE15074,end=0xE15075) + def run(count): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x101A),(1,2),(14,1),(15,0),(16,count)): + sd.wr32(req+4*i,v) + return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert run(16)==0x80001004 + assert run(0x100010)==0x80001004 + assert run(0x100000)==0x80001004 + assert len(lookups)==3 and len(captures)==3 + assert [x[2] for x in captures]==[0x10000,0x10000,0],captures + assert all(x[:2]==(2,0x10000) for x in captures),captures + print('signed 0x101a: msg16=16 and 0x100010 both passed truncated 0x10000-byte length to stubbed sink; 0x100000 became zero; synthetic lookup, no real metadata/flash') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_101c_state_gate.py b/psp_emu/harness/probe_sysdrv_101c_state_gate.py new file mode 100644 index 0000000..3c799d2 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_101c_state_gate.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Synthetic sOS-style per-slot state setup -> signed 0x101C gate only. + +No hardware, no real key material, and no output-overflow request. This calls +SYS_DRV's signed 0x3105 leaf directly (bypassing sOS/SVC ACL) with synthetic +bytes for caller slot 2, then exercises the unhooked signed crypto-state gate. +Model CCP and unhandled-SVC behavior are not evidence of card completion. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.ccp import CCP, CCPError # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +SETTER, GATE = 0xE0E74C, 0xE0E7B8 +FLAG = 0xEB1280 + 66 * 2 + 64 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1 + assert sd.read(FLAG, 2) == b'\0\0' + ccp = CCP(sd, base=0x0CB00000) + assert sd.run_function(0xE05008, (0x0CB00000,)) == 0 + assert sd.run_function(0xE050F0) == 0 + ring = sd.alloc(16 * 32, align=16 * 32) + assert sd.run_function(0xE0503C, (0, ring, ring, 16)) == 0 + for addr, value in ((0xE257D0, 0xE50000), (0xE257D4, 0xE51000), + (0xE257D8, ring), (0xE257DC, ring)): + sd.wr32(addr, value) + before = sd.run_function(GATE, (2,), count=100_000, timeout=1_000_000) + assert before == 0xFFFF000B, hex(before) + key32, iv16 = sd.alloc(32), sd.alloc(16) + sd.write(key32, bytes(range(32))) # fake, public, synthetic test bytes + sd.write(iv16, bytes(range(32, 48))) + set_rv = sd.run_function(SETTER, (key32, 32, iv16, 16), stack_args=(2,), + count=200_000, timeout=2_000_000) + assert set_rv == 0 and sd.read(FLAG, 2) == b'\x01\0', (hex(set_rv), sd.read(FLAG, 2)) + print('synthetic 0x3105 leaf set caller-2 flag:', sd.read(FLAG, 2).hex(), flush=True) + try: + gate_rv = sd.run_function(GATE, (2,), count=3_000_000, + timeout=10_000_000) + except CCPError as exc: + # The model does not implement this signed setup's SHA descriptor. + # This is NOT evidence of either success or failure on hardware. + assert str(exc) == 'unsupported SHA type 12', str(exc) + assert sd.read(FLAG, 2) == b'\x01\x00', sd.read(FLAG, 2) + print('unhooked gate entered CCP; emulator blocked on:', exc, + '; descriptor:', ccp.errors[-1] if ccp.errors else 'not logged', + '; gate completion UNKNOWN', flush=True) + return + print('unhooked signed crypto gate return:', hex(gate_rv), + 'flags:', sd.read(FLAG, 2).hex(), flush=True) + assert gate_rv == 0 and sd.read(FLAG, 2) == b'\x01\x01', ( + hex(gate_rv), sd.read(FLAG, 2), sd.faults[-2:]) + _ = ccp # retain device hook + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_101c_wrapped_output.py b/psp_emu/harness/probe_sysdrv_101c_wrapped_output.py new file mode 100644 index 0000000..89d6061 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_101c_wrapped_output.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Signed .54 SYS_DRV 0x101C wrapped output guard, conditional crypto gate. + +Synthetic B5 window and protected page; only the preceding crypto-state gate +is forced to success. Stop just before the gigantic source copy, *after* the +signed output memset/header writes. This is not a card effect or full crypto. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE, UC_PROT_NONE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0, UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY, LEAF, STATE_GATE, BEFORE_BULK_COPY = 0xE087E4, 0xE215A8, 0xE0E7B8, 0xE2160C +FAIL_EPILOGUE = 0xE215DC # load error code, restore stack and return; skip huge source copy +BASE, END = 0x400000, 0x800000 +WRAPPED = 0xFFFFFFC0 # (WRAPPED + 80) mod 2**32 == 16 +STATE_FLAG_BASE = 0xEB1280 # signed sub_E0E7B8 per-caller gate, +66*caller+64 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + gate_flag = sd.read(STATE_FLAG_BASE+66*2+64, 1)[0] + assert gate_flag == 0, f'modeled GCP crypto-state flag unexpectedly enabled: {gate_flag}' + natural_gate = sd.run_function(STATE_GATE, (2, 0, 0, 0), count=100_000, + timeout=1_000_000) + assert natural_gate == 0xFFFF000B, hex(natural_gate) + print('modeled init GCP crypto-state flag = 0; signed gate returned', + hex(natural_gate),'(forced-success test below)',flush=True) + sd.uc.mem_map(BASE, 0x2000) + if not any(lo <= END-16 <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_map(END-0x1000, 0x1000) + if any(lo <= END <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_protect(END, 0x1000, UC_PROT_NONE) # synthetic, NOT live PTE + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, val in enumerate((BASE, END, 0, 0)): + comp.wr32(out + 4*i, val) + return 0 + sd.svc.register(0xB5, svc_window) + + leaves, gates, prebulk = [], [], [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, size, data: leaves.append(ea), + begin=LEAF, end=LEAF+1) + def force_gate(uc, ea, size, data): + gates.append(ea) + uc.reg_write(UC_ARM_REG_R0, 0) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE, force_gate, begin=STATE_GATE, end=STATE_GATE+1) + def stop_before_bulk_copy(uc, ea, size, data): + prebulk.append(ea) + uc.reg_write(UC_ARM_REG_PC, FAIL_EPILOGUE | 1) + sd.uc.hook_add(UC_HOOK_CODE, stop_before_bulk_copy, + begin=BEFORE_BULK_COPY, end=BEFORE_BULK_COPY+1) + + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + source, dest, edge = BASE+0x1000, BASE+0x1800, END-16 + sd.write(source, b'Q'*128) + + def request(input_len: int, outptr: int, outlen: int, input_ptr: int) -> int: + sd.write(req, b'\x00' * 0x100) + for i, word in ((0, 0x101C), (2, input_ptr), (3, input_len), + (4, outptr), (5, outlen), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4*i, word) + return sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + + sd.write(dest, b'\xA5'*128) + guard_negative = request(4, dest, 16, source) + assert len(leaves) == len(gates) == 1 and not prebulk + assert sd.read(dest, 128) == b'\xA5'*128 + print('small input 4/output 16: signed guard rejected, output unchanged', + hex(guard_negative),flush=True) + + sd.write(dest, b'\xA5'*128) + validator_denied = request(WRAPPED, dest, 16, source) + assert validator_denied == 0xFFFF000F and len(leaves) == len(gates) == 1 + assert sd.read(dest,128) == b'\xA5'*128 + print('wrapped mapped input: pre-dispatch length cap rejected', + hex(validator_denied),flush=True) + + # A NULL input pointer skips the dispatcher's pointer-pair validation, + # even with the same oversized declared length. The leaf writes output + # before it tries to read from that NULL source. Synthetic-only. + wrapped_rv = request(WRAPPED, dest, 16, 0) + got = sd.read(dest, 128) + assert len(leaves) == len(gates) == 2 and len(prebulk) == 1, ( + hex(wrapped_rv),len(leaves),len(gates),len(prebulk),got[:80].hex(),sd.faults[-2:]) + assert got[:48] == bytes(48) and got[48:52] == (16).to_bytes(4,'little') + assert got[52:56] == WRAPPED.to_bytes(4,'little') and got[56:80] == bytes(24) + assert got[80:] == b'\xA5'*48 + print('wrapped input/output declared 16: signed leaf changed bytes 0..79, ' + 'bytes 80..127 untouched; stopped before gigantic bulk copy', + hex(wrapped_rv),flush=True) + + n = (len(leaves),len(gates),len(prebulk)) + rejected = request(WRAPPED, edge, 80, 0) + assert rejected != 0 and (len(leaves),len(gates),len(prebulk)) == n + print('synthetic edge output declared 80: rejected before leaf',hex(rejected),flush=True) + + try: + request(WRAPPED, edge, 16, 0) + except Fault as exc: + assert END <= exc.addr < END+80 and any(f['addr'] == END for f in sd.faults), (exc, sd.faults) + assert (len(leaves),len(gates),len(prebulk)) == (n[0]+1,n[1]+1,n[2]) + print('synthetic edge output declared 16: signed memset fault',hex(exc.addr), + 'before bulk copy',flush=True) + else: + raise AssertionError('wrapped short output unexpectedly completed at edge') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_101d_nested_gate.py b/psp_emu/harness/probe_sysdrv_101d_nested_gate.py new file mode 100644 index 0000000..03d881b --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_101d_nested_gate.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Signed .54 0x101D: determine if default per-caller crypto gate reaches +52 read. +No crypto/session state is planted or modified on hardware. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,inp,out=[sd.alloc(n) for n in (0x100,0x400,0x80,0x40)] + sd.write(cfg,bytes(0x400));sd.write(inp,bytes(0x80));sd.write(out,bytes(0x40)) + reads=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:reads.append(ea),begin=0xE1F6EA,end=0xE1F6EB) + sd.write(req,bytes(0x100)) + for i,v in ((0,0x101D),(1,2),(2,inp),(3,4),(4,out),(5,4), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert not reads and not sd.faults,(rv,reads) + print(f'signed 0x101D default emulator: gate_status={rv:#x}, nested+52_read_reached=False; conditional signed path remains untested') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1021_global_flags.py b/psp_emu/harness/probe_sysdrv_1021_global_flags.py new file mode 100644 index 0000000..98af852 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1021_global_flags.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 free 0x1021: synthetic global flag stores, no card. + +State-changing command: emulator only. Assert direct globals and restore the +emulator snapshot; no live post-load trigger of the downstream bringup is +claimed. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn.arm_const import UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +FLAGS, ENTRY = 0xE252D8, 0xE087E4 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, bytes(0x400)) + sd.uc.mem_map(0x400000, 0x1000) + out = 0x400100 + + def svc_window(comp, n): + ptr = comp.uc.reg_read(UC_ARM_REG_R1) + for i, word in enumerate((0x400000, 0x800000, 0, 0)): + comp.wr32(ptr+4*i, word) + return 0 + + sd.svc.register(0xB5, svc_window) + before = sd.read(FLAGS, 3) + + def request(mode: int, value: int = 0) -> tuple[int, bytes]: + sd.write(req, bytes(0x100)) + sd.wr32(out, 0) + for i, word in ((0, 0x1021), (2, out), (3, 4), (14, mode), (15, value), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4*i, word) + ret = sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + assert sd.rd32(out) == 1, (mode, hex(ret), hex(sd.rd32(out))) + return ret, sd.read(FLAGS, 3) + + try: + for mode, value, offset, expected in ((0, 0x41, 0, 0x41), + (1, 0, 2, 0), + (2, 0, 2, 1), + (5, 0, 1, 0)): + ret, after = request(mode, value) + assert ret == 0 and after[offset] == expected, (mode, hex(ret), after) + if mode == 0: + assert after[1] == 1 + print('mode',mode,'signed status',hex(ret),'globals',after.hex()) + finally: + sd.write(FLAGS, before) + assert sd.read(FLAGS, 3) == before + print('synthetic globals restored:',before.hex()) + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1023_ccp_natural.py b/psp_emu/harness/probe_sysdrv_1023_ccp_natural.py new file mode 100644 index 0000000..ca19f9f --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1023_ccp_natural.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""One bounded signed 0x1023 run with the emulator's CCP model, no crypto hook. + +The SYS_DRV boot configuration, sOS SVC handlers and CCP MMIO are still +synthetic. This is only a feasibility discriminator; no card claim. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.ccp import CCP # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + ccp = CCP(sd, base=0x0CB00000) + assert sd.run_function(0xE05008, (0x0CB00000,)) == 0 + assert sd.run_function(0xE050F0) == 0 + ring = sd.alloc(16 * 32, align=16 * 32) + assert sd.run_function(0xE0503C, (0, ring, ring, 16)) == 0 + for addr, value in ((0xE257D0, 0xE50000), (0xE257D4, 0xE51000), + (0xE257D8, ring), (0xE257DC, ring)): + sd.wr32(addr, value) + src1, src2, output, req, cfg = (sd.alloc(n) for n in (64, 4, 128, 0x100, 0x400)) + sd.write(src1, b'\x43'*64) + sd.write(src2, b'\x58'*4) + sd.write(cfg, b'\x00'*0x400) + sd.write(req, b'\x00'*0x100) + for i, val in ((0, 0x1023), (2, src1), (3, 64), (4, src2), (5, 4), + (8, output), (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + i*4, val) + outputs = [] + for declared in (64, 4): + sd.write(output, b'\xA5'*128) + sd.wr32(req + 9*4, declared) + try: + status = sd.run_function(0xE087E4, (req, 2, 0, 0), count=2_000_000, + timeout=6_000_000) + except Fault as exc: + raise AssertionError(f'bounded signed crypto fault at declared {declared}') from exc + blob = sd.read(output, 128) + changed = [i for i, b in enumerate(blob) if b != 0xA5] + assert status == 0 and changed == list(range(64)), (declared, hex(status), changed) + outputs.append(blob[:64]) + print('signed crypto with emulator CCP: declared', declared, + 'status', hex(status), 'changed bytes 0..63; no crypto hook') + assert outputs[0] == outputs[1] + print('controls: both declared sizes yielded same 64-byte output under ' + 'synthetic device/config/SVC environment; NOT hardware proof') + _ = ccp # keep device hook alive until both requests finish + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1023_short_output.py b/psp_emu/harness/probe_sysdrv_1023_short_output.py new file mode 100644 index 0000000..4534065 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1023_short_output.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""SYS_DRV .54 opcode 0x1023: 64-byte output vs declared 4-byte window. + +Synthetic PSP caller window and stubbed *final crypto completion* only. Signed +request validation, crypto descriptor setup, and final output-copy leaf run. +No claim of a successful live crypto operation or hardware write. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0, UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY, LEAF, CRYPTO = 0xE087E4, 0xE05140, 0xE1C85C +BASE, END = 0x400000, 0x41C000 # synthetic type-5 caller window + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + sd.uc.mem_map(BASE, END-BASE) + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, val in enumerate((BASE, END, 0, 0)): + comp.wr32(out + i*4, val) + return 0 + + sd.svc.register(0xB5, svc_window) + leaves: list[int] = [] + crypto_stubs: list[int] = [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, sz, ctx: leaves.append(ea), + begin=LEAF, end=LEAF+1) + + def complete_crypto(uc, ea, size, ctx): + crypto_stubs.append(ea) + uc.reg_write(UC_ARM_REG_R0, 0) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE, complete_crypto, begin=CRYPTO, end=CRYPTO+1) + + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + src1, src2, normal, edge = BASE+0x1000, BASE+0x2000, BASE+0x3000, END-4 + sd.write(src1, b'\x43' * 64) + sd.write(src2, b'\x58' * 4) + + def request(output: int, declared: int) -> int: + sd.write(req, b'\x00' * 0x100) + for i, word in ((0, 0x1023), (2, src1), (3, 64), (4, src2), (5, 4), + (8, output), (9, declared), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + i*4, word) + return sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + + sd.write(normal, b'\xA5' * 0x80) + before_l, before_c = len(leaves), len(crypto_stubs) + status = request(normal, 4) + changed = [i for i, b in enumerate(sd.read(normal, 0x80)) if b != 0xA5] + assert status == 0 and len(leaves) == before_l + 1 + assert len(crypto_stubs) == before_c + 1 + assert changed == list(range(64)), changed + print('synthetic crypto success: declared output 4, signed leaf stored all 64 bytes') + + sd.write(edge, b'\xA5' * 4) + before_l, before_c = len(leaves), len(crypto_stubs) + rejected = request(edge, 64) + assert rejected != 0 and len(leaves) == before_l + assert len(crypto_stubs) == before_c and sd.read(edge, 4) == b'\xA5' * 4 + print('synthetic window-edge declared 64 rejected before leaf:', hex(rejected)) + + try: + request(edge, 4) + except Fault as exc: + assert len(leaves) == before_l + 1 + assert len(crypto_stubs) == before_c + 1 + assert END <= exc.addr < END+64, (exc, sd.faults) + print('synthetic window-edge declared 4 reached output store and faulted:', + hex(exc.addr)) + else: + raise AssertionError('synthetic edge unexpectedly completed') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1027_sos94_block.py b/psp_emu/harness/probe_sysdrv_1027_sos94_block.py new file mode 100644 index 0000000..2bc68e6 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1027_sos94_block.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Signed .54 SYS_DRV 0x1027 with signed sOS .54 SVC0x94=9. +Read-only model: demonstrates first lookup stops before inline output. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + calls=[] + def signed_sos_case67(comp,n): + calls.append(n) + return 9 + sd.svc.register(0x94,signed_sos_case67) + req=sd.alloc(0x100);sd.write(req,bytes(0x100)) + for i,v in ((0,0x1027),(1,2),(14,0x12345678)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert rv==9 and len(calls)==1 and sd.rd32(req+14*4)==0x12345678,(rv,calls,hex(sd.rd32(req+14*4))) + print('signed 0x1027: connected sOS .54 SVC94 case67 returned9 on first fixed descriptor, inline output unchanged; no fabric reads/writes') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1028_base_readonly.py b/psp_emu/harness/probe_sysdrv_1028_base_readonly.py new file mode 100644 index 0000000..993aa01 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1028_base_readonly.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Read-only modeled .54 0x1028 base pointer; no leaf invocation or data values.""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + pre=sd.rd32(0xE2579C) + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + base=sd.rd32(0xE2579C) + regions=[(lo,hi,perms) for lo,hi,perms in sd.uc.mem_regions() if lo<=base+0x500<=hi] + print(f'signed modeled init: *E2579C {pre:#x}->{base:#x}; 0x1028 indexed window {base+0x500:#x}..{base+0x53f:#x}; emulator-mapped={bool(regions)} (address arithmetic only, no word contents read)') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1028_synthetic_window.py b/psp_emu/harness/probe_sysdrv_1028_synthetic_window.py new file mode 100644 index 0000000..4e9ad45 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1028_synthetic_window.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1028 getter/setter into a swapped synthetic buffer only. +The original CPU window is never read or written and pointer is restored. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +BASEPTR=0xE2579C + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + old=sd.rd32(BASEPTR);old2=sd.rd32(0xE257A0) + fake=sd.alloc(0x800);req=sd.alloc(0x100) + sd.write(fake,bytes(0x800));sd.wr32(BASEPTR,fake);sd.wr32(0xE257A0,fake) + def run(index,mode,value,opcode=0x1028): + sd.write(req,bytes(0x100)) + for i,v in ((0,opcode),(1,2),(14,index),(15,value),(16,mode)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + return rv,sd.rd32(req+15*4) + try: + w=run(3,0,0x2468ACE0) + assert w[0]==0 and sd.rd32(fake+0x50C)==0x2468ACE0 and sd.rd32(fake+0x508)==0 and sd.rd32(fake+0x510)==0,w + read=run(3,1,0) + assert read==(0,0x2468ACE0),read + invalid=run(16,0,0xA5A5A5A5) + assert invalid[0]!=0 and sd.rd32(fake+0x540)==0,invalid + fixed=run(0,0,0,opcode=0x1029) + assert fixed[0]==0 and sd.rd32(fake+0x69C)==0xFFFFFFFF and sd.rd32(fake+0x698)==0 and sd.rd32(fake+0x6A0)==0,fixed + set2=run(0x13579BDF,0,0,opcode=0x102E) + assert set2[0]==0 and sd.rd32(fake+0xD8)==0x13579BDF and sd.rd32(fake+0xD4)==0 and sd.rd32(fake+0xDC)==0,set2 + get2=run(0,0,1,opcode=0x102E) + assert get2[0]==0 and sd.rd32(req+14*4)==0x13579BDF,get2 + print(f'signed 0x1028: synthetic +0x50c chosen store/read, index16 rejected={invalid[0]:#x}; 0x1029 fixed +0x69c=ffffffff; 0x102e chosen +0xd8 store/read; original bases {old:#x}/{old2:#x} untouched') + finally: + sd.wr32(BASEPTR,old) + sd.wr32(0xE257A0,old2) + assert sd.rd32(BASEPTR)==old and sd.rd32(0xE257A0)==old2 +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_102c_label_reach.py b/psp_emu/harness/probe_sysdrv_102c_label_reach.py new file mode 100644 index 0000000..af6f2b3 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_102c_label_reach.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Signed .54 0x102C: check whether caller label reaches strlen past msg[3]. +Any early crypto/model fault remains separate from reached label reads. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from pspemu.component import Fault +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,label,out=[sd.alloc(n) for n in (0x100,0x400,0x40,0x40)] + sd.write(cfg,bytes(0x400));sd.write(label,b'NAME_XYZ\0'+bytes(0x37));sd.write(out,bytes(0x40)) + entry=[];length=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:entry.append(uc.reg_read(UC_ARM_REG_R0)),begin=0xE01864,end=0xE01865) + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:length.append(uc.reg_read(UC_ARM_REG_R0)),begin=0xE07AD0,end=0xE07AD1) + sd.write(req,bytes(0x100)) + for i,v in ((0,0x102C),(1,2),(2,label),(3,4),(4,out),(5,32),(14,0x28), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + def dispatch(): + try: + return sd.run_function(0xE087E4,(req,2,0,0),count=4_000_000,timeout=4_000_000) + except Fault as exc: + return exc + cold=dispatch() + assert isinstance(cold,Fault) and (cold.pc,cold.addr)==(0xE11712,0x200) and label not in entry,(cold,entry) + old=[sd.rd32(a) for a in (0xE257D8,0xE257DC)] + base=sd.alloc(0x6000,align=0x1000) + sd.write(base,bytes(0x6000)) + try: + for a in (0xE257D8,0xE257DC):sd.wr32(a,base) + modeled=dispatch() + finally: + for a,v in zip((0xE257D8,0xE257DC),old):sd.wr32(a,v) + status=(f'fault_pc={modeled.pc:#x} FAR={modeled.addr:#x}' if isinstance(modeled,Fault) else f'return={modeled:#x}') + print(f'signed 0x102C: cold missing-base fault=0x200; synthetic shared-base {status}; label_seen={label in entry}, label_lengths={length}; derived bytes redacted') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_102f_fixed_map.py b/psp_emu/harness/probe_sysdrv_102f_fixed_map.py new file mode 100644 index 0000000..5be2dac --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_102f_fixed_map.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Signed .54 0x102F fixed 128KiB mapping-copy path; no real source map. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,out=sd.alloc(0x100),sd.alloc(0x400),sd.alloc(0x20010) + sd.write(cfg,bytes(0x400)) + counts={x:0 for x in (0xE06314,0xE06328,0xE06338)} + for ea in counts: + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,pc,size,data:counts.__setitem__(pc,counts[pc]+1),begin=ea,end=ea+1) + def run(size): + sd.write(req,bytes(0x100));sd.write(out,b'\xA5'*0x20010) + for i,v in ((0,0x102F),(1,2),(2,out),(3,size), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert not sd.faults + return rv,sd.read(out,0x20010) + short,untouched=run(0x1FFFF) + assert short==0xFFFF0010 and untouched==b'\xA5'*0x20010 and not any(counts.values()) + full,still=run(0x20000) + assert counts[0xE06314]==1 and counts[0xE06328]==counts[0xE06338]==32,(counts,full) + assert full==0 and still[:0x20000]!=b'\xA5'*0x20000 and still[0x20000:]==b'\xA5'*16 + print(f'signed 0x102F: short gate={short:#x}, modeled fixed-map completion={full:#x}, page_copies=32; output bytes redacted') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1030_fixed_mode_flag.py b/psp_emu/harness/probe_sysdrv_1030_fixed_mode_flag.py new file mode 100644 index 0000000..9c09da1 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1030_fixed_mode_flag.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1030 decimal selector 25 sets fixed private mode byte. +No firmware loading or fabric calls; private state restored only in emulator. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +FLAG=0xE2663D + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + ret,fault,_=sysdrv_env.run_init(sd) + assert ret==0 and not fault and sysdrv_env.init_flag(sd)==1 + req,cfg=sd.alloc(0x100),sd.alloc(0x400) + sd.write(req,bytes(0x100));sd.write(cfg,bytes(0x400)) + old=sd.read(FLAG,1) + assert old==b'\0' + sd.wr32(req,0x1030);sd.wr32(req+4,2) + sd.wr32(req+4*sysdrv_env.REQ_CONFIG_DWORD,cfg) + sd.wr32(req+56,25);sd.wr32(req+60,0) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert rv==0 and sd.read(FLAG,1)==b'\x01' and not sd.faults + sd.write(FLAG,old) # emulator snapshot only; NOT a card restoration oracle + assert sd.read(FLAG,1)==old + print('signed free 0x1030 selector 25 set private mode byte 0->1; restored emulator snapshot') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1030_flash_class_gate.py b/psp_emu/harness/probe_sysdrv_1030_flash_class_gate.py new file mode 100644 index 0000000..0251f9c --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1030_flash_class_gate.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x1030 changes the two flash-class selector bytes. +The table is synthetic and lookup-only; NO SPI operation/card mutation. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY,LOOKUP,TABLE=0xE087E4,0xE0AE90,0xE7904C +GATE0,GATE5=0xE257A8,0xE257AC + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + init,fault,_=sysdrv_env.run_init(sd) + assert init==0 and not fault and sysdrv_env.init_flag(sd)==1 + before=(sd.rd32(GATE0),sd.rd32(GATE5)) + assert (before[0]&255,before[1]&255)==(255,255),before + # Artificial nonoverlapping flash-table rows; no signed ROM/RA2 provenance. + sd.write(TABLE+72,bytes(0x100));sd.wr32(TABLE+72,3) + for i,(tag,base) in enumerate(((34,0x10000),(4,0x20000),(84,0x30000))): + at=TABLE+76+16*i + sd.wr32(at,tag);sd.wr32(at+4,base);sd.wr32(at+8,0x1000) + req,cfg,out=sd.alloc(0x100),sd.alloc(0x400),sd.alloc(4) + sd.write(cfg,bytes(0x400));sd.write(req,bytes(0x100)) + sd.wr32(req,0x1030);sd.wr32(req+4,2) + sd.wr32(req+4*sysdrv_env.REQ_CONFIG_DWORD,cfg) + def lookup(cls): + sd.wr32(out,0xAAAAAAAA) + rc=sd.run_function(LOOKUP,(cls,0,0x10,out),count=100_000,timeout=1_000_000) + return rc,sd.rd32(out) + assert lookup(0)[0]!=0 and lookup(5)[0]!=0 + assert lookup(1)==(0,0x10010) + def setter(sel,val): + sd.wr32(req+56,sel);sd.wr32(req+60,val) + return sd.run_function(ENTRY,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert setter(5,0)==0 and sd.rd32(GATE0)==0 + assert lookup(0)==(0,0x20010) + # SVC 0x66 returns *SYS_DRV's* identity (modeled as 0), not class 5. + assert setter(13,0)==0 and sd.rd32(GATE5)==0 + # The selector-0 comparison runs first. Both matching gates still + # reject class 5; disabling gate 0 lets the class-5 branch run. + assert lookup(5)[0]!=0 + assert setter(5,before[0])==0 + got=lookup(5) + assert got==(0,0x30010),(got,hex(sd.rd32(GATE5)),hex(sd.rd32(TABLE+72))) + assert lookup(0x20)[0]!=0 + assert setter(5,before[0])==0 and setter(13,before[1])==0 + assert (sd.rd32(GATE0),sd.rd32(GATE5))==before and not sd.faults + print('signed 0x1030 selectors 5/13 enabled synthetic flash classes 0/5; restored modeled bytes') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1031_mode_oracle.py b/psp_emu/harness/probe_sysdrv_1031_mode_oracle.py new file mode 100644 index 0000000..4387aba --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1031_mode_oracle.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 free 0x1031 selector 0xf returns mode byte inline. +Only emulator-private value is changed in the second control; no card work. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + assert sd.read(0xE26259,1)==b'\x01' + req=sd.alloc(0x100) + def query(mode): + sd.write(req,bytes(0x100)) + sd.wr32(req,0x1031);sd.wr32(req+4,2) + sd.wr32(req+14*4,mode);sd.wr32(req+15*4,0xAA55AA55) + ret=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + return ret,sd.rd32(req+15*4) + assert query(0xF)==(0,1) + sd.write(0xE26259,b'\x00') + assert query(0xF)==(0,0) + status,marker=query(0x19) + assert status!=0 and marker==0xAA55AA55,(hex(status),hex(marker)) + print('signed 0x1031 selector15 returns isolated mode byte 1 then planted 0 inline; invalid selector leaves marker; no card query') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1032_status_flag.py b/psp_emu/harness/probe_sysdrv_1032_status_flag.py new file mode 100644 index 0000000..5c10b4b --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1032_status_flag.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x1032 / 0x1077 private flag roundtrip (emulator only).""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +FLAG=0xE26150 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg=sd.alloc(0x100),sd.alloc(0x400) + sd.write(cfg,bytes(0x400)) + original=sd.read(FLAG,1) + def run(op): + sd.write(req,bytes(0x100)) + sd.wr32(req,op);sd.wr32(req+4,2) + sd.wr32(req+sysdrv_env.REQ_CONFIG_DWORD*4,cfg) + result=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + return result,sd.read(req+14*4,16) + before=run(0x1077) + set_result,_=run(0x1032) + after=run(0x1077) + try: + assert original==b'\x00' and sd.read(FLAG,1)==b'\x01' + assert set_result==0xFFFF0008,(hex(set_result),sd.faults) + assert before[0]==after[0]==0 and before[1][8]==0 and after[1][8]==1,(before,after) + print('signed 0x1032 returned 0xffff0008 after setting E26150=1; 0x1077 status byte+8 changed 0->1 (model only)') + finally:sd.write(FLAG,original) + assert sd.read(FLAG,1)==original +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1033_short_input.py b/psp_emu/harness/probe_sysdrv_1033_short_input.py new file mode 100644 index 0000000..5e67621 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1033_short_input.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""SYS_DRV .54 0x1033: signed 40-byte read despite 4-byte input declaration. + +Synthetic caller window with the main type-5 0x400000..0x800000 bounds from +signed sOS SVC 0xB5 (not its live PCB state or PTEs). State-changing helper +sub_E09E70 is intercepted after the signed 40-byte copy. Never infer a +hardware or out-of-process read from the synthetic edge fault. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE, UC_PROT_NONE +from unicorn.arm_const import UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0, UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY, LEAF, MUTATOR = 0xE087E4, 0xE134B8, 0xE09E70 +BASE, END = 0x400000, 0x800000 +SENTINEL = 0x8000000A + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + sd.uc.mem_map(BASE, 0x2000) + regions = sd.uc.mem_regions() + if not any(lo <= END-4 <= hi for lo, hi, _ in regions): + sd.uc.mem_map(END-0x1000, 0x1000) + # sysdrv_env's synthetic flash mapping can overlap the 8-MB type-5 + # descriptor boundary. Deliberately remove read permission on the next + # emulator page; this models a boundary, NOT the card's real PTEs. + if any(lo <= END <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_protect(END, 0x1000, UC_PROT_NONE) + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, val in enumerate((BASE, END, 0, 0)): + comp.wr32(out + 4*i, val) + return 0 + + sd.svc.register(0xB5, svc_window) + leaf_visits: list[int] = [] + captured: list[bytes] = [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, size, data: leaf_visits.append(ea), + begin=LEAF, end=LEAF+1) + + def block_mutation(uc, ea, size, data): + captured.append(bytes(uc.mem_read(uc.reg_read(UC_ARM_REG_R1), 40))) + uc.reg_write(UC_ARM_REG_R0, SENTINEL) + uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE, block_mutation, begin=MUTATOR, end=MUTATOR+1) + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + src, edge = BASE+0x1000, END-4 + planted = bytes(0x31 + i for i in range(40)) + sd.write(src, planted) + sd.write(edge, planted[:4]) + + def request(ptr: int, declared: int) -> int: + sd.write(req, b'\x00' * 0x100) + for i, word in ((0, 0x1033), (2, ptr), (3, declared), (14, 0), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4*i, word) + return sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + + rv = request(src, 4) + assert rv == SENTINEL and len(leaf_visits) == 1 and captured == [planted] + print('declared input 4: signed leaf copied 40 bytes before state helper (blocked)') + + before_leaf, before_cap = len(leaf_visits), len(captured) + denied = request(edge, 40) + assert denied != 0 and len(leaf_visits) == before_leaf + assert len(captured) == before_cap + print('synthetic window edge declared 40: rejected before leaf:', hex(denied)) + + try: + request(edge, 4) + except Fault as exc: + assert len(leaf_visits) == before_leaf + 1 and len(captured) == before_cap + assert END <= exc.addr < END+40, (exc, sd.faults) + print('synthetic edge declared 4: signed leaf fault before state helper:', + hex(exc.addr)) + else: + raise AssertionError('short-declared window edge unexpectedly completed') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1034_failed_status_store.py b/psp_emu/harness/probe_sysdrv_1034_failed_status_store.py new file mode 100644 index 0000000..77092ba --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1034_failed_status_store.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1034: simulated config success + status-refresh failure +still writes fixed +0x994 word. Swapped CPU window, no hardware. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +BASEPTR=0xE2579C + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + old=sd.rd32(BASEPTR);fake=sd.alloc(0x1000);req=sd.alloc(0x100) + sd.write(fake,bytes(0x1000));sd.wr32(fake+0x990,0x11111111) + sd.wr32(fake+0x994,0xA5A5A5A5);sd.wr32(fake+0x998,0x22222222) + sd.wr32(BASEPTR,fake) + calls=[];modes=[] + def ret(uc,value): + uc.reg_write(UC_ARM_REG_R0,value) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + def synth_getter(uc,ea,size,data): + calls.append(uc.reg_read(UC_ARM_REG_R0)) + if len(calls)==1: + sd.wr32(uc.reg_read(UC_ARM_REG_R0),0) + sd.wr32(uc.reg_read(UC_ARM_REG_R1),0) + ret(uc,0) + else:ret(uc,9) + def synth_config(uc,ea,size,data): + modes.append(uc.reg_read(UC_ARM_REG_R0)) + ret(uc,0) # synthetic success, NOT actual CCP/hardware + sd.uc.hook_add(UC_HOOK_CODE,synth_getter,begin=0xE0A352,end=0xE0A353) + sd.uc.hook_add(UC_HOOK_CODE,synth_config,begin=0xE09E70,end=0xE09E71) + try: + sd.write(req,bytes(0x100)) + for i,v in ((0,0x1034),(1,2),(14,0)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert rv==9 and len(calls)==2 and modes==[1],(hex(rv),calls,modes) + assert [sd.rd32(fake+x) for x in (0x990,0x994,0x998)]==[0x11111111,0,0x22222222] + print(f'signed 0x1034: synthetic config success + status getter error9 -> fixed synthetic CPU word +0x994 cleared despite error; original base {old:#x} untouched') + finally:sd.wr32(BASEPTR,old) + assert sd.rd32(BASEPTR)==old +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1038_length_gate.py b/psp_emu/harness/probe_sysdrv_1038_length_gate.py new file mode 100644 index 0000000..69dc404 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1038_length_gate.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x1038 forwards declared output length as loop count. +No large loop is run: default emulator lacks shared crypto base. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from pspemu.component import Fault +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,out=[sd.alloc(n) for n in (0x100,0x400,0x40)] + sd.write(cfg,bytes(0x400));entry=[];crypto=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:entry.append(tuple(uc.reg_read(x) for x in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))),begin=0xE07C80,end=0xE07C81) + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:crypto.append(ea),begin=0xE146A0,end=0xE146A1) + def request(width,selector): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x1038),(1,2),(2,out),(3,width),(16,selector), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + try:return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + except Fault as exc:return exc + invalid=request(16,0) + assert not isinstance(invalid,Fault) and len(entry)==1 and len(crypto)==0 + valid=request(16,1) + assert isinstance(valid,Fault) and len(entry)==2 and len(crypto)==1 + wide=request(0x800000,1) + assert isinstance(wide,Fault) and len(entry)==3 and len(crypto)==2 + assert entry[-1]==(out,0x800000,1),entry + print('signed 0x1038: length 0x800000 passed modeled role window into crypto loop; first helper faulted on missing shared base; no long loop or card effect') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_103e_1047_key_store.py b/psp_emu/harness/probe_sysdrv_103e_1047_key_store.py new file mode 100644 index 0000000..6c5de28 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_103e_1047_key_store.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 shared-key store: modeled 0x2A000 SVC 0x7D mapping. + +Synthetic pool, caller windows and second caller index. This is NOT a card +experiment, natural sOS mapping, cross-process impact or persistence proof. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn.arm_const import UC_ARM_REG_R0, UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +POOL_SIZE, HEADER_OFF, MAGIC = 0x2A000, 0x29FF0, 0x50535024 +ENTRY, WINDOW, LIMIT = 0xE087E4, 0x400000, 0x800000 +KEY = 0x28 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + pool = sd.alloc(POOL_SIZE) + sd.write(pool, bytes(POOL_SIZE)) + visits: list[int] = [] + + def svc_pool(comp, num): + visits.append(num) + return pool + + def svc_window(comp, num): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, word in enumerate((WINDOW, LIMIT, 0, 0)): + comp.wr32(out + 4*i, word) + return 0 + + sd.svc.register(0x7D, svc_pool) + sd.svc.register(0x7E, lambda comp, num: comp.uc.reg_read(UC_ARM_REG_R0)) + sd.svc.register(0xB5, svc_window) + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + # The SVC 0x7D mapping contents have no modeled signed sOS producer. + # Plant only the fixed header and a 16-byte initial used-size; subsequent + # record creation and overwrite execute signed SYS_DRV code unmodified. + sd.wr32(pool + HEADER_OFF, MAGIC) + sd.wr32(pool + HEADER_OFF + 4, 0) + sd.wr32(pool + HEADER_OFF + 8, 16) + sd.uc.mem_map(WINDOW, 0x3000) + a, b = WINDOW + 0x1000, WINDOW + 0x2000 + sd.write(a, b'AAAA') + sd.write(b, b'BBBB!') + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, bytes(0x400)) + + def request(opcode: int, caller: int, key: int, src: int, length: int) -> int: + sd.write(req, bytes(0x100)) + for i, word in ((0, opcode), (2, src), (3, length), (14, key), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4*i, word) + return sd.run_function(ENTRY, (req, caller, 0, 0), count=5_000_000, + timeout=4_000_000) + + r1 = request(0x1047, 2, KEY, a, 4) + count1 = sd.rd32(pool + HEADER_OFF + 4) + before = bytes(sd.uc.mem_read(pool, 4)) + assert r1 == 0 and count1 == 1 and before == b'AAAA', (hex(r1), count1, before) + r2 = request(0x1047, 3, KEY, b, 4) + count2 = sd.rd32(pool + HEADER_OFF + 4) + after = bytes(sd.uc.mem_read(pool, 4)) + assert r2 == 0 and count2 == 1 and after == b'BBBB', (hex(r2), count2, after) + r3 = request(0x1047, 3, KEY, b, 5) + assert r3 != 0 and bytes(sd.uc.mem_read(pool, 4)) == b'BBBB' + bad_low_mode = request(0x103E, 2, KEY, a, 4) + bad_high_mode = request(0x1047, 2, 0x80000028, a, 4) + assert bad_low_mode != 0 and bad_high_mode != 0 + assert sd.rd32(pool + HEADER_OFF + 4) == 1 + assert bytes(sd.uc.mem_read(pool, 4)) == b'BBBB' + print('signed dispatcher: caller 2 creates low-key record, caller 3 replaces same-key bytes') + print('count',count1,'->',count2,'data',before,'->',after, + 'wrong-length status',hex(r3),'mode mismatch',hex(bad_low_mode), + hex(bad_high_mode),'SVC 0x7D visits',len(visits)) + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1041_104f_count_boundary.py b/psp_emu/harness/probe_sysdrv_1041_104f_count_boundary.py new file mode 100644 index 0000000..0e405b5 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1041_104f_count_boundary.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1041/0x104F: 64-entry status depends on index placement. +All 97 activity bytes and values synthetic; no live firmware-table data. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +FLAGS,VALUES_A,VALUES_B=0xE60A48,0xE60BB0,0xE60D38 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + init,fault,_=sysdrv_env.run_init(sd) + assert init==0 and not fault and sysdrv_env.init_flag(sd)==1 + req,cfg,out=sd.alloc(0x100),sd.alloc(0x400),sd.alloc(0x240,fill=b'\xA5') + sd.write(cfg,bytes(0x400)) + for i in range(1,98): + sd.wr32(VALUES_A+4*i,0x10000+i) + sd.wr32(VALUES_B+4*i,0x20000+i) + for op in (0x1041,0x104F): + for label,indices,expected in ( + ('first63',range(1,64),0), + ('first64',range(1,65),0x80000309), + ('last64',range(34,98),0), + ): + sd.write(FLAGS,bytes(100)) + for i in indices:sd.write(FLAGS+i,b'\x01') + sd.write(req,bytes(0x100));sd.write(out,b'\xA5'*0x240) + for i,val in ((0,op),(1,2),(2,out),(3,0x204), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,val) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert rv==expected and sd.rd32(out)==len(indices) and not sd.faults,(op,label,rv,sd.rd32(out)) + assert sd.read(out+0x204,0x3C)==b'\xA5'*0x3C + print(f'{op:#x} {label}: status={rv:#x} count={sd.rd32(out)}') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1044_default_gate.py b/psp_emu/harness/probe_sysdrv_1044_default_gate.py new file mode 100644 index 0000000..75a1c26 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1044_default_gate.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x1044 default resource gate only; never call +stateful SVC 0x8B or plant a resource descriptor. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + rv,f,_=sysdrv_env.run_init(sd) + assert rv==0 and not f and sysdrv_env.init_flag(sd)==1 + assert sd.rd32(0xE45AA0)==0 + def forbidden(comp,n): + raise AssertionError('stateful SVC 0x8B reached') + sd.svc.register(0x8B,forbidden) + req=sd.alloc(0x100);sd.write(req,bytes(0x100)) + for i,v in ((0,0x1044),(1,2),(14,0x12345678),(15,0x87654321)): + sd.wr32(req+4*i,v) + status=sd.run_function(0xE087E4,(req,2,0,0),count=200_000,timeout=2_000_000) + assert status==0xFFFF0007 and sd.rd32(req+14*4)==0x12345678 and sd.rd32(req+15*4)==0x87654321 + print('signed 0x1044: isolated init E45AA0=0 -> 0xffff0007 before stateful SVC8B; inline sentinel unchanged; no live mapping') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_104d_property_prefix.py b/psp_emu/harness/probe_sysdrv_104d_property_prefix.py new file mode 100644 index 0000000..660020d --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_104d_property_prefix.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x104D: static type-1 property accepts short prefixes. +Do not print property values or treat embedded ELF properties as secrets. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + ret,fault,_=sysdrv_env.run_init(sd) + assert ret==0 and not fault and sysdrv_env.init_flag(sd)==1 + req,cfg,inp=sd.alloc(0x100),sd.alloc(0x400),sd.alloc(0x100) + sd.write(cfg,bytes(0x400)) + def query(key): + sd.write(req,bytes(0x100));sd.write(inp,bytes(0x100)) + sd.write(inp,key) + for i,v in ((0,0x104D),(1,2),(2,inp),(3,len(key)),(14,0xFFFFFFFD), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert not sd.faults + return rv,sd.rd32(req+60) + full=b'gpd.tee.systemTime.protectionLevel\x00' + prefix=b'gpd.tee.systemTime.' + rv,value=query(full) + assert rv==0 + pval=query(prefix) + assert pval==(0,value),('prefix unexpectedly rejected',pval[0]) + assert query(b'not_a_property')[0]!=0 + assert query(b'')[0]!=0 # first embedded property is type 4, not type 1 + print('signed 0x104D(-3) accepted short prefix of ELF-embedded type-1 property; value redacted') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1058_default_gate.py b/psp_emu/harness/probe_sysdrv_1058_default_gate.py new file mode 100644 index 0000000..6b059e9 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1058_default_gate.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1058 default-state gate, no fabric mapping or card traffic. +This does NOT prove the gate stays zero in a real session. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env + +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY,LEAF,FABRIC=0xE087E4,0xE13170,0xE131B8 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + rv,fault,_=sysdrv_env.run_init(sd) + assert rv==0 and not fault and sysdrv_env.init_flag(sd)==1 + assert sd.read(0xE256F1,1)==b'\0' + assert sd.read(0xE256F6,1)!=b'\x18' + req,cfg=sd.alloc(0x100),sd.alloc(0x400) + sd.write(req,bytes(0x100));sd.write(cfg,bytes(0x400)) + sd.wr32(req,0x1058);sd.wr32(req+4,2) + sd.wr32(req+4*sysdrv_env.REQ_CONFIG_DWORD,cfg) + seen=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,n,data:seen.append(ea),begin=LEAF,end=LEAF) + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,n,data:seen.append(ea),begin=FABRIC,end=FABRIC) + def no_mapping(comp,num): + raise AssertionError('0x1058 unexpectedly attempted fabric map') + sd.svc.register(0x7D,no_mapping) + status=sd.run_function(ENTRY,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert status==0xFFFF0007 and not sd.faults and seen==[LEAF],(hex(status),sd.faults,seen) + print('signed default init: gate+1=0, 0x1058 returns 0xffff0007 before SVC 0x7d') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1059_error_side_effect.py b/psp_emu/harness/probe_sysdrv_1059_error_side_effect.py new file mode 100644 index 0000000..bbf1e4f --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1059_error_side_effect.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""SYS_DRV .54 0x1059: synthetic descriptor-bit mutation before error return. + +Signed dispatcher and leaf execute; SVC 0x8B returns modeled failure. This +never maps a hardware region or establishes live ownership/restoration. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY, TABLE = 0xE087E4, 0xE45A20 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + ret, fault, _ = sysdrv_env.run_init(sd) + assert ret == 0 and fault is None and sysdrv_env.init_flag(sd) == 1 + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, bytes(0x400)) + calls: list[int] = [] + def deny_region(comp, number): + calls.append(number) + return 9 + sd.svc.register(0x8B, deny_region) + slot, neighbor, last = TABLE + 8*3, TABLE+8*4, TABLE+8*15 + old0, old1, old15 = sd.rd32(slot), sd.rd32(neighbor), sd.rd32(last) + sd.wr32(slot, 0) + sd.wr32(neighbor, 0x11223344) + sd.wr32(last, 0) + + def request(index: int, flag: int) -> int: + sd.write(req, bytes(0x100)) + for i,word in ((0,0x1059),(14,0x1234),(15,index),(16,flag), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req + 4*i, word) + rv = sd.run_function(ENTRY,(req,2,0,0),count=3_000_000, + timeout=4_000_000) + assert not sd.faults, (hex(rv),sd.faults) + return rv & 0xffffffff + + try: + rv=request(3,1) + assert rv == 0xffff000a and sd.rd32(slot)==0 and not calls, (hex(rv),calls) + rv=request(16,0) + assert rv == 0xffff000a and sd.rd32(slot)==0 and not calls, (hex(rv),calls) + rv=request(3,0) + print('status',hex(rv),'selected',hex(sd.rd32(slot)), + 'neighbor',hex(sd.rd32(neighbor)),'SVC 0x8B calls',len(calls)) + assert rv==0xffff000a and sd.rd32(slot)==5 + assert sd.rd32(neighbor)==0x11223344 and calls==[0x8b], calls + rv=request(15,0) + assert rv==0xffff000a and sd.rd32(last)==5 and len(calls)==2 + # Only exercise the signed 0x311A leaf's early invalid-index branch; + # never invoke the stateful clear/unmap path for an accepted selector. + denied=sd.run_function(0xE1B48C,(15,),count=10_000,timeout=1_000_000) + assert not sd.faults and denied != 0 and sd.rd32(last)==5 + assert len(calls)==2, calls + print('slot 15 set to',hex(sd.rd32(last)), + 'signed 0x311A early guard returns',hex(denied & 0xffffffff)) + finally: + sd.wr32(slot,old0) + sd.wr32(neighbor,old1) + sd.wr32(last,old15) + assert sd.rd32(slot)==old0 and sd.rd32(neighbor)==old1 and sd.rd32(last)==old15 + print('synthetic descriptor snapshot restored; no card request') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_105a_error_flag.py b/psp_emu/harness/probe_sysdrv_105a_error_flag.py new file mode 100644 index 0000000..9f0da97 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_105a_error_flag.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Signed .54 0x105A selector3: model only fixed-register helper failure +and check whether private global status is still changed. No card I/O. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +FLAG=0xE26154;GATE=0xE26164 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req=sd.alloc(0x100);record=sd.alloc(0x40) + sd.write(record,bytes(0x40));sd.wr32(record,4);sd.wr32(record+4,528) + before=sd.rd32(FLAG);gate=sd.rd32(GATE) + assert gate==0x100 and before==0,(hex(gate),hex(before)) # default signed-model gate is closed + seen=[];consumed=[] + def fail_hw(uc,ea,size,data): + seen.append(uc.reg_read(UC_ARM_REG_R0)) + uc.reg_write(UC_ARM_REG_R0,0x80000306) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,fail_hw,begin=0xE02A38,end=0xE02A39) + def capture_consumer(uc,ea,size,data): + consumed.append(uc.reg_read(UC_ARM_REG_R0)) + uc.reg_write(UC_ARM_REG_R0,0x4242) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,capture_consumer,begin=0xE201BC,end=0xE201BD) + try: + assert sd.run_function(0xE20108,(record,),count=200_000,timeout=200_000)==0 and not consumed + sd.write(req,bytes(0x100)) + for i,v in ((0,0x105A),(1,2),(14,3)): + sd.wr32(req+4*i,v) + blocked=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert blocked!=0 and not seen and sd.rd32(FLAG)==before,(hex(blocked),seen) + sd.wr32(GATE,0) # synthetic firmware-derived gate, restored below + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + assert seen==[528] and rv==0x80000306 and sd.rd32(FLAG)==before|8,(seen,hex(rv),hex(before),hex(sd.rd32(FLAG))) + consumer=sd.run_function(0xE20108,(record,),count=200_000,timeout=200_000) + assert consumer==0x4242 and consumed==[528],(hex(consumer),consumed) + print(f'signed 0x105a(3): default gate={gate:#x} blocked={blocked:#x}; planted gate0 + modeled helper failure={rv:#x} changed flag {before:#x}->{sd.rd32(FLAG):#x}; signed type4/528 consumer then reached stubbed helper (was gated before); no hardware') + finally: + sd.wr32(FLAG,before) + sd.wr32(GATE,gate) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_105d_zero_length.py b/psp_emu/harness/probe_sysdrv_105d_zero_length.py new file mode 100644 index 0000000..5cdc40d --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_105d_zero_length.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Signed .54 0x105D zero-length range: capture modeled fixed TMR writes only. +No card, fabric, or external client's descriptor is modified. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_R6,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def run(start,length): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + # Bypass only the model's external-timebase-dependent mutex helper; + # the signed validation, descriptor allocator and TMR writes still run. + def mutex_success(uc,ea,size,data): + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,mutex_success,begin=0xE16210,end=0xE16211) + hits=[];entered=[];types=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,pc,size,data:types.append((uc.reg_read(UC_ARM_REG_R6),uc.reg_read(UC_ARM_REG_R1),sd.rd32(uc.reg_read(UC_ARM_REG_R6)+16))),begin=0xE14550,end=0xE14551) + for ea in (0xE1447C,0xE144D2,0xE144F2,0xE144FA,0xE14500,0xE14508,0xE145E2,0xE17EF4,0xE218FC): + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,pc,size,data:entered.append(pc),begin=ea,end=ea+1) + def on_write(comp,n): + hits.append(tuple(comp.uc.reg_read(reg) for reg in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))) + return 0 + sd.svc.register(0x7C,on_write) + req=sd.alloc(0x100) + sd.write(req,bytes(0x100)) + # msg[18] is the subtype for 0x105D. The config pointer at the + # same index is needed only for init (0x1000), not this request. + for i,v in ((0,0x105D),(1,2),(14,start),(15,0),(16,length),(17,0),(18,49)): + sd.wr32(req+i*4,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + return rv,hits,sd.rd32(req+19*4),entered,types + +def main(): + for start,length,want in ((0,0,(0,0xffffffff,0x37,0x30038)), + (0,0x10000,(0,0,0x37,0x30038)), + (0x10000,0,(1,0,0x37,0x30038))): + rv,hits,slot,entered,types=run(start,length) + assert rv==0 and types and types[0][1:]==(49,49) and 0xE17EF4 in entered,(rv,entered,types) + assert hits==[(0x49007a00+4*i,v,4) for i,v in enumerate(want)],hits + print(f'start={start:#x} length={length:#x} status={rv:#x} slot={slot:#x} model-fabric={[hex(v) for v in want]}') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_105f_reg_tables.py b/psp_emu/harness/probe_sysdrv_105f_reg_tables.py new file mode 100644 index 0000000..1233056 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_105f_reg_tables.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Read only modeled SYS_DRV .54 0x105F indexed register-address tables. +Local signed init uses synthetic flash and default-success unrelated SVCs. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + groups=((0xE2664C,16),(0xE2668C,16),(0xE266CC,16),(0xE2670C,16), + (0xE2674C,16),(0xE2678C,16),(0xE267CC,16), + (0xE2685C,18),(0xE268A4,18)) + before=[tuple(sd.rd32(addr+4*i) for i in range(n)) for addr,n in groups] + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + after=[tuple(sd.rd32(addr+4*i) for i in range(n)) for addr,n in groups] + for (addr,n),pre,post in zip(groups,before,after): + print(f'table={addr:#x} n={n} pre-nonzero={sum(bool(x) for x in pre)} post-nonzero={sum(bool(x) for x in post)} changed={sum(a!=b for a,b in zip(pre,post))} addresses={[hex(x) for x in post]}') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_105f_short_input.py b/psp_emu/harness/probe_sysdrv_105f_short_input.py new file mode 100644 index 0000000..27bd803 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_105f_short_input.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Signed .54 0x105F reads +32/+61 of a four-byte-declared input. +Synthetic caller-window edge; no card/fabric write. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from pspemu.component import Fault +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2 +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,inp=sd.alloc(0x100),sd.alloc(0x80) + sd.write(inp,bytes(0x80)) + writes=[] + sd.svc.register(0x7C,lambda c,n:writes.append(tuple(c.uc.reg_read(reg) for reg in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))) or 0) + def run(ptr,length): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x105F),(1,2),(2,ptr),(3,length)): + sd.wr32(req+4*i,v) + try:return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + except Fault as exc:return exc + mapped=run(inp,4) + assert mapped==0 and not writes,(mapped,writes) + edge=sd.cmap.scratch+sd.cmap.scratch_size-4 + sd.write(edge,b'ABCD') + def svc_window(comp,n): + p=comp.uc.reg_read(UC_ARM_REG_R1) + for i,v in enumerate((sd.cmap.scratch,sd.cmap.scratch+sd.cmap.scratch_size,0,0)): + comp.wr32(p+4*i,v) + return 0 + sd.svc.register(0xB5,svc_window) + fault=run(edge,4) + assert isinstance(fault,Fault) and (fault.pc,fault.addr)==(0xE148EA,edge+32),(fault,writes) + denied=run(edge,63) + assert denied==0xFFFF000F and not writes,(denied,writes) + # A separate, wholly GCP-owned mapped control demonstrates the later + # signed register command: modeled SVC 0x7C captures but never executes it. + sd.wr32(inp+24,0x13579BDF);sd.wr32(inp+28,0x2468ACE0) + rv_short=run(inp,4);short=tuple(writes);writes.clear() + rv_full=run(inp,64);full=tuple(writes) + expected=((0x69EAC,0x13579BDF,4),(0x69EB0,0x2468ACE0,4), + (0x69F2C,0,4),(0x69F30,0,4),(0x69FAC,0,4),(0x69FB0,0,4)) + assert rv_short==rv_full==0 and short==full==expected,(rv_short,rv_full,short,full) + print(f'signed 0x105F: four-byte-declared first read fault pc={fault.pc:#x} FAR={fault.addr:#x}; declared63 edge rejected {denied:#x}; modeled declared4/64 controls captured identical six fixed-address writes (no hardware)') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1063_short_output.py b/psp_emu/harness/probe_sysdrv_1063_short_output.py new file mode 100644 index 0000000..d79bf9c --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1063_short_output.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 free 0x1063 writes 172 bytes with 4 declared output. +All destinations are synthetic GCP-owned mapped memory; no card effect. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,out=sd.alloc(0x100),sd.alloc(0x400),sd.alloc(0xC0,fill=b'\xA5') + sd.write(cfg,bytes(0x400)) + def run(declared,count): + sd.write(req,bytes(0x100));sd.write(out,b'\xA5'*0xC0) + for i,v in ((0,0x1063),(1,2),(2,out),(3,declared),(14,count), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert not sd.faults + return rv,sd.read(out,0xC0) + err,untouched=run(4,5) + assert err!=0 and untouched==b'\xA5'*0xC0 + rv,short=run(4,6) + assert rv==0 and short[168:172]!=b'\xA5'*4 and short[172:]==b'\xA5'*20 + assert int.from_bytes(short[:4],'little')==1 + assert sd.rd32(req+56)==6 # count field updated, independent of output length + full,wide=run(172,6) + assert full==0 and wide[:172]==short[:172] and wide[172:]==b'\xA5'*20 + print('signed 0x1063: 4-byte declared output, selected store through +171; full-length and count-5 controls passed') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1067_failed_lock_count.py b/psp_emu/harness/probe_sysdrv_1067_failed_lock_count.py new file mode 100644 index 0000000..c40a2a9 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1067_failed_lock_count.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1067: bounded modeled lock failure still increments count. +Synthetic fixed-register SVCs and time yield; no card/fabric effects. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +COUNT=0xE252D0 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + original=sd.read(COUNT,1) + assert original==b'\x00',original + req=sd.alloc(0x100) + reads=[];writes=[];clears=[] + def read_fixed(comp,n): + addr=comp.uc.reg_read(UC_ARM_REG_R0) + dest=comp.uc.reg_read(UC_ARM_REG_R1) + reads.append(addr) + assert addr in (0x3B10024,0x49005300),hex(addr) + sd.wr32(dest,0 if addr==0x3B10024 or reads.count(0x49005300)==1 else 1) + return 0 + def write_fixed(comp,n): + writes.append(tuple(comp.uc.reg_read(reg) for reg in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))) + return 0 + def masked_clear(comp,n): + clears.append(tuple(comp.uc.reg_read(reg) for reg in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))) + return 0 + sd.svc.register(0x7B,read_fixed) + sd.svc.register(0x7C,write_fixed) + sd.svc.register(0xA6,masked_clear) + def model_yield(uc,ea,size,data): + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,model_yield,begin=0xE1ECEE,end=0xE1ECEF) + def run(selector,timeout): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x1067),(1,2),(14,selector),(15,timeout)): + sd.wr32(req+4*i,v) + return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + try: + failed=run(0,1) + assert failed==0x80000306 and sd.read(COUNT,1)==b'\x01',(hex(failed),sd.read(COUNT,1),reads,writes) + assert writes==[(0x49005300,0x20,4)],writes + read_count=len(reads) + false_success=run(0,1) + assert false_success==0 and sd.read(COUNT,1)==b'\x02' and len(reads)==read_count and len(writes)==1,(false_success,reads,writes) + first_release=run(1,1) + assert first_release==0 and sd.read(COUNT,1)==b'\x01' and not clears,(first_release,clears) + second_release=run(1,1) + assert second_release==0 and sd.read(COUNT,1)==b'\x00',(second_release,clears) + assert clears==[(0x49005300,0,0x20)],clears + print('signed 0x1067: modeled timeout1 returned 0x80000306 yet count 0->1; next acquire returned false success 0 and count2 without register read; two releases reached fixed SVC A6 bit5 clear (no hardware)') + finally:sd.write(COUNT,original) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_106a_short_spans.py b/psp_emu/harness/probe_sysdrv_106a_short_spans.py new file mode 100644 index 0000000..d024752 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_106a_short_spans.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x106A selector 2: input/output declared span mismatch. +Artificial gate only if natural emulator init leaves E25010 disabled. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +GATE=0xE25010 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,inp,out=sd.alloc(0x100),sd.alloc(0x400),sd.alloc(0x40),sd.alloc(0x80) + sd.write(cfg,bytes(0x400));sd.write(inp,bytes(0x40)) + sd.wr32(inp+4,2);sd.wr32(inp+12,0) + old=sd.rd32(GATE) + def run(inlen,outlen): + sd.write(req,bytes(0x100));sd.write(out,b'\xA5'*0x80) + for i,v in ((0,0x106A),(1,2),(2,inp),(3,inlen),(4,out),(5,outlen), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert not sd.faults + return rv,sd.read(out,0x80) + if old==0: + rv,unchanged=run(16,4) + assert rv==0x80000310 and unchanged==b'\xA5'*0x80 + sd.wr32(GATE,1) # synthetic private state; never done on hardware + try: + rv,short=run(16,4) + assert rv==0 and short[20:24]!=b'\xA5'*4 and short[64:68]!=b'\xA5'*4 and short[68:]==b'\xA5'*0x3C + rv,wide=run(16,68) + assert rv==0 and wide[:68]==short[:68] + rv,shortboth=run(4,4) + assert rv==0 and shortboth[:68]==short[:68] + finally: + sd.wr32(GATE,old) + print(f'signed 0x106A(2): gate-init={old}, declared input/output 4/4 still reaches +12 read and +64 store; restored modeled gate') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_106c_length_wrap.py b/psp_emu/harness/probe_sysdrv_106c_length_wrap.py new file mode 100644 index 0000000..8513598 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_106c_length_wrap.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x106C: rounded request wraps to zero, repeats same result. +Plants private pool base only in emulator and restores snapshots. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +BASE_LO,BASE_HI,USED=0xE5FE84,0xE5FE88,0xE25790 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + initial=[sd.rd32(a) for a in (BASE_LO,BASE_HI,USED)] + req,cfg=sd.alloc(0x100),sd.alloc(0x400) + sd.write(cfg,bytes(0x400)) + def run(length): + sd.write(req,bytes(0x100)) + for i,v in ((0,0x106C),(1,2),(16,length), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert not sd.faults + return rv,sd.rd32(req+56),sd.rd32(req+60),sd.rd32(USED) + default_status=run(0xFFFFF000)[0] + assert [sd.rd32(a) for a in (BASE_LO,BASE_HI,USED)]==initial + try: + sd.wr32(BASE_LO,0x10000000);sd.wr32(BASE_HI,0);sd.wr32(USED,0) + normal=run(0x1000) + assert normal[0]==0xFFFF000C and normal[3]==0 + assert run(0xFFFFE000)[0]==0xFFFF000C + assert run(0xFFFFE001)[0]==0 + first=run(0xFFFFF000) + second=run(0xFFFFF000) + assert first[0]==second[0]==0 and first[1:]==second[1:] and first[3]==0 + assert run(0xFFFFF001)[0]==0xFFFF000C + sd.wr32(USED,1) + assert run(0xFFFFF000)[0]==0xFFFF000C + finally: + for a,v in zip((BASE_LO,BASE_HI,USED),initial):sd.wr32(a,v) + print(f'signed 0x106C: default base_present={int(bool(initial[0] or initial[1]))} default_status={default_status:#x}; wrapped length repeated success with zero progress, controls passed') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1076_dispatch.py b/psp_emu/harness/probe_sysdrv_1076_dispatch.py new file mode 100644 index 0000000..61f1189 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1076_dispatch.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 opcode 0x1076 short-output test, synthetic caller region. + +No sOS/GCP transport or hardware effects are modeled. The successful selector-1 +path also clears SYS_DRV internal state and invokes SVC 0x7B: never use this +harness as a justification for an unplanned hardware request. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY = 0xE087E4 +LEAF = 0xE2005C +TABLE = 0xE6873C +END_OF_GCP = 0x41C000 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes(), 'image differs from signed .54' + init_ret, init_fault, _ = sysdrv_env.run_init(sd) + assert init_ret == 0 and not init_fault and sysdrv_env.init_flag(sd) == 1 + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + seen = [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, size, data: seen.append(ea), + begin=LEAF, end=LEAF) + + sd.write(req, b'\x00' * 0x100) + for i, v in ((0, 0x1076), (1, 2), (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4 * i, v) + for declared in (4, 52): + out = sd.alloc(0x80, fill=b'\xA5') + sd.wr32(out, 1) # selector 1 selects the fixed 24-byte zeroing branch + sd.wr32(req + 8, out) + sd.wr32(req + 12, declared) + sd.wr32(TABLE + 4, 0x55667788) + before = len(seen) + rv = sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + after = sd.read(out, 0x80) + assert rv == 0 and len(seen) == before + 1 and not sd.faults, (rv, sd.faults) + assert after[:4] == b'\x01\x00\x00\x00' + assert after[4:28] == b'\xA5' * 24 and after[28:52] == b'\x00' * 24 + assert after[52:] == b'\xA5' * (0x80 - 52) + assert sd.rd32(TABLE + 4) == 0 + print('declared', declared, 'status', hex(rv), 'leaf', len(seen) - before, + 'changed offsets 28..51; synthetic table cleared') + + # Caller-window model, not the live sOS/GCP mapping. The declaration of 52 + # must be rejected, while 4 may pass and then fault at the first store. + base, end = 0x400000, END_OF_GCP + sd.uc.mem_map(base, end - base) + edge = end - 4 + sd.wr32(edge, 1) + sd.wr32(req + 8, edge) + sd.wr32(TABLE + 4, 0x55667788) + + def caller_window(comp, num): + descriptor = comp.uc.reg_read(UC_ARM_REG_R1) + for i, val in enumerate((base, end, 0, 0)): + comp.wr32(descriptor + 4 * i, val) + return 0 + + sd.svc.register(0xB5, caller_window) + before = len(seen) + sd.wr32(req + 12, 52) + rejected = sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + assert rejected != 0 and len(seen) == before + assert sd.rd32(edge) == 1 and sd.rd32(TABLE + 4) == 0x55667788 + sd.wr32(req + 12, 4) + try: + sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + except Fault as exc: + assert end <= exc.addr < end + 0x40, (exc, sd.faults) + assert len(seen) == before + 1 and sd.rd32(TABLE + 4) == 0x55667788 + assert sd.rd32(edge) == 1 + print('synthetic window: length52 rejected', hex(rejected), + 'length4 first out-of-range store faults at', hex(exc.addr)) + else: + raise AssertionError('short length did not fault at synthetic window edge') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1079_fixed_write_gate.py b/psp_emu/harness/probe_sysdrv_1079_fixed_write_gate.py new file mode 100644 index 0000000..0b69e2a --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1079_fixed_write_gate.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1079 fixed-fabric path with restricted vs privileged +synthetic SVC 0x76 caps. Captures SVC 0x7C; no hardware effects. +""" +from pathlib import Path +import sys +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +GATE=0xE26314 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + original=sd.rd32(GATE) + req=sd.alloc(0x100);writes=[] + caps=[0x10] # Synthetic restricted profile, NOT measured GCP rights. + sd.svc.register(0x76,lambda c,n:int((c.uc.reg_read(UC_ARM_REG_R0)&~caps[0])==0)) + sd.svc.register(0x7C,lambda c,n:writes.append(tuple(c.uc.reg_read(reg) for reg in (UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2))) or 0) + def run(op,value): + sd.write(req,bytes(0x100)) + for i,v in ((0,op),(1,2),(14,value)): + sd.wr32(req+4*i,v) + return sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + try: + assert original==2,hex(original) + assert run(0x1079,0x12345678)==0 and writes==[(0x3B30600,0x12345678,4)],writes + denied=run(0x3131,1) + assert denied==0xFFFF0001 and sd.rd32(GATE)==original,(denied,sd.rd32(GATE)) + assert run(0x1079,0xEE000123)==0 and len(writes)==1,writes + assert run(0x1079,0xEF000123)==0 and len(writes)==1,writes + caps[0]=0xFFFF0 # Synthetic privileged issuer, NOT GCP. + setter=run(0x3131,0) + assert setter==0 and sd.rd32(GATE)==0,(setter,sd.rd32(GATE)) + caps[0]=0x10 + assert run(0x1079,0x12345678)==0 and len(writes)==1 + caps[0]=0xFFFF0 + assert run(0x3131,original)==0 and sd.rd32(GATE)==original + print(f'signed modeled init gate={original:#x}: restricted caps=0x10 0x1079 emitted chosen-value SVC7C (captured); 0x3131 denied={denied:#x}, EE/EF skipped; privileged gate0 suppressed it, restored original; no hardware') + finally: + caps[0]=0xFFFF0 + if sd.rd32(GATE)!=original: + run(0x3131,original) + assert sd.rd32(GATE)==original +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_107b_dispatch.py b/psp_emu/harness/probe_sysdrv_107b_dispatch.py new file mode 100644 index 0000000..9fe208b --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_107b_dispatch.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Bounded signed SYS_DRV .54 dispatcher/validator test for 0x107B output size. + +Synthetic SVC 0xB5 reports a permissive region: this is NOT a full sOS/GCP +service-1 test or a hardware mapping/oracle. No hardware state is changed. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 +from unicorn.arm_const import UC_ARM_REG_R1 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY = 0xE087E4 +LEAF = 0xE0AB74 +HEADER = 0xE26170 +TABLE = 0xE6873C + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes(), 'emulated image is not signed .54' + init_ret, init_fault, _ = sysdrv_env.run_init(sd) + assert init_ret == 0 and not init_fault and sysdrv_env.init_flag(sd) == 1, (init_ret, init_fault) + print('signed image:', Path(sd.image_path).name, 'init gate:', sysdrv_env.init_flag(sd)) + req = sd.alloc(0x100) + cfg = sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + seen = [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, n, data: seen.append(ea), + begin=LEAF, end=LEAF) + for n in (4, 52): + out = sd.alloc(0x80, fill=b'\xA5') + sd.write(HEADER, b'\x00' * 8) + sd.write(TABLE, b'\x00' * 2048) + sd.wr32(TABLE + 4, 0x55667788) + sd.write(HEADER + 4, b'\x01\x00') + sd.write(req, b'\x00' * 0x100) + for i, v in ((0, 0x107B), (1, 2), (2, out), (3, n), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4 * i, v) + before = len(seen) + rv = sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + changed = [i for i, b in enumerate(sd.read(out, 0x80)) if b != 0xA5] + print('declared', n, 'status', hex(rv), 'leaf entry', len(seen) - before, + 'changed', (min(changed), max(changed)) if changed else None, + 'table cleared', sd.rd32(TABLE + 4) == 0) + assert rv == 0 and len(seen) - before == 1 and changed + assert min(changed) == 28 and max(changed) == 51 + assert sd.rd32(TABLE + 4) == 0 + + # Substitute only the SVC B5 caller-region descriptor with the previously + # measured GCP-window model. This is STILL synthetic, not real sOS/GCP. + base, end = 0x400000, 0x41C000 + sd.uc.mem_map(base, end - base) + edge = end - 4 + sd.write(edge, b'\xA5' * 4) + sd.wr32(TABLE + 4, 0x55667788) + sd.write(HEADER + 2, b'\x00\x00\x01\x00') + + def svc_gcp_window(comp, n): + descriptor = comp.uc.reg_read(UC_ARM_REG_R1) + for i, val in enumerate((base, end, 0, 0)): + comp.wr32(descriptor + i * 4, val) + return 0 + + sd.svc.register(0xB5, svc_gcp_window) + sd.wr32(req + 2 * 4, edge) + before = len(seen) + sd.wr32(req + 3 * 4, 52) + too_long = sd.run_function(ENTRY, (req, 2, 0, 0), + count=3_000_000, timeout=4_000_000) + assert too_long != 0 and len(seen) == before + assert sd.read(edge, 4) == b'\xA5' * 4 + sd.wr32(req + 3 * 4, 4) + try: + sd.run_function(ENTRY, (req, 2, 0, 0), + count=3_000_000, timeout=4_000_000) + except Fault as exc: + assert end <= exc.addr < end + 0x40, (exc, sd.faults) + assert len(seen) == before + 1 + assert sd.rd32(TABLE + 4) == 0x55667788 + print('synthetic GCP-window edge: length52 rejected', hex(too_long), + 'length4 reaches leaf then faults', hex(exc.addr)) + else: + raise AssertionError('short length unexpectedly returned at synthetic window edge') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_107b_multi_record_loss.py b/psp_emu/harness/probe_sysdrv_107b_multi_record_loss.py new file mode 100644 index 0000000..25aa497 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_107b_multi_record_loss.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Signed .54 0x107B multi-record snapshot loss, synthetic helper inputs/count. +No live RAP session or card mutation. Signed dispatcher/leaf run unchanged; +SYS_DRV init retains emulator's unrelated default-success SVCs. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY,INSERT,HEADER,TABLE=0xE087E4,0xE0D60C,0xE26170,0xE6873C + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + ret,fault,_=sysdrv_env.run_init(sd) + assert ret==0 and fault is None and sysdrv_env.init_flag(sd)==1 + req,cfg,out=sd.alloc(0x100),sd.alloc(0x400),sd.alloc(0x80,fill=b'\xA5') + sd.write(cfg,bytes(0x400));sd.write(req,bytes(0x100)) + for i,x in ((0,0x107B),(1,2),(2,out),(3,52),(sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,x) + def scenario(records): + sd.write(HEADER,bytes(8));sd.write(TABLE,bytes(2048)) + sd.write(out,bytes([0xA5])*0x80) + # Drive the signed table-insertion helper; synthetic keys/values, not + # signed RAP/ASIC producers. The caller must account for count as the + # real selector does; the helper itself tracks the high-water index. + for vals in records: + result=sd.run_function(INSERT,(vals[0],vals[1],vals[2],vals[3]), + count=100_000,timeout=1_000_000) + assert result==0,(result,vals) + sd.write(HEADER+4,len(records).to_bytes(2,'little')) + rv=sd.run_function(ENTRY,(req,2,0,0),count=3_000_000,timeout=4_000_000) + assert not sd.faults and rv==0,(rv,sd.faults) + assert tuple(sd.rd32(out+4*i) for i in range(7,13))==( + len(records),0,records[-1][1],records[-1][2],records[-1][3],records[-1][0]) + assert sd.read(TABLE,32)==bytes(32) and sd.read(HEADER+2,4)==bytes(4) + print(f'signed-helper records={len(records)}: returned count={sd.rd32(out+28)}, ' + f'only last key={sd.rd32(out+36):#x}; table zeroed') + a=(0x11111111,0xAAAA0001,0x11112222,0x11113333) + b=(0x22222222,0xBBBB0002,0x22223333,0x22224444) + scenario([a]);scenario([a,b]) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_107b_output_size.py b/psp_emu/harness/probe_sysdrv_107b_output_size.py new file mode 100644 index 0000000..2aff30e --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_107b_output_size.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Isolated signed SYS_DRV .54 0x107B leaf: actual stores vs declared length. + +Does NOT emulate the sOS/SYS_DRV/GCP service transport or establish a hardware +cross-process write. Executes only signed sub_E0AB74 with synthetic globals. +""" +from __future__ import annotations + +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import Component # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +REPO = Path(__file__).resolve().parents[2] +IMAGE = REPO / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +LEAF = 0xE0AB74 +HEADER = 0xE26170 +TABLE = 0xE6873C +DECLARED = 4 + + +def main() -> None: + comp = Component('sys_drv', image_path=str(IMAGE)) + output = comp.alloc(0x80, fill=b'\xA5') + comp.write(HEADER, b'\x00' * 8) + comp.write(TABLE, b'\x00' * 2048) + comp.wr32(TABLE, 0x11223344) + comp.wr32(TABLE + 4, 0x55667788) + comp.wr32(TABLE + 8, 0x99AABBCC) + comp.wr32(TABLE + 12, 0xDDEEFF00) + comp.write(HEADER + 2, b'\x00\x00') # last occupied slot = 0 + comp.write(HEADER + 4, b'\x01\x00') # occupied count = 1 + + ret = comp.run_function(LEAF, (output,), thumb=True, count=20000, + timeout=2_000_000) + after = comp.read(output, 0x80) + changed = [i for i, (a, b) in enumerate(zip(after, b'\xA5' * 0x80)) if a != b] + assert ret == 0 and not comp.faults, (ret, comp.faults) + assert all(b == 0xA5 for b in after[:DECLARED]) + assert changed and min(changed) == 28 and max(changed) == 51, changed + assert int.from_bytes(after[48:52], 'little') == 0x11223344 + assert comp.read(TABLE, 2048) == b'\x00' * 2048 + assert comp.read(HEADER + 2, 4) == b'\x00' * 4 + print('signed leaf', hex(LEAF), 'declared length', DECLARED, + 'changed output offsets', min(changed), '..', max(changed), + 'table/reset yes; no service transport modeled') + + # A separate synthetic caller-window edge: the nominal four-byte output + # is mapped, but the later fixed-size writes are not. This deliberately + # models an unmapped adjacent page, not the card's real next-page PTE. + edge = Component('sys_drv', image_path=str(IMAGE)) + guard = edge.cmap.scratch + 0x3000 + edge.wr32(TABLE + 4, 0x55667788) + edge.write(HEADER + 2, b'\x00\x00\x01\x00') + edge.write(guard - DECLARED, b'\xA5' * DECLARED) + edge.uc.mem_unmap(guard, 0x1000) + try: + edge.run_function(LEAF, (guard - DECLARED,), thumb=True, + count=20000, timeout=2_000_000) + except Fault as exc: + assert guard <= exc.addr < guard + 0x40, (exc, edge.faults) + assert edge.rd32(TABLE + 4) == 0x55667788 # reset was not reached + print('synthetic adjacent guard fault', hex(exc.addr), + 'while declared four bytes remained mapped') + else: + raise AssertionError('missing guard-page fault') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_1082_privileged_scalar.py b/psp_emu/harness/probe_sysdrv_1082_privileged_scalar.py new file mode 100644 index 0000000..d532dda --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1082_privileged_scalar.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Signed .54 0x1082 reads global word set by privileged 0x3133. +Synthetic sOS SVC76 profiles; public marker only, no card access. +""" +from pathlib import Path +import sys +from unicorn.arm_const import UC_ARM_REG_R0 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +STATE=0xE2584C + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + original=sd.rd32(STATE) + assert original==0,hex(original) + caps=[0x10] # synthetic restricted, not measured GCP caps + sd.svc.register(0x76,lambda c,n:int((c.uc.reg_read(UC_ARM_REG_R0)&~caps[0])==0)) + req=sd.alloc(0x100) + def run(op,val=0): + sd.write(req,bytes(0x100)) + for i,v in ((0,op),(1,2),(14,val)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + return rv,sd.rd32(req+14*4) + try: + assert run(0x1082)==(0,0) + denied=run(0x3133,0x12345678) + assert denied[0]==0xFFFF0001 and sd.rd32(STATE)==0,denied + caps[0]=0xFFFF0 # synthetic privileged issuer, NOT GCP + assert run(0x3133,0x12345678)[0]==0 and sd.rd32(STATE)==0x12345678 + caps[0]=0x10 + read=run(0x1082) + assert read==(0,0x12345678),read + caps[0]=0xFFFF0 + assert run(0x3133,original)[0]==0 and sd.rd32(STATE)==original + print('signed 0x3133 restricted denied; synthetic privileged marker set; free 0x1082 restricted read marker inline; signed privileged restoration, no live state') + finally: + caps[0]=0xFFFF0 + if sd.rd32(STATE)!=original: + run(0x3133,original) + assert sd.rd32(STATE)==original +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1084_short_output_inline.py b/psp_emu/harness/probe_sysdrv_1084_short_output_inline.py new file mode 100644 index 0000000..290e731 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1084_short_output_inline.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x1084 inline status words vs declared output length. +Plants only public synthetic bytes in SYS_DRV private state, restores model. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +WORD_A=0xE6027C;SOURCE=0xE60280;WORD_B=0xE60384 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req=sd.alloc(0x100);out=sd.alloc(0x40) + old=(sd.read(WORD_A,20),sd.read(WORD_B,4)) + source=bytes(range(0x10)) + def run(n): + sd.write(req,bytes(0x100));sd.write(out,b'Z'*0x40) + for i,v in ((0,0x1084),(1,2),(2,out),(3,n)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + return rv,sd.rd32(req+14*4),sd.rd32(req+15*4),sd.read(out,0x20) + try: + sd.wr32(WORD_A,0x13579BDF) + sd.write(SOURCE,source) + sd.wr32(WORD_B,0x2468ACE0) + short=run(4) + assert short[:3]==(0xFFFF300F,0x13579BDF,0x2468ACE0) and short[3]==b'Z'*0x20,short + full=run(16) + assert full[:3]==(0,0x13579BDF,0x2468ACE0) and full[3]==source+b'Z'*0x10,full + print('signed 0x1084: 4-byte output declaration returned 0xffff300f but inline synthetic metadata, no output copy; 16-byte declaration copied synthetic 16 bytes within span; no live disclosure') + finally: + sd.write(WORD_A,old[0]);sd.write(WORD_B,old[1]) +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1094_cross_partition_helper.py b/psp_emu/harness/probe_sysdrv_1094_cross_partition_helper.py new file mode 100644 index 0000000..134adf1 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1094_cross_partition_helper.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Signed .54 flash parser/helpers permit a tag-34 offset in another row. +Planted unsigned metadata; no real firmware table, SPI command or hardware mutation. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from unicorn import UC_HOOK_CODE +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +PARSE,LOOKUP,CHECK,TABLE,SRC=0xE067F8,0xE0AE90,0xE207A4,0xE7904C,0xE5FA48 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + ret,fault,_=sysdrv_env.run_init(sd) + assert ret==0 and not fault and sysdrv_env.init_flag(sd)==1 + assert sd.read(0xE26258,2)==b'\0\x01' # gate-1 SPI route, no region-check override + # The real parser's input is a private firmware metadata area. Plant only + # two artificial rows, then let signed sub_E067F8 validate and copy them. + sd.write(TABLE,bytes(0x160)) + def plant_ra2(): + sd.write(SRC+1676,bytes(256)) + sd.wr32(SRC+1668,0x32415224);sd.wr32(SRC+1672,2) + for i,(tag,base,length) in enumerate(((34,0x10000,0x1000),(4,0x20000,0x1000))): + at=SRC+1676+16*i + sd.wr32(at,tag);sd.wr32(at+4,base);sd.wr32(at+8,length) + # sysdrv_env's earlier hook overwrites $RA2 with an EMPTY synthetic table + # on every parser entry. Override that fixture only for this isolated call. + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,n,data:plant_ra2(),begin=PARSE,end=PARSE) + parse=sd.run_function(PARSE,(),count=200_000,timeout=1_000_000) + assert parse==0 and sd.rd32(TABLE+72)==2,(parse,sd.rd32(TABLE+72),sd.rd32(SRC+1672)) + assert [sd.rd32(TABLE+76+16*i) for i in range(2)]==[34,4] + result=sd.alloc(4) + def resolve(offset): + sd.wr32(result,0xAAAAAAAA) + rc=sd.run_function(LOOKUP,(1,0,offset,result),count=100_000,timeout=1_000_000) + return rc,sd.rd32(result) + rc,first=resolve(0x10) + assert rc==0 and first==0x10010 + assert sd.run_function(CHECK,(first,0x10),count=100_000,timeout=1_000_000)==0 + rc,other=resolve(0x10010) + assert rc==0 and other==0x20010 + assert sd.run_function(CHECK,(other,0x10),count=100_000,timeout=1_000_000)==0 + rc,gap=resolve(0x8010) + assert rc==0 and gap==0x18010 + denied=sd.run_function(CHECK,(gap,0x10),count=100_000,timeout=1_000_000) + assert denied!=0 and not sd.faults,(hex(denied),sd.faults) + print(f'tag34 own {first:#x} accepted; offset to tag4 {other:#x} accepted; gap {gap:#x} rejected {denied:#x}') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1097_wait_flag.py b/psp_emu/harness/probe_sysdrv_1097_wait_flag.py new file mode 100644 index 0000000..8048c39 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1097_wait_flag.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Signed .54 free 0x1097 writes shared wait flag; no mailbox/flash call. +""" +from pathlib import Path +import sys +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +FLAG=0xE26648 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg=sd.alloc(0x100),sd.alloc(0x400) + sd.write(cfg,bytes(0x400));old=sd.rd32(FLAG) + def read_flag(): + sd.write(req,bytes(0x100)) + for i,x in ((0,0x1096),(1,2),(14,0xA5A5A5A5), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,x) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert rv==0 and not sd.faults + return sd.rd32(req+56) + assert read_flag()==old + def set_flag(v): + sd.write(req,bytes(0x100)) + for i,x in ((0,0x1097),(1,2),(14,v),(sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,x) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=1_000_000,timeout=2_000_000) + assert rv==0 and sd.rd32(FLAG)==v and not sd.faults,(rv,sd.rd32(FLAG)) + assert read_flag()==v + try: + set_flag(1);set_flag(0xFFFFFFFF) + finally: + set_flag(old) # signed opcode, emulator only + print(f'signed 0x1096 getter verified free 0x1097 setter/restoration; default_zero={old==0}, emulator only') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_1098_short_input.py b/psp_emu/harness/probe_sysdrv_1098_short_input.py new file mode 100644 index 0000000..6840b04 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_1098_short_input.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Signed SYS_DRV .54 0x1098: fixed 1024-byte first read vs declared input. + +The B5 type-5 descriptor and page protection are synthetic; SVC 0x79 and +other unmodeled SVCs are emulator defaults. This does NOT show card mappings, +a hardware fault, a signed-image acceptance, or out-of-process disclosure. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE, UC_PROT_NONE +from unicorn.arm_const import UC_ARM_REG_R1, UC_ARM_REG_R7 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY, LEAF, AFTER_COPY = 0xE087E4, 0xE1217C, 0xE06570 +BASE, END = 0x400000, 0x800000 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + sd.uc.mem_map(BASE, 0x2000) + if not any(lo <= END-4 <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_map(END-0x1000, 0x1000) + # The emulator's synthetic flash mapping overlaps END: force a modeled + # protection edge, NOT a real CPU PTE or neighboring process. + if any(lo <= END <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_protect(END, 0x1000, UC_PROT_NONE) + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, val in enumerate((BASE, END, 0, 0)): + comp.wr32(out + 4*i, val) + return 0 + sd.svc.register(0xB5, svc_window) + + visited, copied = [], [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, size, data: visited.append(ea), + begin=LEAF, end=LEAF+1) + def observe_copy(uc, ea, size, data): + copied.append(bytes(uc.mem_read(uc.reg_read(UC_ARM_REG_R7), 1024))) + sd.uc.hook_add(UC_HOOK_CODE, observe_copy, begin=AFTER_COPY, end=AFTER_COPY+1) + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + src, edge = BASE+0x1000, END-4 + marker = bytes((0x40 + (i % 47)) for i in range(1024)) + sd.write(src, marker) + sd.write(edge, marker[:4]) + + def request(ptr: int, declared: int, chosen_output: int = 0) -> int: + sd.write(req, b'\x00' * 0x100) + # 0x1098's signed pointer-role row declares only slot 0 (type 2). + # msg[8] is NOT a validated output slot. The chosen-output control + # below must reject; normal requests keep it zero and use invalid + # image marker bytes, so no successful-result write is attempted. + for i, word in ((0, 0x1098), (2, ptr), (3, declared), + (8, chosen_output), (9, 4 if chosen_output else 0), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + 4*i, word) + return sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + + status = request(src, 4) + assert len(visited) == len(copied) == 1 and copied[0] == marker, ( + hex(status), len(visited), len(copied), + copied[0][:16].hex() if copied else '<not copied>', sd.faults[-2:]) + print('normal mapped input: declared 4, signed 0x1098 copied 1024 bytes;', + 'subsequent parser status', hex(status), flush=True) + + before = len(visited), len(copied) + chosen_denied = request(src, 4, BASE+0x1800) + assert chosen_denied == 0xFFFF000F and (len(visited),len(copied)) == before + print('nonzero msg[8] chosen-output control rejected before leaf:', + hex(chosen_denied),flush=True) + + denied = request(edge, 1024) + assert denied != 0 and (len(visited),len(copied)) == before + print('synthetic edge declared 1024 rejected before leaf:',hex(denied),flush=True) + try: + request(edge, 4) + except Fault as exc: + assert (len(visited),len(copied)) == (before[0]+1,before[1]) + assert exc.addr == END, (exc, sd.faults) + print('synthetic edge declared 4: signed first read faulted at', + hex(exc.addr),'before parser or crypto',flush=True) + else: + raise AssertionError('short-declared protected edge unexpectedly completed') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_109d_label_span.py b/psp_emu/harness/probe_sysdrv_109d_label_span.py new file mode 100644 index 0000000..b8cf043 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_109d_label_span.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Signed .54 0x109D reaches label strlen past declared four-byte span. +Synthetic GCP-owned buffers; do not print derived crypto bytes. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1 +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from pspemu.component import Fault +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,key,label,salt,out=[sd.alloc(n) for n in (0x100,0x400,0x40,0x40,0x40,0x40)] + sd.write(cfg,bytes(0x400));sd.write(key,b'K'*32+bytes(32)) + sd.write(label,b'NAME_XYZ\0'+bytes(0x37));sd.write(salt,b'SALT'+bytes(0x3C)) + entry=[];strlen=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:entry.append(uc.reg_read(UC_ARM_REG_R0)),begin=0xE01864,end=0xE01865) + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:strlen.append(uc.reg_read(UC_ARM_REG_R0)),begin=0xE07AD0,end=0xE07AD1) + def run(declared,ptr=label): + sd.write(req,bytes(0x100));sd.write(out,bytes(0x40)) + for i,v in ((0,0x109D),(1,2),(2,key),(3,32),(4,ptr),(5,declared), + (6,salt),(7,4),(8,out),(9,32), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + try: + return sd.run_function(0xE087E4,(req,2,0,0),count=4_000_000,timeout=4_000_000) + except Fault as exc: + if ptr==label: + assert (exc.pc,exc.addr)==(0xE01940,0x680),(exc.pc,exc.addr) + return None + return exc + short=run(4) + assert label in entry and 8 in strlen,(short,entry,strlen) + assert short is None + full=run(9) + assert full is None and entry.count(label)==2 and strlen.count(8)==2,(full,entry,strlen) + # Artificial boundary oracle: only four bytes remain in this synthetic + # caller window; this is NOT a measured sOS PTE or another TA allocation. + edge=sd.cmap.scratch+sd.cmap.scratch_size-4 + sd.write(edge,b'ZZZZ') + def svc_window(comp,n): + p=comp.uc.reg_read(UC_ARM_REG_R1) + for i,v in enumerate((sd.cmap.scratch,sd.cmap.scratch+sd.cmap.scratch_size,0,0)): + comp.wr32(p+4*i,v) + return 0 + sd.svc.register(0xB5,svc_window) + before=len(entry) + fault=run(4,edge) + assert isinstance(fault,Fault) and fault.addr==edge+4 and 0xE01864<=fault.pc<0xE01900,(fault,entry) + assert edge in entry[before:] + before=len(entry) + denied=run(9,edge) + assert denied!=0 and not isinstance(denied,Fault) and len(entry)==before,(denied,entry) + print(f'signed 0x109D: declared4 label strlen=8, synthetic edge fault={fault.addr:#x}, declared9 edge denied={denied:#x}; no live PTE claim, derived bytes redacted') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_10a2_label_reach.py b/psp_emu/harness/probe_sysdrv_10a2_label_reach.py new file mode 100644 index 0000000..fc62927 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_10a2_label_reach.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Check if signed 0x10A2 reaches input-label strlen after natural crypto gate. +No crypto result bytes are printed. Early return is a useful negative control. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_R1,UC_ARM_REG_R2,UC_ARM_REG_SP,UC_ARM_REG_LR +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +from pspemu.component import Fault +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +STRLEN=0xE01864 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,cfg,label,salt,out=[sd.alloc(n) for n in (0x100,0x400,0x40,0x40,0x40)] + sd.write(cfg,bytes(0x400));sd.write(label,b'NAME_XYZ\0'+bytes(0x37)) + sd.write(salt,b'ABCD'+bytes(0x3C));sd.write(out,bytes(0x40)) + seen=[];suspicious_copies=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,size,data:seen.append(uc.reg_read(UC_ARM_REG_R0)), + begin=STRLEN,end=STRLEN+1) + def copy_entry(uc,ea,size,data): + dest=uc.reg_read(UC_ARM_REG_R0) + if dest<0x10000: + suspicious_copies.append((dest,uc.reg_read(UC_ARM_REG_R1), + uc.reg_read(UC_ARM_REG_R2),uc.reg_read(UC_ARM_REG_LR))) + sd.uc.hook_add(UC_HOOK_CODE,copy_entry,begin=0xE0192C,end=0xE0192D) + sd.write(req,bytes(0x100)) + for i,v in ((0,0x10A2),(1,2),(2,label),(3,4),(4,salt),(5,4),(6,out),(7,32), + (sysdrv_env.REQ_CONFIG_DWORD,cfg)): + sd.wr32(req+4*i,v) + try: + rv=sd.run_function(0xE087E4,(req,2,0,0),count=4_000_000,timeout=4_000_000) + print(f'signed 0x10A2 natural emulator: status={rv:#x}, label_strlen_seen={label in seen}, strlen_calls={len(seen)}') + except Fault as exc: + assert exc.pc==0xE01940 and exc.addr==0x680 and label not in seen,(exc.pc,exc.addr,len(seen)) + assert suspicious_copies==[(0x680,0xE23050,32,0xE146E5)],suspicious_copies + print('signed 0x10A2 emulator stopped before label strlen: ' + f'copy_caller={suspicious_copies[0][3]:#x}, FAR={exc.addr:#x}, ' + 'zero synthetic crypto base; no key material printed') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_10a3_error_flag_clear.py b/psp_emu/harness/probe_sysdrv_10a3_error_flag_clear.py new file mode 100644 index 0000000..a366eff --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_10a3_error_flag_clear.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Signed .54 0x10A3: error from modeled mailbox map clears global flag. +Synthetic pre-existing flag and SVC 0x7D failure; no live mailbox effect. +""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY,LEAF,FLAG=0xE087E4,0xE12670,0xE25844 + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + init,fault,_=sysdrv_env.run_init(sd) + assert init==0 and not fault and sysdrv_env.init_flag(sd)==1 + req,cfg=sd.alloc(0x100),sd.alloc(0x400) + sd.write(req,bytes(0x100));sd.write(cfg,bytes(0x400)) + sd.wr32(req,0x10A3);sd.wr32(req+4,2) + sd.wr32(req+4*sysdrv_env.REQ_CONFIG_DWORD,cfg) + sd.write(FLAG,b'\x01') + sd.write(req+56,b'\xA5\xA5') + seen=[];maps=[] + sd.uc.hook_add(UC_HOOK_CODE,lambda uc,ea,n,data:seen.append(ea),begin=LEAF,end=LEAF) + def unavailable(comp,num): + maps.append(num) + return 0 + sd.svc.register(0x7D,unavailable) + status=sd.run_function(ENTRY,(req,2,0,0),count=2_000_000,timeout=3_000_000) + assert status==0xFFFF000C and seen==[LEAF] and maps==[0x7D] + assert sd.read(FLAG,1)==b'\x00' and sd.read(req+56,2)==b'\x00\x00' and not sd.faults + print('signed 0x10A3 returned 0xffff000c on modeled SVC 0x7d failure; global status flag 1->0') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_33_zero_handle.py b/psp_emu/harness/probe_sysdrv_33_zero_handle.py new file mode 100644 index 0000000..36017fa --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_33_zero_handle.py @@ -0,0 +1,50 @@ +"""Bounded .54 SYS_DRV opcode-0x33 handler trace for object handle zero. + +This is a *separate* signed-image emulator observation from the HDCP caller +failure-injection test. Stop before the fatal function executes; do not turn +a simulated branch into a claimed hardware effect. +""" +from __future__ import annotations + +import os +import sys + +from unicorn import UC_HOOK_CODE + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from pspemu import Component # noqa: E402 + +SIGNED_SYSDRV = os.path.abspath(os.path.join( + os.path.dirname(__file__), "..", "..", "psp_out", "elf", + "sys_drv_0.49.0.54_18bd9d26.elf")) +HANDLER_33 = 0xE1EA54 +FATAL = 0xE0F890 + + +def main(): + comp = Component("sys_drv", image_path=SIGNED_SYSDRV) + req = comp.alloc(0x80) + attrs = comp.alloc(0x20) + comp.wr32(req + 0x38, 0) + seen = [] + + def stop_at_fatal(uc, addr, size, user_data): + seen.append(addr) + uc.emu_stop() + + # The callback marks the *first instruction* of sub_E0F890. Stopping + # there avoids executing its uninitialized standalone-emulator globals + # and prevents a deliberate infinite loop from obscuring the branch. + comp.uc.hook_add(UC_HOOK_CODE, stop_at_fatal, begin=FATAL, end=FATAL) + comp.run_function(HANDLER_33, (req, attrs, 1, 1), thumb=True, + count=20000, timeout=2_000_000) + print("signed_sysdrv_image:", os.path.basename(comp.image_path)) + print("handler:", hex(HANDLER_33), "object_handle:", hex(comp.rd32(req + 0x38))) + print("fatal_entry_reached:", [hex(a) for a in seen]) + print("faults:", comp.faults) + assert seen == [FATAL], seen + assert not comp.faults + + +if __name__ == "__main__": + main() diff --git a/psp_emu/harness/probe_sysdrv_35_ignored_rng_failure.py b/psp_emu/harness/probe_sysdrv_35_ignored_rng_failure.py new file mode 100644 index 0000000..b7b5bb5 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_35_ignored_rng_failure.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Signed .54 low 0x35 discards its RNG helper failure (synthetic RNG only).""" +from pathlib import Path +import sys +from unicorn import UC_HOOK_CODE +from unicorn.arm_const import UC_ARM_REG_R0,UC_ARM_REG_LR,UC_ARM_REG_PC +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env +SIGNED=Path(__file__).resolve().parents[2]/'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' + +def main(): + sd=sysdrv_env.make() + assert Path(sd.image_path).read_bytes()==SIGNED.read_bytes() + r,f,_=sysdrv_env.run_init(sd) + assert r==0 and not f and sysdrv_env.init_flag(sd)==1 + req,out=sd.alloc(0x100),sd.alloc(0x40) + print('signed default RNG gate E269B8=',sd.read(0xE269B8,1).hex()) + mode={'fail_after':0,'calls':0} + def modeled_entropy(uc,ea,size,data): + mode['calls']+=1 + if mode['calls']>mode['fail_after']: + uc.reg_write(UC_ARM_REG_R0,49) + else: + sd.wr32(uc.reg_read(UC_ARM_REG_R0),0x11223344) + uc.reg_write(UC_ARM_REG_R0,0) + uc.reg_write(UC_ARM_REG_PC,uc.reg_read(UC_ARM_REG_LR)) + sd.uc.hook_add(UC_HOOK_CODE,modeled_entropy,begin=0xE0BA34,end=0xE0BA35) + def run(fail_after): + mode.update(fail_after=fail_after,calls=0) + sd.write(req,bytes(0x100));sd.write(out,b'\xa5'*0x40) + for i,v in ((0,0x35),(1,2),(2,out),(3,4)): + sd.wr32(req+4*i,v) + rv=sd.run_function(0xE087E4,(req,2,0,0),count=2_000_000,timeout=2_000_000) + return rv,sd.read(out,8),mode['calls'] + mode.update(fail_after=0,calls=0) + sd.write(out,b'\xa5'*0x40) + helper_status=sd.run_function(0xE04F38,(out,4,0,0),count=2_000_000,timeout=2_000_000) + assert helper_status==0xFFFF000B and sd.read(out,8)==b'\xa5'*8,(helper_status,mode) + first=run(0);second=run(1);success=run(2) + assert first==(0,b'\xa5'*8,1),first + assert second==(0,b'\x11\x22\x33\x44'+b'\xa5'*4,2),second + assert success==(0,b'\x11\x22\x33\x44'+b'\xa5'*4,2),success + print('signed helper returned 0xffff000b on modeled RNG failure, but low 0x35 dispatch returned 0 with marker unchanged; failure on extra call2 likewise masked after full four bytes; no real entropy sampled') +if __name__=='__main__':main() diff --git a/psp_emu/harness/probe_sysdrv_53_log_prevalidate.py b/psp_emu/harness/probe_sysdrv_53_log_prevalidate.py new file mode 100644 index 0000000..4f0f03c --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_53_log_prevalidate.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Scope signed .54 low opcode 0x53 logging before caller-string validation. + +Synthetic SVC/PCB/caller window only. No card request or cross-process claim. +Signed SYS_DRV init/dispatch/logging string copy run; MMIO/log-drain effects +are not hardware evidence. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from unicorn import UC_HOOK_CODE, UC_PROT_NONE +from unicorn.arm_const import UC_ARM_REG_R1 + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pspemu import sysdrv_env # noqa: E402 +from pspemu.component import Fault # noqa: E402 + +SIGNED = Path(__file__).resolve().parents[2] / 'psp_out/elf/sys_drv_0.49.0.54_18bd9d26.elf' +ENTRY = 0xE087E4 +BASE, END = 0x400000, 0x800000 +LOG_FLAG, LOG_LEVEL, BACKEND_FLAG = 0xE264E0, 0xE264E4, 0xE269CC +STRING_HELPER, FORMATTER, DRAIN = 0xE2232C, 0xE01A34, 0xE21D44 +LOG_BUFFER = 0xEB5FB0 + + +def main() -> None: + sd = sysdrv_env.make() + assert Path(sd.image_path).read_bytes() == SIGNED.read_bytes() + rv, fault, _ = sysdrv_env.run_init(sd) + assert rv == 0 and fault is None and sysdrv_env.init_flag(sd) == 1, (rv, fault) + flags = (sd.read(LOG_FLAG, 1)[0], sd.rd32(LOG_LEVEL), sd.read(BACKEND_FLAG, 1)[0]) + print('signed init logging (frontend, level, backend)=', flags, flush=True) + assert flags[0] == 1 and flags[2] == 1, 'both modeled init log gates required' + sd.uc.mem_map(BASE, 0x3000) + regs = sd.uc.mem_regions() + if not any(lo <= END-4 <= hi for lo, hi, _ in regs): + sd.uc.mem_map(END-0x1000, 0x1000) + if any(lo <= END <= hi for lo, hi, _ in sd.uc.mem_regions()): + sd.uc.mem_protect(END, 0x1000, UC_PROT_NONE) + + def svc_window(comp, n): + out = comp.uc.reg_read(UC_ARM_REG_R1) + for i, v in enumerate((BASE, END, 0, 0)): + comp.wr32(out + 4*i, v) + return 0 + sd.svc.register(0xB5, svc_window) + req, cfg = sd.alloc(0x100), sd.alloc(0x400) + sd.write(cfg, b'\x00' * 0x400) + src, edge = BASE+0x1000, END-4 + sd.write(src, b'J' * 40) + sd.write(edge, b'K' * 4) + seen, fmt, before_drain = [], [], [] + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, size, data: seen.append(ea), + begin=STRING_HELPER, end=STRING_HELPER+1) + sd.uc.hook_add(UC_HOOK_CODE, lambda uc, ea, size, data: fmt.append(ea), + begin=FORMATTER, end=FORMATTER+1) + sd.uc.hook_add(UC_HOOK_CODE, + lambda uc, ea, size, data: before_drain.append(bytes( + uc.mem_read(LOG_BUFFER+16, 39))), + begin=DRAIN, end=DRAIN+1) + + def request(ptr: int, declared: int) -> int: + sd.write(req, b'\x00' * 0x100) + for i, word in ((0, 0x53), (2, ptr), (3, declared), (14, 16), + (sysdrv_env.REQ_CONFIG_DWORD, cfg)): + sd.wr32(req + i*4, word) + return sd.run_function(ENTRY, (req, 2, 0, 0), count=3_000_000, + timeout=4_000_000) + + r = request(src, 4) + assert r == 0 and len(seen) == len(fmt) == 1 + assert before_drain == [b'J' * 39], before_drain + print('mapped non-NUL input: declared4, signed logger copied 39 bytes ' + 'before later NUL check; return', hex(r), flush=True) + + n, m, d = len(seen), len(fmt), len(before_drain) + denied = request(edge, 40) + assert denied == 0xFFFF000F and (len(seen),len(fmt),len(before_drain)) == (n,m,d) + print('synthetic edge declared40 rejected before logger',hex(denied),flush=True) + try: + request(edge, 4) + except Fault as exc: + assert exc.addr == END, (exc, sd.faults) + assert (len(seen),len(fmt),len(before_drain)) == (n+1,m+1,d) + print('synthetic edge declared4: signed logger fault at',hex(exc.addr), + 'before later NUL check and log drain',flush=True) + else: + raise AssertionError('short-declared synthetic edge unexpectedly completed') + + +if __name__ == '__main__': + main() diff --git a/psp_emu/harness/probe_sysdrv_underflow_copy.py b/psp_emu/harness/probe_sysdrv_underflow_copy.py new file mode 100644 index 0000000..3d9dc57 --- /dev/null +++ b/psp_emu/harness/probe_sysdrv_underflow_copy.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Execute signed SYS_DRV's memcpy AND count consumer under a 2-page workspace model. + +Bounded and invalid-source controls distinguish a fault before any second-copy +store from a mapped-source fault at the workspace boundary. A final probe runs +sub_E04B68 through both *real* copies with count 0x500. Only the SVC lock is +stubbed; the caller/ownership gates and sOS MMU protections are NOT emulated. + +Run: python3 psp_emu/harness/probe_sysdrv_underflow_copy.py +""" +from __future__ import annotations + +from pathlib import Path +from unicorn import Uc, UcError, UC_ARCH_ARM, UC_MODE_ARM, UC_MODE_THUMB, UC_PROT_ALL +from unicorn import UC_HOOK_INTR, UC_HOOK_MEM_INVALID +from unicorn.arm_const import (UC_ARM_REG_LR, UC_ARM_REG_PC, UC_ARM_REG_R0, + UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3, + UC_ARM_REG_SP) + +REPO = Path(__file__).resolve().parents[2] +IMAGE = (REPO / 'psp_out/sys_drv_code.bin').read_bytes() +IMAGE_BASE = 0xE00100 +HELPER = 0xE018A0 # ARM entry via BLX from Thumb sub_E04B68 +WORK = 0xECC000 # hardware base; emulated SVC78 size 0x2000 +DST = WORK + 0x6C0 + 0x500 # second-copy destination for count 0x500 +SRC = 0x23000000 +STACK = 0x22000000 +RET = 0xF0000000 + + +def probe(src_pages: int, length: int, source: int, marker: int) -> tuple[str, int, bytes]: + uc = Uc(UC_ARCH_ARM, UC_MODE_ARM) + img_start = IMAGE_BASE & ~0xFFF + img_size = (IMAGE_BASE + len(IMAGE) - img_start + 0xFFF) & ~0xFFF + uc.mem_map(img_start, img_size, UC_PROT_ALL) + uc.mem_write(IMAGE_BASE, IMAGE) + uc.mem_map(WORK, 0x2000, UC_PROT_ALL) + uc.mem_write(WORK, b'\xA5' * 0x2000) + uc.mem_map(STACK, 0x1000, UC_PROT_ALL) + if src_pages: + uc.mem_map(SRC, src_pages * 0x1000, UC_PROT_ALL) + uc.mem_write(SRC, bytes([marker]) * (src_pages * 0x1000)) + faults = [] + + def invalid(_uc, access, addr, size, _value, _data): + faults.append((access, addr, size)) + return False + + uc.hook_add(UC_HOOK_MEM_INVALID, invalid) + uc.reg_write(UC_ARM_REG_SP, STACK + 0x800) + uc.reg_write(UC_ARM_REG_LR, RET) + uc.reg_write(UC_ARM_REG_R0, DST) + uc.reg_write(UC_ARM_REG_R1, source) + uc.reg_write(UC_ARM_REG_R2, length) + try: + uc.emu_start(HELPER, RET, timeout=5_000_000, count=50000) + outcome = 'returned' + except UcError as e: + outcome = '%s at %#x, pc=%#x' % (type(e).__name__, faults[0][1] if faults else 0, + uc.reg_read(UC_ARM_REG_PC)) + return outcome, (faults[0][1] if faults else -1), bytes(uc.mem_read(DST, 0x1440)) + + +def probe_full_function() -> None: + """Run sub_E04B68 with real memcpy, rather than stubbing its copy calls.""" + uc = Uc(UC_ARCH_ARM, UC_MODE_THUMB) + uc.mem_map(0xE00000, 0x30000, UC_PROT_ALL) + uc.mem_write(IMAGE_BASE, IMAGE) + staging, context = 0x20000000, 0x21000000 + for base, size in ((WORK, 0x2000), (staging, 0x1000), + (context, 0x1000), (STACK, 0x1000)): + uc.mem_map(base, size, UC_PROT_ALL) + uc.mem_write(staging, b'\x41' * 0x1000) + uc.mem_write(staging + 0x88, (0x500).to_bytes(4, 'little')) + uc.mem_write(WORK, b'\xA5' * 0x2000) + uc.mem_write(0xE257D8, WORK.to_bytes(4, 'little')) + uc.reg_write(UC_ARM_REG_SP, STACK + 0x800) + uc.reg_write(UC_ARM_REG_LR, RET | 1) + uc.reg_write(UC_ARM_REG_R0, context) + uc.reg_write(UC_ARM_REG_R1, staging) + uc.reg_write(UC_ARM_REG_R2, 0) # NULL nominal source + uc.reg_write(UC_ARM_REG_R3, 0) # zero nominal length + faults, svcs = [], [] + + def invalid(uc_, access, addr, size, value, user): + faults.append((addr, uc_.reg_read(UC_ARM_REG_PC))) + return False + + def on_svc(uc_, intno, user): + # Only SVC 0x5E (the SYS_DRV workspace lock) is reached before fault. + svcs.append(uc_.reg_read(UC_ARM_REG_PC)) + uc_.reg_write(UC_ARM_REG_R0, 0) + uc_.reg_write(UC_ARM_REG_PC, uc_.reg_read(UC_ARM_REG_PC) | 1) + + uc.hook_add(UC_HOOK_MEM_INVALID, invalid) + uc.hook_add(UC_HOOK_INTR, on_svc) + try: + uc.emu_start(0xE04B69, RET, timeout=5_000_000, count=50000) + except UcError: + pass + copied = bytes(uc.mem_read(WORK + 0x6C0, 0x500)) + print('real sub_E04B68(count=0x500, source=NULL): SVCs=%s fault=%s' + % ([hex(pc) for pc in svcs], [(hex(addr), hex(pc)) for addr, pc in faults])) + assert svcs == [0xE04B8E] and faults == [(0, 0xE01938)] + assert copied[:0x80] == b'\x41' * 0x80 + assert copied[0x80:0x84] == (0x500).to_bytes(4, 'little') + assert copied[0x84:] == b'\x41' * (0x500 - 0x84) + assert bytes(uc.mem_read(WORK + 0xBC0, 0x2000 - 0xBC0)) == b'\xA5' * (0x2000 - 0xBC0) + print(' first copy filled +0x6C0..+0xBC0; second copy read NULL before any store') + + +def main() -> int: + # Control: the real helper copies normally for the safe size. + short, sfault, dst = probe(1, 0x40, SRC, 0x41) + print('bounded copy length=0x40:', short, 'first=%#x' % int.from_bytes(dst[:4], 'little')) + assert short == 'returned' and sfault == -1 and dst[:0x40] == b'\x41' * 0x40 + assert dst[0x40:] == b'\xA5' * (0x1440 - 0x40) + + # One actual underflowed call with NULL source: the helper reads source 0 + # before storing anything (the first copy in sub_E04B68 ran before this). + null, nfault, dst = probe(0, 0xFFFFFF00, 0, 0x41) + print('underflow, NULL source:', null, 'workspace unchanged=', dst == b'\xA5' * len(dst)) + assert nfault == 0 and dst == b'\xA5' * len(dst) + + # With a separately mapped source, writes reach the two-page workspace + # boundary in this model. This does not establish live hardware PTEs. + mapped, mfault, dst = probe(3, 0xFFFFFF00, SRC, 0x43) + print('underflow, mapped source:', mapped, 'filled-to-guard=', dst == b'\x43' * len(dst)) + assert mfault == WORK + 0x2000 and dst == b'\x43' * len(dst) + probe_full_function() + print('PASS: source mapping determines first fault; synthetic guard contains the write') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) |
