# OpenFUT — Closure & Preservation Record *A clean-room reverse-engineering effort to restore FIFA 23 Ultimate Team offline after EA's server shutdown (October 2025). This document records what was built, what was achieved, and the precise technical wall at which the effort concludes.* **Status: the online/FUT route is closed at a characterized wall. The offline-menu / EA-App-emulation layer works and is preserved.** **Provenance:** everything here derives from observing the running client's own behaviour — live memory reads, static disassembly of the shipped binary, and the game's responses to our synthesized inputs. No leaked EA source was used or referenced at any point. That clean-room discipline is the legal foundation of the work and is the reason this record can exist. --- ## 1. What OpenFUT set out to do FIFA 23's online services were permanently shut down in October 2025, which removed FIFA Ultimate Team — the mode is backed by EA's online infrastructure and simply cannot start without it. OpenFUT's goal was game preservation: let a legitimately-owned copy run FUT against a **local, emulated backend** instead of EA's dead servers. The intended shape was three cooperating pieces: ``` FIFA 23 client (offline, under Proton/Wine or native Windows) │ EA-App / Blaze / FUT protocols openfut_hook.dll — injected; redirects EA traffic, bypasses cert pinning, observes │ localhost openfut-bridge — answers the EA-App (LSX) protocol; was to answer Blaze + FUT │ openfut-core — the game-independent FUT economy backend (cards, packs, SBCs…) ``` The entry sequence the client runs, in order, each gating the next: 1. **LSX** — the EA App ↔ game local handshake (login, entitlements, config). 2. **Blaze redirector** — "where is my game server?" 3. **Blaze preauth / login / postauth** — establish the online session. 4. **FUT entry** — eligibility, then the FUT hub loads over REST. The plan was to walk these gates one at a time, synthesizing each response and verifying it against the live client (the client is the oracle — a gate is "done" when the game advances). --- ## 2. What was achieved This is preservation documentation, so the wins are recorded first and plainly. They are real, and they stand independent of the wall reached later. ### 2.1 Clean-room injection and TLS neutralization - A `version.dll` / FLE-loaded hook that injects into FIFA 23 under Proton/Wine without tripping the (neutralized) anti-cheat, writing a durable log for observation. - ProtoSSL / cert-verify bypass sufficient to let the game accept our substituted endpoints at the layers we terminate. - Winsock interception (`getaddrinfo`, `connect`, `WSAConnect`, `ConnectEx`) with EA-host and EA-port redirection to the local bridge — including, by the end, **IPv6 (IPv4-mapped) redirection**, which closed a real leak the earlier IPv4-only path had missed. ### 2.2 The LSX / EA-App layer works — the game reaches a fully-authenticated menu offline This is the substantive achievement. The bridge's native LSX server (port 3217) emulates the EA App / EbisuSDK local protocol completely enough that FIFA 23, with EA's servers gone, **boots to its main menu believing it is logged in and online.** Confirmed working, from the bridge's own logs of a live session: - `EALS ChallengeResponse` — the EA login-service challenge/response handshake completes (`handshake complete`). - `EbisuSDK GetConfig / GetProfile / GetGameInfo / GetSetting` — all answered; the game gets its service list, profile, and configuration. - `Utility GetInternetConnectedState → connected=1` — the connectivity check passes. - `XMPP SetPresence → INGAME` — presence is set. - `Login IsLoggedIn=true` and `OnlineStatusEvent isOnline=true` — pushed continuously; the game's own online-status state flips to "online." Everything the EA-App layer is asked for, it receives. This is a genuine, reusable clean-room EA-App/EbisuSDK emulator. ### 2.3 Reverse-engineering infrastructure The effort produced durable tooling and knowledge, all clean-room: - Live memory inspection via `/proc//mem` (Wine maps `FIFA23.exe` flat at ImageBase `0x140000000`), including a stable algorithm to resolve the live Blaze connection manager (`G = *[0x14acd02c0]` → `M = *[G+0x360]` → `ctx = *[M+0x778]`; then a heap scan for the object `P` with `[P+0] == base+0x80200b8` and `[P+8] == M`). - Read-only in-process probes (menu-time snapshots, a write-watchpoint on the dispatch container, transport observation) — all env-gated, none altering game state. - Handshake-independent TLS **SNI capture** on the bridge (peek the ClientHello, parse SNI before the handshake, so the hostname is learned even when the client rejects our cert). - A documented **Blaze handshake reference** (`docs/blaze-handshake.md`) mapping the three-layer LSX / Blaze-core / UTAS model and the six-step Blaze session ordering. --- ## 3. The wall — reached from three independent directions FUT never loaded. The reason is a single wall, and the strongest evidence for it is that three unrelated lines of investigation arrived at the same place. ### 3.1 The forcing arc (memory side) We confirmed the live Blaze connection manager and the in-process "dial" handler (`0x144f4d360`) that begins the Blaze connection when the network comes online. Both exist at the menu. But: - **Forcing the dial with the connection scaffold empty crashes** in normal `.text` (not the VM) at `0x144fd6b6c`: a binary search over a per-connection message-handler table whose `begin` pointer is an uninitialized sentinel (`1`). The table is empty because nothing has populated it. - **Forcing the online state machine to "fully online" (state 2) crashes the anti-tamper VM** deterministically — the game's own protected online code runs against a forced-but- absent session and faults. A forced state cannot substitute for a real session. Conclusion of the arc: the dial and the online state are *primed but unpumped* — real infrastructure that only a genuine connection flow drives, and that cannot be safely forced. ### 3.2 The network / transport arc With the entire EA-App layer satisfied (§2.2) and the network path fully instrumented (including IPv6 and SNI capture), we watched a complete FUT "connecting to EA Servers" attempt. The complete outbound inventory: | Traffic | Identity | Fate | |---|---|---| | 2× IPv6 `:443` → `rl.data.ea.com`, `pin-river.data.ea.com` | EA **PIN/River telemetry** (fire-and-forget) | reached the bridge, **cert-rejected by ProtoSSL** | | 6× IPv6 UPnP (`:1900`, `:80`, LAN) | NAT traversal | — | | LSX (`:3217`) | EA-App layer | fully answered | **No `gosredirector` / `redirector.ea.com`. No Blaze-port (`:42127` / `:10041`) connect. On any transport.** Starting FUT added *zero* new dials. The game is satisfied enough by the EA-App layer that it never attempts a Blaze connection at all. (This also corrected an earlier false belief — "zero outbound sockets" — which turned out to be undecoded IPv6 traffic.) Conclusion of the arc: the wall is **not** transport, **not** an unanswered LSX request, **not** the cert. The game simply never initiates Blaze. ### 3.3 The two arcs meet The forcing arc found, from memory, that the connection scaffold is never populated. The network arc found, from the wire, that no connection is ever attempted. Same wall, two sides. The final chapter explains *why*, from the binary itself. --- ## 4. Final chapter — the dial-initiation gate (why it is circular) This is the concluding technical result: a static reverse-engineering pass answering the one question both arcs left open — *beyond "online," what does the code check before it fires the Blaze dial, and is that condition reachable offline?* ### 4.1 Method Read-only static analysis of the shipped `FIFA23.exe` (`objdump`), two cross-reference techniques: an 8-byte absolute-address search across the whole file (finds function pointers stored in data — vtables, tables) and a disassembly grep for `call`/`jmp`/`lea` targets (finds code references; a `lea reg,[…] # 0x…` is an address being *taken* for registration, a `call 0x…` is a direct invocation). Addresses below are `FIFA23.exe` virtual addresses (ImageBase `0x140000000`). The anti-tamper VM region `[0x14c1cd000, 0x160eb1000)` is unreadable; everything traced here is in normal `.text`. ### 4.2 The caller graph ``` dial handler 0x144f4d360 ▲ address-taken only (lea r9,[dial]) — NEVER called directly, NEVER in a vtable │ inserted into the container [connMgr+0xc38] by: F1 registrar 0x144f4b7b0 (exactly ONE caller) ▲ F2 0x144f4b960 (builds a callback, calls F1 with rcx = connMgr) ▲ TWO callers, identical guard: cmp bpl,[this+0x42] ; cmp bp,[this+0x142] ├── F3a 0x144f44770 └── F3b 0x144f4bbf0 ``` Every reference is a normal-`.text` `lea` — the dial handler is *registered as a callback*, never called by address. The registrar `F1` is a "get-or-create into the message-handler container": on a miss it allocates a handler and copies a caller-supplied callback into it. Its sole caller `F2` supplies the callback and resolves the connection manager (`rcx = [[this+8]+0xca0]`). ### 4.3 The decisive structural fact `F3a` and `F3b` — the two functions that actually decide to register the dial — are, from the whole-binary sweep, **in zero vtables and never directly called.** They are `lea`'d at four sites and **registered into `[connMgr+0xc38]`, each keyed by a `.rdata` message-type descriptor** (`0x1483fcf98`, `0x14808d7d0`). In other words they are themselves **message handlers**, dispatched indirectly by the connection manager when a matching Blaze message arrives — not methods invoked by a state machine you can drive locally. (In Rust terms: not methods on a trait object, but closures inserted into a `HashMap` and called by a dispatcher.) The functions that register *them* (`G_a 0x144f4a350`, `G_b 0x144f45220`) are ordinary helpers inside the same cluster, and the entire cluster `F1…G_b` is vtable-free. So the whole thing is one **self-registering, message-driven state machine on `[connMgr+0xc38]`:** each handler fires when its Blaze message arrives, checks connection state, and registers the next handler — until `F3a`/`F3b` register the dial. The state each step checks (`[this+0x42]`, `[this+0x142]`, …) is connection-lifecycle state, advanced only as messages are processed. ### 4.4 Reachability, and why it is circular Offline, the container `[connMgr+0xc38]` is **empty** (measured directly: the crashing table's `begin` is `0`/garbage; the network arc saw zero Blaze messages). Nothing ever dispatches `F3a`/`F3b`, so their guards never even execute — the wall sits *upstream* of them. The one thing that does fire offline is the connection-state constructor (on the online-status event), so the machine's entrance is *partially* reached and then stalls immediately, because no Blaze message follows to drive the first dispatch. **Outcome: circular gate.** The trigger is *found* — we can see precisely how the dial gets registered and fired. But its precondition is *prior Blaze connection-lifecycle messages having populated and dispatched the handler container* — i.e. the very network exchange the connection is meant to produce. The state that gates the dial requires the dial's own earlier connection steps to have already run. It is not a settable flag (there is none between "online" and "dial"); it is an entire message exchange that never begins. This is the exact mechanical explanation of both arcs: the container is empty because no messages flow (network arc), and forcing the dial into that empty container crashes (memory arc). --- ## 5. What this means — the boundary, stated plainly **OpenFUT can take FIFA 23 to a fully-authenticated main menu, offline, believing it is logged in and online — but it cannot take it into FUT.** FUT requires a Blaze session, and on this build the Blaze connection is never initiated. The bootstrap is circular: the client will only walk its connection state machine as real Blaze messages arrive, and no messages arrive because no connection is begun. "Online," at the EA-App level, is genuinely disconnected from "reach out to Blaze," and the gap between them is not a flag we failed to set — it is a live protocol exchange that has no counterpart to talk to. Two secondary walls sit behind the first, and would matter only if it were solved: - **ProtoSSL cert pinning** (`CertificateUnknown`) rejects our self-signed cert on the DirtySDK path — so even redirected EA HTTPS cannot complete a handshake as-is. - **The anti-tamper VM** faults deterministically whenever forced state is used by the game's own protected online code — so "force the state and let the game run" is not viable. --- ## 6. Paths not taken, scoped honestly Only one route could still reach FUT, and this record scopes it so the decision is clear-eyed rather than reopened casually. **In-process Blaze state-machine injection.** Rather than answer Blaze over a wire (there is no wire — the game never dials), blaze_brain would **drive the game's in-process connection state machine by injecting synthesized Blaze response frames into its dispatch layer.** From the final-chapter RE, that requires: 1. Making the connection appear *initiated*, so the game registers its first response handler. 2. Feeding synthesized Blaze responses **in order and keyed to the same `.rdata` message-type descriptors**, so each registered handler fires, advances the connection-state flags, and registers its successor — walking the machine forward until the dial is registered and fires. This is a weeks-scale build requiring per-message protocol RE done from the *receive* side, against a client that is the only available oracle (the servers are dead, so every frame must be synthesized and validated by whether the client advances). It also inherits an unsolved sub-problem: the initial "connection initiated" state must itself be synthesized or forced, since the game does not produce it offline. It is possible in principle; it is not a small task, and its scope should be understood before it is begun. The alternative — the one this document records — is to treat the fully-authenticated offline menu as the achieved preservation state and close the online/FUT route here. --- ## 7. Preservation value Even without FUT, the effort produced things worth keeping: - **A working clean-room EA-App / EbisuSDK (LSX) emulator** that boots FIFA 23 to an authenticated main menu with no live EA servers — a genuine preservation artifact for the offline single-player state. - **A documented map of exactly where and why online play is unreachable** on this build, grounded in the binary: the dial caller graph, the message-driven container mechanism, and the circular bootstrap. Future work (on this title or the engine family) starts from a known wall, not a blank page. - **Reusable RE tooling and references** — live-memory resolution of the connection manager, the transport/SNI instrumentation, and the Blaze handshake reference doc. - **A clean provenance record** — nothing derived from leaked source, so the work remains usable and shareable. --- ## 8. Closing note OpenFUT reached the last gate it could reach without a live server to talk to, and then proved — from memory, from the wire, and from the binary — that the next gate is not a lock we failed to pick but a door that only opens from the far side. That is a complete and honest result. The offline menu stands; the map is drawn; the wall is named. *This record is the concluding chapter of the OpenFUT reverse-engineering arc.*