13 KiB
SBC "problem communicating with the FIFA Ultimate Team servers" — definitive analysis
Date: 2026-08-07
Binary under study: /tmp/fut/cardsdll.dll (on-disk PE, image base 0x180000000; copy of /mnt/games/FIFA 17/CardsDLL_Win64_retail.dll)
Method: clean-room, read-only. On-disk objdump re-verified in this pass; live values quoted from prior read-only /proc/<pid>/mem reads (pid 12201, slide 0x6ffe7c140000, FNV control MATCH). No memory was written; FIFA was not touched.
VERDICT (one line)
The SBC modal is a CLIENT-SIDE, per-feature completion-path defect — the FUT client never re-arms a fetch/re-render for sbs/sets the way it does for the hub — so NO server response can cure it; the only offline lever is a client-memory patch, and the clean single-byte patch (model+0x1fa00 = 1) only SUPPRESSES the modal by forcing the completion predicate true, rendering from an empty, never-populated cache. It is NOT the go-online wall.
1. What the SBC completion predicate actually checks (CONFIRMED on-disk)
The SBC menu entry runs a completion continuation whose gate is the shared predicate 0x180065d40, called as [cache_vtable+0x08]. Re-disassembled this pass, byte-for-byte:
180065d40 call 0x1801642c0 ; online/liveness sub-check
180065d4e test al,al
180065d50 je fail
180065d52 cmp byte [rbx+0x28],0 ; <-- THE GATE: "value ready" flag
180065d56 je fail
180065d58 cmp qword [rbx+0x8],0 ; pending-op ptr
180065d5d je pass (mov al,1) ; empty-collection shortcut -> success
180065d5f lea rcx,[rsp+0x38]
180065d64 call QueryPerformanceCounter ; [rip]->0x1801e50c0
180065d6a mov rax,[rbx+0x20] ; QPC deadline
180065d6e sub rax,[rsp+0x38]
180065d73 js fail ; deadline passed -> fail
180065d75 mov al,1 ; pass
...
180065d7d xor al,al ; fail
Reduces to: subcheck() && byte[cache+0x28]!=0 && (qword[cache+0x08]==0 || deadline[cache+0x20] not yet past).
- The online/liveness sub-check
0x1801642c0is stubbed OUT. On-disk bytes areb0 01 c3=mov al,1; ret— always true, in the shipped file (not a live loader patch). This is the reason SBC is NOT the go-online wall (see §5). cache(rbx) is an embedded sub-object of the FUT root singletonA = *[0x1802e6398], selected by a vtable thunk (see §2). Its+0x28byte is a "value-ready" flag (init 0 by ctor0x180062460);+0x08is a pending-op pointer;+0x20is a QPC deadline. This is a copyable future/async-result value type. The predicate never reads the parsed SBC categories, HTTP status, session, or any live-connection boolean.
AUTHORITY BOUNDARY: everything the predicate reads lives inside client process memory (
A+…). Nothing in thesbs/setsHTTP response is an input to it. This is a client-authority decision end to end.
2. Why hub passes but sbs/sets fails (CORRECTED after adversarial verification)
Both features run the same predicate function 0x180065d40, but on different embedded caches, reached through different per-response-class continuations. That structural divergence is real and confirmed. The originally-stated reason ("hub passes because its cache +0x28 is set") is WRONG and is corrected here — corroborated by a live measurement (HUB cache +0x28 = 0 while the hub is displayed with no modal) and by the on-disk FALSE-branch disassembly gathered this pass.
The two continuations, side by side (on-disk, this pass)
SBC (FutLoadSetTypesServerResponse) |
HUB (FutGetHubDataServerResponse) |
|
|---|---|---|
| continuation | 0x180154860 |
0x180173770 |
| get singleton A | call 0x18011a830 (mov rax,[0x1802e6398]) |
same |
| select cache | call [rdx+0x4e8] → thunk 0x18011c1f0 = lea rax,[rcx+0x1f9d8] → SBC cache A+0x1f9d8 |
call [rdx+0x1f8] → thunk 0x18011a810 = lea rax,[rcx+0x1fd70] → HUB cache A+0x1fd70 |
| predicate | call [rdx+0x08] = 0x180065d40 |
same 0x180065d40 |
| on TRUE (jne) | render 0x18015491a → 0x180154600 |
render 0x18017383d → 0x1801735e0 |
| on FALSE | lea rdx,[rbp-0x9] (descriptor 0x18020a8b8); call 0x18016c330; jmp return |
call 0x1801213b0 (state reset); lea 0x1801736f0 (continuation fn); call 0x18011f8e0 (register completion closure); lea 0x18022cd30 (descriptor); call 0x18016c330; call 0x18011f900 (cleanup) |
What this proves
-
0x18016c330is NOT an SBC-only "modal" function. The HUB continuation calls the very same0x18016c330(at0x18017382c) on its own not-ready branch. It is a shared, descriptor-parameterized async dispatcher; SBC passes descriptor0x18020a8b8, hub passes0x18022cd30. -
At idle both predicates return FALSE. Live: HUB cache
A+0x1fd70+0x28 = 0and SBC cacheA+0x1f9d8+0x28 = 0, both+0x08 = 0. The hub is on screen with no modal while its own predicate would return FALSE. So "hub+0x28is set" is false; a set flag is not what makes the hub pass. -
The real asymmetry is the FALSE-branch work. On not-ready the HUB continuation resets its request-state region (
0x1801213b0), registers a completion closure (0x18011f8e0, continuation0x1801736f0) so the arriving response re-runs the continuation and re-renders, then cleans up (0x18011f900). It is a proper get-or-fetch: cache-miss → (re)issue request → render on completion. The SBC continuation does NONE of that — it fires the dispatcher once with delegate0x180154590/descriptor0x18020a8b8and returns. It never re-arms a fetch and never wires thesbs/setsresponse back into a re-render.
Conclusion: hub and SBC diverge at the cache-selection call site ([rdx+0x1f8] vs [rdx+0x4e8], one instruction apart), and — decisively — in the not-ready handling. The modal is produced downstream in the SBC dispatched path (dispatcher 0x18016c330 + delegate 0x180154590), because the SBC feature is wired as a one-shot with no re-fetch/re-render, whereas the hub is wired as a self-rearming get-or-fetch. It is not decided by cache selection alone, not by the shared predicate, and not by the +0x28 byte value at idle.
3. VERDICT by route — is SBC beatable, and how?
| Route | Outcome | Why |
|---|---|---|
| A. Server response field / header / status | RULED OUT — no offline fix here | No field in the sbs/sets body reaches the predicate (client-authority §1). Deeper: the SBC continuation never registers a completion closure to consume the response and re-render, so even a perfect response is dropped on the floor. The deserializer 0x18017b2b0 returning TRUE is genuinely irrelevant. |
B. Client memory byte patch model+0x1fa00 = 1 |
Suppresses the modal, but empty menu — cosmetic | Forces predicate TRUE → routes to the SBC render branch 0x18015491a → 0x180154600, which reads the embedded SBC cache. That cache was never populated (+0x08 == 0, empty collection), so the likely result is an empty / non-functional SBC screen, not populated SBCs. Untested under the read-only rule. |
C. Config FUT/SBC_USE_STUBS (rdata 0x1802270f8) |
Not the gate | Read at the deser top only; the normal (off) path already runs. Flipping it does not touch +0x28 or the continuation wiring. |
| D. "Needs the go-online wall solved" | REFUTED | The only connection-like sub-check on this path (0x1801642c0) is stubbed to always-true on-disk. SBC is blocked by local per-feature completion wiring, not by the reconnect gate. See §5. |
| E. Client CODE patch of the SBC FALSE-branch | The only route to a functional SBC menu | Make 0x180154860's not-ready branch replicate the hub's sequence: state reset 0x1801213b0 + register completion closure 0x18011f8e0/0x1801736f0 + dispatch + cleanup 0x18011f900, so the sbs/sets response is fetched and rendered. This is a code patch, not a byte flip and not a server change. Out of scope for a server-side preservation fix; a client-side authority modification. |
Bottom line: there is no server-side fix. SBC is "beatable" only in the client-authority sense — either cosmetically (byte B, hides the modal over an empty menu) or functionally (route E, a code patch replicating the hub's re-arm). Neither is a change our offline server can make.
4. Memory patch details (if used) — flagged CLIENT-SIDE AUTHORITY
CLIENT-SIDE AUTHORITY — this is a modification of the FIFA client's own process memory, not an OpenFUT server response. It changes what the client decides, and it violates the current read-only rule; it is documented for completeness, not endorsed as the fix.
-
Cosmetic modal-suppression (route B):
- Absolute displacement into FUT root singleton:
A + 0x1f9d8 + 0x28=model + 0x1fa00, whereA = *[0x1802e6398]. - Live absolute (pid 12201 snapshot):
0xb8402538 + 0x28 = 0xb8402560. - Value: write
0x01(one byte). - Effect: predicate
0x180065d40short-circuits atcmp byte[rbx+0x28],0→ with+0x08==0the empty-collection shortcut returns TRUE → continuationjne 0x18015491arenders. Modal gone; SBC cache empty → expect an empty/possibly-broken menu. Not verified (read-only). - Persistence: the object is embedded in the singleton (singleton lifetime). The SBC path calls only
[vt+0x08]; nothing on this path calls the invalidator[vt+0x10]=0x180065d20, so a write should persist across menu re-entry (inferred from structure, not demonstrated).
- Absolute displacement into FUT root singleton:
-
Functional fix (route E) requires a
.textpatch to the SBC continuation, not a data byte — see §3 row E. Do not confuse the two.
5. Relationship to the online-modes / go-online-wall finding
SBC is not the same wall as online Draft's "PRESS Q TO RECONNECT":
- The single connection-like sub-check reachable from the SBC predicate,
0x1801642c0, is compiled out (mov al,1; ret) in the shipped binary. The SBC gate therefore encodes no unmet network condition — it is a purely local completion-wiring problem. - The online modes differ structurally: their gate keeps a real pending network op at
+0x08and/or a non-stubbed sub-check, so their predicate encodes a network state a local byte-flip cannot satisfy. That is why the online wall is not beatable by a byte and SBC's modal is (cosmetically). - This is consistent with the prior "refusing modes = no server fix" finding: no field, count, header, or status in any HTTP response flips the client-side completion state for these features. SBC extends that finding with the precise mechanism — the client never re-arms the
sbs/setsfetch/re-render at all.
Appendix — confirmed addresses (image base 0x180000000)
| Symbol | Address | Note |
|---|---|---|
| FUT root singleton getter | 0x18011a830 |
mov rax,[0x1802e6398]; ret |
| FUT root singleton ptr | [0x1802e6398] |
live A = 0xb83e2b60 |
| FUT root vtable (static) | 0x18021c2a0 |
|
| SBC cache selector thunk | 0x18011c1f0 |
lea rax,[rcx+0x1f9d8] (slot A.vt+0x4e8) |
| HUB cache selector thunk | 0x18011a810 |
lea rax,[rcx+0x1fd70] (slot A.vt+0x1f8) |
| SBC cache | A+0x1f9d8 |
vtable 0x1801fae70; live 0xb8402538 |
| HUB cache | A+0x1fd70 |
vtable 0x18021c1e0 |
shared predicate isValid |
0x180065d40 |
cache.vt+0x08 for both |
| stubbed online sub-check | 0x1801642c0 |
b0 01 c3 = mov al,1; ret |
| cache ctor / copy-ctor | 0x180062460 / 0x1800c21f3 |
init byte[+0x28]=0 |
| invalidator | 0x180065d20 |
cache.vt+0x10; not called on SBC path |
| SBC continuation | 0x180154860 |
class RS4:FutLoadSetTypesServerResponse (str 0x1802270b8, vt row 0x180227090) |
| HUB continuation | 0x180173770 |
class RS4:FutGetHubDataServerResponse (str 0x18022ce40, vt row 0x18022ce18) |
| shared async dispatcher | 0x18016c330 |
called by BOTH FALSE-branches (SBC 0x180154913, HUB 0x18017382c) |
| SBC delegate / descriptor | invoke 0x180154590 / desc 0x18020a8b8 |
|
| HUB re-arm: state reset | 0x1801213b0 |
HUB-only, 0x1801737bd |
| HUB re-arm: register closure | 0x18011f8e0 (cont. 0x1801736f0) |
HUB-only, 0x180173815 |
| HUB re-arm: cleanup | 0x18011f900 |
HUB-only, 0x180173836 |
| SBC render branch (on TRUE) | 0x18015491a → 0x180154600 |
reads empty SBC cache |
sbs/sets deserializer |
0x18017b2b0 |
returns TRUE unconditionally (mov al,1 @0x18017b751); irrelevant to predicate |
| QueryPerformanceCounter import | 0x1801e50c0 |
Which prior conclusion won: the structural divergence (same predicate, different cache, different continuation; online sub-check stubbed; not server-fixable) is upheld. The specific pass/fail reason is corrected: it is the FALSE-branch re-arm asymmetry, not a set +0x28 byte and not an SBC-exclusive 0x18016c330.