#!/usr/bin/env python3 """SBC menu render probe + minimal gate-arm poke (FIFA 17 CardsDLL). WHAT THIS DOES -------------- READ-ONLY BY DEFAULT. With no flags it opens /proc//mem O_RDONLY, proves the CardsDLL slide against the on-disk FNV prologue, and reports the exact live state of the SBC data flow so the human can see whether a poke would render anything: A = FUT root singleton = *(0x1802e6398) (vtable static 0x18021c2a0) B = SBC request/TTL cache = A + 0x1f9d8 (vtable static 0x1801fae70) B+0x08 collection ptr, B+0x20 QPC deadline, B+0x28 READY byte (the gate) B offset 0x1f9d8 is DECODED live from A.vtable[+0x4e8] thunk (48 8d 81 = lea rax,[rcx+disp32]), not taken on faith. M = SBC categories store = *(A + 0x20a68) (THE RENDER SOURCE) lazy getter A.vtable[+0x9b0] = 0x18011b7d0; category count = WORD[M+0x50]; category vector M+0x58..M+0x60 (stride 0xf0). The SBC menu draws WORD[M+0x50] + 2 tiles. M is NULL until the menu is opened (lazily built, empty offline) or the sbs/sets response is parsed. HUB = sibling cache = A + 0x1fd70 SBC req-mgr = A + 0x2a0 THE VERIFIED GATE (proven byte-exact against the shipped DLL, isValid 0x180065d40): call 0x1801642c0 ; online sub-check -- STUBBED `mov al,1; ret`, not the wall cmp BYTE[rbx+0x28],0 ; je fail ; <-- the READY gate cmp QWORD[rbx+0x8],0 ; je RET_1 ; <-- SHORT-CIRCUIT: coll==0 => return 1 ; only reached when B+0x08 != 0 So isValid returns TRUE with B+0x28=1 AND B+0x08=0 (short-circuit). Writing B+0x08 forces the deadline branch; with a stale/past B+0x20 that returns 0 -> the error modal. That is why this tool NEVER writes B+0x08 or B+0x20 -- doing so can DEFEAT the fix and is a crash risk if the pointer is not a real EASTL collection. THE INTERVENTION THIS TOOL CAN APPLY (--apply) ---------------------------------------------- The ONLY write blessed by adversarial verification as non-crashing from a bare /proc/mem poke is: BYTE[B+0x28] = 1 (arm the SBC ready gate; leave B+0x08 and B+0x20 alone) This OPENS the SBC menu (isValid short-circuits to true) instead of the error modal. It renders EMPTY (2 placeholder tiles) unless M is populated, because tiles come from WORD[M+0x50], not from B. It is the proven-safe NEGATIVE CONTROL / gate-open step. WHY A POPULATED MENU NEEDS THE INJECTED DLL, NOT THIS TOOL ---------------------------------------------------------- Populating M means running the client's OWN parser (deser 0x18017b2b0) over a real sbs/sets response, so it clears+builds M with the correct 0xf0/0x3570 geometry and rebuilds the indices. That requires executing code IN-PROCESS (the openfut-hook DLL) or serving GET ut/game/fifa17/sbs/sets through the bridge so the native completion path populates M and arms B for you. A /proc/mem byte poke cannot build M's nested EASTL vectors safely (hand-building 0xf0/0x3570 records is the highest-crash option all three verifiers rejected), and it cannot call the deser with a seated SAX cursor. Cold-calling the deser with a null cursor WIPES M (clear runs before append) and parses nothing. So: this tool arms the gate; the DLL (spec printed by --spec) does the populate. See docs and the openfut-hook integration notes. RISK / SAFETY ------------- * Default run = READ ONLY. Nothing here writes unless you pass --apply. * --apply WRITES LIVE GAME MEMORY (/proc//mem O_WRONLY): one byte, B+0x28=1. Do this only on a client sitting in the FUT hub, ideally with the SBC menu CLOSED (never mutate while the menu is mid-iterate). Then re-open the SBC menu to render. * --apply re-proves the slide AND re-verifies B.vtable == static 0x1801fae70 before writing, and aborts on any mismatch. It refuses to write anything but B+0x28. * If FIFA17.exe is not running or CardsDLL is not mapped, the tool says so and exits 0 -- static analysis is authoritative; live steps are best-effort. USAGE python3 sbc_hook_poke.py # read-only probe + dry-run plan (default) python3 sbc_hook_poke.py --spec # also print the injected-DLL populate spec python3 sbc_hook_poke.py --apply # WRITE BYTE[B+0x28]=1 (arm gate) -- HUMAN ONLY """ import os, struct, sys # ---- static VAs (image base 0x180000000; add live slide) -------------------- A_SINGLETON = 0x1802e6398 # slot holding A = FUT root singleton ptr CTRL_VA = 0x180180d00 # FNV atom-hash prologue used to prove the slide A_VT_STATIC = 0x18021c2a0 # A.vtable (verify live == this + slide) B_VT_STATIC = 0x1801fae70 # B.vtable (verify live == this + slide) A_VT_BGETTER = 0x4e8 # A.vtable slot -> thunk lea rax,[rcx+0x1f9d8] A_VT_MGETTER = 0x9b0 # A.vtable slot -> M lazy getter 0x18011b7d0 M_CACHE_OFF = 0x20a68 # M cache slot on A (decoded from getter cmp) HUB_OFF = 0x1fd70 REQMGR_OFF = 0x2a0 ISVALID_VA = 0x180065d40 ONLINE_STUB = 0x1801642c0 # expect b0 01 c3 (mov al,1; ret) B_READY_OFF = 0x28 PE_PATHS = ['/tmp/fut/cardsdll.dll', '/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll'] 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 load_pe(): for p in PE_PATHS: try: return open(p, 'rb').read() except Exception: continue return None def print_spec(): print(""" == INJECTED-DLL POPULATE SPEC (openfut-hook / version.dll) =================== The poke tool arms the gate; the DLL must POPULATE M. Preferred, lowest-risk, zero-forged-state path (run ON THE GAME MAIN/UI THREAD, SBC menu CLOSED): Option 1 (best) -- serve the response, let the native chain do everything: Route GET ut/game/fifa17/sbs/sets through the bridge/core with real JSON. The client's own dispatcher builds the response-msg (ctor 0x18017b1c0, deser slot +0x20 = 0x18017b2b0), seats a genuine SAX cursor, and its own chain populates M and arms B via the completion callback 0x1800b8c30 (subscribed in svc ctor 0x1800b5765). No memory forging at all. NOTE: the front-end refuses to ISSUE the fetch offline and the "ut/%s/sbs" template (0x18021d908) has no native xref, so the DLL must inject the RESPONSE at the message-receive layer (not rely on the client to send the GET). Option 2 (fallback) -- drive the real parser from the hook: 1. reg = 0x1800d7170() ; -> ®istry 0x1802c2988 2. mgr = 0x180009c80(&out, reg) ; hashes 0xed84b11/0xed84b12 3. build a REAL seated SAX cursor over canned sbs/sets JSON: ctx = 0x1801c63e0(...) + lexer 0x1801c8060 + an input-source object whose vtable[+0x8] yields your JSON bytes. A null-source cursor parses nothing AND the deser clears M first -> do not cold-call with null. 4. 0x18017b2b0(rcx=ignored, rdx=cursor) ; self-locates mgr, clears M, per-cat ctor 0x180159da0 / cat-deser 0x18017ab80 / finalize 0x180160e50 / append 0x18015a770, then store finalizers 0x180160e00 + 0x180160f30 + 0x180161020, then commit mgr.vtable[+0x8]. Sets WORD[M+0x50]=N. 5. arm gate: A.vtable[+0x4e8](A) -> B; set ONLY BYTE[B+0x28]=1. Do NOT write B+0x08 or B+0x20 (short-circuit; see isValid proof). 6. trigger render: re-open the SBC menu, or fire refresh events 0x756c-0x7574 so the controller re-reads WORD[M+0x50] at 0x1800b5eda. DO NOT: hand-build 0xf0 category / 0x3570 set records for a direct append (deep-copy ctor 0x18015a2b0 derefs inner EASTL sub-vectors -> heap corruption); skip the index-rebuild finalizers (by-index getter 0x180160a80 reads OOB); mutate M while the menu iterates; or run any of this off the main thread. ============================================================================= """) def main(): apply = '--apply' in sys.argv if '--spec' in sys.argv: print_spec() pid = find_pid() if not pid: print("FIFA17.exe not running -> skipping live steps. Static analysis is " "authoritative; no write possible. (see --spec for the DLL plan)") return 0 print("pid %d" % pid) 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 (client not in Ultimate Team yet). Skip live step.") return 0 slide = base - 0x180000000 print("base %#x slide %#x" % (base, slide)) fdr = os.open('/proc/%d/mem' % pid, os.O_RDONLY) rd = lambda va, n: os.pread(fdr, n, va) q = lambda va: struct.unpack(' refuse." % PE_PATHS) os.close(fdr); return 1 f = lambda va: va - 0x180000000 - 0x1000 + 0x400 # .text rva 0x1000 rawptr 0x400 ctl_ok = pe[f(CTRL_VA):f(CTRL_VA)+24] == rd(CTRL_VA + slide, 24) print("CONTROL FNV %s" % ("MATCH" if ctl_ok else "MISMATCH -> ABORT")) if not ctl_ok: os.close(fdr); return 1 # ---- prove the two gate facts from on-disk bytes ------------------------ online_stub = pe[f(ONLINE_STUB):f(ONLINE_STUB)+3] print("online sub-check 0x1801642c0 on-disk = %s %s" % (online_stub.hex(), "(stubbed mov al,1;ret -- NOT the wall)" if online_stub == b'\xb0\x01\xc3' else "(UNEXPECTED)")) # ---- A root + vtable ---------------------------------------------------- A = q(A_SINGLETON + slide) A_vt = q(A) - slide print("A(FUT root) = %#x A.vtable %#x %s" % (A, A_vt, "(match)" if A_vt == A_VT_STATIC else "(MISMATCH static %#x)" % A_VT_STATIC)) # ---- decode B offset live from A.vtable[+0x4e8] thunk ------------------- bthunk = q((A_vt + slide) + A_VT_BGETTER) # A.vtable slot -> thunk VA (live) stub = rd(bthunk, 7) b_off = None if stub[:3] == b'\x48\x8d\x81': # lea rax,[rcx+disp32] b_off = struct.unpack(' %#x stub=%s decoded B offset=%s" % (bthunk - slide, stub.hex(), hex(b_off) if b_off is not None else "?? (expected 0x1f9d8)")) if b_off is None: b_off = 0x1f9d8 # fall back to the model constant, but we warned above B = A + b_off # ---- B cache fields ----------------------------------------------------- def show_cache(name, C, expect_vt=None): vt = q(C) - slide coll = q(C + 0x08); dl = q(C + 0x20); ready = rd(C + B_READY_OFF, 1)[0] tag = "" if expect_vt is not None: tag = "(match)" if vt == expect_vt else "(MISMATCH static %#x)" % expect_vt print(" %-4s @%#x vt=%#x %s coll(+8)=%#x deadline(+0x20)=%#x ready(+0x28)=%d" % (name, C, vt, tag, coll, dl, ready)) return vt, coll, dl, ready print("live cache state:") b_vt, b_coll, b_dl, b_ready = show_cache("SBC", B, B_VT_STATIC) show_cache("HUB", A + HUB_OFF) print(" reqmgr @%#x +0x08=%#x" % (A + REQMGR_OFF, q(A + REQMGR_OFF + 0x08))) # ---- M = the render source --------------------------------------------- M = q(A + M_CACHE_OFF) if M == 0: print(" M (render source, *(A+0x20a68)) = 0 -> NOT built yet " "(SBC menu not opened this session). Empty offline.") cat_count = 0 else: cat_count = w(M + 0x50) print(" M (render source) = %#x WORD[M+0x50] category count = %d " "(menu would draw %d tiles)" % (M, cat_count, cat_count + 2)) # ---- the plan / dry-run ------------------------------------------------- print("\n-- INTERVENTION PLAN --") print(" Verified-safe write (this tool, --apply): BYTE @ %#x (B+0x28) = 1" % (B + B_READY_OFF)) print(" effect: isValid short-circuits TRUE -> SBC menu OPENS instead of modal.") print(" render: EMPTY unless M is populated (tiles = WORD[M+0x50], not B).") print(" REFUSED here (footgun): writing B+0x08 or B+0x20 -> deadline branch,") print(" can return FALSE (modal) and/or crash on a bogus collection ptr.") print(" Populated render: needs the injected DLL to fill M (run with --spec).") if not apply: cur = rd(B + B_READY_OFF, 1)[0] print("\n[DRY-RUN] default mode -- no memory written. current BYTE[%#x]=%d, " "would set =1. Pass --apply to write (HUMAN ONLY)." % (B + B_READY_OFF, cur)) os.close(fdr) return 0 # ---- --apply: the single blessed byte write ----------------------------- # re-verify EVERYTHING load-bearing before touching live memory. if not ctl_ok or A_vt != A_VT_STATIC or b_vt != B_VT_STATIC: print("\n[ABORT] slide/vtable sanity failed at write time -> refusing to write.") os.close(fdr); return 1 if b_off != 0x1f9d8: print("\n[ABORT] B offset decoded as %s (expected 0x1f9d8) -> refusing to write." % hex(b_off)) os.close(fdr); return 1 target = B + B_READY_OFF before = rd(target, 1)[0] print("\n[APPLY] target BYTE @ %#x before=%d" % (target, before)) if before == 1: print("[APPLY] already 1 -> nothing to do (idempotent).") os.close(fdr); return 0 fdw = os.open('/proc/%d/mem' % pid, os.O_WRONLY) n = os.pwrite(fdw, b'\x01', target) os.close(fdw) after = rd(target, 1)[0] print("[APPLY] wrote %d byte(s); read-back BYTE @ %#x = %d %s" % (n, target, after, "(OK)" if after == 1 else "(WRITE FAILED)")) print("[APPLY] now RE-OPEN the SBC menu. Expect: menu opens (no modal); tiles will") print(" be EMPTY/placeholder unless M was populated by the DLL first.") os.close(fdr) return 0 if __name__ == '__main__': sys.exit(main())