#!/usr/bin/env python3 """Trace FIFA17 ScenarioModeStart sources, scheduling, and publication. The candidate profile samples the four statically recovered publisher clusters. The scheduler profile follows the exact mode-0 chain: FreeRoam event ``0x128``, its scheduler, the ScenarioManager start method, and the mode-0 publisher. The upstream profile traces all three literal ``0x128`` sources and the FreeRoam case they must reach. Both generated GDB programs use at most four simultaneously enabled hardware execution breakpoints. They only read registers and client memory, log, and continue. They never call client functions, write client memory, emit events, or drive input. scenario_mode_start_trace.py [pid] --profile candidates scenario_mode_start_trace.py [pid] --profile scheduler scenario_mode_start_trace.py [pid] --profile upstream scenario_mode_start_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 CANDIDATE_A_RVA = 0x07AFDCC0 CANDIDATE_B_RVA = 0x07B1BA90 CANDIDATE_C_RVA = 0x07B1C190 CANDIDATE_D_RVA = 0x07E0B200 FREE_ROAM_EVENT_128_CALLSITE_RVA = 0x07A92B0F SCENARIO_SCHEDULER_RVA = 0x07AC3A40 SCENARIO_MANAGER_START_RVA = 0x07B1C2B0 MODE_ZERO_PUBLISHER_RVA = 0x07B1C190 EVENT_SOURCE_27F9_RVA = 0x07DBD16B EVENT_SOURCE_30_RVA = 0x07DCA4E5 EVENT_SOURCE_280E_RVA = 0x07DD065F def trace_addresses(fifa_base: int) -> dict[str, int]: return { "candidate_a": fifa_base + CANDIDATE_A_RVA, "candidate_b": fifa_base + CANDIDATE_B_RVA, "candidate_c": fifa_base + CANDIDATE_C_RVA, "candidate_d": fifa_base + CANDIDATE_D_RVA, "free_roam_event_128_callsite": fifa_base + FREE_ROAM_EVENT_128_CALLSITE_RVA, "scenario_scheduler": fifa_base + SCENARIO_SCHEDULER_RVA, "scenario_manager_start": fifa_base + SCENARIO_MANAGER_START_RVA, "mode_zero_publisher": fifa_base + MODE_ZERO_PUBLISHER_RVA, "event_source_27f9": fifa_base + EVENT_SOURCE_27F9_RVA, "event_source_30": fifa_base + EVENT_SOURCE_30_RVA, "event_source_280e": fifa_base + EVENT_SOURCE_280E_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 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} """ def build_candidate_script(pid: int, fifa_base: int, output: str) -> str: address = trace_addresses(fifa_base) sections = [gdb_prelude(pid, output)] for label in ("a", "b", "c", "d"): sections.append( f"""hbreak *0x{address[f'candidate_{label}']:x} commands silent python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d CANDIDATE_{label.upper()}" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d rip=%p caller_return=%p object=%p vtable=%p edx=%#x r8=%p r9=%p field_40=%#x field_50=%#x field_b8=%#x\\n", $_thread, $pc, *(void**)$rsp, $rcx, *(void**)$rcx, $edx, $r8, $r9, *(unsigned char*)($rcx+0x40), *(unsigned int*)($rcx+0x50), *(unsigned int*)($rcx+0xb8) bt 16 continue end """ ) sections.append( "printf \"SCENARIOTRACE ARMED profile=candidates " f"pid={pid} candidate_a=0x{address['candidate_a']:x} " f"candidate_b=0x{address['candidate_b']:x} " f"candidate_c=0x{address['candidate_c']:x} " f"candidate_d=0x{address['candidate_d']:x}\\n\"\ncontinue\n" ) return "".join(sections) def build_scheduler_script(pid: int, fifa_base: int, output: str) -> str: address = trace_addresses(fifa_base) return ( gdb_prelude(pid, output) + f"""hbreak *0x{address['free_roam_event_128_callsite']:x} commands silent set $owner = $rbx python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d FREE_ROAM_EVENT_128" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d owner=%p event=%#x payload=%p mode=%d manager=%p caller_return=%p\\n", $_thread, $owner, $esi, $rdi, *(unsigned int*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp bt 16 continue end hbreak *0x{address['scenario_scheduler']:x} commands silent set $owner = $rcx python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d SCENARIO_SCHEDULER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d owner=%p event=%#x payload=%p mode=%d manager=%p caller_return=%p\\n", $_thread, $owner, $edx, $r8, *(unsigned int*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp bt 16 continue end hbreak *0x{address['scenario_manager_start']:x} commands silent set $manager = $rcx python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d SCENARIO_MANAGER_START" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d manager=%p vtable=%p requested_countdown=%d scenario_mode=%d child=%p caller_return=%p\\n", $_thread, $manager, *(void**)$manager, $edx, *(unsigned int*)($manager+0x50), *(void**)($manager+0x8), *(void**)$rsp bt 16 continue end hbreak *0x{address['mode_zero_publisher']:x} commands silent python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d MODE_ZERO_PUBLISHER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d object=%p vtable=%p requested_countdown=%d r8=%p r9=%p caller_return=%p\\n", $_thread, $rcx, *(void**)$rcx, $edx, $r8, $r9, *(void**)$rsp bt 20 continue end printf "SCENARIOTRACE ARMED profile=scheduler pid={pid} event_callsite=0x{address['free_roam_event_128_callsite']:x} scheduler=0x{address['scenario_scheduler']:x} manager_start=0x{address['scenario_manager_start']:x} mode_zero_publisher=0x{address['mode_zero_publisher']:x}\\n" continue """ ) def build_upstream_script(pid: int, fifa_base: int, output: str) -> str: address = trace_addresses(fifa_base) sections = [gdb_prelude(pid, output)] for label, trigger in (("27f9", 0x27F9), ("30", 0x30), ("280e", 0x280E)): sections.append( f"""hbreak *0x{address[f'event_source_{label}']:x} commands silent set $gameplay = $rax set $listeners = 0 set $listener0 = 0 set $listener1 = 0 set $listener2 = 0 if $gameplay != 0 set $listeners = *(void**)$gameplay if $listeners != 0 set $listener0 = *(void**)$listeners set $listener1 = *(void**)($listeners+8) set $listener2 = *(void**)($listeners+16) end end python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d EVENT_128_SOURCE_{label.upper()}" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d trigger=%#x gameplay=%p listeners=%p entries=%p,%p,%p caller_return=%p\\n", $_thread, {trigger}, $gameplay, $listeners, $listener0, $listener1, $listener2, *(void**)$rsp bt 20 continue end """ ) sections.append( f"""hbreak *0x{address['free_roam_event_128_callsite']:x} commands silent set $owner = $rbx python import time; print("SCENARIOTRACE epoch_ns=%d mono_ns=%d FREE_ROAM_EVENT_128" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d owner=%p event=%#x payload=%p mode=%d manager=%p caller_return=%p\\n", $_thread, $owner, $esi, $rdi, *(unsigned int*)($owner+0x124), *(void**)($owner+0x168), *(void**)$rsp bt 20 continue end printf "SCENARIOTRACE ARMED profile=upstream pid={pid} source_27f9=0x{address['event_source_27f9']:x} source_30=0x{address['event_source_30']:x} source_280e=0x{address['event_source_280e']:x} event_callsite=0x{address['free_roam_event_128_callsite']:x}\\n" continue """ ) return "".join(sections) def selftest() -> None: address = trace_addresses(0x140000000) assert address["candidate_a"] == 0x147AFDCC0 assert address["candidate_b"] == 0x147B1BA90 assert address["candidate_c"] == 0x147B1C190 assert address["candidate_d"] == 0x147E0B200 assert address["free_roam_event_128_callsite"] == 0x147A92B0F assert address["scenario_scheduler"] == 0x147AC3A40 assert address["scenario_manager_start"] == 0x147B1C2B0 assert address["mode_zero_publisher"] == 0x147B1C190 assert address["event_source_27f9"] == 0x147DBD16B assert address["event_source_30"] == 0x147DCA4E5 assert address["event_source_280e"] == 0x147DD065F candidate_script = build_candidate_script(61470, 0x140000000, "/tmp/candidates.log") scheduler_script = build_scheduler_script(61470, 0x140000000, "/tmp/scheduler.log") upstream_script = build_upstream_script(61470, 0x140000000, "/tmp/upstream.log") assert candidate_script.count("hbreak *") == 4 assert all(f"CANDIDATE_{label}" in candidate_script for label in "ABCD") assert scheduler_script.count("hbreak *") == 4 assert "FREE_ROAM_EVENT_128" in scheduler_script assert "SCENARIO_SCHEDULER" in scheduler_script assert "SCENARIO_MANAGER_START" in scheduler_script assert "MODE_ZERO_PUBLISHER" in scheduler_script assert upstream_script.count("hbreak *") == 4 assert "EVENT_128_SOURCE_27F9" in upstream_script assert "EVENT_128_SOURCE_30" in upstream_script assert "EVENT_128_SOURCE_280E" in upstream_script assert "FREE_ROAM_EVENT_128" in upstream_script assert "0x147ac9900" not in scheduler_script.lower() assert "CANDIDATE_B_GATE" not in scheduler_script assert "set *(" not in candidate_script assert "set *(" not in scheduler_script assert "set *(" not in upstream_script print("scenario_mode_start_trace selftest: PASS") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("pid", nargs="?", type=int) parser.add_argument( "--profile", choices=("candidates", "scheduler", "upstream"), default="candidates", ) 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, ) output = args.output or f"/tmp/fifa17-scenario-{args.profile}-{pid}.log" if args.profile == "candidates": script = build_candidate_script(pid, fifa_base, output) elif args.profile == "scheduler": script = build_scheduler_script(pid, fifa_base, output) else: script = build_upstream_script(pid, 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-scenario-{args.profile}-{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())