# -*- coding: utf-8 -*- """ADVERSARIAL VERIFY 1: the packOpeningAnimationEnabled gate chain (D4 claim 1 + 2). HYPOTHESES UNDER ATTACK H1 FUN_18013c6d0 case 0x20e writes param_2[0x1d] H2 FUN_18011dc50 line ~40 writes +0x1fd45 = param_2[0x1d] == 1 H3 vtable 0x18021c2a0 slot +0x2e0 -> 0x18011c590 -> movzx eax,[rcx+0x1fd45] H4 FUN_18006cc60 never uses slot 0x2e0 (ABSENCE -- attacked with a different method: I enumerate EVERY vtable-slot displacement the publisher calls, from the DISASSEMBLY, not from the decompile text.) H5 exactly one reader of slot +0x2e0 in CardsDLL (ABSENCE) CONTROLS * class_deser("FutSquadSave") must be 0x180171a60 and class_deser("FutSquadList") 0x180172140. If those come back empty the whole harness is suspect. * vtable slots +0x2b0 and +0x2c8 must decode to 0x1fd3a and 0x1fd3d, which is what the known IS_FRIENDLY_SEASON_ENABLED / IS_DRAFT_MODE_ENABLED publisher demands. * the byte scan for `call [reg+0x2e0]` is run alongside the SAME scan for +0x2b0, which has a known-present site inside FUN_18006cc60. """ import traceback, struct, re OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres" def dump(name, s): p = "%s/v_%s.txt" % (OUT, name) with open(p, "w") as f: f.write(s) print("[wrote %s %d chars]" % (p, len(s))) try: print("=" * 78) print("CONTROL: class_deser") for n, exp in (("FutSquadSave", 0x180171a60), ("FutSquadList", 0x180172140), ("FutCreateMatch", 0x180120380)): try: r = class_deser(n) except Exception as e: r = "EXC %s" % e print(" class_deser(%-16s) = %s expected %#x" % (n, r, exp)) print("=" * 78) print("H1: FUN_18013c6d0 settings deserializer -- FULL decompile length + case 0x20e") s = dec(0x18013c6d0) print("len(src) = %d chars, %d lines <-- FULL, not truncated" % (len(s), s.count("\n") + 1)) dump("q1_settings_deser", s) for i, ln in enumerate(s.split("\n")): if "0x20e" in ln or "[0x1d]" in ln or "0x1d]" in ln: print(" L%-4d %s" % (i + 1, ln.strip())) print("=" * 78) print("H2: FUN_18011dc50 applier -- FULL decompile") s2 = dec(0x18011dc50) print("len(src) = %d chars, %d lines" % (len(s2), s2.count("\n") + 1)) dump("q1_applier", s2) print(s2) print("=" * 78) print("H3: vtable 0x18021c2a0 slots decoded from raw bytes") VT = 0x18021c2a0 for slot in range(0x260, 0x310, 8): try: p = qword(VT + slot) except Exception as e: print(" +%#05x qword failed %s" % (slot, e)); continue if not p: continue try: b = read_bytes(p, 12) except Exception: b = b"" bb = bytes(bytearray([(x & 0xff) for x in b])) disp = None kind = "" if len(bb) >= 7 and bb[0] == 0x0f and bb[1] == 0xb6 and bb[2] == 0x81: disp = struct.unpack_from("= 6 and bb[0] == 0x8b and bb[1] == 0x81: disp = struct.unpack_from(" %#x %s %s %s" % (slot, p, bb.hex(), kind, fname(p) or "")) print("=" * 78) print("H4: FUN_18006cc60 publisher -- FULL decompile, then DISASSEMBLY slot list") s3 = dec(0x18006cc60) print("len(src) = %d chars, %d lines" % (len(s3), s3.count("\n") + 1)) dump("q1_publisher", s3) print(s3) f = func(0x18006cc60) print("--- disassembly-derived indirect-call displacements in %s ---" % f.getName()) it = listing.getInstructions(f.getBody(), True) slots = [] while it.hasNext(): ins = it.next() t = str(ins) if t.startswith("CALL") and "[" in t and "+" in t: m = re.search(r"\+\s*(0x[0-9a-fA-F]+)\]", t) if m: slots.append((int(m.group(1), 16), int(ins.getAddress().getOffset()))) # also LEA/MOV of a string arg is noise; skip print(" indirect-call displacements used:", sorted(set(x[0] for x in slots))) for d, a in slots: print(" %#x at %#x" % (d, a)) print(" 0x2e0 present? ", 0x2e0 in set(x[0] for x in slots)) print(" 0x2b0 present? ", 0x2b0 in set(x[0] for x in slots), " <-- CONTROL, must be True") print("=" * 78) print("H5: xrefs to the stub 0x18011c590") try: for r in xrefs_to(0x18011c590): print(" ", r) except Exception as e: print(" xrefs_to raised", e) print("callers(0x18011c590):") try: print(" ", callers(0x18011c590)) except Exception as e: print(" ", e) print("=" * 78) print("H5b: whole-.text disassembly scan for CALL [reg+0x2e0] and CALL [reg+0x2b0]") blk = None for b in mem.getBlocks(): if b.getName() == ".text": blk = b print(" .text %s - %s" % (blk.getStart(), blk.getEnd())) from ghidra.program.model.address import AddressSet aset = AddressSet(blk.getStart(), blk.getEnd()) it = listing.getInstructions(aset, True) found = {0x2e0: [], 0x2b0: [], 0x2c8: []} n = 0 while it.hasNext(): ins = it.next() n += 1 t = str(ins) if t[0] != "C" or not t.startswith("CALL"): continue if "[" not in t: continue m = re.search(r"\+\s*(0x[0-9a-fA-F]+)\]", t) if not m: continue d = int(m.group(1), 16) if d in found: found[d].append((int(ins.getAddress().getOffset()), t)) print(" instructions walked: %d" % n) for d in (0x2e0, 0x2b0, 0x2c8): print(" --- displacement %#x : %d call sites ---" % (d, len(found[d]))) for a, t in found[d]: fn = fname(a) print(" %#x in %-24s %s" % (a, fn, t)) except Exception: traceback.print_exc() print("QUERY DONE")