"""D3 run 4: close the consumer set for the pack record. WHAT RUN 3 SETTLED Of everything in .text that touches +0x144..+0x154 as a memory displacement, only four functions belong to the 0x158-stride pack record: 0x1801342d0 ctor (zeroes them) 0x18013af30 deser (writes them) 0x180133210 uninitialised_copy (0x158) 0x1801340e0 copy-assign The rest were offset collisions on unrelated structs, proven by their stride or their size: 0x180133af0 iterates with stride 0x168, 0x180134b50 copies out to +0x163, 0x180173e00's object extends to +0x2f8 and sums 0x148+0x14c+0x150 as a win/draw/loss total. WHAT THIS RUN DOES 1. The 0x158 STRIDE CENSUS. Any loop over the pack vector must advance a pointer by 0x158 or multiply an index by it. Enumerate every instruction that uses 0x158 in pointer arithmetic (ADD/LEA/IMUL on a register), not as a stack frame size. Control: 0x180133210 and 0x18013af30 must both appear. 2. Locate the FutStoreGetPackTypesServerResponse vtable by searching .rdata for the deserializer pointer 0x1801234e0, dump it, and take xrefs to the vtable so the owner class and any accessor are visible. (The factory and the deser have zero direct callers, so they are dispatched through this vtable.) 3. Complete caller closure over the record's lifecycle functions: anything that can own a pack record must construct, copy or destroy one. 4. CreatePack side: numberItems store offset, and every reader of it, to answer whether the reveal is sized from a declared count or from the actual list. """ import traceback, sys, os OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres/" os.makedirs(OUT, exist_ok=True) def dump(tag, va, path, echo=True): src = dec(va) hdr = "%s %#x fname=%s len(src)=%d (FULL, NOT TRUNCATED)" % ( tag, va, fname(va), len(src)) if echo: print("=" * 78) print(hdr) print("=" * 78) print(src) with open(path, "w") as fh: fh.write("// " + hdr + "\n" + src) return src def insn_at(h): for back in range(0, 14): i2 = listing.getInstructionContaining(addr(h - back)) if i2 is not None: return i2 return None try: print("### 1. 0x158 STRIDE CENSUS (pointer arithmetic only, not frame sizes)") pat = bytes([0x58, 0x01, 0x00, 0x00]) seen = set() keep = [] for h in find_all(pat, blocks=(".text",)): ins = insn_at(h) if ins is None: continue a = int(ins.getAddress().getOffset()) if a in seen: continue seen.add(a) t = str(ins) if "0x158" not in t: continue mn = t.split()[0] if mn in ("SUB", "ADD") and t.split()[1].startswith("RSP"): continue # stack frame if mn in ("ADD", "LEA", "IMUL", "MOV", "CMP", "SHL"): keep.append((a, fname(a), t)) byfn = {} for a, fn, t in keep: byfn.setdefault(fn, []).append((a, t)) print(" %d instruction(s) in %d function(s)" % (len(keep), len(byfn))) for fn in sorted(byfn): print(" %s" % fn) for a, t in byfn[fn]: print(" %#x %s" % (a, t)) print(" CONTROL: 0x180133210 present=%s 0x18013af30 present=%s" % ("FUN_180133210" in byfn, "FUN_18013af30" in byfn)) print("\n### 2. STORE RESPONSE VTABLE") dp = (0x1801234e0).to_bytes(8, "little") for h in find_all(dp, blocks=(".rdata", ".data")): print(" deser pointer 0x1801234e0 found in .rdata/.data at %#x" % h) for base in (h - 8, h - 0x10, h): print(" candidate vtable base %#x:" % base) for off, tgt, nm in vtable(base, 14): print(" +%#04x %#018x %s" % (off, tgt, nm)) break for frm, typ, fn, ent in xrefs_to(h - 8): print(" xref to (vtbl base %#x): %#x %s %s" % (h - 8, frm, typ, fn)) for frm, typ, fn, ent in xrefs_to(h): print(" xref to (slot itself %#x): %#x %s %s" % (h, frm, typ, fn)) print("\n### 3. CALLER CLOSURE OVER PACK-RECORD LIFECYCLE") LIFE = {0x1801342d0: "record ctor", 0x1801340e0: "copy-assign", 0x180133210: "uninit_copy(0x158)", 0x180132180: "vector grow", 0x1801232a0: "record dtor", 0x18013af30: "element deser"} lvl1 = {} for va, tag in LIFE.items(): cs = callers(va) print(" %#x %-20s callers: %s" % (va, tag, [(hex(a), n) for a, n in cs])) for a, n in cs: lvl1.setdefault(a, set()).add(tag) print("\n level-2 (callers of those callers):") for a in sorted(lvl1): if a in LIFE: continue print(" %#x %-20s via %s ; its callers: %s" % (a, fname(a), sorted(lvl1[a]), [(hex(x), n) for x, n in callers(a)])) print("\n full decompiles of every non-lifecycle caller:") for a in sorted(lvl1): if a in LIFE: continue dump("LIFECYCLE CALLER", a, OUT + "d3_life_caller_%x.txt" % a, echo=True) print("\n### 4. CREATEPACK numberItems") src = dump("createpack deser", 0x180162880, OUT + "d3_createpack_deser.txt", echo=True) for va, tag in ((0x180162880, "createpack deser"),): pass dpc = (0x180162880).to_bytes(8, "little") for h in find_all(dpc, blocks=(".rdata", ".data")): print(" createpack deser pointer at %#x (vtable slot)" % h) for off, tgt, nm in vtable(h - 8, 12): print(" +%#04x %#018x %s" % (off, tgt, nm)) for frm, typ, fn, ent in xrefs_to(h - 8): print(" xref to vtbl base: %#x %s %s" % (frm, typ, fn)) except Exception: traceback.print_exc() sys.stdout.flush()