tools: trace FIFA17 scenario start source chain
This commit is contained in:
+296
@@ -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())
|
||||
Reference in New Issue
Block a user