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.
This commit is contained in:
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hunt for specific wire instance ids anywhere in the client's writable memory.
|
||||
|
||||
Answers whether a served item was materialised into a record at all, versus
|
||||
materialised but not attached to a collection. A record is recognised by its
|
||||
established layout: id at +0x08, resourceId at +0x18, cardtype at +0x4c.
|
||||
|
||||
Read-only. Never writes.
|
||||
|
||||
usage: probe_hunt.py PID id [id ...]
|
||||
"""
|
||||
import re, struct, sys
|
||||
|
||||
PID = int(sys.argv[1])
|
||||
IDS = [int(a) for a in sys.argv[2:]]
|
||||
if not IDS:
|
||||
sys.exit("give at least one wire id")
|
||||
mem = open(f"/proc/{PID}/mem", "rb", buffering=0)
|
||||
|
||||
regions = []
|
||||
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 not m:
|
||||
continue
|
||||
lo, hi, perms, path = int(m.group(1), 16), int(m.group(2), 16), m.group(3), m.group(4).strip()
|
||||
if "w" not in perms:
|
||||
continue
|
||||
if path.startswith("/") and not path.endswith(".dll") and not path.endswith(".exe"):
|
||||
continue
|
||||
regions.append((lo, hi, perms, path))
|
||||
total = sum(hi - lo for lo, hi, _, _ in regions)
|
||||
print(f" {len(regions)} writable regions, {total/2**20:.0f} MiB to scan")
|
||||
|
||||
needles = {struct.pack("<I", i): i for i in IDS}
|
||||
hits = {i: [] for i in IDS}
|
||||
CHUNK = 8 << 20
|
||||
scanned = 0
|
||||
for lo, hi, perms, path in regions:
|
||||
a = lo
|
||||
while a < hi:
|
||||
n = min(CHUNK, hi - a)
|
||||
try:
|
||||
mem.seek(a)
|
||||
data = mem.read(n)
|
||||
except OSError:
|
||||
a += n
|
||||
continue
|
||||
if not data:
|
||||
a += n
|
||||
continue
|
||||
scanned += len(data)
|
||||
for nd, wid in needles.items():
|
||||
start = 0
|
||||
while True:
|
||||
j = data.find(nd, start)
|
||||
if j < 0:
|
||||
break
|
||||
start = j + 1
|
||||
va = a + j
|
||||
# a record would place this id at +0x08
|
||||
rec = va - 0x08
|
||||
try:
|
||||
mem.seek(rec)
|
||||
r = mem.read(0x100)
|
||||
except OSError:
|
||||
continue
|
||||
if len(r) < 0x100:
|
||||
continue
|
||||
ct = struct.unpack_from("<i", r, 0x4c)[0]
|
||||
res = struct.unpack_from("<I", r, 0x18)[0]
|
||||
sub = struct.unpack_from("<i", r, 0x50)[0]
|
||||
cat = struct.unpack_from("<i", r, 0x60)[0]
|
||||
looks = 0 <= ct <= 32 and res > 1000
|
||||
hits[wid].append((va, rec, ct, sub, cat, res, looks))
|
||||
a += n
|
||||
print(f" scanned {scanned/2**20:.0f} MiB\n")
|
||||
for wid in IDS:
|
||||
hs = hits[wid]
|
||||
recs = [h for h in hs if h[6]]
|
||||
print(f" id {wid}: {len(hs)} raw occurrence(s), {len(recs)} record-shaped")
|
||||
for va, rec, ct, sub, cat, res, _ in recs[:6]:
|
||||
print(f" record {rec:#x}: cardtype={ct} subtype={sub} category={cat} resourceId={res}")
|
||||
if not recs:
|
||||
print(" NOT MATERIALISED as a record anywhere in writable memory")
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Identify every resident record by its wire instance id.
|
||||
|
||||
Record layout established from known wire values:
|
||||
+0x08 id (wire instance) +0x18 resourceId +0x1c/+0x20 assetId
|
||||
+0x38 discardValue +0x4c cardtype +0x50 cardsubtypeid
|
||||
+0x5c itemState +0x60 category +0x94 teamid
|
||||
+0xb4 rating +0xba teamkittypetechid (u16)
|
||||
|
||||
Walks the contiguous 0x180-stride pool around the manager slot record so records
|
||||
that are resident but not in any collection are still seen. Read-only.
|
||||
|
||||
usage: probe_ids.py PID [expected_id ...]
|
||||
"""
|
||||
import re, struct, sys
|
||||
|
||||
PID = int(sys.argv[1])
|
||||
WANT = {int(a) for a in 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
|
||||
|
||||
def dec(rec):
|
||||
r = rd(rec, 0x180)
|
||||
g = lambda o: struct.unpack_from("<i", r, o)[0]
|
||||
return dict(id=struct.unpack_from("<I", r, 0x8)[0], res=struct.unpack_from("<I", r, 0x18)[0],
|
||||
ct=g(0x4c), sub=g(0x50), st=g(0x5c), cat=g(0x60), team=g(0x94),
|
||||
rating=struct.unpack_from("<I", r, 0xb4)[0],
|
||||
kt=struct.unpack_from("<H", r, 0xba)[0])
|
||||
|
||||
mgr_rec = q(mgr + 0xc0 + 0x10)
|
||||
print(f" manager-slot record = {mgr_rec:#x}")
|
||||
anchor = mgr_rec if mgr_rec else q(q(mgr + 0xd8) + 0x10)
|
||||
|
||||
# walk backwards to the start of the contiguous run, then forwards
|
||||
lo = anchor
|
||||
for _ in range(64):
|
||||
prev = lo - RECSZ
|
||||
try:
|
||||
d = dec(prev)
|
||||
except OSError:
|
||||
break
|
||||
if not (0 < d["ct"] < 64) or d["id"] == 0:
|
||||
break
|
||||
lo = prev
|
||||
|
||||
print(f" pool run starts at {lo:#x}\n")
|
||||
print(f" {'idx':>3} {'addr':>12} {'id':>10} {'resource':>9} {'ct':>3} {'sub':>4} "
|
||||
f"{'st':>3} {'cat':>4} {'team':>5} {'rate':>5} {'kt':>6}")
|
||||
found = {}
|
||||
k = 0
|
||||
addr = lo
|
||||
while k < 48:
|
||||
try:
|
||||
d = dec(addr)
|
||||
except OSError:
|
||||
break
|
||||
if d["id"] == 0 and d["ct"] == 0:
|
||||
break
|
||||
tag = ""
|
||||
if d["ct"] == 7:
|
||||
tag = " <== CARDTYPE 7"
|
||||
if d["id"] in WANT:
|
||||
tag += " <== WANTED"
|
||||
found[d["id"]] = addr
|
||||
slot = " [manager slot]" if addr == mgr_rec else ""
|
||||
print(f" {k:>3} {addr:#12x} {d['id']:>10} {d['res']:>9} {d['ct']:>3} {d['sub']:>4} "
|
||||
f"{d['st']:>3} {d['cat']:>4} {d['team']:>5} {d['rating']:>5} {d['kt']:>6}{tag}{slot}")
|
||||
addr += RECSZ
|
||||
k += 1
|
||||
|
||||
if WANT:
|
||||
print(f"\n wanted ids: {sorted(WANT)}")
|
||||
for w in sorted(WANT):
|
||||
print(f" {w}: {'FOUND at ' + hex(found[w]) if w in found else 'NOT RESIDENT'}")
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/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]}")
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Enumerate FIFA 17's resident item map authoritatively.
|
||||
|
||||
Layout recovered from the lower_bound at 0x180119640:
|
||||
owner+0x160c8 sentinel / end marker
|
||||
owner+0x160d8 root
|
||||
owner+0x160e8 count
|
||||
node+0x00, node+0x08 children
|
||||
node+0x20 key = wire instance id (qword)
|
||||
node+0x28 the item record
|
||||
On miss the client returns the static sentinel 0x1802c2a28 whose +0x10 is NULL.
|
||||
|
||||
Read-only. usage: probe_map2.py PID [id ...]
|
||||
"""
|
||||
import re, struct, sys, collections
|
||||
|
||||
PID = int(sys.argv[1]); WANT = {int(a) for a in 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"))
|
||||
if rd(base + (0x180026fea - 0x180000000), 5) != bytes.fromhex("ba75750000"):
|
||||
sys.exit("SANITY FAILED")
|
||||
owner = q(base + (0x1802e6398 - 0x180000000))
|
||||
SENT, ROOT, COUNT = owner + 0x160c8, q(owner + 0x160d8), q(owner + 0x160e8) & 0xffffffff
|
||||
print(f" owner={owner:#x} sentinel={SENT:#x} root={ROOT:#x} count={COUNT}")
|
||||
|
||||
nodes, seen, stack = [], set(), [ROOT]
|
||||
while stack:
|
||||
n = stack.pop()
|
||||
if not n or n == SENT or n in seen or len(seen) > 5000:
|
||||
continue
|
||||
seen.add(n)
|
||||
try:
|
||||
h = rd(n, 0x30)
|
||||
except OSError:
|
||||
continue
|
||||
if len(h) < 0x30:
|
||||
continue
|
||||
nodes.append(n)
|
||||
stack.append(struct.unpack_from("<Q", h, 0)[0])
|
||||
stack.append(struct.unpack_from("<Q", h, 8)[0])
|
||||
print(f" nodes reached: {len(nodes)} (count field says {COUNT})\n")
|
||||
|
||||
print(f" {'key':>11} {'record':>12} {'id':>10} {'resource':>10} {'ct':>3} {'sub':>4} {'st':>4} {'cat':>4}")
|
||||
hist = collections.Counter(); found = {}
|
||||
rows = []
|
||||
for n in nodes:
|
||||
key = q(n + 0x20)
|
||||
rec = n + 0x28
|
||||
try: r = rd(rec, 0x180)
|
||||
except OSError: continue
|
||||
if len(r) < 0x180: continue
|
||||
g = lambda o: struct.unpack_from("<i", r, o)[0]
|
||||
rid = struct.unpack_from("<I", r, 0x8)[0]
|
||||
res = struct.unpack_from("<I", r, 0x18)[0]
|
||||
ct, sub, st, cat = g(0x4c), g(0x50), g(0x5c), g(0x60)
|
||||
hist[ct] += 1
|
||||
if rid in WANT: found[rid] = rec
|
||||
rows.append((key, rec, rid, res, ct, sub, st, cat))
|
||||
for key, rec, rid, res, ct, sub, st, cat in sorted(rows):
|
||||
tag = " <== CARDTYPE 7" if ct == 7 else (" <== WANTED" if rid in WANT else "")
|
||||
print(f" {key:>11} {rec:#12x} {rid:>10} {res:>10} {ct:>3} {sub:>4} {st:>4} {cat:>4}{tag}")
|
||||
print(f"\n cardtype histogram: {dict(sorted(hist.items()))} total={sum(hist.values())}")
|
||||
for w in sorted(WANT):
|
||||
print(f" id {w}: {'RESIDENT' if w in found else 'ABSENT'}")
|
||||
Reference in New Issue
Block a user