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

202 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Trace FIFA17 create-response readiness versus UI provider dispatch.
The probe correlates four concrete lifecycle boundaries:
* FutCreateMatchServerResponse data-source request;
* the POST /match network response callback;
* the response readiness/completion callback;
* FIFA's global UI dispatch of providers 0x7563 and 0x7565.
This distinguishes network completion from the separate DataManager readiness
lifecycle without assuming any screen or heap-object identity. The generated GDB
program uses hardware-assisted execution breakpoints only. It never writes
client memory and never drives game input.
match_provider_producer_trace.py [pid] [--output PATH]
match_provider_producer_trace.py --print-script [pid]
match_provider_producer_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
DATA_SOURCE_REQUEST_RVA = 0x120270
NETWORK_RESPONSE_RVA = transition.RESPONSE_CALLBACK_RVA
CREATE_COMPLETE_RVA = 0x120000
GLOBAL_UI_DISPATCH_RVA = advance.GLOBAL_UI_DISPATCH_RVA
CREATE_RESPONSE_OFFSET = 0xA0
CREATE_DATA_SOURCE_OFFSET = CREATE_RESPONSE_OFFSET + 0x50
CREATE_READY_OFFSET = CREATE_RESPONSE_OFFSET + 0x88
ACTIVE_CALLBACK_OFFSET = 0x47D0
def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]:
return {
"data_source_request": cards_base + DATA_SOURCE_REQUEST_RVA,
"network_response": cards_base + NETWORK_RESPONSE_RVA,
"create_complete": cards_base + CREATE_COMPLETE_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 $last_central = 0
set $last_response = 0
set $last_data_source = 0
set $last_descriptor = 0
hbreak *0x{address['data_source_request']:x}
commands
silent
set $request_data_source = $rcx
set $request_response = $rcx - 0x50
python import time; print("RESPTRACE 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 callback_adapter=%p callback_context=%p callback_target=%p\\n", $_thread, $request_response, $request_data_source, $rdx, *(unsigned char*)($request_data_source+0x38), *(void**)($request_response+0x90), *(void**)($request_response+0x98), *(void**)($request_response+0xa0)
bt 10
continue
end
hbreak *0x{address['network_response']:x}
commands
silent
set $last_central = $rcx
set $last_response = $rcx + 0x{CREATE_RESPONSE_OFFSET:x}
set $last_data_source = $rcx + 0x{CREATE_DATA_SOURCE_OFFSET:x}
set $last_descriptor = $rdx
python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d NETWORK_RESPONSE" % (time.time_ns(), time.monotonic_ns()), end=" ")
if $rdx == 0
printf "thread=%d central=%p descriptor=(nil) status=UNKNOWN wire_payload=(nil) response=%p data_source=%p ready=%#x active_adapter=%p active_context=%p active_target=%p\\n", $_thread, $last_central, $last_response, $last_data_source, *(unsigned char*)($last_central+0x{CREATE_READY_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 8:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 16:x})
else
printf "thread=%d central=%p descriptor=%p status=%#x wire_payload=%p response=%p data_source=%p ready=%#x active_adapter=%p active_context=%p active_target=%p\\n", $_thread, $last_central, $rdx, *(unsigned int*)($rdx+0x1c), *(void**)($rdx+0x28), $last_response, $last_data_source, *(unsigned char*)($last_central+0x{CREATE_READY_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 8:x}), *(void**)($last_central+0x{ACTIVE_CALLBACK_OFFSET + 16:x})
end
bt 10
continue
end
hbreak *0x{address['create_complete']:x}
commands
silent
python import time; print("RESPTRACE epoch_ns=%d mono_ns=%d CREATE_COMPLETE" % (time.time_ns(), time.monotonic_ns()), end=" ")
if $rdx == 0
printf "thread=%d response=%p data_source=%p ready_before=%#x descriptor=(nil) status=UNKNOWN last_response=%p same_response=%d\\n", $_thread, $rcx, $rcx+0x50, *(unsigned char*)($rcx+0x88), $last_response, $rcx==$last_response
else
printf "thread=%d response=%p data_source=%p ready_before=%#x descriptor=%p status=%#x last_response=%p same_response=%d\\n", $_thread, $rcx, $rcx+0x50, *(unsigned char*)($rcx+0x88), $rdx, *(unsigned int*)($rdx+0x1c), $last_response, $rcx==$last_response
end
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("RESPTRACE epoch_ns=%d mono_ns=%d UI_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
if $last_response == 0
printf "thread=%d provider=%#x payload=%p ui_manager=%p last_response=(nil) ready=UNKNOWN\\n", $_thread, $r8d, $rdx, $rcx
else
printf "thread=%d provider=%#x payload=%p ui_manager=%p last_response=%p data_source=%p ready=%#x descriptor=%p\\n", $_thread, $r8d, $rdx, $rcx, $last_response, $last_data_source, *(unsigned char*)($last_response+0x88), $last_descriptor
end
bt 10
continue
end
printf "RESPTRACE ARMED pid={pid} data_source_request=0x{address['data_source_request']:x} network_response=0x{address['network_response']:x} create_complete=0x{address['create_complete']:x} ui_dispatch=0x{address['ui_dispatch']:x}\\n"
continue
"""
def selftest() -> None:
address = trace_addresses(0x180000000, 0x140000000)
assert address == {
"data_source_request": 0x180120270,
"network_response": 0x180114D90,
"create_complete": 0x180120000,
"ui_dispatch": 0x1480D1070,
}
script = build_gdb_script(
38872, 0x180000000, 0x140000000, "/tmp/response-lifecycle.log"
)
assert script.count("hbreak *") == 4
assert "DATA_SOURCE_REQUEST" in script
assert "NETWORK_RESPONSE" in script
assert "CREATE_COMPLETE" in script
assert "UI_DISPATCH" 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 "set *(" not in script
print("match_provider_producer_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-response-lifecycle-{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-response-lifecycle-{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())