Files
OpenFUT/fifa17-recon/tools/match_delivery_lifecycle_trace.py
T
2026-08-25 23:18:08 +00:00

189 lines
7.5 KiB
Python
Executable File

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