Files
OpenFUT/fifa17-recon/tools/gdb_engine_overwrite_entry_trace.py
T
funman300 433a9b22dd tool(fifa17-recon): trace Offline Seasons team assignment hardware-only
Add a fail-closed, fresh-process native trace workflow for the Offline Seasons
fixture-to-match-team boundary. The supervisor ignores UMU's short-lived
FIFA17.exe process, requires CardsDLL, verifies TracerPid and hardware arming,
rejects pre-existing records, structurally locates fixtures/final records, and
detaches cleanly after capture.

The four payloads reproduce the measured chain without client writes:

  FUN_1800fc500
    -> actual season vector, fixture index 0 / team 73
    -> temporary [73,130000] pair (not the final record)

  CardsDLL service -> engine 0x147c652ce
    -> live final +0x14 writes at the 0x45c side stride
    -> correct [73,130000], then local overwrite [130000,130000]

  engine wrapper 0x147ce47e0
    <- CardsDLL 0x180031861
    <- CardsGameSetupAdapter local `teams` query result already 130000

All execute breakpoints and watchpoints are hardware-only. /proc/PID/mem is
opened rb. No INT3, write_memory, patch, game input, server behavior, or Rust
code. Locator uses zero-based --fixture-index (the live selector is 0 when
season/user.round is 1) and never filters on transient +0x18 handles.
2026-08-25 17:25:41 +00:00

127 lines
3.6 KiB
Python

"""Hardware-only trace of the engine-local overwrite wrapper entry.
Breaks before the prologue of FUN_147ce47e0, where [rsp] is the exact direct
caller return address and R8D is the team ID later written to the final match
record. This closes the one frame Wine PE unwinding could not recover.
No INT3/software breakpoints. No client memory writes.
"""
from __future__ import annotations
import json
import os
import struct
import time
import traceback
import gdb
WRAPPER_VA = 0x147CE47E0
_STATE = None
def _reg(name: str) -> int:
return int(gdb.parse_and_eval(f"${name}"))
def _read(address: int, size: int) -> bytes | None:
if not address or address < 0:
return None
try:
return bytes(gdb.selected_inferior().read_memory(address, size))
except gdb.error:
return None
def _u64(address: int) -> int | None:
data = _read(address, 8)
return struct.unpack("<Q", data)[0] if data else None
def _thread() -> dict:
thread = gdb.selected_thread()
if thread is None:
return {}
return {"name": thread.name, "ptid": list(thread.ptid), "global_num": thread.global_num}
def _registers() -> dict:
names = (
"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp",
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", "rip",
)
return {name: _reg(name) for name in names}
class State:
def __init__(self, path: str):
self.path = path
self.index = 0
def log(self, kind: str, **payload):
self.index += 1
thread = _thread()
event = {
"event": kind,
"event_index": self.index,
"time_unix": time.time(),
"thread": thread,
**payload,
}
with open(self.path, "a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
handle.flush()
os.fsync(handle.fileno())
class WrapperBreakpoint(gdb.Breakpoint):
def __init__(self, state: State):
self.state = state
super().__init__(
f"*0x{WRAPPER_VA:x}", type=gdb.BP_HARDWARE_BREAKPOINT, internal=False
)
self.silent = True
def stop(self):
try:
stack = _reg("rsp")
caller_return = _u64(stack)
self.state.log(
"engine_overwrite_wrapper_entry",
wrapper_va=WRAPPER_VA,
caller_return_address=caller_return,
source_team_id=_reg("r8") & 0xFFFFFFFF,
side_argument=_reg("rdx") & 0xFFFFFFFF,
registers=_registers(),
caller_disassembly=(
gdb.execute(f"x/12i 0x{caller_return - 32:x}", to_string=True)
if caller_return else None
),
backtrace=gdb.execute("bt 32", to_string=True),
)
except Exception as exc:
self.state.log(
"trace_error", where="engine_overwrite_wrapper", error=str(exc),
traceback=traceback.format_exc()
)
return False
def _on_exit(event):
if _STATE is not None:
_STATE.log("inferior_exited", detail=str(event))
def start_trace(log_path: str, _cards_base: int):
global _STATE
open(log_path, "w", encoding="utf-8").close()
_STATE = State(log_path)
breakpoint = WrapperBreakpoint(_STATE)
gdb.events.exited.connect(_on_exit)
_STATE.log(
"trace_armed",
breakpoints={"engine_overwrite_wrapper": {"number": breakpoint.number, "va": WRAPPER_VA}},
hardware_only=True,
client_memory_writes=False,
)