#!/usr/bin/env python3 """Trace FIFA17's PMA ScenarioModeStart-to-event-5 producer chain. The generated GDB program uses hardware breakpoints, only reads registers and client memory, logs, and continues. Breakpoints are rotated so no more than four are enabled. It never calls client functions, writes client memory, emits an event, or drives input. pma_producer_trace.py [pid] [--variant mode0|alternate] [--output PATH] pma_producer_trace.py --selftest """ from __future__ import annotations import argparse 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 VARIANTS = { "mode0": { "scenario_rva": 0x07B1C190, "writer_rva": 0x07B1C26B, "register_rva": 0x07B1C282, "writer_context": "$rsi", "writer_async_requested": "1", "arm_condition": "1", }, "alternate": { "scenario_rva": 0x07B1C050, "writer_rva": 0x07B1C12F, "register_rva": 0x07B1C146, "writer_context": "$rbp", "writer_async_requested": "$sil", "arm_condition": "$tracked_ctx != 0 && $rcx == $tracked_ctx", }, } PMA_COMPLETION_ARM_RVA = 0x07B1AE60 PMA_COMPLETION_ARM_WRITER_RVA = 0x07B1AF33 ASYNC_COMPLETION_RVA = 0x07B046C0 CALLBACK_DISPATCHER_RVA = 0x07AC87B0 PMA_INSTRUCTIONS_HANDLER_RVA = 0x07AC91E0 GAMEPLAY_GLOBAL_RVA = 0x04BFB910 PMA_INSTRUCTIONS_VTABLE_RVA = 0x03AF2750 def addresses(base: int, variant: str) -> dict[str, int]: config = VARIANTS[variant] return { "scenario": base + config["scenario_rva"], "writer": base + config["writer_rva"], "register": base + config["register_rva"], "arm": base + PMA_COMPLETION_ARM_RVA, "arm_writer": base + PMA_COMPLETION_ARM_WRITER_RVA, "completion": base + ASYNC_COMPLETION_RVA, "dispatcher": base + CALLBACK_DISPATCHER_RVA, "instructions": base + PMA_INSTRUCTIONS_HANDLER_RVA, "gameplay_global": base + GAMEPLAY_GLOBAL_RVA, "instructions_vtable": base + PMA_INSTRUCTIONS_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, variant: str) -> str: address = addresses(fifa_base, variant) config = VARIANTS[variant] return ( gdb_prelude(pid, output) + f"""define snapshot_pma_context set $snap_ctx = $arg0 set $snap_flag40 = -1 set $snap_callback_vtable = 0 set $snap_callback_owner = 0 set $snap_dispatcher = 0 set $snap_dispatcher_vtable = 0 set $snap_pma = 0 set $snap_pma_flag18 = -1 set $snap_pma_parent = 0 set $snap_pma_machine = 0 set $snap_pma_current = 0 if $snap_ctx != 0 set $snap_flag40 = *(unsigned char*)($snap_ctx+0x40) set $snap_callback_vtable = *(void**)($snap_ctx+0x48) set $snap_callback_owner = *(void**)($snap_ctx+0x78) set $snap_dispatcher = $snap_ctx+0x80 set $snap_dispatcher_vtable = *(void**)$snap_dispatcher set $snap_sentinel = $snap_ctx+0x88 set $snap_node = *(void**)$snap_sentinel set $snap_scan = 0 while $snap_node != 0 && $snap_node != $snap_sentinel && $snap_scan < 8 set $snap_candidate = *(void**)($snap_node+0x10) if $snap_candidate != 0 if *(void**)$snap_candidate == 0x{address['instructions_vtable']:x} set $snap_pma = $snap_candidate end end set $snap_node = *(void**)$snap_node set $snap_scan = $snap_scan+1 end if $snap_pma != 0 set $snap_pma_flag18 = *(unsigned char*)($snap_pma+0x18) set $snap_pma_parent = *(void**)($snap_pma+0x8) if $snap_pma_parent != 0 set $snap_pma_machine = *(void**)($snap_pma_parent+0x8) end if $snap_pma_machine != 0 set $snap_pma_current = *(void**)($snap_pma_machine+0x10) end end end end 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_roam_state = -1 set $snap_free_roam_111 = -1 set $snap_free_roam_112 = -1 set $snap_free_roam_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_roam_state = *(int*)($snap_free_roam+0x30) set $snap_free_roam_111 = *(unsigned char*)($snap_free_roam+0x111) set $snap_free_roam_112 = *(unsigned char*)($snap_free_roam+0x112) set $snap_free_roam_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 $tracked_ctx = 0 hbreak *0x{address['scenario']:x} commands silent set $ctx = $rcx set $tracked_ctx = $ctx snapshot_pma_context $ctx snapshot_gameplay python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d SCENARIO_MODE_START" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p ctx=%p ctx_vtable=%p arg_descriptor=%p arg_scenario=%p async_requested=%d flag40=%d callback_vtable=%p callback_owner=%p dispatcher=%p dispatcher_vtable=%p pma=%p pma_flag18=%d pma_parent=%p pma_machine=%p pma_current=%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, $r8, $r9b, $snap_flag40, $snap_callback_vtable, $snap_callback_owner, $snap_dispatcher, $snap_dispatcher_vtable, $snap_pma, $snap_pma_flag18, $snap_pma_parent, $snap_pma_machine, $snap_pma_current, $snap_free_roam, $snap_free_roam_state, $snap_free_roam_111, $snap_free_roam_112, $snap_free_roam_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 1 enable 2 continue end hbreak *0x{address['writer']:x} condition 2 $tracked_ctx != 0 && {config['writer_context']} == $tracked_ctx disable 2 commands silent set $ctx = {config['writer_context']} snapshot_pma_context $ctx snapshot_gameplay python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d CONTEXT_ARM_WRITER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d instruction=%p caller_return=%p ctx=%p original_async_requested=%d flag40_before=%d callback_vtable=%p callback_owner_before=%p dispatcher=%p dispatcher_vtable=%p pma=%p pma_flag18=%d pma_current=%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, {config['writer_async_requested']}, $snap_flag40, $snap_callback_vtable, $snap_callback_owner, $snap_dispatcher, $snap_dispatcher_vtable, $snap_pma, $snap_pma_flag18, $snap_pma_current, $snap_free_roam, $snap_free_roam_state, $snap_free_roam_111, $snap_free_roam_112, $snap_free_roam_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 2 enable 3 continue end hbreak *0x{address['register']:x} condition 3 $tracked_ctx != 0 && $rdx == $tracked_ctx+0x48 disable 3 commands silent set $callback = $rdx set $ctx = $callback-0x48 snapshot_pma_context $ctx python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d ASYNC_REGISTER_CALL" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d callsite=%p caller_return=%p service=%p service_vtable=%p callback=%p callback_vtable=%p ctx=%p flag40=%d callback_owner=%p dispatcher=%p dispatcher_vtable=%p\\n", $_thread, $pc, *(void**)$rsp, $rcx, *(void**)$rcx, $callback, *(void**)$callback, $ctx, $snap_flag40, $snap_callback_owner, $snap_dispatcher, $snap_dispatcher_vtable disable 3 continue end hbreak *0x{address['arm']:x} condition 4 {config['arm_condition']} commands silent set $ctx = $rcx if $tracked_ctx == 0 set $tracked_ctx = $ctx end snapshot_pma_context $ctx snapshot_gameplay python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d COMPLETION_ARM_ENTRY" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p ctx=%p ctx_vtable=%p flag40_before=%d callback_vtable=%p callback_owner=%p dispatcher=%p dispatcher_vtable=%p result_source=%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, $snap_flag40, $snap_callback_vtable, $snap_callback_owner, $snap_dispatcher, $snap_dispatcher_vtable, *(void**)($ctx+0xa8), $snap_free_roam, $snap_free_roam_state, $snap_free_roam_111, $snap_free_roam_112, $snap_free_roam_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 4 enable 5 continue end hbreak *0x{address['arm_writer']:x} condition 5 $tracked_ctx != 0 && $rsi == $tracked_ctx disable 5 commands silent set $ctx = $rsi snapshot_pma_context $ctx snapshot_gameplay python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d COMPLETION_ARM_WRITER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d instruction=%p caller_return=%p ctx=%p flag40_before=%d result_object=%p result_state28=%d callback_owner=%p dispatcher=%p dispatcher_vtable=%p pma=%p pma_flag18=%d pma_current=%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, $snap_flag40, $rax, *(int*)($rax+0x28), $snap_callback_owner, $snap_dispatcher, $snap_dispatcher_vtable, $snap_pma, $snap_pma_flag18, $snap_pma_current, $snap_free_roam, $snap_free_roam_state, $snap_free_roam_111, $snap_free_roam_112, $snap_free_roam_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 5 continue end hbreak *0x{address['completion']:x} condition 6 $tracked_ctx != 0 && $rcx == $tracked_ctx+0x48 commands silent set $callback = $rcx set $ctx = *(void**)($callback+0x30) snapshot_pma_context $ctx snapshot_gameplay python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d ASYNC_COMPLETION" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p callback=%p callback_vtable=%p ctx=%p callback_matches_ctx48=%d flag40_before=%d arg_rdx=%p arg_r8=%p arg_r9=%p dispatcher=%p dispatcher_vtable=%p pma=%p pma_flag18=%d pma_current=%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, $callback, *(void**)$callback, $ctx, $callback == $ctx+0x48, $snap_flag40, $rdx, $r8, $r9, $snap_dispatcher, $snap_dispatcher_vtable, $snap_pma, $snap_pma_flag18, $snap_pma_current, $snap_free_roam, $snap_free_roam_state, $snap_free_roam_111, $snap_free_roam_112, $snap_free_roam_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode continue end hbreak *0x{address['dispatcher']:x} condition 7 $tracked_ctx != 0 && $rcx == $tracked_ctx+0x80 && $edx == 5 commands silent set $ctx = $rcx-0x80 snapshot_pma_context $ctx snapshot_gameplay python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d DISPATCHER_EVENT_5" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p dispatcher=%p event=%d arg_r8=%p arg_r9=%p ctx=%p flag40=%d callback_owner=%p pma=%p pma_flag18=%d pma_current=%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, $rcx, $edx, $r8, $r9, $ctx, $snap_flag40, $snap_callback_owner, $snap_pma, $snap_pma_flag18, $snap_pma_current, $snap_free_roam, $snap_free_roam_state, $snap_free_roam_111, $snap_free_roam_112, $snap_free_roam_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 7 enable 8 continue end hbreak *0x{address['instructions']:x} disable 8 commands silent set $listener = $rcx set $parent = *(void**)($listener+0x8) set $machine = 0 set $current = 0 if $parent != 0 set $machine = *(void**)($parent+0x8) end if $machine != 0 set $current = *(void**)($machine+0x10) end snapshot_gameplay python import time; print("PMAPRODUCER epoch_ns=%d mono_ns=%d INSTRUCTIONS_AFTER_EVENT_5" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d handler=%p caller_return=%p listener=%p listener_vtable=%p event=%d flag18=%d parent=%p machine=%p current=%p current_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, $listener, *(void**)$listener, $edx, *(unsigned char*)($listener+0x18), $parent, $machine, $current, $current ? *(void**)$current : 0, $snap_free_roam, $snap_free_roam_state, $snap_free_roam_111, $snap_free_roam_112, $snap_free_roam_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 6 continue end printf "PMAPRODUCER ARMED pid={pid} variant={variant} scenario=0x{address['scenario']:x} writer=0x{address['writer']:x} register=0x{address['register']:x} arm=0x{address['arm']:x} arm_writer=0x{address['arm_writer']:x} completion=0x{address['completion']:x} dispatcher=0x{address['dispatcher']:x} instructions=0x{address['instructions']: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: mode0 = addresses(0x140000000, "mode0") alternate = addresses(0x140000000, "alternate") script = build_script(1234, 0x140000000, "/tmp/pma-producer.log", "mode0") assert mode0["scenario"] == 0x147B1C190 assert mode0["writer"] == 0x147B1C26B assert mode0["register"] == 0x147B1C282 assert alternate["scenario"] == 0x147B1C050 assert alternate["writer"] == 0x147B1C12F assert alternate["register"] == 0x147B1C146 assert mode0["completion"] == 0x147B046C0 assert mode0["dispatcher"] == 0x147AC87B0 assert mode0["instructions"] == 0x147AC91E0 assert mode0["arm"] == 0x147B1AE60 assert mode0["arm_writer"] == 0x147B1AF33 assert script.count("hbreak *") == 8 assert "condition 7 $tracked_ctx != 0" in script assert "disable 2" in script and "enable 2" in script assert "disable 3" in script and "enable 3" in script assert "disable 5" in script and "enable 5" in script assert "disable 8" in script and "enable 8" in script assert "set *(" not in script print("pma_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("--variant", choices=tuple(VARIANTS), default="mode0") 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, cards_path = transition.cards_mapping(pid) transition.validate_cards(cards_path) output = args.output or f"/tmp/fifa17-pma-producer-{args.variant}-{pid}.log" script = build_script(pid, fifa_base, output, args.variant) environment = effective_environment(pid) print( "PMAPRODUCER PREPARED " f"pid={pid} variant={args.variant} fifa_base={fifa_base:#x} cards_base={cards_base:#x} " 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-pma-producer-{args.variant}-{pid}.gdb" Path(script_path).write_text(script, encoding="utf-8") import os os.execvp("gdb", ["gdb", "-q", "-nx", "-batch", "-x", script_path]) return 127 if __name__ == "__main__": raise SystemExit(main())