Files
OpenFUT/fifa17-recon/tools/gdb_team_overwrite_origin_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

171 lines
5.3 KiB
Python

"""Hardware-only origin trace for CardsDLL team-pair submissions.
Distinguishes the three callers of the engine team-id service that can submit a
full two-team pair, plus the mode-76 builder that prepares its pair:
0x1800c7583 correct fixture pair control
0x1800c6c23 generic pair submitter
0x1800c8dc1 mode-76 pair submitter
0x1800c8bf0 mode-76 pair builder entry
No INT3/software breakpoints. No client memory writes.
"""
from __future__ import annotations
import json
import os
import struct
import time
import traceback
import gdb
CARDS_IMAGE_BASE = 0x180000000
SITES = {
0x1800C7583: ("fixture_pair_submit", "r14", "rsi"),
0x1800C6C23: ("generic_pair_submit", "r14", "rsi"),
0x1800C8DC1: ("mode76_pair_submit", "r15", "rbp"),
}
MODE76_BUILDER = 0x1800C8BF0
_STATE = None
def _reg(name: str) -> int:
return int(gdb.parse_and_eval(f"${name}"))
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 _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 _pair(address: int) -> list[int] | None:
data = _read(address, 8)
return list(struct.unpack("<2i", data)) if data else None
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, log_path: str, cards_base: int):
self.log_path = log_path
self.cards_base = cards_base
self.event_index = 0
def log(self, kind: str, **payload):
self.event_index += 1
event = {
"event": kind,
"event_index": self.event_index,
"time_unix": time.time(),
"thread": _thread(),
**payload,
}
with open(self.log_path, "a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
handle.flush()
os.fsync(handle.fileno())
class PairSubmitBreakpoint(gdb.Breakpoint):
def __init__(self, state: State, image_va: int, name: str, pointer_reg: str, index_reg: str):
self.state = state
self.image_va = image_va
self.name = name
self.pointer_reg = pointer_reg
self.index_reg = index_reg
address = state.cards_base + (image_va - CARDS_IMAGE_BASE)
super().__init__(f"*0x{address:x}", type=gdb.BP_HARDWARE_BREAKPOINT, internal=False)
self.silent = True
def stop(self):
try:
pointer = _reg(self.pointer_reg)
index = _reg(self.index_reg) & 0xFFFFFFFF
pair_base = pointer - index * 4
self.state.log(
self.name,
instruction_image_va=self.image_va,
source_value=_reg("r8") & 0xFFFFFFFF,
side=_reg("rdx") & 0xFF,
engine_base=_reg("rcx"),
pair_pointer=pointer,
pair_index=index,
pair_base=pair_base,
pair=_pair(pair_base),
registers=_registers(),
disassembly=gdb.execute("x/5i $pc", to_string=True),
backtrace=gdb.execute("bt 24", to_string=True),
)
except Exception as exc:
self.state.log(
"trace_error", where=self.name, error=str(exc),
traceback=traceback.format_exc()
)
return False
class Mode76BuilderBreakpoint(gdb.Breakpoint):
def __init__(self, state: State):
self.state = state
address = state.cards_base + (MODE76_BUILDER - CARDS_IMAGE_BASE)
super().__init__(f"*0x{address:x}", type=gdb.BP_HARDWARE_BREAKPOINT, internal=False)
self.silent = True
def stop(self):
try:
self.state.log(
"mode76_builder_entry",
instruction_image_va=MODE76_BUILDER,
object=_reg("rcx"),
registers=_registers(),
backtrace=gdb.execute("bt 24", to_string=True),
)
except Exception as exc:
self.state.log(
"trace_error", where="mode76_builder", 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, cards_base)
breakpoints = {}
for image_va, (name, pointer_reg, index_reg) in SITES.items():
bp = PairSubmitBreakpoint(_STATE, image_va, name, pointer_reg, index_reg)
breakpoints[name] = {"number": bp.number, "image_va": image_va}
builder = Mode76BuilderBreakpoint(_STATE)
breakpoints["mode76_builder"] = {"number": builder.number, "image_va": MODE76_BUILDER}
gdb.events.exited.connect(_on_exit)
_STATE.log(
"trace_armed",
breakpoints=breakpoints,
hardware_only=True,
client_memory_writes=False,
)