Booted the title and read the two gate words live:
0x828F40C0 = 0x00000002 word A
0x828F4814 = 0x00000000 word B
Word A = 2 = bit 1. The profile's save is Stage 02 "At Standby" -- stage 01
cleared -- so the mask is exactly one bit, at the index of the one cleared
stage, 1-BASED. Reproduced across two cold boots. That confirms against a known
progress state, on the real game:
- the singleton is the static object at 0x828F4070, as derived statically;
- word A is a cleared-stage bitmask (not achievements, not a stage number);
- bit index = stage id, 1-based, so TimeAttack's REQUIREMENT 16 means "clear
stage 16" -- the last story mission;
- word B is the challenge half and is 0 on a story-only profile.
New tools: gpoke.py (live guest-memory WRITE, companion to gmem.py, prints
before/after for every word), pad.py (drives the new --hid=file pad; replaces
vgamepad, which leaked to the host through /dev/uinput), challenge_probe.sh
(one blocking session: boot, wait for title, drive in, poke, screenshot).
Poking both words did NOT surface a challenge entry in EXTRAS -- and that menu
was built 26 s after the poke, so it is not staleness. Entering MISSION SELECT
then failed, but the log names the real cause and it is not the gate:
MmAllocatePhysicalMemoryEx could not satisfy a 128 MB request (parent free
30633/131072 pages), the guest threw a C++ exception, and Xenia surfaced its
generic "Disc Read Error". It is preceded by "BaseHeap::Release failed because
address is not a region start" -- a failed release leaking the range. Recorded
as an emulator heap problem, with the control run (same navigation, no poke)
named as the next step.
77 lines
2.4 KiB
Python
Executable File
77 lines
2.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""WRITE to the live guest memory of a running Xenia Canary.
|
|
|
|
The read-side companion is `gmem.py`, and this shares its guest-VA → file-offset
|
|
table. Canary backs the whole guest address space with one shared-memory file
|
|
(`/dev/shm/xenia_memory_*`), so a write here lands in the running guest with no
|
|
debugger and no pause.
|
|
|
|
gpoke.py w32 <va> <value> [...] write big-endian u32s at consecutive VAs
|
|
gpoke.py r32 <va> [n] read back n big-endian u32s (verify)
|
|
|
|
Values and addresses accept `0x` form. Every write prints the before/after word,
|
|
because a poke you cannot see is a poke you cannot trust.
|
|
|
|
⚠️ This mutates a running game. It is a research tool: there is no undo, and a
|
|
wrong address will corrupt whatever it lands on. Read back before believing.
|
|
"""
|
|
|
|
import os
|
|
import struct
|
|
import sys
|
|
|
|
from gmem import MAP, va_to_off # noqa: F401 (MAP re-exported for callers)
|
|
|
|
|
|
def shm_path():
|
|
cands = [f"/dev/shm/{n}" for n in os.listdir("/dev/shm") if n.startswith("xenia_memory")]
|
|
if not cands:
|
|
raise SystemExit("no /dev/shm/xenia_memory_* — is Canary running?")
|
|
if len(cands) > 1:
|
|
raise SystemExit(f"several guest images, refusing to guess: {cands}")
|
|
return cands[0]
|
|
|
|
|
|
def read32(f, va):
|
|
f.seek(va_to_off(va))
|
|
return struct.unpack(">I", f.read(4))[0]
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print(__doc__)
|
|
return 1
|
|
cmd = sys.argv[1]
|
|
path = shm_path()
|
|
if cmd == "r32":
|
|
va = int(sys.argv[2], 0)
|
|
n = int(sys.argv[3], 0) if len(sys.argv) > 3 else 1
|
|
with open(path, "rb") as f:
|
|
for i in range(n):
|
|
a = va + 4 * i
|
|
print(f" {a:#010x} = {read32(f, a):#010x}")
|
|
return 0
|
|
if cmd == "w32":
|
|
va = int(sys.argv[2], 0)
|
|
vals = [int(v, 0) for v in sys.argv[3:]]
|
|
if not vals:
|
|
print("nothing to write")
|
|
return 1
|
|
with open(path, "r+b") as f:
|
|
for i, v in enumerate(vals):
|
|
a = va + 4 * i
|
|
before = read32(f, a)
|
|
f.seek(va_to_off(a))
|
|
f.write(struct.pack(">I", v))
|
|
f.flush()
|
|
after = read32(f, a)
|
|
ok = "OK" if after == v else "!! MISMATCH"
|
|
print(f" {a:#010x}: {before:#010x} -> {after:#010x} {ok}")
|
|
return 0
|
|
print(f"unknown command {cmd!r}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|