6ddd5e9d47
Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).
Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
reads identity/rating/face from a resolved record at item+0x10, filled
by a lookup (0x18011cca0) in the FUT item-definition std::map at
CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
(JSON fields routed to the skip handler). Owned items don't auto-trigger
a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
ready; the fetch trigger lives in the packed FIFA17.exe.
New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
93 lines
4.5 KiB
Python
93 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Forge a FifaOnline::FirstPartyAuthCodeFutureImpl node and enqueue it so DoTick
|
|
(@0x146f199c0) fires GetAuthCode over LSX. Clean-room; from our own RE (ENQUEUE_PLAN.md,
|
|
adversarially verified). ptrace_scope=0 required. LSX responder MUST be answering
|
|
GetAuthCode first (the auth call is synchronous with a 15s timeout).
|
|
|
|
Two gates (both live-verified 0): the retriever queue slot AND OriginSDK[+0x3a0] default-user.
|
|
This sets BOTH. Treat the first run as a PROBE: setting the default user flips ~15 other
|
|
GetDefaultUser consumers; `--revert` restores the originals.
|
|
|
|
Usage: python3 forge_node.py # forge + enqueue
|
|
python3 forge_node.py --revert # restore SDK default-user + clear the slot
|
|
"""
|
|
import sys, struct, glob, os, json
|
|
|
|
ONLINEMGR_PP = 0x1448a3b20 # *-> OnlineManager ; retriever = +0x4e98
|
|
RETR_OFF = 0x4e98
|
|
GUARD = 0x1448a3ac3 # enqueue guard byte (must be 1)
|
|
SDK_PP = 0x144b7c7a0 # *-> OriginSDK
|
|
SDK_DEFUSER = 0x3a0 # default-user slot (BLOCKER) -- set +0x3a0 AND +0x3a8
|
|
SDK_DEREF = 0x3b0 # deref'd unconditionally downstream; must stay non-null
|
|
VPTR = 0x1438f5d58 # node primary vtable (AddRef/Release/dtor/GetStatus/GetResult)
|
|
VPTR2 = 0x1438f5d90 # node secondary vtable -- MUST be this, never 0 (Release calls [this+8]->[0])
|
|
NODE_VA = 0x14300a380 # validated zero/unreferenced scratch (ENQUEUE_PLAN §4) -- re-checked below
|
|
CLIENTID = b"FIFA17PC" # only proven constraint: non-empty
|
|
SAVE = "/tmp/forge_node_orig.json"
|
|
|
|
def pid():
|
|
for d in glob.glob('/proc/[0-9]*'):
|
|
try:
|
|
if open(d+'/comm').read().strip()=='FIFA17.exe': return int(d.split('/')[-1])
|
|
except Exception: pass
|
|
raise SystemExit("FIFA17.exe not running")
|
|
|
|
def build_node():
|
|
b = bytearray(0xF0)
|
|
struct.pack_into('<Q', b, 0x00, VPTR)
|
|
struct.pack_into('<Q', b, 0x08, VPTR2)
|
|
struct.pack_into('<I', b, 0x10, 2) # refcount=2 -> survives DoTick's Release, never freed
|
|
b[0x18:0x18+len(CLIENTID)] = CLIENTID # inline clientId, NUL-terminated
|
|
return bytes(b)
|
|
|
|
def main():
|
|
p = pid(); mp = f"/proc/{p}/mem"
|
|
f = open(mp, "r+b")
|
|
def rq(va): f.seek(va); return struct.unpack('<Q', f.read(8))[0]
|
|
def rd(va,n): f.seek(va); return f.read(n)
|
|
def wr(va,b): f.seek(va); f.write(b)
|
|
|
|
onlinemgr = rq(ONLINEMGR_PP); retr = onlinemgr + RETR_OFF
|
|
sdk = rq(SDK_PP)
|
|
slot = retr + 0x08
|
|
|
|
if "--revert" in sys.argv:
|
|
orig = json.load(open(SAVE)) if os.path.exists(SAVE) else {}
|
|
wr(sdk+SDK_DEFUSER, struct.pack('<Q', orig.get("defuser",0)))
|
|
wr(sdk+0x3a8, struct.pack('<Q', orig.get("defuser8",0)))
|
|
wr(slot, struct.pack('<Q', orig.get("slot",0)))
|
|
print(f"[revert] SDK+0x3a0/0x3a8 -> {orig.get('defuser',0):#x}/{orig.get('defuser8',0):#x}, slot -> {orig.get('slot',0):#x}")
|
|
return
|
|
|
|
# --- preconditions (verify, do not assume) ---
|
|
assert rd(GUARD,1)[0] == 1, "guard byte != 1"
|
|
assert rq(sdk+SDK_DEREF) != 0, "SDK+0x3b0 is NULL (would fault downstream) -- abort"
|
|
assert rq(slot) == 0, f"queue slot already non-zero ({rq(slot):#x}) -- abort"
|
|
scratch = rd(NODE_VA, 0xF0)
|
|
assert all(x==0 for x in scratch), "scratch NODE_VA not zero -- abort"
|
|
assert rq(VPTR) == 0x147e8f160, "node vtable[0] mismatch -- wrong build?"
|
|
|
|
# save originals for --revert
|
|
json.dump({"defuser": rq(sdk+SDK_DEFUSER), "defuser8": rq(sdk+0x3a8), "slot": rq(slot)}, open(SAVE,"w"))
|
|
|
|
# 1) forge the node into scratch (BEFORE anything is armed)
|
|
wr(NODE_VA, build_node())
|
|
assert rd(NODE_VA,0xF0) == build_node(), "node write-back mismatch"
|
|
print(f"[+] node forged @ {NODE_VA:#x} clientId={CLIENTID.decode()} refcount=2")
|
|
|
|
# 2) gate 2: set the Origin default user (both slots, like the real SDK)
|
|
wr(sdk+SDK_DEFUSER, struct.pack('<Q', sdk))
|
|
wr(sdk+0x3a8, struct.pack('<Q', sdk))
|
|
assert rq(sdk+SDK_DEFUSER)==sdk and rq(sdk+0x3a8)==sdk, "defuser write-back mismatch"
|
|
print(f"[+] OriginSDK[+0x3a0]/[+0x3a8] set -> {sdk:#x} (default user)")
|
|
|
|
# 3) gate 1 (the TRIGGER, set last): enqueue the node
|
|
wr(slot, struct.pack('<Q', NODE_VA))
|
|
assert rq(slot)==NODE_VA, "slot write-back mismatch"
|
|
print(f"[+] retriever+0x8 ({slot:#x}) -> {NODE_VA:#x} *** ENQUEUED ***")
|
|
print(" Watch /tmp/lsx.log for <GetAuthCode ClientId=\"FIFA17PC\">. Then node+0xE8 -> 1,")
|
|
print(" node+0xE0=200 = error (read node+0x58 msg). --revert to undo.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|