# -*- coding: utf-8 -*- """ q_pack_inv_5 -- DIMENSION 1 pass 5: DECOMPILE-BASED deserializer atlas. WHY THIS PASS EXISTS Pass 4's instruction-level CMP/SUB scan FAILED its own control: FUN_18014cc60 provably dispatches on atom 0x2cd (squad) yet the scan did not see it. Reason: the dispatch is a RUNNING-SUM ladder ("sub eax,0x5d / jz / sub eax,0x148 / jz" where 0x5d+0x148 = 0x1a5), so the raw immediates are DIFFERENCES, not atoms. Ghidra's decompiler already folds the ladder back into `== 0x2cd`, so this pass reads the atoms out of the decompiled C instead of the instruction stream. HYPOTHESIS Decompiling every function that calls the FNV hasher 0x180180d00 or the wrapper FUN_180141ee0 and regexing `== 0xNNN` / `!= 0xNNN` / `case 0xNNN` gives the complete key-set of every JSON parser in CardsDLL. CONTROLS (the pass is void if any fails) * FUN_18014cc60 must report 0x2cd(squad) AND 0x2e5(starterPack). * FUN_18013ec10 must report 0x35e(unopenedPacks) AND 0x24b(preOrderPacks). * FUN_18013af30 must report 0x20c(packContentInfo) AND 0x35d(unopened). * atom_name(0x2e5) must be 'starterPack'. """ import re import traceback OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres" ATOM_TABLE = 0x1802D2760 _nc = {} def atom_name(i): if i in _nc: return _nc[i] v = "?" try: p = qword(ATOM_TABLE + i * 8) if 0x180001000 <= p <= 0x1802EFC08: v = rd_str(p, 60) except Exception: pass _nc[i] = v return v EQ = re.compile(r"(?:==|!=)\s*(0x[0-9a-fA-F]+|\d+)") CASE = re.compile(r"case\s+(0x[0-9a-fA-F]+|\d+)\s*:") NOISE = {6, 10, 0xB, 0xD, 0x38C, 0, 1, 2, 3, 4, 5, 7, 8, 9} try: print("CONTROL atom_name(0x2e5) = %r" % atom_name(0x2E5)) hashers = set() for tgt in (0x180180D00, 0x180141EE0): for (fr, ty, fn, en) in xrefs_to(tgt): if en: hashers.add(en) print("parser candidates: %d" % len(hashers)) atlas = {} bodies = {} for e in sorted(hashers): try: src = dec(e, 240) except Exception: src = "" bodies[e] = src ats = set() for m in EQ.finditer(src): v = int(m.group(1), 0) if v not in NOISE and 1 <= v <= 0x38C: ats.add(v) for m in CASE.finditer(src): v = int(m.group(1), 0) if v not in NOISE and 1 <= v <= 0x38C: ats.add(v) atlas[e] = ats print("\n########## CONTROLS ##########") ck = [(0x18014CC60, 0x2CD), (0x18014CC60, 0x2E5), (0x18013EC10, 0x35E), (0x18013EC10, 0x24B), (0x18013AF30, 0x20C), (0x18013AF30, 0x35D)] ok = True for fn, at in ck: hit = at in atlas.get(fn, set()) ok = ok and hit print(" %#x has %#x(%-16s): %s" % (fn, at, atom_name(at), "PASS" if hit else "FAIL")) print("ATLAS CONTROL OVERALL: %s" % ("PASS" if ok else "FAIL")) print("\n########## ATLAS ##########") lines = [] for e in sorted(atlas): ats = sorted(atlas[e]) lines.append("\n%#x len(src)=%d keys=%d" % (e, len(bodies[e]), len(ats))) lines.append(" " + ", ".join("%#x=%s" % (a, atom_name(a)) for a in ats)) print("\n".join(lines)) open(OUT + "/d1_atlas.txt", "w").write("\n".join(lines)) print("\n########## PACK-INVENTORY ATOM OWNERSHIP ##########") for a in (0x20D, 0x35D, 0x35E, 0x2E5, 0x5D, 0x24B, 0x27B, 0x260, 0x262, 0x264, 0x20C, 0x16E, 0x16B, 0xEC, 0x1DD, 0xBC, 0x2E3, 0x2EB, 0x37D): owners = [e for e in atlas if a in atlas[e]] print(" atom %#x %-22s -> %s" % (a, atom_name(a), [hex(x) for x in sorted(owners)] or "NONE")) print("\n########## EXTRA DECOMPILES ##########") for va, tag in ((0x180122C50, "f180122c50"), (0x18017FC20, "f18017fc20"), (0x180161B00, "f180161b00"), (0x18013C3A0, "f18013c3a0"), (0x180144E80, "f180144e80"), (0x180138E10, "dupidlist"), (0x1801234E0, "storepacktypes_root")): s = bodies.get(va) or dec(va) print("\n--- %s %#x len=%d" % (tag, va, len(s))) print(s) open(OUT + "/d1_%s.txt" % tag, "w").write(s) print("\n########## hub-tile table around 0x1802097f8 ##########") for va in range(0x1802096C0, 0x180209900, 8): try: q = qword(va) except Exception: continue tag = "" f = fm.getFunctionAt(addr(q)) if 0x180001000 <= q <= 0x1801E4F62 else None if f: tag = "FUNC " + f.getName() elif 0x180001000 <= q <= 0x1802EFC08: try: s = rd_str(q, 60) if s.isprintable() and len(s) > 1: tag = repr(s) except Exception: pass if tag: print(" %#x -> %#x %s" % (va, q, tag)) print("\nDONE q_pack_inv_5") except Exception: traceback.print_exc()