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