afdbb364ca
A twelve-agent pass over the parts of pack opening we did not understand, run against
the live client (CardsDLL slide proven, not assumed) plus static CardsDLL. Findings
below survived an adversarial verification round that corrected several of them; where
a verifier and a finder disagreed, the verifier won.
THE HEADLINE IS A NEGATIVE, and it deletes work rather than creating it. There is no
pack-inventory endpoint in FIFA 17 and there never was. Proven three independent ways:
the 48-entry UTAS route template array at 0x18021df80, a regex for "ut/" over the whole
PE, and the 125-row client action table at 0x1802caa20, which is the complete set of
requests the client can originate. "Serve the pack inventory" comes off the backlog.
The unclaimed-pack tile and My Packs are two fields on responses we already build.
Corrections to ENDPOINT_MAP.md, both freeze-risky as written:
* duplicateItemIdList is an array of OBJECTS (element parser 0x180138e10: itemId
0x16d, duplicateItemId 0xeb, itemLoans 0x16f, duplicateItemLoans 0xed), not the
int list documented at :1095 and :218. Control that this is not a misread:
dreamSquads 0xe9 in FutMoveCard genuinely is a bare int array and parses with no
inner object loop. We serve [], so this is a docs bug today and a live freeze the
moment somebody implements it from the map as written.
* FutDiscardCardServerResponse is {"items":[{"id":N}],"totalCredits":N}. There is no
top-level id. :968-971 is wrong twice over.
packContentInfo is DECORATIVE. It is read only into a store-tile view model, and
nothing compares the declared counts against the delivered itemList, so open_pack()
does not have to honour the distribution.
The reveal is entirely CLIENT-SIDE. Walkout, tiering, colours and ordering are
arithmetic over fields we already send. Genuine outstanding server work reduces to
three items: duplicates, quick-sell credit, unopenedPacks.
Perishable intel captured: the real FIFA 17 retail pack catalogue, 41 SKUs with Origin
offer ids, recovered from the client heap as a parsed copy of data/store/storecfg.xml.
It is in no file on disk, only in a running process.
futmem/ is a standalone read-only Rust crate for this kind of work (maps, find,
strings, read). Read-only by construction: it opens /proc/<pid>/mem with File::open
and there is no code path in it that can write to another process, because a live game
session depends on that. Its own [workspace] table keeps it out of the parent
workspace. Chunked scanning overlaps by pattern_len-1 so a match spanning a chunk
boundary is still found.
utas_server.py gains FUT_PORT/FUT_LOG so a throwaway instance can be started without
bouncing the one the live client is using. Defaults unchanged (8099, /tmp/utas_server.log).
Noted for the record: this edit came from a research agent that had been told not to
touch server code. It is benign and useful, but it was out of scope.
Not committed: the doc proposes ENDPOINT_MAP.md changes as pasteable text rather than
applying them, and every proposed server change defaults off per the house rule.
Nothing in this commit changes a response the client sees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
137 lines
5.7 KiB
Python
137 lines
5.7 KiB
Python
"""D3 run 4: close the consumer set for the pack record.
|
|
|
|
WHAT RUN 3 SETTLED
|
|
Of everything in .text that touches +0x144..+0x154 as a memory displacement,
|
|
only four functions belong to the 0x158-stride pack record:
|
|
0x1801342d0 ctor (zeroes them) 0x18013af30 deser (writes them)
|
|
0x180133210 uninitialised_copy (0x158) 0x1801340e0 copy-assign
|
|
The rest were offset collisions on unrelated structs, proven by their stride or
|
|
their size: 0x180133af0 iterates with stride 0x168, 0x180134b50 copies out to
|
|
+0x163, 0x180173e00's object extends to +0x2f8 and sums 0x148+0x14c+0x150 as a
|
|
win/draw/loss total.
|
|
|
|
WHAT THIS RUN DOES
|
|
1. The 0x158 STRIDE CENSUS. Any loop over the pack vector must advance a pointer
|
|
by 0x158 or multiply an index by it. Enumerate every instruction that uses
|
|
0x158 in pointer arithmetic (ADD/LEA/IMUL on a register), not as a stack frame
|
|
size. Control: 0x180133210 and 0x18013af30 must both appear.
|
|
2. Locate the FutStoreGetPackTypesServerResponse vtable by searching .rdata for
|
|
the deserializer pointer 0x1801234e0, dump it, and take xrefs to the vtable so
|
|
the owner class and any accessor are visible. (The factory and the deser have
|
|
zero direct callers, so they are dispatched through this vtable.)
|
|
3. Complete caller closure over the record's lifecycle functions: anything that
|
|
can own a pack record must construct, copy or destroy one.
|
|
4. CreatePack side: numberItems store offset, and every reader of it, to answer
|
|
whether the reveal is sized from a declared count or from the actual list.
|
|
"""
|
|
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)
|
|
|
|
|
|
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 insn_at(h):
|
|
for back in range(0, 14):
|
|
i2 = listing.getInstructionContaining(addr(h - back))
|
|
if i2 is not None:
|
|
return i2
|
|
return None
|
|
|
|
|
|
try:
|
|
print("### 1. 0x158 STRIDE CENSUS (pointer arithmetic only, not frame sizes)")
|
|
pat = bytes([0x58, 0x01, 0x00, 0x00])
|
|
seen = set()
|
|
keep = []
|
|
for h in find_all(pat, blocks=(".text",)):
|
|
ins = insn_at(h)
|
|
if ins is None:
|
|
continue
|
|
a = int(ins.getAddress().getOffset())
|
|
if a in seen:
|
|
continue
|
|
seen.add(a)
|
|
t = str(ins)
|
|
if "0x158" not in t:
|
|
continue
|
|
mn = t.split()[0]
|
|
if mn in ("SUB", "ADD") and t.split()[1].startswith("RSP"):
|
|
continue # stack frame
|
|
if mn in ("ADD", "LEA", "IMUL", "MOV", "CMP", "SHL"):
|
|
keep.append((a, fname(a), t))
|
|
byfn = {}
|
|
for a, fn, t in keep:
|
|
byfn.setdefault(fn, []).append((a, t))
|
|
print(" %d instruction(s) in %d function(s)" % (len(keep), len(byfn)))
|
|
for fn in sorted(byfn):
|
|
print(" %s" % fn)
|
|
for a, t in byfn[fn]:
|
|
print(" %#x %s" % (a, t))
|
|
print(" CONTROL: 0x180133210 present=%s 0x18013af30 present=%s"
|
|
% ("FUN_180133210" in byfn, "FUN_18013af30" in byfn))
|
|
|
|
print("\n### 2. STORE RESPONSE VTABLE")
|
|
dp = (0x1801234e0).to_bytes(8, "little")
|
|
for h in find_all(dp, blocks=(".rdata", ".data")):
|
|
print(" deser pointer 0x1801234e0 found in .rdata/.data at %#x" % h)
|
|
for base in (h - 8, h - 0x10, h):
|
|
print(" candidate vtable base %#x:" % base)
|
|
for off, tgt, nm in vtable(base, 14):
|
|
print(" +%#04x %#018x %s" % (off, tgt, nm))
|
|
break
|
|
for frm, typ, fn, ent in xrefs_to(h - 8):
|
|
print(" xref to (vtbl base %#x): %#x %s %s" % (h - 8, frm, typ, fn))
|
|
for frm, typ, fn, ent in xrefs_to(h):
|
|
print(" xref to (slot itself %#x): %#x %s %s" % (h, frm, typ, fn))
|
|
|
|
print("\n### 3. CALLER CLOSURE OVER PACK-RECORD LIFECYCLE")
|
|
LIFE = {0x1801342d0: "record ctor", 0x1801340e0: "copy-assign",
|
|
0x180133210: "uninit_copy(0x158)", 0x180132180: "vector grow",
|
|
0x1801232a0: "record dtor", 0x18013af30: "element deser"}
|
|
lvl1 = {}
|
|
for va, tag in LIFE.items():
|
|
cs = callers(va)
|
|
print(" %#x %-20s callers: %s" % (va, tag, [(hex(a), n) for a, n in cs]))
|
|
for a, n in cs:
|
|
lvl1.setdefault(a, set()).add(tag)
|
|
print("\n level-2 (callers of those callers):")
|
|
for a in sorted(lvl1):
|
|
if a in LIFE:
|
|
continue
|
|
print(" %#x %-20s via %s ; its callers: %s"
|
|
% (a, fname(a), sorted(lvl1[a]), [(hex(x), n) for x, n in callers(a)]))
|
|
print("\n full decompiles of every non-lifecycle caller:")
|
|
for a in sorted(lvl1):
|
|
if a in LIFE:
|
|
continue
|
|
dump("LIFECYCLE CALLER", a, OUT + "d3_life_caller_%x.txt" % a, echo=True)
|
|
|
|
print("\n### 4. CREATEPACK numberItems")
|
|
src = dump("createpack deser", 0x180162880, OUT + "d3_createpack_deser.txt", echo=True)
|
|
for va, tag in ((0x180162880, "createpack deser"),):
|
|
pass
|
|
dpc = (0x180162880).to_bytes(8, "little")
|
|
for h in find_all(dpc, blocks=(".rdata", ".data")):
|
|
print(" createpack deser pointer at %#x (vtable slot)" % h)
|
|
for off, tgt, nm in vtable(h - 8, 12):
|
|
print(" +%#04x %#018x %s" % (off, tgt, nm))
|
|
for frm, typ, fn, ent in xrefs_to(h - 8):
|
|
print(" xref to vtbl base: %#x %s %s" % (frm, typ, fn))
|
|
except Exception:
|
|
traceback.print_exc()
|
|
sys.stdout.flush()
|