# -*- 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("<I", bb, 3)[0]; kind = "movzx eax,byte[rcx+%#x]" % disp
        elif len(bb) >= 6 and bb[0] == 0x8b and bb[1] == 0x81:
            disp = struct.unpack_from("<I", bb, 2)[0]; kind = "mov eax,[rcx+%#x]" % disp
        print("  slot +%#05x -> %#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")
