tools(re): census FIFA 17's resident club-item vector
Read-only /proc/PID/mem census of the client's resident club-item store, which
is what the pre-match kit selector actually consumes. No writes.
Resolves the whole chain from static RE rather than guessing offsets:
[CardsDLL+0x2e6398] -> owner object (FUN_18011a830 is a plain
global read)
owner->vtable[0x4e8] -> lea rax,[rcx+0x1f9d8]; ret, i.e. mgr is an
EMBEDDED subobject, not a pointer
mgr+0x108 .. mgr+0x110 -> club-item vector, stride 24
element+0x10 -> the item record (FUN_1800d73d0)
CardsDLL is located by its NEAREST PRECEDING NAMED mapping, because Wine maps PE
sections anonymously and the Wine heap is also rwx, so permissions do not
discriminate code from heap. The base is then sanity-checked against a known
immediate (mov edx,0x7575 at 0x180026fea) and the tool aborts rather than
reporting from a wrong base.
Field offsets are the ones already proven, and nothing else is interpreted:
+0x4c cardtype, +0x50 cardsubtypeid, +0x5c itemState, +0x60 category,
+0x94 teamid, +0xba teamkittypetechid (u16).
FIRST RESULT, live on the client parked at the pre-match kit selector:
players mgr+0x0d8: 23 slots, 18 non-null, all (cardtype 1, subtype 0)
club items mgr+0x108: 5 slots, 0 non-null
Five slots, every item pointer NULL. Five is the club's active club-item set --
home kit, away kit, badge, stadium, ball -- so the client knows it should hold
five and holds none. That is why FUN_1800d73d0 returns the 0x1802c2a28 sentinel
whose +0x10 is NULL, and why the tiles are untextured.
This commit is contained in:
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only census of FIFA 17's RESIDENT club-item vector.
|
||||
|
||||
Chain, every link from CardsDLL static RE:
|
||||
[CardsDLL+0x2e6398] -> owner object (FUN_18011a830)
|
||||
owner->vtable[0x4e8] -> getter returning mgr (call *0x4e8(%rdx))
|
||||
mgr+0x108 .. mgr+0x110 -> club-item vector, stride 24
|
||||
element+0x10 -> the item record pointer (FUN_1800d73d0)
|
||||
record+0x4c cardtype (derived from cardsubtypeid by FUN_1800d8330: 9/10/11 -> 7)
|
||||
record+0x50 cardsubtypeid
|
||||
record+0x5c itemState (101 activeHomeKit, 102 activeAwayKit)
|
||||
record+0x60 category (clone driver FUN_1801c3480 requires 4)
|
||||
record+0x94 teamid
|
||||
record+0xba teamkittypetechid (u16)
|
||||
Offsets not in that list are labelled UNVERIFIED and only dumped raw.
|
||||
No writes. Ever.
|
||||
"""
|
||||
import re, struct, sys, collections
|
||||
|
||||
PID = int(sys.argv[1]) if len(sys.argv) > 1 else 44405
|
||||
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]
|
||||
def i32(b, o):
|
||||
return struct.unpack_from("<i", b, o)[0]
|
||||
|
||||
# locate CardsDLL by its NEAREST PRECEDING NAMED mapping (Wine maps PE sections anon)
|
||||
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 = None
|
||||
for s, p in named:
|
||||
if p.endswith("CardsDLL_Win64_retail.dll"):
|
||||
base = s; break
|
||||
if base is None:
|
||||
print(" CardsDLL mapping not found"); sys.exit(1)
|
||||
print(f" CardsDLL base = {base:#x}")
|
||||
def live(static): return base + (static - 0x180000000)
|
||||
|
||||
# sanity: the 0x7575 sender immediate must be where static RE says
|
||||
probe = rd(live(0x180026fea), 6)
|
||||
print(f" sanity @0x180026fea: {probe.hex(' ')} (expect ba 75 75 00 00)")
|
||||
if probe[:5] != bytes.fromhex("ba75750000"):
|
||||
print(" SANITY FAILED - base wrong, aborting"); sys.exit(1)
|
||||
|
||||
owner = q(live(0x1802e6398))
|
||||
print(f" owner object = {owner:#x}")
|
||||
vt = q(owner)
|
||||
getter = q(vt + 0x4e8)
|
||||
print(f" vtable = {vt:#x}")
|
||||
print(f" vtable[0x4e8] = {getter:#x} bytes: {rd(getter,12).hex(' ')}")
|
||||
# expect: mov rax,[rcx+off] ; ret -> 48 8b 81 off32 c3 or 48 8b 41 off8 c3
|
||||
b = rd(getter, 12)
|
||||
mgr = None
|
||||
if b[0:3] == bytes.fromhex("488d81"):
|
||||
off = struct.unpack_from("<I", b, 3)[0]; mgr = owner + off
|
||||
print(f" getter returns owner+{off:#x} (EMBEDDED subobject) -> mgr = {mgr:#x}")
|
||||
elif b[0:3] == bytes.fromhex("488d41"):
|
||||
off = b[3]; mgr = owner + off
|
||||
print(f" getter returns owner+{off:#x} (EMBEDDED subobject) -> mgr = {mgr:#x}")
|
||||
elif b[0:3] == bytes.fromhex("488b81"):
|
||||
off = struct.unpack_from("<I", b, 3)[0]; mgr = q(owner + off)
|
||||
print(f" getter returns [owner+{off:#x}] -> mgr = {mgr:#x}")
|
||||
elif b[0:3] == bytes.fromhex("488b41"):
|
||||
off = b[3]; mgr = q(owner + off)
|
||||
print(f" getter returns [owner+{off:#x}] -> mgr = {mgr:#x}")
|
||||
elif b[0:2] == bytes.fromhex("488b") and b[2] == 0xc1:
|
||||
mgr = owner; print(" getter returns owner itself")
|
||||
else:
|
||||
print(" getter shape unrecognised; trying owner as mgr")
|
||||
mgr = owner
|
||||
|
||||
for label, mgr_try in (("resolved", mgr), ("owner", owner)):
|
||||
try:
|
||||
beg, end = q(mgr_try + 0x108), q(mgr_try + 0x110)
|
||||
except OSError:
|
||||
print(f" [{label}] +0x108/0x110 unreadable"); continue
|
||||
if not (0 < beg <= end) or (end - beg) % 24 or (end - beg) > 24*100000:
|
||||
print(f" [{label}] vector implausible: {beg:#x}..{end:#x}")
|
||||
continue
|
||||
n = (end - beg) // 24
|
||||
print(f"\n === club-item vector via {label}: {beg:#x}..{end:#x} {n} slot(s) ===")
|
||||
hist = collections.Counter(); rows = []
|
||||
for k in range(n):
|
||||
try:
|
||||
rec = q(beg + k*24 + 0x10)
|
||||
except OSError:
|
||||
continue
|
||||
if not rec:
|
||||
hist[("<null slot>", None)] += 1; continue
|
||||
try:
|
||||
r = rd(rec, 0xC0)
|
||||
except OSError:
|
||||
continue
|
||||
if len(r) < 0xC0: continue
|
||||
ct, sub, st, cat = i32(r,0x4c), i32(r,0x50), i32(r,0x5c), i32(r,0x60)
|
||||
team = i32(r,0x94); kt = struct.unpack_from("<H", r, 0xba)[0]
|
||||
hist[(ct, sub)] += 1
|
||||
rows.append((rec, ct, sub, st, cat, team, kt))
|
||||
print(f" (cardtype, cardsubtypeid) histogram:")
|
||||
for key, c in sorted(hist.items(), key=lambda x: -x[1]):
|
||||
tag = " <== KIT (selector needs this)" if key == (7, 9) else ""
|
||||
print(f" {str(key):<18} x{c}{tag}")
|
||||
print(f" cardtype 7 records: {sum(c for (ct,_),c in hist.items() if ct==7)}")
|
||||
print(f"\n first 12 records:")
|
||||
print(f" {'ptr':>14} {'ctype':>5} {'subtype':>7} {'state':>5} {'cat':>4} {'team':>5} {'kittype':>7}")
|
||||
for rec, ct, sub, st, cat, team, kt in rows[:12]:
|
||||
print(f" {rec:#14x} {ct:>5} {sub:>7} {st:>5} {cat:>4} {team:>5} {kt:>7}")
|
||||
break
|
||||
Reference in New Issue
Block a user