Add post-kit gameplay transition tracers
This commit is contained in:
Executable
+185
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Trace FIFA17's post-kit boundary without changing client behavior.
|
||||
|
||||
The probe anchors both ACTION_SAVE_MATCH_KIT (0x7576) actions, their concrete
|
||||
native save call, the action-handler return, and select-team provider teardown.
|
||||
The second 0x7576 action is the temporal boundary for later drill/game-loader
|
||||
instrumentation.
|
||||
|
||||
The generated GDB program uses four hardware-assisted execution breakpoints. It
|
||||
never writes client memory, calls client functions, drives input, emits actions,
|
||||
or alters timing deliberately.
|
||||
|
||||
match_post_kit_trace.py [pid] [--output PATH]
|
||||
match_post_kit_trace.py --print-script [pid]
|
||||
match_post_kit_trace.py --selftest
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import match_advance_trace as advance
|
||||
import match_transition_trace as transition
|
||||
|
||||
SAVE_KIT_ACTION = 0x7576
|
||||
ACTION_HANDLER_RVA = 0x0BFCC0
|
||||
SAVE_CALL_RVA = 0x0BFF25
|
||||
ACTION_RETURN_RVA = 0x0C00AE
|
||||
SELECT_TEAM_DESTRUCTOR_RVA = 0x0BDEC0
|
||||
|
||||
|
||||
def trace_addresses(cards_base: int) -> dict[str, int]:
|
||||
return {
|
||||
"action": cards_base + ACTION_HANDLER_RVA,
|
||||
"save_call": cards_base + SAVE_CALL_RVA,
|
||||
"action_return": cards_base + ACTION_RETURN_RVA,
|
||||
"destructor": cards_base + SELECT_TEAM_DESTRUCTOR_RVA,
|
||||
}
|
||||
|
||||
|
||||
def build_gdb_script(pid: int, cards_base: int, output: str) -> str:
|
||||
if any(character in output for character in "\n\r"):
|
||||
raise ValueError("output path cannot contain a newline")
|
||||
address = trace_addresses(cards_base)
|
||||
return f"""set pagination off
|
||||
set confirm off
|
||||
set print thread-events off
|
||||
set breakpoint always-inserted on
|
||||
set logging file {output}
|
||||
set logging overwrite on
|
||||
set logging redirect off
|
||||
set logging enabled on
|
||||
handle SIGSEGV nostop noprint pass
|
||||
handle SIGILL nostop noprint pass
|
||||
handle SIGFPE nostop noprint pass
|
||||
handle SIGPIPE nostop noprint pass
|
||||
handle SIGALRM nostop noprint pass
|
||||
handle SIGUSR1 nostop noprint pass
|
||||
handle SIGUSR2 nostop noprint pass
|
||||
|
||||
attach {pid}
|
||||
set $save_count = 0
|
||||
set $current_action = 0
|
||||
set $current_controller = 0
|
||||
set $current_payload = 0
|
||||
set $second_save_epoch = 0
|
||||
|
||||
hbreak *0x{address['action']:x}
|
||||
condition 1 $edx == 0x{SAVE_KIT_ACTION:x}
|
||||
commands
|
||||
silent
|
||||
set $save_count = $save_count + 1
|
||||
set $current_action = $edx
|
||||
set $current_controller = $rcx
|
||||
set $current_payload = $r8
|
||||
python import time, gdb; now = time.time_ns(); gdb.set_convenience_variable("event_epoch", now); print("POSTKIT epoch_ns=%d mono_ns=%d SAVE_ACTION" % (now, time.monotonic_ns()), end=" ")
|
||||
if $save_count == 2
|
||||
set $second_save_epoch = $event_epoch
|
||||
end
|
||||
printf "thread=%d ordinal=%d action=%#x controller=%p payload=%p payload_vtable=%p mode=%#x flags_150=%#x flags_151=%#x flags_152=%#x flags_155=%#x second_boundary=%d\\n", $_thread, $save_count, $edx, $rcx, $r8, *(void**)$r8, *(unsigned int*)($rcx+0x140), *(unsigned char*)($rcx+0x150), *(unsigned char*)($rcx+0x151), *(unsigned char*)($rcx+0x152), *(unsigned char*)($rcx+0x155), $save_count==2
|
||||
bt 10
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['save_call']:x}
|
||||
condition 2 $current_action == 0x{SAVE_KIT_ACTION:x}
|
||||
commands
|
||||
silent
|
||||
set $save_target = *(void**)(*(void**)$rcx+0x1d0)
|
||||
python import time; print("POSTKIT epoch_ns=%d mono_ns=%d NATIVE_SAVE_CALL" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d ordinal=%d central=%p central_vtable=%p target=%p side=%#x request=%p payload=%p\\n", $_thread, $save_count, $rcx, *(void**)$rcx, $save_target, $r8d, $rdx, $current_payload
|
||||
x/12gx $rdx
|
||||
bt 10
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['action_return']:x}
|
||||
condition 3 $current_action == 0x{SAVE_KIT_ACTION:x} && $rsi == $current_controller
|
||||
commands
|
||||
silent
|
||||
python import time; print("POSTKIT epoch_ns=%d mono_ns=%d ACTION_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d ordinal=%d controller=%p handled=%#x mode=%#x flags_150=%#x flags_151=%#x flags_152=%#x flags_155=%#x\\n", $_thread, $save_count, $rsi, $al, *(unsigned int*)($rsi+0x140), *(unsigned char*)($rsi+0x150), *(unsigned char*)($rsi+0x151), *(unsigned char*)($rsi+0x152), *(unsigned char*)($rsi+0x155)
|
||||
set $current_action = 0
|
||||
bt 10
|
||||
continue
|
||||
end
|
||||
|
||||
hbreak *0x{address['destructor']:x}
|
||||
condition 4 $save_count >= 2 && $rcx == $current_controller
|
||||
commands
|
||||
silent
|
||||
python import time; print("POSTKIT epoch_ns=%d mono_ns=%d SELECT_TEAM_DESTRUCTOR" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||
printf "thread=%d controller=%p save_count=%d second_save_epoch=%lld vtable=%p mode=%#x\\n", $_thread, $rcx, $save_count, $second_save_epoch, *(void**)$rcx, *(unsigned int*)($rcx+0x140)
|
||||
bt 12
|
||||
continue
|
||||
end
|
||||
|
||||
printf "POSTKIT ARMED pid={pid} action=0x{address['action']:x} save_call=0x{address['save_call']:x} action_return=0x{address['action_return']:x} destructor=0x{address['destructor']:x}\\n"
|
||||
continue
|
||||
"""
|
||||
|
||||
|
||||
def selftest() -> None:
|
||||
address = trace_addresses(0x180000000)
|
||||
assert address == {
|
||||
"action": 0x1800BFCC0,
|
||||
"save_call": 0x1800BFF25,
|
||||
"action_return": 0x1800C00AE,
|
||||
"destructor": 0x1800BDEC0,
|
||||
}
|
||||
script = build_gdb_script(47872, 0x180000000, "/tmp/post-kit.log")
|
||||
assert script.count("hbreak *") == 4
|
||||
assert f"$edx == 0x{SAVE_KIT_ACTION:x}" in script
|
||||
assert "second_boundary" in script
|
||||
assert "NATIVE_SAVE_CALL" in script
|
||||
assert "SELECT_TEAM_DESTRUCTOR" in script
|
||||
assert "set *(" not in script
|
||||
print("match_post_kit_trace selftest: PASS")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("pid", nargs="?", type=int)
|
||||
parser.add_argument("--output")
|
||||
parser.add_argument("--print-script", action="store_true")
|
||||
parser.add_argument("--selftest", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.selftest:
|
||||
selftest()
|
||||
return 0
|
||||
|
||||
pid = args.pid or transition.find_pid()
|
||||
if not pid:
|
||||
print("FIFA17.exe not found", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
cards_base, cards_path = transition.cards_mapping(pid)
|
||||
transition.validate_cards(cards_path)
|
||||
_fifa_base, fifa_path = advance.module_mapping(pid, advance.FIFA_MODULE)
|
||||
advance.validate_file(fifa_path, advance.PINNED_FIFA_SHA256, advance.FIFA_MODULE)
|
||||
output = args.output or f"/tmp/fifa17-match-post-kit-{pid}.log"
|
||||
script = build_gdb_script(pid, cards_base, output)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.print_script:
|
||||
print(script, end="")
|
||||
return 0
|
||||
if not shutil.which("gdb"):
|
||||
print("gdb not found", file=sys.stderr)
|
||||
return 2
|
||||
script_path = f"/tmp/fifa17-match-post-kit-{pid}.gdb"
|
||||
with open(script_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(script)
|
||||
os.execvp("gdb", ["gdb", "-q", "-nx", "-batch", "-x", script_path])
|
||||
return 127
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user