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

209 lines
7.7 KiB
Python
Executable File

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