Files
OpenFUT/fifa17-recon/tools/sbc_populate_poke.py
T

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