"""D3 run 3: TIGHT reader scan + who consumes the store response object. Run 2's disp32 byte scan was correct but too permissive: it accepted any operand whose scalar equalled the offset, so `SUB RSP,0x150` counted. This run requires the offset to appear as a MEMORY-OPERAND DISPLACEMENT (the instruction text must contain "+ 0xNNN]") which is the only form a struct field access can take. ESTABLISHED SO FAR (run 1 + run 2, both derivations agreeing): pack record size 0x158, ctor 0x1801342d0 zeroes +0x144/+0x14c(qwords)/+0x154(dword) itemQuantity +0x144, goldQuantity +0x148, silverQuantity +0x14c, bronzeQuantity +0x150, rareQuantity +0x154, state +0xb0, start +0xb4, useDefaultImage +0xcc, unopened +0xcd store root deser 0x1801234e0 puts the pack vector at responseObject+0x28, timestamp at responseObject+0x5c. POSITIVE CONTROL (already passing in run 2, re-asserted here): the copy-assign 0x1801340e0 must show up reading AND writing +0x144, and the ctor 0x1801342d0 must show up writing +0x144. If they do not, the scan is broken. HYPOTHESES UNDER TEST H1 No function other than the record's own ctor/copy/dtor touches +0x144..+0x154. H2 Nothing counts an item list against those numbers (no function reads a quantity slot and also walks an item vector). H3 `start` +0xb4 and `unopened` +0xcd are likewise unread inside CardsDLL. """ 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) QTY = {0x144: "itemQuantity", 0x148: "goldQuantity", 0x14c: "silverQuantity", 0x150: "bronzeQuantity", 0x154: "rareQuantity"} OTHER = {0x0b4: "start", 0x0cd: "unopened", 0x0b0: "state"} LIFECYCLE = {0x1801342d0: "record ctor", 0x1801340e0: "record copy-assign", 0x180132180: "vector grow", 0x1801232a0: "record dtor", 0x18013af30: "pack element deser"} 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 scan_mem(val): """{func_entry: [(addr, text)]} for MEMORY accesses at displacement val""" pat = bytes([val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF]) tag = "+ %#x]" % val out = {} seen = set() for h in find_all(pat, blocks=(".text",)): ins = None for back in range(0, 12): i2 = listing.getInstructionContaining(addr(h - back)) if i2 is not None: ins = i2 break if ins is None: continue a = int(ins.getAddress().getOffset()) if a in seen: continue seen.add(a) txt = str(ins) if tag not in txt: continue f = fm.getFunctionContaining(ins.getAddress()) key = int(f.getEntryPoint().getOffset()) if f else 0 out.setdefault(key, []).append((a, txt)) return out try: print("### TIGHT MEMORY-DISPLACEMENT SCAN, .text, quantity slots") per_off = {} fn_offs = {} for off in sorted(QTY) + sorted(OTHER): m = scan_mem(off) per_off[off] = m nm = QTY.get(off) or OTHER.get(off) tot = sum(len(v) for v in m.values()) print("\n --- +%#05x %-16s : %d instruction(s) in %d function(s)" % (off, nm, tot, len(m))) for k in sorted(m): fn_offs.setdefault(k, set()).add(off) print(" %#x %-20s" % (k, fname(k) if k else "?")) for a, t in m[k]: print(" %#x %s" % (a, t)) print("\n### CONTROL: lifecycle functions must appear for the quantity slots") for va, tag in LIFECYCLE.items(): got = sorted(fn_offs.get(va, [])) print(" %#x %-22s offsets seen: %s %s" % (va, tag, [hex(x) for x in got], "PASS" if got else "absent")) print("\n### FUNCTIONS TOUCHING >=2 DISTINCT QUANTITY SLOTS (candidate consumers)") cands = [] for k, offs in sorted(fn_offs.items()): q = sorted(o for o in offs if o in QTY) if len(q) >= 2: cands.append((k, q)) print(" %#x %-22s %s %s" % (k, fname(k), [hex(x) for x in q], "(lifecycle)" if k in LIFECYCLE else "<== NON-LIFECYCLE")) print("\n### FUNCTIONS TOUCHING EXACTLY ONE QUANTITY SLOT") for k, offs in sorted(fn_offs.items()): q = sorted(o for o in offs if o in QTY) if len(q) == 1: print(" %#x %-22s %s %s" % (k, fname(k), [hex(x) for x in q], "(lifecycle)" if k in LIFECYCLE else "")) print("\n### DECOMPILE EVERY NON-LIFECYCLE FUNCTION THAT TOUCHES ANY QUANTITY SLOT") for k, offs in sorted(fn_offs.items()): if k in LIFECYCLE or k == 0: continue if not any(o in QTY for o in offs): continue dump("QTY TOUCHER offs=%s" % [hex(x) for x in sorted(offs)], k, OUT + "d3_qty_%x.txt" % k, echo=True) print("\n### WHO CONSUMES THE STORE RESPONSE OBJECT (vector at +0x28)") fac = 0x180123480 dump("store response factory", fac, OUT + "d3_store_factory.txt", echo=True) print(" callers of factory:") for a, n in callers(fac): print(" %#x %s" % (a, n)) print(" callers of deser 0x1801234e0:") for a, n in callers(0x1801234e0): print(" %#x %s" % (a, n)) print("\n### CREATEPACK: numberItems and itemList") dump("createpack deser", 0x180162880, OUT + "d3_createpack_deser.txt", echo=False) for a, n in callers(0x180162880): print(" CALLER %#x %s" % (a, n)) dump("createpack deser caller", a, OUT + "d3_cp_caller_%x.txt" % a, echo=True) except Exception: traceback.print_exc() sys.stdout.flush()