#!/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())