# -*- coding: utf-8 -*- """ q_pack_inv_8 -- DIMENSION 1 pass 8: the model vtable slot 0x4e0, and the STORE request builder that would have to return a My Packs group. HYPOTHESES (a) Pass 7's vtable scan was too strict (it demanded every qword be inside .text, so any vtable containing a NULL or a non-.text thunk was rejected; it found only 2 candidates, neither plausible). Relaxing to "pointer into .text OR zero" should surface the FUT model vtable, whose slot +0x4e0 is the unopenedPacks-total setter reached from FUN_18013ec10 @0x18013f223. (b) FUN_180123430 appends "/purchasegroup" + "/all" + "?ppInfo=true"; its caller is the STORE request builder and shows the exact URL and HTTP verb. CONTROLS * The relaxed vtable scan is controlled by requiring that the reported vtable's slot +0x160 and +0x940 also resolve to real functions (both are used on the same singleton by FUN_18013bd40 and FUN_18017fc20 respectively). A table that satisfies all three slots is the right object. * class_deser("FutSquadSaveServerResponse") must still be 0x180171a60. """ import struct import traceback OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres" LO, HI = 0x180001000, 0x1801E4F62 try: got = sorted(set(x[0] for x in class_deser("FutSquadSaveServerResponse"))) print("CONTROL FutSquadSaveServerResponse -> %s %s" % ([hex(g) for g in got], "PASS" if 0x180171A60 in got else "FAIL")) blk = [b for b in mem.getBlocks() if b.getName() == ".rdata"][0] start = int(blk.getStart().getOffset()) end = int(blk.getEnd().getOffset()) data = read_bytes(start, end - start + 1) n = len(data) // 8 qs = struct.unpack_from("<%dQ" % n, data, 0) def okslot(v): return v == 0 or (LO <= v <= HI) cands = [] i = 0 while i < n: if LO <= qs[i] <= HI: j = i while j < n and okslot(qs[j]): j += 1 if (j - i) * 8 >= 0x950: cands.append((start + i * 8, (j - i) * 8)) i = j else: i += 1 print("relaxed vtable candidates >= 0x950 bytes: %d" % len(cands)) for va, sz in cands: slots = {} good = True for off in (0x8, 0x160, 0x1F8, 0x480, 0x4E0, 0x940): if off >= sz: good = False break t = qword(va + off) f = fm.getFunctionAt(addr(t)) slots[off] = (t, f.getName() if f else None) if f is None: good = False print("\n vtable %#x size %#x allslots=%s" % (va, sz, good)) for off in sorted(slots): print(" +%#05x -> %#x %s" % (off, slots[off][0], slots[off][1])) if good: for off in (0x4E0, 0x940, 0x160): t = slots[off][0] s = dec(t) print("\n ==== slot +%#x %#x len=%d\n%s" % (off, t, len(s), s)) open(OUT + "/d1_vtslot_%x_%x.txt" % (off, t), "w").write(s) print("\n########## STORE REQUEST BUILDER ##########") for (fr, ty, fn, en) in xrefs_to(0x180123430): print(" ref to FUN_180123430: %#x %s @%#x" % (fr, fn, en)) if en: s = dec(en) print(" --- caller %#x len=%d\n%s" % (en, len(s), s)) open(OUT + "/d1_storebuilder_%x.txt" % en, "w").write(s) print("\n########## starting_pack_opened ##########") for va, tag in ((0x180083990, "startingpack_990"), (0x180083B70, "startingpack_b70")): s = dec(va) print("\n--- %s %#x len=%d" % (tag, va, len(s))) print(s) open(OUT + "/d1_%s.txt" % tag, "w").write(s) print("\nDONE q_pack_inv_8") except Exception: traceback.print_exc()