Files
OpenFUT/fifa17-recon/tools/live/probe_layout2.py
T
funman300 2fc335c37d tools(re): interpret FIFA17 atom mappers and enumerate the resident item map
Two tools, both gated on positive controls because the previous pass
produced a confidently wrong negative result.

atom_mapper_emu.py interprets the atom -> field-id dispatch functions
instead of pattern-scanning them. Its selftest encodes the two decoder
traps that caused earlier mistakes - ModRM rm=5 with mod!=0 is [rbp+disp]
rather than RIP-relative, and a constant may reach its use through a
register - plus the live-verified controls that the item mapper maps atom
568 'players' to field id 1 and atom 11 'actives' to 0. The mandatory
manager atom 424 control still fails as a coverage limit: only 2 of the 52
resolver callers are pure dispatch chains, so the tool refuses to support
any absence claim about the squad mapper.

The live probes enumerate the resident item map at owner+0x160c8, whose
layout came from the lower_bound at 0x180119640: key = wire instance id at
node+0x20, record at node+0x28, count at owner+0x160e8. probe_map2 reaches
22 nodes against a count field of 22, so the enumeration validates itself,
and probe_hunt searches all writable memory with its own in-run positive
control. Result: all four club staff are resident, both kit ids are absent
everywhere, and a lookup miss returns the static sentinel 0x1802c2a28
whose +0x10 is NULL - which is exactly the KIT_SCAN symptom.
2026-08-24 17:24:57 +00:00

96 lines
3.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Map FIFA 17 resident record offsets using UNIQUE wire values as ground truth.
v2: identifies each record by its wire instance id (large, unique) and only
accepts a field mapping when the value is distinctive (>= 16) and the same
offset holds the right value for EVERY identified record. This avoids the v1
failure where cardsubtypeid == 0 matched every zeroed field in the struct.
Read-only. Never writes.
usage: probe_layout2.py PID squad_active.json
"""
import re, struct, sys, json, collections
PID = int(sys.argv[1])
SQUAD = json.load(open(sys.argv[2]))
mem = open(f"/proc/{PID}/mem", "rb", buffering=0)
def rd(a, n):
mem.seek(a); return mem.read(n)
def q(a):
return struct.unpack("<Q", rd(a, 8))[0]
named = []
for ln in open(f"/proc/{PID}/maps"):
m = re.match(r"([0-9a-f]+)-([0-9a-f]+) \S{4} \S+ \S+ \S+\s+(.+)", ln)
if m:
named.append((int(m.group(1), 16), m.group(3).strip()))
named.sort()
base = next(s for s, p in named if p.endswith("CardsDLL_Win64_retail.dll"))
live = lambda s: base + (s - 0x180000000)
if rd(live(0x180026fea), 5) != bytes.fromhex("ba75750000"):
sys.exit("SANITY FAILED")
owner = q(live(0x1802e6398))
mgr = owner + 0x1f9d8
RECSZ = 0x180
beg, end = q(mgr + 0xd8), q(mgr + 0xe0)
recs = [r for r in (q(beg + k*24 + 0x10) for k in range((end - beg)//24)) if r]
wire = {}
for p in SQUAD["players"]:
it = p.get("itemData") or {}
if it.get("id"):
wire[it["id"]] = it
# --- identify each record by its wire instance id ---
ident = {}
for rec in recs:
r = rd(rec, RECSZ)
for off in range(0, RECSZ - 4, 4):
v = struct.unpack_from("<I", r, off)[0]
if v in wire:
ident.setdefault(rec, (v, off))
break
print(f" resident player records: {len(recs)}, identified: {len(ident)}")
id_offs = collections.Counter(o for _, o in ident.values())
print(f" wire-id offset candidates: {[(hex(o), c) for o, c in id_offs.most_common()]}")
FIELDS = ("id", "resourceId", "assetId", "definitionId", "cardassetid", "rating",
"teamid", "nation", "leagueId", "contract", "fitness", "playStyle",
"discardValue", "cardsubtypeid", "owners", "rareflag")
# --- for every offset, does it hold field F for every identified record? ---
consistent = {}
for off in range(0, RECSZ - 4, 4):
for f in FIELDS:
ok = 0; total = 0; distinct = set()
for rec, (wid, _) in ident.items():
it = wire[wid]
v = it.get(f)
if not isinstance(v, int) or v < 16: # require distinctive values
continue
total += 1
got = struct.unpack_from("<I", rd(rec, RECSZ), off)[0]
if got == v:
ok += 1; distinct.add(v)
if total >= 5 and ok == total and len(distinct) >= 2:
consistent.setdefault(off, []).append((f, total, len(distinct)))
print(f"\n === offsets consistently holding a distinctive wire field ===")
for off in sorted(consistent):
for f, total, nd in consistent[off]:
print(f" +0x{off:<4x} {f:14s} (matched {total}/{total} records, {nd} distinct values)")
# --- dump the manager and the three club staff for comparison ---
print(f"\n === cardtype-2 slot (manager) ===")
h = q(mgr + 0xc0 + 0x10)
if h:
r = rd(h, RECSZ)
for off in sorted(consistent):
f = consistent[off][0][0]
print(f" +0x{off:<4x} {f:14s} = {struct.unpack_from('<I', r, off)[0]}")
for name, off, sz in (("cardtype", 0x4c, 4), ("cardsubtypeid", 0x50, 4),
("itemState", 0x5c, 4), ("category", 0x60, 4)):
print(f" +0x{off:<4x} {name:14s} = {struct.unpack_from('<i', r, off)[0]}")