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>
133 lines
6.0 KiB
Python
133 lines
6.0 KiB
Python
"""VERIFY-3. Attack the two biggest absence claims and the actionable tables.
|
|
|
|
H11 (D3 #4) NOTHING in CardsDLL compares the delivered itemList against the
|
|
declared packContentInfo quantities. Original method: disp32 BYTE SCAN.
|
|
MY METHOD: whole-.text instruction-operand scalar census via Ghidra's own
|
|
decoded operands, which sees disp8, disp32, SIB and LEA forms alike and does
|
|
not care how the displacement was encoded. Strictly wider than a byte scan.
|
|
H12 (D3 #5) numberItems at FutCreatePackServerResponse+0x28 is read by NOTHING.
|
|
MY METHOD: enumerate every xref to the response vtable 0x180228260, the
|
|
factory 0x180162770, the class literal, and the owning ServerCall vtable
|
|
0x180228270, then look at the completion consumer.
|
|
H13 FUN_18002c3c0 has DATA xrefs at 0x1802f1620 and 0x180244880 that the D3
|
|
report did not mention. Are they vtable slots (=> a second, indirect caller)?
|
|
H14 the HTTP status table FUN_1801844c0 maps only 200 to success.
|
|
H15 the 9-entry transaction state table at 0x1802d02c0.
|
|
H16 the RPC descriptor table at 0x1802cb500, 0x30-byte rows.
|
|
|
|
CONTROL for the census: FUN_18002c3c0 (the known adapter) MUST appear with all
|
|
five offsets. If it does not, the census is broken and every absence is void.
|
|
"""
|
|
import traceback
|
|
from collections import defaultdict
|
|
OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres/"
|
|
|
|
try:
|
|
TARGETS = {0x144, 0x148, 0x14c, 0x150, 0x154}
|
|
print("=" * 74)
|
|
print("H11 CENSUS: every function whose decoded operands carry a displacement/")
|
|
print(" scalar in {0x144,0x148,0x14c,0x150,0x154}")
|
|
hits = defaultdict(set)
|
|
sites = defaultdict(list)
|
|
nfun = 0
|
|
fi = fm.getFunctions(True)
|
|
while fi.hasNext():
|
|
f = fi.next()
|
|
nfun += 1
|
|
ent = int(f.getEntryPoint().getOffset())
|
|
it = listing.getInstructions(f.getBody(), True)
|
|
while it.hasNext():
|
|
ins = it.next()
|
|
for i in range(ins.getNumOperands()):
|
|
try: sc = ins.getScalar(i)
|
|
except Exception: sc = None
|
|
if sc is None: continue
|
|
v = int(sc.getUnsignedValue())
|
|
if v in TARGETS:
|
|
hits[ent].add(v)
|
|
if len(sites[ent]) < 8:
|
|
sites[ent].append("%010x %s" % (int(ins.getAddress().getOffset()), str(ins)))
|
|
print(" functions scanned:", nfun)
|
|
ranked = sorted(hits.items(), key=lambda kv: (-len(kv[1]), kv[0]))
|
|
print(" functions carrying ALL FIVE offsets:")
|
|
allfive = [e for e, s in ranked if len(s) == 5]
|
|
for e in allfive:
|
|
print(" %010x %s" % (e, fname(e) if callable(globals().get("fname")) else ""))
|
|
for s in sites[e]:
|
|
print(" ", s)
|
|
print(" CONTROL 0x18002c3c0 present with 5 offsets:", 0x18002c3c0 in allfive,
|
|
" (offsets seen: %s)" % sorted(hex(x) for x in hits.get(0x18002c3c0, set())))
|
|
print(" functions with 4 offsets:", ["%010x" % e for e, s in ranked if len(s) == 4])
|
|
print(" functions with 3 offsets:", ["%010x" % e for e, s in ranked if len(s) == 3])
|
|
print(" total functions with >=1 of the five: %d" % len(hits))
|
|
with open(OUT + "v3_census.txt", "w") as fh:
|
|
for e, s in ranked:
|
|
fh.write("%010x n=%d %s\n" % (e, len(s), sorted(hex(x) for x in s)))
|
|
for t in sites[e]:
|
|
fh.write(" %s\n" % t)
|
|
|
|
print()
|
|
print("=" * 74)
|
|
print("H13 DATA xrefs to FUN_18002c3c0")
|
|
for a in (0x1802f1620, 0x180244880):
|
|
blk = mem.getBlock(addr(a))
|
|
print(" %010x in block %s" % (a, blk.getName() if blk else "?"))
|
|
for k in range(-4, 6):
|
|
q = qword(a + k * 8)
|
|
f = fm.getFunctionAt(addr(q)) if 0x180000000 <= q < 0x181000000 else None
|
|
print(" [%+3d] %016x %s" % (k * 8, q, f.getName() if f else ""))
|
|
print(" xrefs to that slot address:", xrefs_to(a))
|
|
|
|
print()
|
|
print("=" * 74)
|
|
print("H12 who touches FutCreatePackServerResponse")
|
|
for lit in find_all(b"RS4:FutCreatePackServerResponse\x00"):
|
|
print(" literal at %010x xrefs:" % lit, xrefs_to(lit))
|
|
for v in (0x180228260, 0x180228270):
|
|
print(" vtable %010x xrefs: %s" % (v, xrefs_to(v)))
|
|
for slot, t, n in vtable(v, 20):
|
|
if t == 0: break
|
|
print(" +%03x %016x %s" % (slot, t, n))
|
|
print(" xrefs to factory 0x180162770:", xrefs_to(0x180162770))
|
|
print(" xrefs to ctor 0x180162420:", xrefs_to(0x180162420))
|
|
print(" callers of deser 0x180162880:", xrefs_to(0x180162880))
|
|
|
|
print()
|
|
print("=" * 74)
|
|
print("H14 HTTP status table FUN_1801844c0")
|
|
s = dec(0x1801844c0)
|
|
open(OUT + "v3_http_1801844c0.txt", "w").write(s)
|
|
print(" len(src)=%d lines=%d" % (len(s), s.count("\n")))
|
|
print(s)
|
|
|
|
print()
|
|
print("=" * 74)
|
|
print("H15 transaction state table 0x1802d02c0")
|
|
for k in range(12):
|
|
a = 0x1802d02c0 + k * 16
|
|
v = dword(a); p = qword(a + 8)
|
|
nm = rd_str(p, 40) if 0x180000000 <= p < 0x181000000 else "<%016x>" % p
|
|
print(" [%2d] %010x value=%-6d name=%r" % (k, a, v if v < 0x80000000 else v - (1 << 32), nm))
|
|
|
|
print()
|
|
print("=" * 74)
|
|
print("H16 RPC descriptor table 0x1802cb500 (0x30 rows)")
|
|
bad = 0
|
|
for k in range(40):
|
|
r = 0x1802cb500 + k * 0x30
|
|
try:
|
|
p0 = qword(r); f1 = qword(r + 8); p2 = qword(r + 0x10)
|
|
z3 = qword(r + 0x18); z4 = qword(r + 0x20); fn = qword(r + 0x28)
|
|
except Exception:
|
|
print(" row %d unreadable" % k); break
|
|
n0 = rd_str(p0, 48) if 0x180000000 <= p0 < 0x181000000 else ""
|
|
n2 = rd_str(p2, 48) if 0x180000000 <= p2 < 0x181000000 else ""
|
|
ok = n2.isupper() and n2.isalpha() if n2 else False
|
|
if not ok: bad += 1
|
|
print(" %010x %-28r flags=%-6x %-28r z=%d,%d fn=%010x %s"
|
|
% (r, n0, f1, n2, z3, z4, fn, "" if ok else " <-- third qword not an UPPER token"))
|
|
print(" rows whose third qword is NOT an uppercase token: %d/40" % bad)
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|