fifa17-python: commit working FUT backend deployment (client/server split)

Freeze the running offline FUT backend into version control as
fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a
fresh checkout:

* OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders
  (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is
  required for remote mode (compose and entrypoint fail without it)
* docker-compose.yml reproducing the frozen baseline container exactly
  (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart)
* .env.example / .env for site config - the LAN IP is never hardcoded in source
* tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10,
  verified byte-identical to the running container at freeze time
* client_arm.sh (the 105 client-side arming counterpart)
* Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying
* docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record,
  restore instructions and rebuild-equivalence procedure

Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored.
The live container is untouched pending the .105 launcher audit.
This commit is contained in:
root
2026-08-10 23:54:04 +00:00
parent edab23f04a
commit 70a64e3709
242 changed files with 175860 additions and 0 deletions
@@ -0,0 +1,92 @@
#!/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()