"""Fuzz SYS_DRV's service-1 RPC surface -- the most privileged code reachable from a host-driven path. Chain: host -> HDCP (cmd 0x12/0x13/0x0F) -> ASD -> SVC 0xF2 service 1 -> SYS_DRV sub_E087E4 -> opcode >= 0x1000 -> sub_E105A8 (4372 bytes, 223 handlers) | opcode < 0x1000 -> sub_E1DC30 (crypto) ASD demonstrably emits 11 high opcodes (0x1021 0x103B 0x103C 0x1043 0x1044 0x1057 0x1079 0x1087 0x1095 0x109A 0x109B), so this dispatcher is on a host-reachable path. It has never been audited handler-by-handler. Request layout (from sub_E087E4 / sub_E105A8): a dword array, 0x7C bytes copied. [0] opcode [2..9] four {ptr,len} argument pairs [14..18] additional scalar fields sub_E105A8 region-validates each pair via sub_E0DEF4 before use. Artifact discipline carried over from the ASD campaign (all four bit us there): * never write PC from a UC_HOOK_CODE (fabricated 194 fake "PC control" hits) * MMIO aperture is mapped (benign register reads looked like OOB) * classify by INPUT, not fault address (scalars in pointer slots clobber stack) * signature tail is zeroed at load (sig bytes masqueraded as initialised data) """ import os import random import sys from collections import Counter sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from pspemu import Component, consts, sysdrv_env # noqa: E402 ENTRY = 0xE087E4 # SYS_DRV RPC entry (opcode split lives here) HIGH = 0xE105A8 # high dispatcher CRYPTO = 0xE1DC30 # crypto dispatcher GUARD = 0xE01F20 GATE_ACL = 0xE0AB44 # sub_E0AB44(opcode, arg idx) -> may this arg be a pointer RED = b"\x5A\xFE\x5A\xFE" * 4 ASD_EMITTED = [0x1021, 0x103B, 0x103C, 0x1043, 0x1044, 0x1057, 0x1079, 0x1087, 0x1095, 0x109A, 0x109B] # ACL ranges from the table at 0xE23398 ACL_RANGES = [(0x1100, 0x11FF), (0x2000, 0x20FF), (0x3000, 0x30FF), (0x3100, 0x31FF), (0x4000, 0x400F), (0x6000, 0x60FF), (0x7000, 0x70FF), (0x8000, 0x80FF), (0x103D, 0x103D), (0x103F, 0x103F)] class SysDrvFuzzer: def __init__(self, seed=0, only_asd_opcodes=False): self.rng = random.Random(seed) self.only_asd = only_asd_opcodes # Build on sysdrv_env, which implements the SVC semantics SYS_DRV's init # actually depends on. A bare Component("sys_drv") plus zero-returning # SVC stubs does NOT get init through: stubs return success while # installing none of the pointers later initialisers dereference. # # Do NOT re-register those SVCs as `lambda: 0` afterwards -- that is what # the previous version did, and it clobbered the faithful handlers. c = self.c = sysdrv_env.make() # Map the fabric/HW-counter aperture at 0x3200000. The component map only # covers 0x3010000..0x3020000, so benign register touches here surfaced # as READ/WRITE_UNMAPPED "findings" -- exactly the artifact this file's # own header warns about ("MMIO aperture is mapped"). Measured: it turned # 2 of 17 reported faults into noise (0x32001E0 = HW_COUNTER, # 0x320016C), both outside any mapped region. try: c.uc.mem_map(0x03200000, 0x10000) except Exception: pass # sub_E105A8 gates every {ptr,len} pair on sub_E0AB44(opcode,idx) != 0 # and sub_E0DEF4(ptr,len,type,ctx) != 0. # # sub_E0DEF4 is not restrictive -- it accepted every address tried. # sub_E0AB44 is an (opcode, arg-index) ALLOWLIST: may this argument be a # pointer? It is sparse. Only 2 of the 11 ASD-emitted opcodes permit any # pointer slot, and 18 of 132 sampled ACL-range opcodes, always index 0. # # There are three ways to run against it, and only one is both faithful # and useful. Measured over 800 iterations, distinct SYS_DRV blocks: # # allowlist patched to 1 (the old default) 447 blocks, 2 findings # allowlist real, pointers placed anywhere 107 blocks, 0 findings # allowlist real, pointers only where permitted 604 blocks, 4 findings # # The middle row is the trap: restoring the real gate while still # offering pointers in forbidden slots makes sub_E105A8 reject requests # wholesale, so almost nothing runs and the campaign looks clean for the # wrong reason. The last row is the default now -- it is faithful AND # covers more code than the bypass ever did. # SYSDRV_ACL=0 restores the historical behaviour: patch the allowlist to # 1 and pass pointers wherever we like. That is what every result from # this harness before now was produced under, and it explores states the # host cannot reach. Default is now to RESPECT the allowlist. self.respect_acl = os.environ.get("SYSDRV_ACL", "1") != "0" RET1 = bytes([0x01, 0x20, 0x70, 0x47]) # MOVS R0,#1 ; BX LR if not self.respect_acl: c.write(0xE0AB44, RET1) c.write(0xE0DEF4, RET1) self._acl = {} # SYS_DRV genuinely needs init (unlike ASD): sub_E087E4 gates on # *(off_E08850 + 12) == 1 and returns immediately otherwise. # # This used to call sub_E0CA2C directly and never check the result. That # DID NOT open the gate -- measured, init_flag stayed 0 -- so every # iteration bounced off the gate after 4 basic blocks and the campaign # reported "5000/5000 ok, no faults" while reaching nothing. Only # opcode 0x1000 through the real entry sets the flag. # # Measured for one dispatch of opcode 0x1021: # gate closed (flag 0): 4 blocks, high dispatcher NOT reached # gate open (flag 1): 42 blocks, high dispatcher reached sysdrv_env.run_init(c) self.init_flag = sysdrv_env.init_flag(c) if self.init_flag != 1: raise RuntimeError( "SYS_DRV init did not open the gate (init_flag=%r); every" " iteration would bounce off sub_E087E4 and the run would" " report a meaningless clean result" % (self.init_flag,)) self.req = c.alloc(0x100) self.bufs = [c.alloc(0x1000 + 64) for _ in range(4)] self.reds = [b + 0x1000 for b in self.bufs] self.stats = Counter() self.findings = [] def ptr_ok(self, op, idx): """May argument `idx` of `op` be a pointer? (sub_E0AB44, cached.) Measured density: only 2 of the 11 ASD-emitted opcodes permit ANY pointer slot, and 18 of 132 sampled ACL-range opcodes -- always index 0. So the genuinely pointer-taking RPC surface is small, and a fuzzer that ignores this is mostly exercising argument shapes the firmware refuses. """ if not self.respect_acl: return True key = (op, idx) if key not in self._acl: try: self._acl[key] = self.c.run_function(GATE_ACL, (op, idx)) != 0 except Exception: self._acl[key] = False return self._acl[key] def pick_opcode(self): r = self.rng.random() if self.only_asd or r < 0.35: return self.rng.choice(ASD_EMITTED) if r < 0.80: lo, hi = self.rng.choice(ACL_RANGES) return self.rng.randint(lo, hi) return self.rng.choice([0x1000, 0x1001, 0x1009, 0x1019, 0x103D, 0x108F, 0x2005, 0x3008, 0x3102, 0x3103, 0x3104, 0x4000]) def run_one(self): c, rng = self.c, self.rng for b, r in zip(self.bufs, self.reds): c.write(b, b"\x00" * 0x1000) c.write(r, RED) c.write(self.req, b"\x00" * 0x100) op = self.pick_opcode() c.wr32(self.req, op) used_scalar = False for i in range(31): if i == 0: continue if 2 <= i <= 9: if i % 2 == 0: # pointer slot idx = (i - 2) // 2 if not self.ptr_ok(op, idx): v = 0 # firmware refuses a pointer here else: roll = rng.random() if roll < 0.15: v = 0 elif roll < 0.30: v = rng.getrandbits(16); used_scalar = True else: v = self.bufs[rng.randrange(4)] else: # length slot v = rng.choice([0, 1, 4, 0x10, 0x40, 0x100, 0x1000, 0x2000, 0xFFFF, rng.getrandbits(16)]) else: v = rng.choice([0, 1, 2, 4, rng.getrandbits(8), rng.getrandbits(32)]) c.wr32(self.req + 4 * i, v) for b in self.bufs: c.write(b, bytes(rng.getrandbits(8) for _ in range(rng.choice([16, 64, 256, 0x400])))) try: c.run_function(ENTRY, (self.req, 2, 0, 0), count=2_000_000) fault = None except Exception as e: fault = e if fault is not None: f = c.faults[-1] if c.faults else {} addr = f.get("addr", 0) kind = f.get("kind", "FAULT") if addr < 0x100000 or used_scalar: kind = "scalar_noise" self.stats[kind] += 1 if kind != "scalar_noise": self.findings.append((kind, hex(op), hex(f.get("pc", 0)), hex(addr))) else: self.stats["ok"] += 1 for i, r in enumerate(self.reds): if c.read(r, 16) != RED: self.stats["REDZONE_argbuf"] += 1 self.findings.append(("REDZONE_argbuf%d" % i, hex(op), "-", "-")) def main(iters=5000, seed=1, only_asd=False): f = SysDrvFuzzer(seed, only_asd) for _ in range(iters): f.run_one() print("SYS_DRV fuzz: iters=%d seed=%d asd_opcodes_only=%s acl=%s" % (iters, seed, only_asd, "RESPECTED" if f.respect_acl else "BYPASSED (results not host-reachable)")) for k, v in f.stats.most_common(): print(" %-22s %d" % (k, v)) if f.findings: print("\ndistinct findings:") seen = set() for t in f.findings: key = (t[0], t[2], t[3]) if key in seen: continue seen.add(key) print(" kind=%-18s op=%-10s pc=%-12s addr=%s" % t) if len(seen) >= 25: break else: print("\nno faults or redzone violations") if __name__ == "__main__": main(int(sys.argv[1]) if len(sys.argv) > 1 else 5000, int(sys.argv[2]) if len(sys.argv) > 2 else 1, len(sys.argv) > 3 and sys.argv[3] == "asd")