70a64e3709
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.
106 lines
4.8 KiB
Python
106 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""SBC cache populate/arm probe + poke (FIFA 17 CardsDLL).
|
|
|
|
READ-ONLY BY DEFAULT. The write path exists for the human's morning test but is
|
|
NEVER reached unless you pass --arm AND --i-mean-it. Running with no args only
|
|
READS /proc/<pid>/mem (O_RDONLY) and prints what a poke WOULD do.
|
|
|
|
Object graph (all static VAs, image base 0x180000000; add the live slide):
|
|
A = FUT root singleton = *(0x1802e6398) (getter 0x18011a830)
|
|
B = SBC TTL cache = A + 0x1f9d8 (vtable 0x1801fae70)
|
|
B+0x08 collection ptr, B+0x20 QPC deadline, B+0x28 ready byte
|
|
isValid = 0x180065d40 (B.vtable[+0x08]); clear = 0x180065d20 (B.vtable[+0x10])
|
|
HUB TTL cache = A + 0x1fd70 (same class, armed online)
|
|
SBC set-data req mgr = A + 0x2a0 (vtable 0x18022be90, ctor 0x18016fac0)
|
|
|
|
Populate path (normal, online):
|
|
fetch sbs/sets -> req mgr A+0x2a0 -> response obj (factory 0x18016fca0, vt 0x18022be80)
|
|
-> SAX drive 0x18016c330 -> top deser 0x18017b2b0
|
|
(which does service.[+0x9b0] to get the SBC manager, then)
|
|
category deser 0x18017ab80 / set deser 0x18017ad60
|
|
-> finalizers 0x180160e00 / 0x180160e50 / 0x180161020 build the SBC manager's
|
|
category array (stride 0xf0) + set array (stride 0x3570); select/rebuild 0x180160b80
|
|
-> generic cache commit copy-assigns a stack temp {collection, deadline, ready=1}
|
|
into B (assign 0x1800c21a0), arming B+0x28 and pointing B+0x08 at the manager data.
|
|
|
|
The UI renders by polling isValid(B) each frame and iterating *(B+0x08). Arming
|
|
B+0x28 alone (see --arm-flag-only) opens the menu but draws EMPTY (collection NULL).
|
|
A populated render needs *(B+0x08) to point at a real set/category collection.
|
|
"""
|
|
import os, struct, sys
|
|
|
|
SLIDE_KNOWN = 0x6ffe7c140000 # informational; actual slide is read from maps
|
|
A_SINGLETON = 0x1802e6398
|
|
B_OFF = 0x1f9d8
|
|
HUB_OFF = 0x1fd70
|
|
REQMGR_OFF = 0x2a0
|
|
CTRL_VA = 0x180180d00
|
|
|
|
def find_pid():
|
|
for d in os.listdir('/proc'):
|
|
if d.isdigit():
|
|
try:
|
|
if open('/proc/%s/comm' % d).read().strip() == 'FIFA17.exe':
|
|
return int(d)
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def main():
|
|
arm = '--arm' in sys.argv
|
|
flag_only = '--arm-flag-only' in sys.argv
|
|
confirm = '--i-mean-it' in sys.argv
|
|
pid = find_pid()
|
|
if not pid:
|
|
print("FIFA17.exe not running -> nothing to read. (static analysis is authoritative)")
|
|
return
|
|
base = None
|
|
for ln in open('/proc/%d/maps' % pid):
|
|
if 'CardsDLL' in ln:
|
|
base = int(ln.split('-')[0], 16); break
|
|
if not base:
|
|
print("CardsDLL not mapped yet (client not in Ultimate Team). Skip live step.")
|
|
return
|
|
slide = base - 0x180000000
|
|
fdr = os.open('/proc/%d/mem' % pid, os.O_RDONLY)
|
|
rd = lambda va, n: os.pread(fdr, n, va)
|
|
q = lambda va: struct.unpack('<Q', rd(va, 8))[0]
|
|
# prove slide against on-disk FNV prologue
|
|
pe = open('/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll', 'rb').read()
|
|
f = lambda va: va - 0x180000000 - 0x1000 + 0x400
|
|
ok = pe[f(CTRL_VA):f(CTRL_VA)+24] == rd(CTRL_VA + slide, 24)
|
|
print("pid %d base %#x slide %#x CONTROL %s" % (pid, base, slide, "OK" if ok else "MISMATCH-ABORT"))
|
|
if not ok:
|
|
os.close(fdr); return
|
|
A = q(A_SINGLETON + slide)
|
|
B = A + B_OFF
|
|
HUB = A + HUB_OFF
|
|
print("A(FUT root)=%#x B(SBC cache)=%#x HUB=%#x reqmgr=%#x" % (A, B, HUB, A + REQMGR_OFF))
|
|
def show(name, C):
|
|
coll = q(C + 0x08); dl = q(C + 0x20); ready = rd(C + 0x28, 1)[0]
|
|
vt = q(C) - slide
|
|
print(" %-4s vt=%#x coll(+8)=%#x deadline(+0x20)=%#x ready(+0x28)=%d"
|
|
% (name, vt, coll, dl, ready))
|
|
return coll, dl, ready
|
|
print("live cache state:")
|
|
show("SBC", B); show("HUB", HUB)
|
|
# what a poke WOULD do
|
|
print("\n-- INTERVENTION PLAN (dry-run) --")
|
|
print(" [flag-only] write BYTE @ %#x = 1 (opens menu, EMPTY render)" % (B + 0x28))
|
|
print(" [real fix] preferred = force the client to issue sbs/sets so its own")
|
|
print(" parser populates the SBC manager and commits B. The offline")
|
|
print(" block is the FUT front-end refusing to call the native fetch;")
|
|
print(" route the issued GET ut/game/fifa17/sbs/sets through the bridge.")
|
|
if flag_only and arm and confirm:
|
|
# guarded, explicit, single-byte only
|
|
fdw = os.open('/proc/%d/mem' % pid, os.O_WRONLY)
|
|
os.pwrite(fdw, b'\x01', B + 0x28)
|
|
os.close(fdw)
|
|
print("\n[WROTE] BYTE @ %#x = 1 (flag-only). Expect menu opens, likely empty." % (B + 0x28))
|
|
elif arm:
|
|
print("\n[SAFE] --arm given but not both --arm-flag-only and --i-mean-it; no write performed.")
|
|
os.close(fdr)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|