"""D2 QUICK SELL, batch 9: the wallet. Q2 assign-vs-add, final attempt. PLAN. Find the credit-carrying response classes and their deserialisers: RS4:FutUserCreditsServerResponse @ 0x18021dc18 RS4:FutUpdateCreditsServerResponse @ 0x18022cc10 RS4:FutDiscardCardServerResponse @ 0x180220540 (vtable 0x180220488, deser 0x180127300) Resolve each by find_all(b"RS4:"+name) then xrefs_to(hit-4) -> factory -> the .rdata vtable it installs -> slot +0x08. Decompile all three deserialisers and every virtual they invoke on the FUT manager singleton, so the wallet field and its writers are visible. Then enumerate every writer of that field. CONTROL: the discard chain must resolve to 0x180127300, which is already known independently (qword 0x180127300 sits at 0x180220490 = vtable+0x08). If the same mechanism yields a plausible deser for the two credits classes, it is working. """ import traceback, os, struct OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/store/qs/" def dump(tag, va, path=None): try: src = dec(va) except Exception as e: src = "// threw %r" % (e,) print("=" * 78) print("%s %#x fname=%s len(src)=%d (FULL)" % (tag, va, fname(va), len(src))) print("=" * 78) print(src) if path: open(OUT + path, "w").write(src) return src def resolve(name): out = [] for h in find_all(b"RS4:" + name.encode() + b"\x00"): for frm, typ, fn, ent in xrefs_to(h - 4): if ent: out.append((h, frm, ent, fn)) return out try: for nm in ("FutDiscardCardServerResponse", "FutUserCreditsServerResponse", "FutUpdateCreditsServerResponse"): print("##### %s #####" % nm) for h, frm, ent, fn in resolve(nm): print(" literal %#x factory %#x %s (ref at %#x)" % (h, ent, fn, frm)) src = dump("factory", ent, "qs_r_fac_%x.txt" % ent) # find the PTR_FUN_ vtable it installs import re for m in re.finditer(r"PTR_FUN_([0-9a-f]+)", src): vt = int(m.group(1), 16) print(" vtable %#x, slot+0x08 = %#x %s" % (vt, qword(vt + 8), fname(qword(vt + 8)))) dump("deser", qword(vt + 8), "qs_r_deser_%x.txt" % qword(vt + 8)) for off, tgt, fnm in vtable(vt, 24): print(" +%#05x %#x %s" % (off, tgt, fnm)) print() print("##### the FUT manager slots used by the discard deser #####") # the deser calls (**(code**)(*plVar4 + 0xa30))(plVar4, id) after parsing an id # find every function that calls a virtual at +0xa30 / +0xa08 / +0xa48 and the # ones that call slots near them, so a credit setter can be spotted by name. it = fm.getFunctions(True) want = ("0xa08", "0xa30", "0xa48", "0x9f8", "0xa00", "0xa10", "0xa18", "0xa20", "0xa28", "0xa38", "0xa40", "0xa50", "0xa58", "0xa60") tally = {} while it.hasNext(): f = it.next() ii = listing.getInstructions(f.getBody(), True) got = [] while ii.hasNext(): i = ii.next() t = str(i) if t.startswith("CALL qword ptr [") and any(w in t for w in want): got.append((int(i.getAddress().getOffset()), t)) if got: tally[int(f.getEntryPoint().getOffset())] = (f.getName(), got) print(" %d functions call one of those slots" % len(tally)) for e in sorted(tally): nm, got = tally[e] print(" %#x %s" % (e, nm)) for a, t in got: print(" %#x %s" % (a, t)) except Exception: traceback.print_exc()