#!/usr/bin/env python3 """Trace FIFA17 provider dispatch and ACTION_ADVANCE delivery boundaries. This probe correlates the global UI dispatch of FUT_CREATE_MATCH_DP and FUT_GET_MATCH_KITS_DP, the subscribed CardsDLL provider, the internal 0x7546 create-response callback that can replay FUT_CREATE_MATCH_DP, and the final native-to-UI bridge. At global dispatch, r8d is the provider ID and rdx is the payload; neither register is a screen key. 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} 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 python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d PROVIDER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x payload=%p controller=%p caller=%p\\n", $_thread, $edx, $r8, $rcx, *(void**)$rsp continue end hbreak *0x{address['global_dispatch']:x} condition 2 $r8d == 0x{transition.FUT_CREATE_MATCH_DP:x} || $r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x} 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 provider=%#x payload=%p manager=%p caller=%p\\n", $_thread, $r8d, $rdx, $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 f"$r8d == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script assert f"$r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" 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())