re(challenge): the cleared-stage mask is CONFIRMED on the running game
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.
This commit is contained in:
76
tools/re-capture/gpoke.py
Executable file
76
tools/re-capture/gpoke.py
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user