#!/usr/bin/env python3 """Trace the FIFA17 ACTION_CREATE_MATCH-to-provider lifecycle. The probe correlates: * the select-team action handler for UIF action IDs 0x7574..0x757b; * DataManager's request dispatch for FutCreateMatchServerResponse (0x7546); * the concrete FutCreateMatchServerResponse data-source request method; * FIFA's global UI dispatch of providers 0x7563 and 0x7565. Static decoding identifies action 0x7577 as the branch that constructs the create-match request and calls DataManager for source 0x7546. The trace proves whether that authentic trigger executes in the failing flow. It uses four hardware-assisted execution breakpoints, never writes client memory, and never drives game input. match_create_action_trace.py [pid] [--output PATH] match_create_action_trace.py --print-script [pid] match_create_action_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 SELECT_TEAM_ACTION_HANDLER_RVA = 0x0BFCC0 DATA_MANAGER_REQUEST_RVA = 0x80D2340 DATA_SOURCE_REQUEST_RVA = 0x120270 GLOBAL_UI_DISPATCH_RVA = advance.GLOBAL_UI_DISPATCH_RVA FIRST_SELECT_TEAM_ACTION = 0x7574 LAST_SELECT_TEAM_ACTION = 0x757B ACTION_CREATE_MATCH = 0x7577 CREATE_DATA_SOURCE = 0x7546 def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]: return { "action_handler": cards_base + SELECT_TEAM_ACTION_HANDLER_RVA, "manager_request": fifa_base + DATA_MANAGER_REQUEST_RVA, "data_source_request": cards_base + DATA_SOURCE_REQUEST_RVA, "ui_dispatch": fifa_base + GLOBAL_UI_DISPATCH_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 $create_action_seen = 0 set $manager_request_seen = 0 set $data_source_request_seen = 0 hbreak *0x{address['action_handler']:x} condition 1 $edx >= 0x{FIRST_SELECT_TEAM_ACTION:x} && $edx <= 0x{LAST_SELECT_TEAM_ACTION:x} commands silent if $edx == 0x{ACTION_CREATE_MATCH:x} set $create_action_seen = 1 end python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d SELECT_TEAM_ACTION" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d action=%#x is_create=%d controller=%p payload=%p create_seen=%d\\n", $_thread, $edx, $edx==0x{ACTION_CREATE_MATCH:x}, $rcx, $r8, $create_action_seen bt 10 continue end hbreak *0x{address['manager_request']:x} condition 2 $edx == 0x{CREATE_DATA_SOURCE:x} commands silent set $manager_request_seen = 1 set $tree_sentinel = $rcx + 0x10 set $tree_cursor = *(void**)($rcx+0x20) set $data_node = $tree_sentinel while $tree_cursor != 0 && $tree_cursor != $tree_sentinel if *(unsigned int*)($tree_cursor+0x20) >= 0x{CREATE_DATA_SOURCE:x} set $data_node = $tree_cursor set $tree_cursor = *(void**)($tree_cursor+0x08) else set $tree_cursor = *(void**)$tree_cursor end end set $data_source = 0 set $request_method = 0 if $data_node != $tree_sentinel && *(unsigned int*)($data_node+0x20) == 0x{CREATE_DATA_SOURCE:x} set $data_source = *(void**)($data_node+0x28) if $data_source != 0 set $request_method = *(void**)(*(void**)$data_source+0x18) end end python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d MANAGER_REQUEST" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d source=%#x manager=%p request=%p node=%p data_source=%p request_method=%p create_seen=%d\\n", $_thread, $edx, $rcx, $r8, $data_node, $data_source, $request_method, $create_action_seen bt 10 continue end hbreak *0x{address['data_source_request']:x} commands silent set $data_source_request_seen = 1 python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d DATA_SOURCE_REQUEST" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d response=%p data_source=%p request=%p ready_before=%#x create_seen=%d manager_seen=%d\\n", $_thread, $rcx-0x50, $rcx, $rdx, *(unsigned char*)($rcx+0x38), $create_action_seen, $manager_request_seen bt 10 continue end hbreak *0x{address['ui_dispatch']:x} condition 4 $r8d == 0x{transition.FUT_CREATE_MATCH_DP:x} || $r8d == 0x{transition.FUT_GET_MATCH_KITS_DP:x} commands silent python import time; print("ACTIONTRACE epoch_ns=%d mono_ns=%d UI_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x payload=%p ui_manager=%p create_seen=%d manager_seen=%d data_source_seen=%d\\n", $_thread, $r8d, $rdx, $rcx, $create_action_seen, $manager_request_seen, $data_source_request_seen bt 10 continue end printf "ACTIONTRACE ARMED pid={pid} action_handler=0x{address['action_handler']:x} manager_request=0x{address['manager_request']:x} data_source_request=0x{address['data_source_request']:x} ui_dispatch=0x{address['ui_dispatch']:x}\\n" continue """ def selftest() -> None: address = trace_addresses(0x180000000, 0x140000000) assert address == { "action_handler": 0x1800BFCC0, "manager_request": 0x1480D2340, "data_source_request": 0x180120270, "ui_dispatch": 0x1480D1070, } script = build_gdb_script( 45949, 0x180000000, 0x140000000, "/tmp/create-action.log" ) assert script.count("hbreak *") == 4 assert f"$edx == 0x{ACTION_CREATE_MATCH:x}" in script assert f"$edx == 0x{CREATE_DATA_SOURCE: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 "request_method" in script assert "set *(" not in script print("match_create_action_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-create-action-{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-create-action-{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())