3649c3d6f0
- blaze-handshake.md: three-layer LSX/Blaze-core/UTAS model + six-step Blaze session ordering, tagged CONFIRMED/SHAPE/UNKNOWN. - connection-gate-findings.md: accumulated RE of the online-connect gate. - closure-and-preservation.md: the capstone. Records what was achieved (clean-room LSX/EbisuSDK emulator booting FIFA to an authenticated offline menu), the wall reached from three directions, the dial-initiation gate RE as the final chapter (Outcome B, circular), and the boundary. Clean-room throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
301 lines
16 KiB
Markdown
301 lines
16 KiB
Markdown
# 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/<pid>/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<MessageType, Handler>` 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.*
|