#!/usr/bin/env python3 """Trace FIFA17 ACTION_ADVANCE dispatch after MATCH_CREATED delivery. Stage one (`match_transition_trace.py`) proves the HTTP response, deserializer, and FUT_CREATE_MATCH_DP delivery. This stage uses that provider callback to capture the screen key, then records every nested global UI dispatch for that same screen until the positive-control FUT_GET_MATCH_KITS_DP arrives. It also captures the low-level create event and final native-to-UI provider bridge. The generated GDB program uses hardware-assisted execution breakpoints only. It never writes client memory and never drives game input. match_advance_trace.py [pid] [--output PATH] match_advance_trace.py --print-script [pid] match_advance_trace.py --selftest """ from __future__ import annotations import argparse import hashlib import os from pathlib import Path import shutil import sys sys.path.insert(0, str(Path(__file__).resolve().parent)) import match_transition_trace as transition FIFA_MODULE = "FIFA17.exe" PINNED_FIFA_SHA256 = "29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899" GLOBAL_UI_DISPATCH_RVA = 0x80D1070 CREATE_MATCH_CONTROLLER_RVA = 0xBF950 PROVIDER_BRIDGE_CALL_RVA = 0x1A4D41 def module_mapping(pid: int, module: str) -> tuple[int, str]: with open(f"/proc/{pid}/maps", encoding="utf-8") as handle: for line in handle: fields = line.split(maxsplit=5) path = fields[5].rstrip() if len(fields) == 6 else "" if not path.endswith(module): continue return int(fields[0].split("-", 1)[0], 16), path raise RuntimeError(f"{module} is not mapped in PID {pid}") def validate_file(path: str, expected: str, label: str) -> None: digest = hashlib.sha256() with open(path, "rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) actual = digest.hexdigest() if actual != expected: raise RuntimeError(f"unsupported {label}: sha256={actual}; expected={expected}") def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]: return { "provider": cards_base + transition.PROVIDER_DISPATCH_RVA, "global_dispatch": fifa_base + GLOBAL_UI_DISPATCH_RVA, "controller": cards_base + CREATE_MATCH_CONTROLLER_RVA, "bridge": cards_base + PROVIDER_BRIDGE_CALL_RVA, } def build_gdb_script(pid: int, cards_base: int, fifa_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, fifa_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 $screen_key = 0 set $target_seen = 0 hbreak *0x{address['provider']:x} condition 1 $edx == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x} commands silent if $edx == 0x{transition.FUT_CREATE_MATCH_DP:x} set $screen_key = $r8 set $target_seen = 1 end python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d PROVIDER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x screen_key=%p controller=%p caller=%p\\n", $_thread, $edx, $r8, $rcx, *(void**)$rsp continue end hbreak *0x{address['global_dispatch']:x} condition 2 $target_seen != 0 && $r8 == $screen_key commands silent python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d GLOBAL_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d id=%#x screen_key=%p registry=%p caller=%p\\n", $_thread, $rdx, $r8, $rcx, *(void**)$rsp continue end hbreak *0x{address['controller']:x} condition 3 $edx == 0x7546 commands silent python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d CREATE_MATCH_CONTROLLER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d event=%#x controller=%p caller=%p\\n", $_thread, $edx, $rcx, *(void**)$rsp continue end hbreak *0x{address['bridge']:x} condition 4 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x} commands silent python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d PROVIDER_BRIDGE" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x target=%p bridge=%p callback=%p\\n", $_thread, $edi, $rsi, $rbx, *(void**)(*(void**)$rbx+0x48) continue end printf "ADVTRACE ARMED pid={pid} provider=0x{address['provider']:x} global=0x{address['global_dispatch']:x} controller=0x{address['controller']:x} bridge=0x{address['bridge']:x}\\n" continue """ def selftest() -> None: address = trace_addresses(0x180000000, 0x140000000) assert address == { "provider": 0x1801A4CD0, "global_dispatch": 0x1480D1070, "controller": 0x1800BF950, "bridge": 0x1801A4D41, } script = build_gdb_script(28804, 0x180000000, 0x140000000, "/tmp/advance.log") assert script.count("hbreak *") == 4 assert f"$edx == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script assert f"$edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script assert "$r8 == $screen_key" in script assert "CREATE_MATCH_CONTROLLER" in script assert "PROVIDER_BRIDGE" in script assert "set *(" not in script print("match_advance_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 = module_mapping(pid, FIFA_MODULE) validate_file(fifa_path, PINNED_FIFA_SHA256, FIFA_MODULE) output = args.output or f"/tmp/fifa17-match-advance-{pid}.log" script = build_gdb_script(pid, cards_base, fifa_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-advance-{pid}.gdb" with open(script_path, "w", encoding="utf-8") as handle: handle.write(script) os.execvp("gdb", ["gdb", "-q", "-nx", "-x", script_path]) return 127 if __name__ == "__main__": raise SystemExit(main())