feat(frida): probe_handlers — confirms interpreter runs from module 0x400000
operand-fetch (call 0x41b940) fires ~8500/sec => the VM interpreter executes from the in-place unpacked module at 0x400000 (NOT the heap copy) => handlers are hookable by dump address. gfx-family(0x212-0x215)=0 at the title (no CG commands until a scene runs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
74
tools/frida/probe_handlers.py
Normal file
74
tools/frida/probe_handlers.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Probe: does the engine execute opcode handlers from the in-place module image (0x400000) or the
|
||||
per-run heap copy (0x62411000)? Determines where to hook for the gfx-object-manager RE.
|
||||
|
||||
Hooks (module-relative offsets):
|
||||
+0x74f1f AGF decoder (KNOWN stable control — fires on every AGF/texture load)
|
||||
+0x1b940 operand-fetch helper (call 0x41b940 in handlers — fires per operand)
|
||||
+0x21090 the 0x212..0x215 gfx-command family entry
|
||||
Reports per-hook fire counts each second. Drive the game (start a new game / click the opening,
|
||||
which loads CGs) to generate graphics activity.
|
||||
|
||||
Usage: py -3.11 -u -X utf8 tools/frida/probe_handlers.py [pid|AGE.EXE]
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
|
||||
JS = r"""
|
||||
const mod = Process.getModuleByName('AGE.EXE');
|
||||
const targets = { 'agf-decoder@+74f1f': 0x74f1f, 'operand-fetch@+1b940': 0x1b940, 'gfx-family@+21090': 0x21090 };
|
||||
const counts = {};
|
||||
for (const [name, off] of Object.entries(targets)){
|
||||
counts[name] = 0;
|
||||
try {
|
||||
Interceptor.attach(mod.base.add(off), {
|
||||
onEnter(args){ counts[name]++; }
|
||||
});
|
||||
send({kind:'hooked', name:name, addr: mod.base.add(off).toString()});
|
||||
} catch(e){ send({kind:'hookfail', name:name, err:''+e}); }
|
||||
}
|
||||
setInterval(() => send({kind:'counts', counts: counts}), 1000);
|
||||
send({kind:'ready', base: mod.base.toString()});
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
import frida
|
||||
proc = next((a for a in sys.argv[1:] if not a.startswith("-")), "AGE.EXE")
|
||||
|
||||
def on_message(msg, data):
|
||||
if msg.get("type") == "error":
|
||||
print("[frida-error]", msg.get("description")); return
|
||||
if msg.get("type") != "send":
|
||||
return
|
||||
pl = msg["payload"]
|
||||
k = pl.get("kind")
|
||||
if k == "ready":
|
||||
print(f"[frida] AGE.EXE base={pl['base']} — drive the game (new game / click opening) now.")
|
||||
elif k == "hooked":
|
||||
print(f"[frida] hooked {pl['name']} @ {pl['addr']}")
|
||||
elif k == "hookfail":
|
||||
print(f"[frida] HOOK FAILED {pl['name']}: {pl['err']}")
|
||||
elif k == "counts":
|
||||
print(" " + " ".join(f"{n}={c}" for n, c in pl["counts"].items()))
|
||||
|
||||
target = int(proc) if str(proc).isdigit() else proc
|
||||
try:
|
||||
session = frida.attach(target)
|
||||
except (frida.ProcessNotFoundError, frida.ServerNotRunningError):
|
||||
hits = [(p.pid, p.name) for p in frida.get_local_device().enumerate_processes()
|
||||
if p.name.upper().startswith("AGE")]
|
||||
print("[frida] AGE.EXE not found. AGE* processes:", hits or "(none)")
|
||||
return 2
|
||||
script = session.create_script(JS)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
try:
|
||||
time.sleep(30)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user