From e14d3cd0637d640a771b64a52599640bf57caaf3 Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 28 Aug 2026 01:12:37 +0000 Subject: [PATCH] Trace FIFA17 command 0x128 lifecycle --- fifa17-recon/tools/command_128_trace.py | 332 ++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100755 fifa17-recon/tools/command_128_trace.py diff --git a/fifa17-recon/tools/command_128_trace.py b/fifa17-recon/tools/command_128_trace.py new file mode 100755 index 0000000..1d6125c --- /dev/null +++ b/fifa17-recon/tools/command_128_trace.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +"""Trace FIFA17 Screen event 0x30 through command 0x128 and ScenarioModeStart. + +The generated GDB program uses hardware breakpoints, only reads registers and +client memory, logs, and continues. Seven breakpoints are rotated so no more +than four are enabled. It never calls client functions, writes client memory, +emits events, or drives input. + + command_128_trace.py [pid] [--output PATH] + command_128_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 + +MANAGER_SELECT_ACTION_SOURCE_RVA = 0x0705A620 +MANAGER_SELECT_ACTION_RESULT_RVA = 0x07CDC4A6 +SCREEN_EVENT_CHANNEL_ROUTER_RVA = 0x080CE230 +SCREEN_EVENT_DISPATCH_RVA = 0x080CF790 +SKILL_INSTRUCTIONS_SCREEN_RVA = 0x07DCA400 +GAMEPLAY_COMMAND_DISPATCH_RVA = 0x07A8F6C0 +FREE_ROAM_COMMAND_128_RVA = 0x07A92B0F +SCENARIO_SCHEDULER_RVA = 0x07AC3A40 +SCENARIO_MANAGER_START_RVA = 0x07B1C2B0 +MODE_ZERO_SCENARIO_START_RVA = 0x07B1C190 +GAMEPLAY_GLOBAL_RVA = 0x04BFB910 +SCREEN_VTABLE_RVA = 0x03B3ECC0 +FREE_ROAM_VTABLE_RVA = 0x03AEDF58 +MODE_ZERO_CHILD_VTABLE_RVA = 0x03AE9C00 + + +def addresses(base: int) -> dict[str, int]: + return { + "manager_select_source": base + MANAGER_SELECT_ACTION_SOURCE_RVA, + "manager_select_action": base + MANAGER_SELECT_ACTION_RESULT_RVA, + "screen_event_router": base + SCREEN_EVENT_CHANNEL_ROUTER_RVA, + "screen_event_dispatch": base + SCREEN_EVENT_DISPATCH_RVA, + "instructions_screen": base + SKILL_INSTRUCTIONS_SCREEN_RVA, + "command_dispatch": base + GAMEPLAY_COMMAND_DISPATCH_RVA, + "free_roam_case": base + FREE_ROAM_COMMAND_128_RVA, + "scheduler": base + SCENARIO_SCHEDULER_RVA, + "manager_start": base + SCENARIO_MANAGER_START_RVA, + "scenario_start": base + MODE_ZERO_SCENARIO_START_RVA, + "gameplay_global": base + GAMEPLAY_GLOBAL_RVA, + "screen_vtable": base + SCREEN_VTABLE_RVA, + "free_roam_vtable": base + FREE_ROAM_VTABLE_RVA, + "mode_zero_child_vtable": base + MODE_ZERO_CHILD_VTABLE_RVA, + } + + +def gdb_prelude(pid: int, output: str) -> str: + if any(character in output for character in "\n\r"): + raise ValueError("output path cannot contain a newline") + return f"""set pagination off +set confirm off +set print thread-events off +set breakpoint always-inserted off +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} +""" + + +def build_script(pid: int, fifa_base: int, output: str) -> str: + address = addresses(fifa_base) + return ( + gdb_prelude(pid, output) + + f"""define snapshot_gameplay + set $snap_gameplay_global = *(void**)0x{address['gameplay_global']:x} + set $snap_listener_manager = 0 + set $snap_listener_table = 0 + set $snap_listener_index = -1 + set $snap_free_roam = 0 + set $snap_free_state = -1 + set $snap_free_111 = -1 + set $snap_free_112 = -1 + set $snap_free_124 = -1 + set $snap_selected = 0 + set $snap_selected_vtable = 0 + set $snap_selected_mode = -1 + if $snap_gameplay_global != 0 + set $snap_listener_manager = *(void**)($snap_gameplay_global+0x58) + end + if $snap_listener_manager != 0 + set $snap_listener_table = *(void**)$snap_listener_manager + end + if $snap_listener_table != 0 + set $snap_free_roam = *(void**)$snap_listener_table + set $snap_listener_index = *(int*)($snap_listener_table+0x20) + if $snap_listener_index >= 0 && $snap_listener_index < 3 + set $snap_selected = *(void**)($snap_listener_table+$snap_listener_index*8) + end + end + if $snap_free_roam != 0 + set $snap_free_state = *(int*)($snap_free_roam+0x30) + set $snap_free_111 = *(unsigned char*)($snap_free_roam+0x111) + set $snap_free_112 = *(unsigned char*)($snap_free_roam+0x112) + set $snap_free_124 = *(int*)($snap_free_roam+0x124) + end + if $snap_selected != 0 + set $snap_selected_vtable = *(void**)$snap_selected + set $snap_selected_mode = *(int*)($snap_selected+0x18) + end +end + +set $action_count = 0 +hbreak *0x{address['manager_select_action']:x} +commands + silent + set $action_count = $action_count+1 + set $provider = $rbx + snapshot_gameplay + if $action_count <= 128 + python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d MANAGER_SELECT_ACTION" % (time.time_ns(), time.monotonic_ns()), end=" ") + printf "thread=%d ordinal=%d instruction=%p caller_return=%p provider=%p provider_vtable=%p action_id=%#x free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $action_count, $pc, *(void**)($rsp+0x58), $provider, *(void**)$provider, $eax, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode + end + if $eax == 0x30 + bt 16 + end + continue +end + +hbreak *0x{address['instructions_screen']:x} +condition 2 $edx == 0x30 && *(void**)$rcx == 0x{address['screen_vtable']:x} +commands + silent + set $screen = $rcx + set $screen_owner = *(void**)($screen+0x140) + set $screen_owner_vtable = 0 + if $screen_owner != 0 + set $screen_owner_vtable = *(void**)$screen_owner + end + snapshot_gameplay + python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d INSTRUCTIONS_SCREEN_EVENT_30" % (time.time_ns(), time.monotonic_ns()), end=" ") + printf "thread=%d handler=%p caller_return=%p screen=%p screen_vtable=%p event=%#x payload=%p allow_advance138=%d owner140=%p owner_vtable=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $screen, *(void**)$screen, $edx, $r8, *(int*)($screen+0x138), $screen_owner, $screen_owner_vtable, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode + bt 16 + continue +end + +hbreak *0x{address['command_dispatch']:x} +condition 3 $edx == 0x128 +commands + silent + set $command_dispatcher = $rcx + set $command_table = *(void**)$command_dispatcher + snapshot_gameplay + python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d GAMEPLAY_COMMAND_128" % (time.time_ns(), time.monotonic_ns()), end=" ") + printf "thread=%d function=%p caller_return=%p dispatcher=%p command=%#x payload=%p arg_r9=%p table=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $command_dispatcher, $edx, $r8, $r9, $command_table, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode + disable 1 + disable 2 + disable 3 + enable 5 + continue +end + +hbreak *0x{address['free_roam_case']:x} +commands + silent + set $owner = $rbx + snapshot_gameplay + python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d FREE_ROAM_COMMAND_128" % (time.time_ns(), time.monotonic_ns()), end=" ") + printf "thread=%d callsite=%p caller_return=%p owner=%p owner_vtable=%p command=%#x payload=%p state=%d previous=%d free111=%d free112=%d free124=%d manager=%p selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $owner, *(void**)$owner, $esi, $rdi, *(int*)($owner+0x30), *(int*)($owner+0x34), *(unsigned char*)($owner+0x111), *(unsigned char*)($owner+0x112), *(int*)($owner+0x124), *(void**)($owner+0x168), $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode + continue +end + +hbreak *0x{address['scheduler']:x} +disable 5 +commands + silent + set $owner = $rcx + set $manager = *(void**)($owner+0x168) + set $manager_vtable = 0 + set $manager_mode = -1 + set $child = 0 + set $child_vtable = 0 + if $manager != 0 + set $manager_vtable = *(void**)$manager + set $manager_mode = *(int*)($manager+0x50) + set $child = *(void**)($manager+0x8) + end + if $child != 0 + set $child_vtable = *(void**)$child + end + python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d SCENARIO_SCHEDULER" % (time.time_ns(), time.monotonic_ns()), end=" ") + printf "thread=%d function=%p caller_return=%p owner=%p owner_vtable=%p free124=%d command=%#x payload=%p manager=%p manager_vtable=%p manager_mode=%d child=%p child_vtable=%p\\n", $_thread, $pc, *(void**)$rsp, $owner, *(void**)$owner, *(int*)($owner+0x124), $edx, $r8, $manager, $manager_vtable, $manager_mode, $child, $child_vtable + disable 4 + disable 5 + enable 6 + continue +end + +hbreak *0x{address['manager_start']:x} +disable 6 +commands + silent + set $manager = $rcx + set $child = *(void**)($manager+0x8) + python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d SCENARIO_MANAGER_START" % (time.time_ns(), time.monotonic_ns()), end=" ") + printf "thread=%d function=%p caller_return=%p manager=%p manager_vtable=%p requested_countdown=%d mode=%d child=%p child_vtable=%p\\n", $_thread, $pc, *(void**)$rsp, $manager, *(void**)$manager, $rdx & 0xff, *(int*)($manager+0x50), $child, $child ? *(void**)$child : 0 + disable 6 + enable 7 + continue +end + +hbreak *0x{address['scenario_start']:x} +disable 7 +commands + silent + set $ctx = $rcx + snapshot_gameplay + python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d MODE_ZERO_SCENARIO_START" % (time.time_ns(), time.monotonic_ns()), end=" ") + printf "thread=%d function=%p caller_return=%p ctx=%p ctx_vtable=%p descriptor=%p scenario_index=%d requested_countdown=%d flag40_before=%d callback_owner78=%p callback_vtable48=%p dispatcher_vtable80=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $ctx, *(void**)$ctx, $rdx, $r8d, $r9 & 0xff, *(unsigned char*)($ctx+0x40), *(void**)($ctx+0x78), *(void**)($ctx+0x48), *(void**)($ctx+0x80), $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode + disable 7 + continue +end + +printf "COMMAND128 ARMED pid={pid} action_id=0x{address['manager_select_action']:x} screen_handler=0x{address['instructions_screen']:x} command_dispatch=0x{address['command_dispatch']:x} free_roam=0x{address['free_roam_case']:x} scheduler=0x{address['scheduler']:x} manager=0x{address['manager_start']:x} scenario=0x{address['scenario_start']:x}\\n" +continue +""" + ) + + +def effective_environment(pid: int) -> dict[str, str]: + values: dict[str, str] = {} + for item in Path(f"/proc/{pid}/environ").read_bytes().split(b"\0"): + if not item.startswith(b"OPENFUT_FIFA17_"): + continue + key, _, value = item.decode("utf-8", errors="replace").partition("=") + values[key] = value + return values + + +def selftest() -> None: + address = addresses(0x140000000) + script = build_script(1234, 0x140000000, "/tmp/command-128.log") + assert address["manager_select_source"] == 0x14705A620 + assert address["manager_select_action"] == 0x147CDC4A6 + assert address["instructions_screen"] == 0x147DCA400 + assert address["command_dispatch"] == 0x147A8F6C0 + assert address["free_roam_case"] == 0x147A92B0F + assert address["scheduler"] == 0x147AC3A40 + assert address["manager_start"] == 0x147B1C2B0 + assert address["scenario_start"] == 0x147B1C190 + assert script.count("hbreak *") == 7 + assert "set $action_count = 0" in script + assert "MANAGER_SELECT_ACTION" in script + assert "condition 2 $edx == 0x30" in script + assert "condition 3 $edx == 0x128" in script + assert "disable 4" in script + assert "disable 5" in script and "enable 5" in script + assert "disable 6" in script and "enable 6" in script + assert "disable 7" in script and "enable 7" in script + assert "set *(" not in script + print("command_128_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: + fifa_base, fifa_path = advance.module_mapping(pid, advance.FIFA_MODULE) + advance.validate_file( + fifa_path, + advance.PINNED_FIFA_SHA256, + advance.FIFA_MODULE, + ) + cards_base = 0 + cards_path = "" + try: + cards_base, cards_path = transition.cards_mapping(pid) + except RuntimeError: + pass + else: + transition.validate_cards(cards_path) + output = args.output or f"/tmp/fifa17-command-128-{pid}.log" + script = build_script(pid, fifa_base, output) + environment = effective_environment(pid) + print( + "COMMAND128 PREPARED " + f"pid={pid} fifa_base={fifa_base:#x} cards_base={cards_base:#x} " + f"cards_path={cards_path} " + f"team_compat={environment.get('OPENFUT_FIFA17_SEASON_TEAM_COMPAT', '')} " + f"pma_fix={environment.get('OPENFUT_FIFA17_OFFLINE_SEASONS_PMA_FIX', '')}" + ) + 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-command-128-{pid}.gdb" + Path(script_path).write_text(script, encoding="utf-8") + os.execvp("gdb", ["gdb", "-q", "-nx", "-batch", "-x", script_path]) + return 127 + + +if __name__ == "__main__": + raise SystemExit(main())