"""D2 QUICK SELL, batch 7: Q2 (assign vs add) and Q3 (bulk discard shape). Q2 CONTEXT. FutDiscardCardServerResponse is a 0x38-byte object freshly allocated per request by FUN_180127160 ("RS4:FutDiscardCardServerResponse", size 0x38, vtable 0x180220488). Its deserialiser stores totalCredits with a plain MOV dword [obj+0x28] and the item id with MOV qword [obj+0x30]; there is no read-modify-write anywhere in the deser, and only two functions in the whole DLL carry the 0x326 immediate. What remains is: who READS obj+0x28, and does that consumer assign or accumulate into the wallet? Route to it: FUN_180127290 (vtable slot +0xa0) dispatches to a delegate stored at servercall+0x50 / +0x60. Q3 CONTEXT. FUN_180126f40 emits {"itemId":[, ...]} using atom 0x16d. Find which of the three discard actions owns it (DiscardCard 0x1802cb230 factory 0x180123cd0, DiscardCardByRes 0x1802cb260 factory 0x180123ce0, DiscardACard 0x1802cb290 factory 0x180123cc0) and what url/method that action uses. CONTROL for Q3: factory 0x180123cd0 must produce an object whose vtable slot +0x08 is the request-body/url set that includes 0x180127570 (the "/%llu" single-id url builder we have already seen on the wire as DELETE /ut/game/fifa17/item/). """ 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 try: print("##### Q3: the three discard action factories #####") for nm, a in (("DiscardACard", 0x180123cc0), ("DiscardCard", 0x180123cd0), ("DiscardCardByRes", 0x180123ce0)): dump("factory " + nm, a, "qs_fac_%x.txt" % a) print("\n##### Q3: the pointer table around 0x1801f3118 #####") for off in range(-0x40, 0x100, 8): a = 0x1801f3118 + off try: v = qword(a) except Exception: continue f = fm.getFunctionAt(addr(v)) if 0x180000000 <= v < 0x181000000 else None print(" %#x -> %#x %s" % (a, v, f.getName() if f else "")) print("\n##### Q3: url/method providers #####") for a in (0x180126f00, 0x1801277c0, 0x180127800, 0x180068320, 0x180127890, 0x180122420, 0x18011f940): dump("provider", a, "qs_prov_%x.txt" % a) print("\n##### Q3: the url-suffix table entry 0x0d / 0x0e / 0x0f #####") # the action rows point at a url index; print the table of url format strings for i in range(0x28): try: p = qword(0x1801f2f00 + i * 8) print(" idx %#04x -> %#x %r" % (i, p, rd_str(p, 60) if p else "")) except Exception as e: print(" idx %#04x ERR %r" % (i, e)) print("\n##### Q2: every RS4 class with 'Credit' or 'User' in the name #####") for h in find_all(b"RS4:Fut"): s = rd_str(h, 90) if "Credit" in s or "UserData" in s or "UserInfo" in s: print(" %#x %r" % (h, s)) for frm, typ, fn, ent in xrefs_to(h - 4): print(" xref %#x %s %s %#x" % (frm, typ, fn, ent)) print("\n##### Q2: functions containing the credits atom 0xc0 as a compare #####") it = fm.getFunctions(True) n = 0 found = [] while it.hasNext(): f = it.next() n += 1 ii = listing.getInstructions(f.getBody(), True) got = [] while ii.hasNext(): i = ii.next() t = str(i) if ("CMP" in t or "SUB" in t) and (",0xc0" in t): got.append((int(i.getAddress().getOffset()), t)) if got: found.append((int(f.getEntryPoint().getOffset()), f.getName(), got)) print(" scanned %d functions, %d contain a CMP/SUB with 0xc0" % (n, len(found))) for e, nm, got in found: print(" %#x %s" % (e, nm)) for a, t in got: print(" %#x %s" % (a, t)) except Exception: traceback.print_exc()