From 2c572e918f3ad24ad29aa1f9a7b134a32a59ba68 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 26 Aug 2026 04:46:42 +0000 Subject: [PATCH] tools: trace FIFA17 scenario start source chain --- .../tools/scenario_mode_start_trace.py | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100755 fifa17-recon/tools/scenario_mode_start_trace.py diff --git a/fifa17-recon/tools/scenario_mode_start_trace.py b/fifa17-recon/tools/scenario_mode_start_trace.py new file mode 100755 index 0000000..aa22156 --- /dev/null +++ b/fifa17-recon/tools/scenario_mode_start_trace.py @@ -0,0 +1,296 @@ +#!/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())