Files
OpenFUT/fifa17-recon/tools/forge_node.py
T
funman300 6ddd5e9d47 fifa17-recon: offline FUT squad-shell working + full card-system RE
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
2026-08-01 20:24:30 -07:00

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()