tools(re): dump resident record fields to settle the kit category gate
The kit clone driver FUN_1801c3480 gates on record+0x60 == 4, and no instruction stores that immediate, so the value had to be read off a genuinely resident record. This dumps cardtype, cardsubtypeid, itemState, category, teamid and teamkittypetechid for both the player vector and the club-item vector, resolving the store through the same chain as the census. Result: all 18 resident players carry category 1 and the five club-item slots are null, which identifies +0x60 as a per-collection tag rather than item data.
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only dump of RESIDENT record fields, for both the player and club-item vectors.
|
||||
|
||||
Purpose: the kit clone driver FUN_1801c3480 gates on record+0x60 (category) == 4.
|
||||
No instruction in CardsDLL writes immediate 4 there, so this reads what value a
|
||||
genuinely resident record actually carries. Read-only. Never writes.
|
||||
|
||||
mgr+0x0c0 cardtype-2 single slot
|
||||
mgr+0x0d8..0x0e0 cardtype-1 (player) vector
|
||||
mgr+0x108..0x110 club-item vector
|
||||
record+0x4c cardtype +0x50 cardsubtypeid +0x5c itemState
|
||||
record+0x60 category +0x94 teamid +0xba teamkittypetechid (u16)
|
||||
"""
|
||||
import re, struct, sys, collections
|
||||
|
||||
PID = int(sys.argv[1])
|
||||
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]
|
||||
|
||||
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")), None)
|
||||
if base is None:
|
||||
sys.exit("CardsDLL mapping not found")
|
||||
def live(static):
|
||||
return base + (static - 0x180000000)
|
||||
|
||||
if rd(live(0x180026fea), 5) != bytes.fromhex("ba75750000"):
|
||||
sys.exit("SANITY FAILED - wrong base")
|
||||
print(f" CardsDLL base = {base:#x} (sanity ok)")
|
||||
|
||||
owner = q(live(0x1802e6398))
|
||||
b = rd(q(owner) + 0x4e8, 12)
|
||||
b = rd(struct.unpack("<Q", struct.pack("<Q", q(q(owner) + 0x4e8)))[0], 12)
|
||||
getter = q(q(owner) + 0x4e8)
|
||||
gb = rd(getter, 12)
|
||||
if gb[0:3] == bytes.fromhex("488d81"):
|
||||
mgr = owner + struct.unpack_from("<I", gb, 3)[0]
|
||||
elif gb[0:3] == bytes.fromhex("488d41"):
|
||||
mgr = owner + gb[3]
|
||||
else:
|
||||
sys.exit(f"unexpected getter shape {gb.hex(' ')}")
|
||||
print(f" owner = {owner:#x} mgr = {mgr:#x}")
|
||||
|
||||
FIELDS = ("ctype", "subtype", "state", "cat", "team", "kittype")
|
||||
def decode(rec):
|
||||
r = rd(rec, 0xC0)
|
||||
if len(r) < 0xC0:
|
||||
return None
|
||||
return (i32(r, 0x4c), i32(r, 0x50), i32(r, 0x5c), i32(r, 0x60),
|
||||
i32(r, 0x94), struct.unpack_from("<H", r, 0xba)[0])
|
||||
|
||||
for label, vbeg, vend in (("players (cardtype 1)", mgr + 0xd8, mgr + 0xe0),
|
||||
("club items", mgr + 0x108, mgr + 0x110)):
|
||||
try:
|
||||
beg, end = q(vbeg), q(vend)
|
||||
except OSError:
|
||||
print(f"\n {label}: vector unreadable")
|
||||
continue
|
||||
span = end - beg
|
||||
print(f"\n === {label}: {beg:#x}..{end:#x} span={span} ===")
|
||||
if not (0 < beg <= end) or span > 24 * 100000:
|
||||
print(" implausible vector, skipping")
|
||||
continue
|
||||
# resolve stride: the element must contain a plausible heap pointer
|
||||
for stride, ptr_off in ((24, 0x10), (16, 0x08), (8, 0x00)):
|
||||
if span % stride:
|
||||
continue
|
||||
n = span // stride
|
||||
recs, nulls = [], 0
|
||||
ok = True
|
||||
for k in range(n):
|
||||
try:
|
||||
rec = q(beg + k * stride + ptr_off)
|
||||
except OSError:
|
||||
ok = False; break
|
||||
if not rec:
|
||||
nulls += 1; continue
|
||||
d = decode(rec)
|
||||
if d is None:
|
||||
ok = False; break
|
||||
recs.append((rec, d))
|
||||
if not ok:
|
||||
continue
|
||||
print(f" stride {stride} (ptr at +{ptr_off:#x}): {n} slots, {len(recs)} populated, {nulls} null")
|
||||
if not recs and nulls != n:
|
||||
continue
|
||||
hist = collections.Counter(d[0:2] for _, d in recs)
|
||||
for key, c in sorted(hist.items(), key=lambda x: -x[1]):
|
||||
print(f" (cardtype,subtype)={key} x{c}")
|
||||
print(f" {'ptr':>14} " + " ".join(f"{f:>8}" for f in FIELDS))
|
||||
for rec, d in recs[:8]:
|
||||
print(f" {rec:#14x} " + " ".join(f"{v:>8}" for v in d))
|
||||
cats = collections.Counter(d[3] for _, d in recs)
|
||||
if cats:
|
||||
print(f" CATEGORY (+0x60) distribution: {dict(cats)}")
|
||||
break
|
||||
Reference in New Issue
Block a user