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:
funman300
2026-08-24 16:54:04 +00:00
parent 96610ccb16
commit 8983998707
+107
View File
@@ -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