docs: Blaze handshake reference, connection-gate findings, closure write-up
- 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>
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
# 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.*
|
||||
Reference in New Issue
Block a user