fifa17-recon: FUT_ID_SWEEP -- use the running game as the player-DB oracle
dbdata.dll is an anti-tamper decoy (getTableData returns a self-integrity blob), so the players table only exists inside the running client. But we do not need to unpack it: the client merges its own DB into every item we serve, keyed on resourceId & 0xffffff, and leaves the result in a map we can already read. So serve a RANGE of candidate playerids as a synthetic club, then read the map back with card_identity_probe.py. One club fetch classifies the whole window. Sends teamid/nation/leagueId as ZERO so the client fills the REAL values (the merge only fills zeros -- confirmed live: one playerid appears twice with two different nations, both ours). Sentinel rating 7, deliberately not 50, so the miss-fill (rating 0x32) can never be mistaken for a surviving sentinel. The window comes from a control FILE read per request, not just the env: a full sweep is many windows and restarting mid-session is what produced 'error connecting to FIFA 17 Ultimate Team' once already. Nothing is written to the save, so clearing the file restores the real club on the next fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
@@ -1250,12 +1250,81 @@ def club_stats_route(h):
|
||||
CLUB_PAGE = os.environ.get("FUT_CLUB_PAGE") == "1"
|
||||
|
||||
|
||||
# ---- FUT_ID_SWEEP: use the game itself as the player-database oracle --------
|
||||
#
|
||||
# Card identity is never taken from the wire. Every item we serve is registered
|
||||
# into the client's CardsDb map, and just before that the client merges in its OWN
|
||||
# local `players` table keyed on `resourceId & 0xffffff`. So serving a RANGE of
|
||||
# candidate playerids and then reading the map back (tools/card_identity_probe.py)
|
||||
# classifies the whole range in one pass. That is the entire database-extraction
|
||||
# problem solved from the outside, without unpacking anything: dbdata.dll turned
|
||||
# out to be an anti-tamper decoy, and the real table only exists inside the running
|
||||
# game.
|
||||
#
|
||||
# Three outcomes per id, all distinguishable in the record (proven live 2026-08-04):
|
||||
# HIT real name at +0xb8/+0xc8; teamid/nation/leagueId filled from the
|
||||
# DB because we send them as ZERO (the merge only fills zeros).
|
||||
# NAMELESS ROW our sentinel rating survives but the name is the DB's default
|
||||
# row, "Jamal Blackman". The row exists and is empty.
|
||||
# TRUE MISS the merge's miss fill: rating 0x32, teamid 0x78d, nation 0xe.
|
||||
# SENTINEL_RATING is deliberately NOT 50, so a miss can never be mistaken for it.
|
||||
#
|
||||
# The sweep is SYNTHETIC: nothing is written to the save, so clearing the flag
|
||||
# restores the real club exactly. Format: FUT_ID_SWEEP="<lo>-<hi>".
|
||||
_ID_SWEEP = os.environ.get("FUT_ID_SWEEP", "")
|
||||
SWEEP_FILE = os.environ.get("FUT_ID_SWEEP_FILE", "/tmp/fut_id_sweep")
|
||||
SWEEP_ID_BASE = 900000000 # item ids, distinct from the save's 100000000+
|
||||
SWEEP_SENTINEL_RATING = 7
|
||||
|
||||
|
||||
def sweep_window():
|
||||
"""The active sweep window: the control FILE if it has content, else the env.
|
||||
|
||||
Read per REQUEST, on purpose. A full database sweep is many windows, and
|
||||
restarting this server to re-aim it is exactly the action that once produced
|
||||
"error connecting to FIFA 17 Ultimate Team" mid-session -- a plain connection
|
||||
refusal, misread for weeks as a protocol bug. Writing a range into the file
|
||||
re-aims the sweep with the client still live; emptying the file restores the
|
||||
real club on the very next fetch.
|
||||
"""
|
||||
try:
|
||||
with open(SWEEP_FILE) as f:
|
||||
s = f.read().strip()
|
||||
if s:
|
||||
return s
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return _ID_SWEEP
|
||||
|
||||
|
||||
def sweep_items():
|
||||
"""Synthetic club contents for one id-sweep window."""
|
||||
win = sweep_window()
|
||||
try:
|
||||
lo, hi = (int(x, 0) for x in win.split("-", 1))
|
||||
except Exception:
|
||||
log(" SWEEP: bad window %r, want '<lo>-<hi>'" % win)
|
||||
return []
|
||||
if hi < lo:
|
||||
lo, hi = hi, lo
|
||||
out = []
|
||||
for i, pid in enumerate(range(lo, hi + 1)):
|
||||
it = _item(SWEEP_ID_BASE + i, pid, SWEEP_SENTINEL_RATING,
|
||||
"ST", 0, 0, 0, [1, 1, 1, 1, 1, 1])
|
||||
out.append(it)
|
||||
log(" SWEEP: serving %d candidate playerid(s) %d..%d as the club "
|
||||
"[synthetic, nothing saved]" % (len(out), lo, hi))
|
||||
return out
|
||||
|
||||
|
||||
def club_route(h):
|
||||
# PUT only -- ENDPOINT_MAP row 3 gives ChangeClubName as PUT. Every other
|
||||
# method keeps the exact body this route served before, so the rename support
|
||||
# cannot change the behaviour of anything that already worked.
|
||||
if h.command == "PUT":
|
||||
return club_rename_route(h)
|
||||
if sweep_window():
|
||||
return 200, {"itemData": sweep_items()}
|
||||
items = STORE.items()
|
||||
if CLUB_PAGE:
|
||||
log(" CLUB: returning %d item(s) [FUT_CLUB_PAGE experiment -- compare this "
|
||||
|
||||
Reference in New Issue
Block a user