#!/usr/bin/env python3 """Trace FIFA17 provider delivery lookup without heap-address assumptions. The probe anchors the real CardsDLL call sequence in FUN_1801a4cd0 and the provider-specific FUT_CREATE_MATCH_DP readiness check in FUN_1800be500: vslot +0x38 call -> create gate return -> returned target -> UI bridge For FUT_CREATE_MATCH_DP and FUT_GET_MATCH_KITS_DP it records the live controller vtable, concrete lookup function, event service, readiness-gate implementation, every register input, returned target, and whether the native-to-UI bridge executes. No post-event object identity is used. The generated GDB program uses hardware-assisted execution breakpoints only. It never writes client memory and never drives game input. match_delivery_lifecycle_trace.py [pid] [--output PATH] match_delivery_lifecycle_trace.py --print-script [pid] match_delivery_lifecycle_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 LOOKUP_CALL_RVA = transition.PROVIDER_DISPATCH_RVA + 0x2C LOOKUP_RETURN_RVA = transition.PROVIDER_DISPATCH_RVA + 0x2F BRIDGE_CALL_RVA = transition.PROVIDER_DISPATCH_RVA + 0x71 CREATE_GATE_RETURN_RVA = 0x0BE647 def trace_addresses(cards_base: int) -> dict[str, int]: return { "lookup_call": cards_base + LOOKUP_CALL_RVA, "gate_return": cards_base + CREATE_GATE_RETURN_RVA, "lookup_return": cards_base + LOOKUP_RETURN_RVA, "bridge": cards_base + BRIDGE_CALL_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 $current_provider = 0 set $current_payload = 0 set $current_controller = 0 set $current_vtable = 0 set $current_lookup = 0 set $current_service = 0 set $current_service_vtable = 0 set $current_gate = 0 hbreak *0x{address['lookup_call']:x} condition 1 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x} commands silent set $current_provider = $edi set $current_payload = $rbp set $current_controller = $rcx set $current_vtable = *(void**)$rcx set $current_lookup = *(void**)(*(void**)$rcx+0x38) set $current_service = *(void**)($rcx+0x18) set $current_service_vtable = *(void**)$current_service set $current_gate = *(void**)($current_service_vtable+0x58) python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d LOOKUP_CALL" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x this=%p outer_controller=%p payload=%p vtable=%p lookup_fn=%p service=%p service_vtable=%p gate_fn=%p controller_mode=%#x controller_flag=%#x rdx=%p r8=%p r9=%p state_rbx=%p state_rbp=%p\\n", $_thread, $edi, $rcx, $rbx, $rbp, $current_vtable, $current_lookup, $current_service, $current_service_vtable, $current_gate, *(unsigned int*)($rcx+0x140), *(unsigned char*)($rcx+0x152), $rdx, $r8, $r9, $rbx, $rbp continue end hbreak *0x{address['gate_return']:x} condition 2 $current_provider == 0x{transition.FUT_CREATE_MATCH_DP:x} && $rbx == $current_controller commands silent python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d CREATE_GATE_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x controller=%p service=%p service_vtable=%p gate_fn=%p selector=0x7546 result_al=%#x\\n", $_thread, $current_provider, $current_controller, $current_service, $current_service_vtable, $current_gate, $al continue end hbreak *0x{address['lookup_return']:x} condition 3 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x} commands silent python import time; print("LOOKUPTRACE epoch_ns=%d mono_ns=%d LOOKUP_RETURN" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x controller=%p payload=%p vtable=%p lookup_fn=%p result=%p\\n", $_thread, $edi, $rbx, $rbp, $current_vtable, $current_lookup, $rax 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("LOOKUPTRACE epoch_ns=%d mono_ns=%d BRIDGE" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d provider=%#x controller=%p payload=%p target=%p bridge=%p callback=%p\\n", $_thread, $edi, $current_controller, $current_payload, $rsi, $rbx, *(void**)(*(void**)$rbx+0x48) continue end printf "LOOKUPTRACE ARMED pid={pid} lookup_call=0x{address['lookup_call']:x} gate_return=0x{address['gate_return']:x} lookup_return=0x{address['lookup_return']:x} bridge=0x{address['bridge']:x}\\n" continue """ def selftest() -> None: address = trace_addresses(0x180000000) assert address == { "lookup_call": 0x1801A4CFC, "gate_return": 0x1800BE647, "lookup_return": 0x1801A4CFF, "bridge": 0x1801A4D41, } script = build_gdb_script(35632, 0x180000000, "/tmp/lookup.log") assert script.count("hbreak *") == 4 assert f"$edi == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script assert f"$edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script assert "LOOKUP_CALL" in script assert "CREATE_GATE_RETURN" in script assert "LOOKUP_RETURN" in script assert "gate_fn" in script assert "BRIDGE" in script assert "set *(" not in script print("match_delivery_lifecycle_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-provider-lookup-{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-provider-lookup-{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())