feb03fec78
Add an unsolicited event-push path to the LSX server: after the session establishes, re-push (every 2s) a Login then OnlineStatusEvent, encrypted with the live session seed and null-terminated like any session frame. Formats RE'd from FIFA23.exe's OriginSDK deserializers (authoritative): <Login UserIndex="0" IsLoggedIn="true" LoginReasonCode="0"/> (LoginT, +0x287a30) <OnlineStatusEvent isOnline="true"/> (OnlineStatusEventT, +0x28a4d0) Value encoding is the true/false string literal (FIFA's bool parser at FIFA23.exe+0x28fb50 compares to "false"); isOnline="1" is parsed as not-online. Result (verified with in-process probes): FIFA parses both events every cycle and returns success, and stops its impatient GetProfile polling / sets INGAME presence — the first time FIFA has been driven "online" at the EbisuSDK level (anadius, being offline DRM, never does this). But FIFA does not proceed to the GetAuthCode→Nucleus →Blaze chain; the gate is its game-side event consumer (virtual dispatch at FIFA23.exe+0x274d4e2), not delivery. Full trail in docs/connection-gate-findings.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
770 lines
43 KiB
Markdown
770 lines
43 KiB
Markdown
# Connection-gate findings (clean-room)
|
||
|
||
**Status:** observed behaviour only — derived from running the client and reading
|
||
the repack's own files. No EA source used. Unconfirmed values are marked
|
||
`TODO/CONFIRM` rather than guessed.
|
||
|
||
## Components (observed)
|
||
|
||
| Component | Role |
|
||
|---|---|
|
||
| `FIFA23.exe` | Game. Statically links EA's **Ebisu SDK** (online) and **DirtySDK/ProtoSSL** (transport). Loads at fixed base `0x140000000` — no ASLR seen across launches. |
|
||
| `_ProtoSSLSendPacket` @ `FIFA23.exe+0xEFA530` | DirtySDK TLS record assembler — plaintext in, encrypts in place. Already located + hooked. |
|
||
| `anadius64.dll` | anadius EA-app/Origin **LSX server** emulator (ASLR'd). Provides offline DRM / entitlements / persona so the game *launches*; deliberately keeps it **offline**. |
|
||
| `FakeEAACLauncher` | Anti-cheat bypass only. Irrelevant to the online gate. |
|
||
|
||
## Observed online-startup flow
|
||
|
||
1. Ebisu SDK opens LSX socket(s) to anadius; a **challenge/response handshake**
|
||
completes — captured XML: `<Challenge>` / `<ChallengeResponse>` /
|
||
`<ChallengeAccepted>`, `sender="EALS"`, `ContentId 16115019`.
|
||
2. River/PIN telemetry `<session type=boot>` is emitted.
|
||
3. **No further socket traffic.** Clicking Ultimate Team → "connecting to the EA
|
||
Servers…" → **zero** network attempts (no Blaze DNS, no `connect`, nothing on
|
||
`send`/`recv`/`WSASend`/`WSARecv`) → falls back to offline.
|
||
|
||
## Conclusion: the decision is upstream and in-process
|
||
|
||
- The online/offline verdict is delivered through anadius's **in-process
|
||
detoured Ebisu calls**, not socket messages. (No `GetAuthCode`/`GoOnline`/
|
||
entitlement query appears on any socket, sync or async.)
|
||
- The game therefore **never reaches DirtySDK/ProtoSSL for Blaze** — it aborts
|
||
before any `ProtoSSLConnect(gosredirector…)`. The gate is an **Ebisu-SDK
|
||
connection-state / online-session check upstream of the transport.**
|
||
- `ONLINE_ACCESS` is a `Blaze::Nucleus::EntitlementType` (enum value `1`).
|
||
anadius's `GoOnline` LSX handler (`anadius64.dll+0x2BB90`) is a success stub;
|
||
`anadius64.dll+0xB6B1` is a large entitlement/profile builder that *does*
|
||
reference `ONLINE_ACCESS`. Reverse-engineering that entitlement-status logic
|
||
is the **wrong fight** (see strategy).
|
||
|
||
## Strategy (decided)
|
||
|
||
Do **not** make anadius report "online." anadius exists to keep the game
|
||
offline, and it can't be removed without breaking launch/DRM. Instead:
|
||
|
||
> Let the game run its **real** online flow and answer that flow ourselves via
|
||
> the hook + brain. Target the Ebisu connection-state check the FUT-entry path
|
||
> polls; make the game *pass* it so it issues the real `ProtoSSLConnect` to the
|
||
> Blaze redirector → our redirect + ProtoSSL hook capture the real frames.
|
||
|
||
This deletes the "reverse-engineer anadius's entitlement logic" problem.
|
||
|
||
## Next RE step (converges with the ProtoSSL hook)
|
||
|
||
1. Locate and **read-only hook `ProtoSSLConnect`** (same DirtySDK module and
|
||
technique that found `_ProtoSSLSendPacket`). Confirms the game never
|
||
initiates the Blaze connection (pins the gate strictly upstream) and gives us
|
||
the exact symbol that will flip from "never called" to "called" on success.
|
||
2. Locate the Ebisu **connection-state getter** the FUT-entry / "connecting" UI
|
||
polls. Candidate string anchors (observed): `ONLINE_STATUS_EVENT`,
|
||
`GoOnline` result handling, the "connecting to the EA Servers" UI trigger.
|
||
3. Determine the minimal intervention to make that getter report **connected**
|
||
(out-hook it in our `version.dll`, winning over anadius's detour) so the game
|
||
proceeds to `ProtoSSLConnect`.
|
||
- `TODO/CONFIRM`: whether out-hooking the getter is sufficient, or secondary
|
||
checks (auth-token presence) also gate the attempt.
|
||
|
||
---
|
||
|
||
## M1 results (read-only investigation)
|
||
|
||
Method: `protossl-scan` (xref / disasm / callers, app-module-restricted) against
|
||
the live client, plus the existing `connect`/DNS/LSX hooks. Observed behaviour
|
||
only — no EA source.
|
||
|
||
### Point 1 — is `ProtoSSLConnect` reached on the "connecting" abort? **NO — gate is upstream. CONFIRMED.**
|
||
- The existing `connect` / `GetAddrInfoW` / `getaddrinfo` hooks show the client
|
||
makes **zero** network attempts during the "connecting to the EA Servers"
|
||
abort: no DNS for any `gosredirector.*` host, no `connect` to any external
|
||
address.
|
||
- The DirtySDK/ProtoSSL transport is therefore never reached — the abort is
|
||
entirely upstream and in-process.
|
||
- `ProtoSSLConnect` has no debug string and sits behind the DirtySDK API, so an
|
||
explicit hook on it isn't needed to prove this; the behavioural evidence is
|
||
conclusive. `TODO/CONFIRM`: locate `ProtoSSLConnect` by signature later, as a
|
||
positive M2 trip-wire (it should fire once we flip the gate).
|
||
|
||
### Surface mapped (clean-room)
|
||
- **Redirector host table** (`FIFA23.exe` .rdata, pointer table @ `+0x83FC858`):
|
||
`spring18.gosredirector.{sdev,stest,scert}.ea.com` + production
|
||
`spring18.gosredirector.ea.com`.
|
||
- **Redirector request/response config** (same region): `X-BLAZE-ERRORCODE`,
|
||
**`Authorization:`**, `<errorCode>` — the redirector request carries an
|
||
**Authorization header (a Nucleus token)**.
|
||
- **Enum-name tables** (serialization only, NOT decision code): `ONLINE_ACCESS`
|
||
(`Blaze::Nucleus::EntitlementType` = 1), `ONLINE_STATUS_EVENT`, … These are
|
||
reflection tables keyed by enum *value*; string-xref of them is a dead end for
|
||
the decision (the decision compares values, not strings).
|
||
|
||
### Point 2 — name the connection-state function: **PARTIAL.**
|
||
- The exact connection-state decision is behind heavy C++ vtable indirection
|
||
(the redirector config's two code pointers resolved to a virtual-dispatch
|
||
thunk @ `FIFA23.exe+0x27BD4C0` and a `ret 0` stub @ `+0x4F3CBC0`). The
|
||
memory-scan toolkit (string / pattern / xref / callers) cannot efficiently
|
||
navigate this.
|
||
- **Pinning the exact function needs a static disassembler with decompilation
|
||
(Ghidra / IDA) on `FIFA23.exe`.** `protossl-scan` remains the bridge to the
|
||
live process (mapping static addresses to the ASLR'd runtime, confirming hits,
|
||
installing hooks).
|
||
- `TODO/CONFIRM`: name the connection-state getter by module+offset via Ghidra.
|
||
|
||
### Point 3 — gate count: **TWO gates (state + token). Evidence-backed.**
|
||
- The online-connect path **reads/requires an auth (Nucleus) token**: the
|
||
redirector request carries an `Authorization:` header, and the SDK surface has
|
||
`GetAuthCode` / `FakeAuth` / `<GameToken>`. So it is **not** a single
|
||
connection-state boolean flip — even with the state forced "online", the client
|
||
needs a valid token to build the redirector request.
|
||
- **Conclusion for M2: plan for two gates** — (a) the connection-state decision,
|
||
and (b) supplying an auth token the client accepts.
|
||
- `TODO/CONFIRM` the exact abort point (no-token vs state-says-offline vs both) —
|
||
needs Ghidra-level control-flow tracing.
|
||
|
||
### Dynamic probe result (read-only) — `GoOnline` is NOT the gate
|
||
A read-only detour on anadius's `GoOnline` handler (`anadius64.dll+0x2BB90`)
|
||
shows the game **does** call `GoOnline` during the "connecting" attempt (incl.
|
||
on the Ultimate Team click) and anadius returns success — **yet no Blaze
|
||
connection follows** (still zero external `CONNECT`/DNS).
|
||
|
||
Therefore the gate is **downstream of `GoOnline`**: the game decides to go
|
||
online and the request is accepted, then it aborts at the **auth-token step
|
||
(`GetAuthCode`) and/or while waiting for the "online established" status
|
||
callback** (`ONLINE_STATUS_EVENT`), and times out into offline.
|
||
|
||
This sharpens the two-gate picture: `GoOnline` passes; the real blocker is the
|
||
**token / online-status step**. `TODO/CONFIRM` which (token-missing vs
|
||
status-never-fires) — via a `GetAuthCode` probe and/or Ghidra.
|
||
|
||
### Recommendation / next
|
||
Name the downstream gate. Two complementary routes:
|
||
- **Dynamic:** probe anadius's `GetAuthCode` handler (does it return a token or
|
||
fail?) — distinguishes token-gate from status-callback.
|
||
- **Static (Ghidra):** xref the `GoOnline` / `GetAuthCode` / `ONLINE_STATUS_EVENT`
|
||
strings in `FIFA23.exe` to find the FUT online-flow code that issues `GoOnline`
|
||
then waits for the token/status, and decompile it. FIFA isn't ASLR'd, so Ghidra
|
||
addresses (image base `0x140000000`) map 1:1 to our recorded offsets.
|
||
|
||
---
|
||
|
||
## M1 COMPLETE — the gate is `GetInternetConnectedState`
|
||
|
||
Found without Ghidra, by reading anadius's LSX **command-registration** function
|
||
(`anadius64.dll+0x14C0`), which lists every command-name → handler inline. NB the
|
||
`lea rax,[handler]` is **offset by one** from the `lea rdx,[name]` in that listing
|
||
(verified empirically: `+0x27060` decompiles to `GetProfile` — it builds a
|
||
`GetProfileResponse` for persona "fun"). Corrected handler map:
|
||
|
||
| anadius LSX command | handler (anadius64.dll + …) |
|
||
|---|---|
|
||
| GetProfile | 0x27060 |
|
||
| **GetInternetConnectedState** | **0x27790** |
|
||
| GoOnline | 0x2BB90 |
|
||
| GetAuthCode | 0x2BBC0 |
|
||
|
||
### The gate, named: `GetInternetConnectedState` @ `anadius64.dll+0x27790`
|
||
It serializes an LSX `InternetConnectedState` response with a `connected`
|
||
attribute whose value is:
|
||
|
||
```
|
||
connected = (byte[+0xCAB1B] != 0 || byte[+0xCAB1A] != 0) ? str(+0xAF530)
|
||
: str(+0xADE64)
|
||
```
|
||
|
||
Both flag bytes **default to 0**, so it reports the offline value (`+0xADE64`).
|
||
That is precisely why the client sits at "connecting to the EA Servers" and falls
|
||
back offline.
|
||
|
||
### Answers to the three M1 points
|
||
1. **ProtoSSLConnect reached on the abort? NO — gate upstream.** Confirmed.
|
||
2. **The connection-state function:** `GetInternetConnectedState` @
|
||
`anadius64.dll+0x27790`. Decision = the two-flag branch above.
|
||
3. **Gate count: ONE actionable gate.** The auth token is already satisfied —
|
||
`GoOnline` (`+0x2BB90`) is called during the attempt and returns success, and
|
||
anadius provides a fake auth code; the blocker is purely the connection-state.
|
||
So M2 = make `GetInternetConnectedState` report **connected** (then the game
|
||
proceeds with its existing token).
|
||
|
||
### M2 attempt results — the gate is an async EVENT, not a poll
|
||
Tried (read/write, EAAC neutralized):
|
||
- Forced `GetInternetConnectedState` → connected (set flags +0xCAB1A/+0xCAB1B → value
|
||
`"1"`; strings confirmed: +0xADE64 = `"0"` offline, +0xAF530 = `"1"` connected).
|
||
- Flipped `GoOnline` to report `"1"` (replicated its builder `+0x25BE0` with the
|
||
connected string instead of `"0"`).
|
||
|
||
**Neither made the game proceed.** Both handlers fire, no crash — but the game
|
||
**keeps retrying `GoOnline` every ~7s** and never attempts the Blaze connect.
|
||
That retry-on-timeout pattern means the FUT-online flow is **event-driven**: the
|
||
game submits `GoOnline`, gets success, then **waits for an async "online
|
||
established" event** (ONLINE_STATUS_EVENT-class) that anadius — being offline-only
|
||
— never pushes (the only `<Event sender="EALS">` it ever sends is the Challenge
|
||
handshake). So flipping poll/return values can't unblock it.
|
||
|
||
**Implication:** getting past "connecting" requires **emulating the EA-app online
|
||
event sequence** the game waits for (inject the online-status event over LSX, in
|
||
the format/order EbisuSDK expects), not a single function flip. This is a
|
||
substantially deeper task (and precedes the Blaze backend emulation).
|
||
|
||
Next: trace the FIFA-side FUT online-flow (what the game does after `GoOnline`
|
||
and exactly which event/condition it waits on) — Ghidra on FIFA23.exe (import
|
||
saved at `C:\openfut\gh-proj`), or RE anadius's LSX event-send path. `TODO/CONFIRM`.
|
||
|
||
### M2 deeper finding — worker-thread + event architecture (confirmed)
|
||
A live call-stack capture from inside the GoOnline detour (manual stack scan,
|
||
bounded by `GetCurrentThreadStackLimits`) found **zero FIFA23.exe frames** and
|
||
showed `sp` sitting ~2.4 KB below the thread's stack top. So the handler runs at
|
||
the top of a short stack — i.e. on an **anadius worker thread** (IOCP/threadpool),
|
||
not FIFA's calling thread. anadius **queues** the GoOnline command and a worker
|
||
services it.
|
||
|
||
Combined with the retry behaviour, the architecture is now clear and three-way
|
||
corroborated: **FIFA calls `EbisuSDK::GoOnline` → anadius queues it → returns →
|
||
FIFA waits for an async "online established" event → anadius (offline-only, no
|
||
online-event code) never pushes it → timeout/retry.** No handler-response flip
|
||
can unblock this; the game waits on a *push* anadius never produces.
|
||
|
||
**Conclusion:** crossing this gate requires emulating the EA-app online-event
|
||
sequence (synthesize + inject the online-status event on the worker→game callback
|
||
/ LSX path, in EbisuSDK's expected format) — a research-grade emulation effort,
|
||
preceding the Blaze backend. The cheap in-process flips are exhausted.
|
||
|
||
Reaching the FIFA-side flow would need either: (a) locate `EbisuSDK::GoOnline` in
|
||
FIFA23.exe via anadius's detour table, then `callers` to the online-flow; or
|
||
(b) find anadius's LSX event-send path and reverse the online-event format. Both
|
||
are deep. `TODO/CONFIRM`.
|
||
|
||
### Path A attempt: reach the FIFA-side online-flow (blocked with live toolkit)
|
||
Goal: find `EbisuSDK::GoOnline` in FIFA23.exe → `callers` → the game's online-flow
|
||
→ read what event it waits on. Every angle our live-memory toolkit offers is
|
||
blocked:
|
||
- **String xref:** FIFA23.exe contains no `"GoOnline"` string (typed SDK call,
|
||
not a string-built command).
|
||
- **Call-stack from the handler:** GoOnline runs on an anadius worker thread; a
|
||
bounded stack scan finds zero FIFA frames.
|
||
- **Detour scan (`jmpscan`):** scanning FIFA23.exe for function-entry `E9` jumps
|
||
leaving the module yields ~3875 hits — overwhelmingly false positives, because
|
||
the 505 MB image is mostly embedded *data* (not code), and the real detours
|
||
don't cleanly cluster. (A .text-section-only scan would help but the chain
|
||
after — isolate GoOnline → callers → event format → emulate — remains long and
|
||
each link is gated by SDK abstraction / anadius indirection / worker threads.)
|
||
|
||
**Verdict:** crossing this gate to *playable* FUT is research-grade. It needs an
|
||
interactive disassembler (IDA/Ghidra GUI, human-driven) to trace the EbisuSDK
|
||
online-flow, and then a full EA-online + Blaze emulator. The live-memory toolkit
|
||
(string/xref/disasm/callers/read/jmpscan) has been exhausted for the FIFA side.
|
||
The clean-room spec (this document) is the finished, valuable artifact.
|
||
- Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius
|
||
config / a hidden option (cheapest flip).
|
||
- Else out-detour `GetInternetConnectedState` in our `version.dll` to force the
|
||
`+0xAF530` ("connected") path.
|
||
- Then watch for the client to attempt the real Blaze connect (our `connect`
|
||
redirect + ProtoSSL hook capture the first plaintext — milestone M3).
|
||
- `TODO/CONFIRM`: exact text of the offline/connected value strings
|
||
(`+0xADE64` / `+0xAF530`); whether any secondary check gates the attempt after
|
||
the state flips.
|
||
|
||
---
|
||
|
||
## LSX delivery channel — 2026-06-30
|
||
|
||
**Question:** does FIFA's port-3216 connection reach the bridge LSX server, or is it
|
||
serviced inside anadius before the TCP stack (making the bridge irrelevant)?
|
||
|
||
### Evidence from `openfut_hook.log` (6 consecutive sessions, no bridge running)
|
||
|
||
```
|
||
connect_hook: call 1.0.0.127:3216 sock_type=1
|
||
connect_hook: result=0 wsa_err=0
|
||
```
|
||
|
||
- `1.0.0.127:3216` — FIFA's EbisuSDK connects to `127.0.0.1:3216` (address in the log
|
||
is little-endian u32; port is big-endian u16 → both decode to `127.0.0.1:3216`).
|
||
- `sock_type=1` — `SOCK_STREAM` (TCP), not a pipe or named-object.
|
||
- `result=0 wsa_err=0` — the OS `connect()` syscall returned success.
|
||
- This line appears exactly once per game session, at early startup (DLL load phase),
|
||
before the game reaches any menu. No other external `connect()` calls are logged.
|
||
|
||
### Implication: anadius is NOT in-process-only for LSX
|
||
|
||
Our hook is an **inline detour on the real `ws2_32.dll!connect`** function (not an IAT
|
||
patch). It fires regardless of who calls `connect()`. Because it fires for the 3216
|
||
call and `result=0`, the connection goes through the real TCP stack — it is NOT
|
||
intercepted inside anadius before the socket layer.
|
||
|
||
In the sessions above, anadius was the answering server (bridge not running); it binds
|
||
`127.0.0.1:3216` as a real Wine/Proton TCP socket. **Anadius is a true TCP server on
|
||
port 3216, not an in-process detour.**
|
||
|
||
### Bridge delivery test (native Linux process)
|
||
|
||
Bridge binary confirmed bindable: `./target/release/openfut-bridge` starts and logs
|
||
`LSX server listening on 127.0.0.1:3216` immediately. Port 3216 is free before FIFA
|
||
launches (anadius only binds it when FIFA/anadius64.dll loads).
|
||
|
||
**Pending live test:** start the bridge (native Linux process → real Linux socket on
|
||
3216), THEN launch FIFA. When EbisuSDK calls `connect(127.0.0.1:3216)`:
|
||
- If bridge receives it → bridge logs `DEBUG openfut_bridge::lsx: LSX: connection from 127.0.0.1:xxxxx`
|
||
- If anadius somehow wins (or game crashes because anadius can't bind) → bridge logs nothing
|
||
|
||
**Run once to confirm:**
|
||
```bash
|
||
cd /home/alex/Documents/OpenFUT/openfut-bridge
|
||
RUST_LOG=openfut_bridge=debug ./target/release/openfut-bridge 2>&1 | tee /tmp/bridge-lsx-test.log &
|
||
# then launch FIFA normally, reach any menu, check log:
|
||
grep "LSX:" /tmp/bridge-lsx-test.log
|
||
```
|
||
|
||
### Pre-test concern: LSX challenge-response AES key
|
||
|
||
The bridge `lsx.rs` uses a placeholder `AES_KEY = [0,1,2,...,15]`. The real key was
|
||
RE'd from anadius but not yet substituted. If FIFA validates the `ChallengeAccepted`
|
||
response the bridge sends, the handshake fails and FIFA may not reach the main menu.
|
||
This does **not** affect the delivery classification — `LSX: connection from ...` fires
|
||
at TCP `accept()`, before any protocol. But it may cause the game to stall/crash after
|
||
the first connection, which would be a separate issue to fix.
|
||
|
||
### Classification — CONFIRMED 2026-07-02 (supersedes the 2026-06-30 prediction)
|
||
|
||
**CONFIRMED NEGATIVE — a host-side bridge on 3216 does NOT receive FIFA's LSX
|
||
traffic. Cause: anadius handles LSX *in-process*, inside FIFA23.exe, before the
|
||
connect reaches the host TCP stack. This is NOT a network/loopback problem — the
|
||
network namespace is shared and the bridge is reachable.**
|
||
|
||
The 2026-06-30 prediction that this subsection replaced was wrong on both counts
|
||
(it guessed `"anadius intercepts in-process" = RULED OUT` and `"bridge receives
|
||
LSX" = EXPECTED`); the live test showed the exact opposite.
|
||
|
||
**Evidence (live test, bridge holding host 127.0.0.1:3216 throughout):**
|
||
- **Namespace is shared, not isolated.** `readlink /proc/<FIFA23.exe>/ns/net`
|
||
equals the host's `/proc/self/ns/net` (both `net:[4026531833]`). Pressure-vessel
|
||
/umu does not unshare the network — Wine's `127.0.0.1` *is* the host's.
|
||
- **The bridge is reachable and logs correctly.** A host-side
|
||
`connect(127.0.0.1:3216)` was accepted, logged `DEBUG openfut_bridge::lsx: LSX:
|
||
connection from 127.0.0.1:34730`, and returned the 157-byte LSX greeting. So
|
||
"bridge logged nothing during FIFA" is a real absence, not a logging/filter
|
||
artefact.
|
||
- **FIFA's connect succeeds but never reaches the bridge.** `openfut_hook.log`
|
||
shows 8× `connect_hook: call 1.0.0.127:3216 ... result=0 wsa_err=0` (real,
|
||
successful connects) while the bridge log stayed at its 8-line startup baseline
|
||
(zero `LSX: connection from`). FIFA then advanced to "connecting to FUT" — anadius
|
||
satisfied LSX in-process; the hook log stopped growing once it did.
|
||
- **Our hook does not fake the 3216 result — it passes it through.** In
|
||
`openfut-launcher/openfut-hook/src/connect_hook.rs`, `redirect_if_ea` rewrites
|
||
only 443/10041/42127; port 3216 falls through to the real `ws2_32!connect` and its
|
||
genuine return is logged. The comment there ("3216 ... handled by the native
|
||
openfut-bridge LSX server — just let connect() go through") encodes the disproven
|
||
premise: "going through" reaches anadius in-process, not the host bridge.
|
||
|
||
**Conclusion:** the "native Linux bridge binds 3216 first and wins the race"
|
||
strategy cannot work as designed — there is no host-stack connect to win; anadius
|
||
answers inside the process. A host-side LSX server is architecturally off the path.
|
||
|
||
> **OVERTURNED 2026-07-02** — see the next section. This conclusion was correct only
|
||
> for *port 3216*. anadius keys its in-process interception on port 3216 specifically;
|
||
> a hook-side redirect of FIFA's connect to a *different* host port (3217) slips past
|
||
> it and lands on the host bridge over the shared loopback. A host-side LSX server
|
||
> **does** work once the redirect is in place. The "fix direction" below is now done.
|
||
|
||
**Consequences / open items:**
|
||
- `TODO/CONFIRM`: the exact interception layer anadius uses (IAT hook on FIFA's
|
||
`connect` import / Winsock LSP / inline hook on a lower function) is undetermined.
|
||
Our inline hook on `ws2_32!connect` *does* fire, so FIFA reaches `ws2_32!connect`;
|
||
yet the real call returns 0 without touching the host socket — naming the layer
|
||
that absorbs it requires inspecting anadius.
|
||
- The placeholder `AES_KEY` was **never exercised** — delivery fails upstream of any
|
||
handshake, so the challenge-response never ran. The earlier "supply the real AES
|
||
key" follow-up is moot unless/until FIFA is actually routed to our LSX server.
|
||
- **Fix direction (not yet implemented):** the viable lever is our own hook, not
|
||
anadius (which is closed, load-bearing, and binds no host port to free). Candidate:
|
||
have the hook redirect the 3216 connect to a distinct host port the bridge listens
|
||
on, to slip past anadius's port-specific in-process interception — routing LSX to
|
||
our bridge on the shared host stack (this revives the real-AES-key requirement
|
||
above). Scoped to `openfut-hook`, not this repo.
|
||
|
||
---
|
||
|
||
## LSX SOLVED via hook redirect + session-seed fix — 2026-07-02
|
||
|
||
**RESULT: FIFA now completes the LSX handshake AND a sustained encrypted session
|
||
against our own bridge, live.** The "fix direction" from the previous section was
|
||
implemented and works. This is the first EA-side gate serviced end-to-end by our code.
|
||
|
||
### 1. Hook redirect gets FIFA onto the bridge (delivery achieved)
|
||
|
||
Added one arm to `redirect_if_ea` in `openfut-launcher/openfut-hook/src/connect_hook.rs`:
|
||
FIFA's connect to `127.0.0.1:3216` is rewritten to `127.0.0.1:3217`, where the bridge
|
||
runs its LSX server (`LSX_ADDR=127.0.0.1:3217`). anadius's in-process interception is
|
||
keyed on port **3216 specifically**, so the redirected connect slips past it and lands
|
||
on the host bridge over the shared loopback.
|
||
|
||
- Hook log: `connect_hook: 1.0.0.127:3216 → 127.0.0.1:3217` then `redirect result=0`.
|
||
- Bridge log: `LSX: connection from 127.0.0.1:…` — **FIFA reached the bridge.** Overturns
|
||
the "off the path" conclusion above.
|
||
|
||
### 2. AES key was never a placeholder — it is `000102…0f`
|
||
|
||
The `AES_KEY = [0,1,2,…,15]` in `src/lsx.rs` (and the hook's `lsx.rs`) is the **real,
|
||
correct** key, confirmed two ways:
|
||
- **Oracle:** a captured FIFA `ChallengeResponse` gives a known plaintext→ciphertext
|
||
pair under the real key — our greeting `cacf897a20b6d612ad0c05e011df52bb` (ASCII) →
|
||
FIFA's `5c52e8ca…`. `AES-ECB-PKCS7([0..15], greeting)` reproduces it byte-for-byte.
|
||
- **Static:** that same 16-byte value is embedded literally in `anadius64.dll` at
|
||
offsets `0xa1710`, `0xa1890`, `0xa3c0f`.
|
||
|
||
The earlier "supply the real AES key" TODO is therefore **closed — no key extraction
|
||
was needed.** The handshake auth was always correct; FIFA accepted our `ChallengeAccepted`.
|
||
|
||
### 3. The real bug: session-seed derivation (now fixed)
|
||
|
||
After a *correct* handshake, the first encrypted session message still decrypted to
|
||
garbage. Cause: `compute_seed()` derived the session-key seed by **parsing** the first
|
||
hex pair of our response (`"0f"` → `0x0F`), when the client uses the **raw ASCII byte
|
||
values of the first two characters** (`'0','f'` → `0x30,0x66`).
|
||
|
||
Confirmed by capturing the raw session ciphertext (bridge patched to log wire bytes)
|
||
and brute-forcing all 65,536 u16 seeds offline through `get_lsx_key(seed)`:
|
||
- Exactly **one** seed, `0x3066` (12390), decrypted the message to valid XML:
|
||
`<LSX><Request recipient="EbisuSDK" id="…"><GetConfig version="3"/></Request></LSX>`.
|
||
- `0x3066` = ASCII `'0','f'` = the first two characters of our response hex `0f73…`,
|
||
**not** the parsed value `0x0F73` (3955) we were using.
|
||
|
||
Fix in `src/lsx.rs` `compute_seed()`: take `hex.as_bytes()[0..2]` directly instead of
|
||
`u8::from_str_radix` on the hex pairs. (Capture preserved at
|
||
`captures/lsx-handshake-capture-2026-07-02.log`; brute-forcer in scratchpad.)
|
||
|
||
### 4. Live confirmation
|
||
|
||
With the redirect deployed and the seed-fixed bridge on 3217, FIFA relaunched and:
|
||
- Held a **single sustained LSX connection** (`ss` shows `ESTAB FIFA23.exe ↔
|
||
openfut-bridge:3217`) instead of the previous connect-once-then-crash.
|
||
- Drove **20 real EbisuSDK requests** in one conversation: `GetConfig`, `GetProfile`
|
||
(×5), `GetSetting`, `GetGameInfo` (×8), `SetPresence`, `GetInternetConnectedState`.
|
||
- **Did not crash** at the LSX phase.
|
||
|
||
### 5. Remaining gap (next step, not a blocker to this result)
|
||
|
||
`GetGameInfo GameInfoId=…` had no handler in the bridge dispatcher → fell to the
|
||
generic `<Ok/>` and logged `unknown request type`. FIFA tolerated it (session stayed
|
||
up), but real `GetGameInfo` responses may be needed to progress toward the next gate
|
||
(Blaze/online).
|
||
|
||
**Update 2026-07-02 (later):** a `get_game_info` handler was added to `dispatch()` in
|
||
`src/lsx.rs`. It routes `GetGameInfo GameInfoId=…`, **logs the requested `GameInfoId`**
|
||
(so the next live run reveals which keys FIFA asks for — we still have no captured real
|
||
response to model), and replies with a well-formed `<GetGameInfoResponse GameInfoId="…"
|
||
Value="" />` echo. A dispatcher routing test (`dispatch_routes_get_game_info_and_echoes_id`)
|
||
pins the fragile positional `split('"')` parsing. `TODO/CONFIRM` remains: the real
|
||
response schema and whether FIFA needs non-empty values — resolve by launching FIFA
|
||
with this build, reading the logged `GameInfoId`s, and observing how far FIFA advances.
|
||
|
||
### Provenance note
|
||
|
||
Clean-room intact: the AES key was recovered by validating candidate byte windows from
|
||
the shipping `anadius64.dll` against the client's own observed output, and the seed rule
|
||
was recovered by brute-forcing against a live capture — nothing derived from EA source.
|
||
|
||
---
|
||
|
||
## LSX fully satisfied — FIFA reaches the Blaze/online gate — 2026-07-02 (later)
|
||
|
||
**RESULT (unconfirmed — read the caveat): FIFA reached the "connecting to EA servers"
|
||
screen alive, but the launch that did so used a CACHED EA session and did NOT re-hit our
|
||
bridge (no new `3216` connect, empty bridge log). The last launch that actually ran a
|
||
fresh LSX session through our bridge still CRASHED. So it is NOT yet established that the
|
||
fixed bridge carries a fresh FIFA launch past the crash — the crash-clear is confounded
|
||
by session caching.** The fixes below are believed-correct and unit-tested, but need the
|
||
clean re-test in the "Confirmation caveat" section before LSX can be called solved.
|
||
Earlier in the day FIFA crashed ~15–17s into LSX, "right before the loading screen."
|
||
Root-causing that crash took several live runs and disproved two wrong theories.
|
||
|
||
### The real root cause: dropped pipelined LSX messages (session-loop framing bug)
|
||
|
||
FIFA batches multiple LSX messages into a single TCP segment — e.g. `SetPresence`
|
||
immediately followed by a `GetGameInfo GameInfoId="LANGUAGES"` query, two
|
||
null-terminated ciphertext-hex blocks in one read. The old session loop decrypted the
|
||
whole read as one blob and dispatched only the **first** message, silently dropping the
|
||
rest. The dropped request left FIFA waiting on a response that never came; it timed out
|
||
after ~15s (the mysterious silent gap in every crash log) and then crashed.
|
||
|
||
Fix: `src/lsx.rs` `process_frame()` splits each read on the null terminators and
|
||
answers **every** sub-message, returning the concatenated null-terminated responses.
|
||
Regression test: `process_frame_answers_every_pipelined_message`. After the fix, FIFA
|
||
flowed through the entire LSX sequence in ~1.3s instead of hanging 15s.
|
||
|
||
### Two theories the live runs DISPROVED first (recorded so we don't repeat them)
|
||
|
||
1. **"Empty/incorrect INSTALLED_LANGUAGE value crashes it."** Changing the value
|
||
(`""` → `en_US`) changed nothing — same 3× re-query, same crash. Because…
|
||
2. **"Wrong attribute name only."** Switching `Value=""` → `InstalledLanguage="en_US"`
|
||
also changed nothing *on its own* — because the crash-relevant request (the
|
||
pipelined `LANGUAGES`) was never being dispatched at all (see root cause). The
|
||
attribute name *did* matter, but only became observable once pipelining was fixed:
|
||
FIFA reads a lowercase `value` attribute (case-sensitive), so the response now emits
|
||
the value under BOTH `value` and the per-key CamelCase name (`InstalledLanguage` /
|
||
`Languages`).
|
||
|
||
### anadius.cfg is the response oracle
|
||
|
||
`/mnt/games/FIFA 23/anadius.cfg` is the config anadius's in-process LSX emu reads to
|
||
build its responses — the clean, correct values to mirror (same provenance basis as the
|
||
AES-key recovery; it is anadius's own config, not EA source):
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| `Language` (INSTALLED_LANGUAGE) | `en_US` |
|
||
| `Languages` (LANGUAGES) | `pt_PT,tr_TR,ko_KR,cs_CZ,zh_CN,zh_HK,da_DK,no_NO,sv_SE,en_US,pt_BR,de_DE,es_ES,fr_FR,it_IT,ja_JP,es_MX,nl_NL,pl_PL,ru_RU,ar_SA` |
|
||
| `PersonaId` | `1144668899` |
|
||
| `UserId` | `1000200030000` |
|
||
| `Username` (Persona) | `fun` |
|
||
|
||
`src/lsx.rs` now returns these real values (GetProfile IDs, GetGameInfo language keys)
|
||
instead of fabricated ones; fabricated persona/user IDs flow into FIFA's online state
|
||
and are a likely downstream break. Also added a real `IsProgressiveInstallationAvailable`
|
||
(`Available="false"`, attribute recovered from `anadius64.dll`).
|
||
|
||
### Confirmation caveat + how to verify cleanly
|
||
|
||
The launch that first reached the online gate did **not** re-hit our bridge — FIFA
|
||
presented a cached EA session (no new `3216` connect; empty bridge log) and jumped
|
||
straight to "connecting to EA servers." So that specific run didn't exercise the final
|
||
`value`-attribute change. To confirm on a clean boot, clear FIFA's cached EA/Origin
|
||
session token in the Wine prefix so it re-runs LSX through the bridge, and verify: one
|
||
sustained LSX session, `INSTALLED_LANGUAGE` queried **once** (not 3×), no crash, and
|
||
FIFA arriving at "connecting to EA servers".
|
||
|
||
### GetGameInfo response shape (RE'd from the anadius64.dll builder, 2026-07-02)
|
||
|
||
Populating `GetAllGameInfo` did **not** fix the crash — FIFA still ran the full LSX
|
||
session (~2.6s, 17 dispatches) then crashed at the same point, ending with the 3×
|
||
`INSTALLED_LANGUAGE` re-query. So `GetAllGameInfo` was never the trigger.
|
||
|
||
Disassembling the **individual** `GetGameInfo` response builder in `anadius64.dll`
|
||
(around `0x18002a076`) settled the actual wire shape. It emits, in order:
|
||
`<` + `GetGameInfoResponse` + ` ` + `GameInfo` + `="` + <value> + `"` + ` />`. The
|
||
attribute name is the **fixed 8-byte literal `GameInfo`** (string constant at
|
||
`.rdata` VA `0x1800af818`), *independent of the requested `GameInfoId`* — the value
|
||
is looked up per key but the attribute is always `GameInfo`. So the correct reply to
|
||
`GetGameInfo GameInfoId=INSTALLED_LANGUAGE` is:
|
||
|
||
```
|
||
<GetGameInfoResponse GameInfo="en_US" />
|
||
```
|
||
|
||
Our bridge had been sending the key as the attribute name
|
||
(`<GetGameInfoResponse InstalledLanguage="en_US" />`), an attribute FIFA never reads.
|
||
That is why FIFA always saw a null installed-language, re-queried `INSTALLED_LANGUAGE`
|
||
three times, and crashed — and why *every* prior value change (empty → `en_US`,
|
||
per-key naming) had no effect: FIFA wasn't reading our attribute at all. `src/lsx.rs`
|
||
`get_game_info()` now emits the fixed `GameInfo="<value>"` attribute.
|
||
|
||
### ROOT CAUSE of the loading-screen crash: GetConfig missing PROGRESSIVE_INSTALLATION (2026-07-02)
|
||
|
||
FIFA's own crash reports (`FIFA 23/Logs/CrashReport_*.json`) pinned it. **Every** run
|
||
through our bridge — all 11 today, both the pre-pipelining (`Up Seconds: 17`) and
|
||
post-pipelining (`Up Seconds: 2`) runs — crashed at the **identical** address:
|
||
|
||
```
|
||
EXCEPTION_ACCESS_VIOLATION, reading 0x18
|
||
RIP = FIFA23.exe+0x133dfc5 → mov rax, QWORD PTR [r13+0x18] with r13 = NULL
|
||
Is Online: False, Blaze State: {} (crash is pre-Blaze, ~2s after LSX)
|
||
```
|
||
|
||
So none of the earlier LSX-response fixes (pipelining, account IDs, GetGameInfo
|
||
attribute, GetAllGameInfo population) ever touched the crash — it was **invariant**,
|
||
i.e. caused by a response that had been wrong the *whole time*. Disassembling the
|
||
faulting function showed r13 is its 3rd argument (a Frostbite ref-counted service
|
||
*handle*) — a null handle deref, i.e. a **failed service lookup**.
|
||
|
||
That pointed straight at `GetConfig` (request #1, the fabricated service list, never
|
||
verified). Extracting anadius's real service-registration table from `anadius64.dll`
|
||
(the `lea rdx=Name / lea r8=Facility` sequence at `0x180026d40`) and diffing it
|
||
against our `get_config()` showed our list was correct on all 32 entries **except it
|
||
omitted two services**:
|
||
|
||
```
|
||
Facility="PROGRESSIVE_INSTALLATION" Name="PI"
|
||
Facility="PROGRESSIVE_INSTALLATION_EVENT" Name="PI"
|
||
```
|
||
|
||
FIFA queries `IsProgressiveInstallationAvailable` in every LSX session; it builds its
|
||
service registry from `GetConfig` and resolves the `PROGRESSIVE_INSTALLATION` handle
|
||
from it during online-init. With the service absent, the handle was null → the
|
||
0x133dfc5 deref. Fix: `get_config()` now declares both services, and
|
||
`is_progressive_installation_available()` responds from `sender="PI"` (the service's
|
||
registry Name). This is the first change that targets the *actual* crash address, so
|
||
it is the real test of the loading-screen gate.
|
||
|
||
### M2 unblock lever RECOVERED: the ONLINE_STATUS_EVENT push format (2026-07-02)
|
||
|
||
With the LSX gate cleared, FIFA sits at "connecting to EA Servers", polling GetProfile
|
||
over LSX. Per the M1/M2 findings this is the async-push wall: FIFA calls GoOnline
|
||
(handled in-process by anadius/EbisuSDK), gets success, then **waits for an
|
||
`ONLINE_STATUS_EVENT` push on the LSX socket that never arrives.** Earlier attempts
|
||
couldn't send it — it was unclear the bridge even owned the LSX socket (anadius
|
||
intercepted in-process). **That blocker is now gone: our bridge provably owns FIFA's
|
||
LSX socket** (full session runs on :3217), so we can finally push the event.
|
||
|
||
RE recovered the exact format the client parses (authoritative — from FIFA23.exe's
|
||
own OriginSDK deserializer, not a guess):
|
||
|
||
- **Event wrapper** (from anadius64.dll's IGOEvent template):
|
||
`<LSX><Event sender="EbisuSDK"><…/></Event></LSX>`
|
||
- **Element** `OnlineStatusEvent` — confirmed via FIFA23.exe's retained RTTI symbol
|
||
`Origin::EventHandler<struct lsx::OnlineStatusEventT,bool>::HandleMessage`
|
||
(source path `OriginSDK/10.6.2.19-fb-fifagame/src/impl/OriginEventClasses`) and the
|
||
event-name dispatch table. Handler parser at FIFA23.exe+0x274d4ae.
|
||
- **Attribute** `isOnline` (a single bool) — recovered from the typed deserializer at
|
||
FIFA23.exe+0x28a4d0: it inits the output byte to 0 (offline), builds an `lsx:`
|
||
namespace prefix, reads the `isOnline` attribute, and string→bool converts it.
|
||
|
||
Best-effort push (value format `1` vs `true` is `TODO/CONFIRM` against the client —
|
||
OriginSDK bool attributes elsewhere use `true`/`false`):
|
||
|
||
```xml
|
||
<LSX><Event sender="EbisuSDK"><OnlineStatusEvent isOnline="1"/></Event></LSX>
|
||
```
|
||
|
||
**Next implementation step:** give the bridge LSX server an async, unsolicited-push
|
||
path — encrypt this event with the live session seed and write it on the open socket
|
||
after the session establishes (or after FIFA's GoOnline settles). Success signal: FIFA
|
||
stops polling and **dials the Blaze redirector** (gosredirector → :42127 via the hook),
|
||
which yields the first Fire2 frame and moves us into M3/M4 (fifa-blaze).
|
||
|
||
### RESULT: OnlineStatusEvent push drives FIFA online — a first — but the auth→Blaze chain is the next wall (2026-07-02)
|
||
|
||
Implemented an unsolicited `OnlineStatusEvent` push in the bridge LSX server
|
||
(`select!` loop, encrypted with the session seed, re-pushed every 2s). Value
|
||
`isOnline="true"` (FIFA's bool parser at FIFA23.exe+0x28fb50 string-compares to
|
||
`false`; `isOnline="1"` was delivered but parsed as not-online — pinned the encoding).
|
||
|
||
**FIFA reacted — the event format is correct and effective:**
|
||
- Rapid GetProfile polling (~0.5s) stopped the instant the pushes began (13s gap).
|
||
- FIFA set `SetPresence … Presence="INGAME"` — an *online* action.
|
||
- Then it went **silent on LSX** (idle, connection alive, no crash).
|
||
|
||
This is the first time FIFA has been driven "online" at the EbisuSDK/LSX level —
|
||
**anadius never does this** (it is offline DRM and deliberately keeps FIFA offline),
|
||
so this path is genuinely uncharted, not a regression from a known-good baseline.
|
||
|
||
**But FIFA does NOT proceed to Blaze.** After going online it:
|
||
- sent **no** `GetAuthCode` (an LSX request the bridge already answers) — so it never
|
||
started the Nucleus-token exchange;
|
||
- resolved **no** hostname (`hooked_getaddrinfo` logs every call; the hook log has
|
||
none) — no gosredirector, no accounts.ea.com;
|
||
- opened **no** socket beyond LSX (:3216).
|
||
|
||
So a single connectivity event ≠ full online login. RE of FIFA23.exe confirms the
|
||
remaining chain and its anchors: `GetAuthCode` (12×) → `NucleusAccessToken` /
|
||
`access_token` / `Bearer` / `Authorization` (redirector header) → `gosredirector`
|
||
(4×) / `BlazeHub` (3×) → Blaze auth. `OnlineStatusEventT::HandleMessage`
|
||
(FIFA23.exe+0x274d4ae) only builds the typed event and dispatches it via the SDK
|
||
callback; the game-side listener that would kick off the auth chain needs more than
|
||
the connectivity flag (candidate: a login/authenticated-online event or an in-process
|
||
EbisuSDK login step that our LSX-only redirect doesn't cover).
|
||
|
||
**Strategic note:** this is the M2→M6 research frontier the 2026-06-30 status review
|
||
priced at months of expert RE, now empirically confirmed. It sits against the
|
||
2026-06-30 direction pivot, which made the app-centric FLE/career-mode path the
|
||
primary plan over full Blaze-backend RE. Decision point before sinking weeks into the
|
||
Nucleus/Blaze stack.
|
||
|
||
### Trigger hunt: the missing link is the `Login` event (OriginLoginT) (2026-07-02)
|
||
|
||
Traced why FIFA stalls after going online. The full post-online chain and its
|
||
FIFA23.exe anchors:
|
||
|
||
```
|
||
OnlineStatusEvent(isOnline=true) ✓ delivered, FIFA set INGAME presence
|
||
│ [MISSING game-side trigger]
|
||
GetAuthCode (LSX request we answer) ── never sent
|
||
│
|
||
Nucleus token exchange (accounts.ea.com HTTPS, Bearer/Authorization) ── no DNS
|
||
│
|
||
spring18.gosredirector.ea.com (HTTPS redirector: Authorization + serviceName,
|
||
env-selected by ENVIRONMENT="production"; headers X-BLAZE-ERRORCODE)
|
||
│
|
||
Blaze server (BlazeHub::LoginManagerImpl, Fire2) ── never reached
|
||
```
|
||
|
||
Login lives inside Blaze (`BlazeHub::mLoginManagers`), which needs a Nucleus token
|
||
first, which needs `GetAuthCode`, which FIFA never issues. So the block is upstream:
|
||
the game-side reaction to online doesn't start the auth chain.
|
||
|
||
**Likely cause — no authenticated session.** FIFA's Origin event set (from retained
|
||
RTTI) includes `Origin::EventHandler<struct lsx::LoginT, struct OriginLoginT>` — a
|
||
`Login` event carrying a full `OriginLoginT` struct (identity/session/token), distinct
|
||
from `OnlineStatusEvent`'s single bool. Our push set *connectivity* (presence went
|
||
INGAME) but not an authenticated *session*. anadius never pushes `Login` (offline DRM),
|
||
so FIFA has a persona (from GetProfile) but no online session token → nothing to drive
|
||
`GetAuthCode`.
|
||
|
||
**Next experiment:** RE the `LoginT` deserializer (element `Login`, payload
|
||
`OriginLoginT`) the same way as OnlineStatusEvent, synthesize a `<Login>` event push
|
||
with a fabricated session, and relaunch. Success signal: FIFA issues `GetAuthCode`
|
||
over LSX and/or resolves `spring18.gosredirector.ea.com` (both logged) — the first
|
||
move into the Nucleus/Blaze stack.
|
||
|
||
### Login event pushed — FIFA still won't start the auth chain (2026-07-02)
|
||
|
||
RE'd the `Login` event from FIFA23.exe's `LoginT` deserializer (0x142787a30): fields
|
||
`UserIndex` (int), `IsLoggedIn` (bool, same true/false parser as isOnline),
|
||
`LoginReasonCode` (int). Pushed `<Login UserIndex="0" IsLoggedIn="true"
|
||
LoginReasonCode="0"/>` before OnlineStatusEvent each cycle.
|
||
|
||
Result: **no change.** FIFA reacts to connectivity (presence INGAME, polling backs
|
||
off) but issues no `GetAuthCode`, resolves no host (`getaddrinfo` count 0), dials no
|
||
Blaze. So Login + OnlineStatus (connectivity + authenticated flags) are **not**
|
||
sufficient to trigger the game-side auth chain.
|
||
|
||
**Boundary reached for black-box LSX emulation.** We can drive every LSX-wire signal
|
||
FIFA reads, and FIFA visibly reacts, but the decision to start GetAuthCode→Nucleus→
|
||
Blaze lives in FIFA's game-side online orchestrator, whose gating state is invisible
|
||
from the LSX wire (it also makes in-process EbisuSDK calls that go to anadius, not our
|
||
socket — we can't see those). Guessing more LSX events is low-yield from here.
|
||
|
||
To progress, the next step must convert guessing into observation — **instrument
|
||
FIFA's in-process online flow** via the hook DLL: detour the EbisuSDK/game functions
|
||
around `GetAuthCode`, `GoOnline` (anadius64.dll+0x2BB90), and the OnlineStatusEvent
|
||
consumer, and log what FIFA does after our events (whether it calls GetAuthCode
|
||
in-process, what it checks, where it returns early). That reveals the exact gate.
|
||
Alternatively this is the wall the 2026-06-30 direction pivot anticipated — the
|
||
app-centric FLE/career path avoids the online-backend entirely.
|
||
|
||
Bank: LSX gate cleared; FIFA driven "online" at the EbisuSDK connectivity/login level
|
||
for the first time (anadius never does); OnlineStatusEvent + Login formats recovered
|
||
and confirmed effective at the event level.
|
||
|
||
### In-process instrumentation results (2026-07-02)
|
||
|
||
Built a `probe` feature into openfut-hook (unhook/rehook logging detours, like
|
||
connect_hook) on four online-flow functions. Deployed + ran with the event push
|
||
active. Results (C:\openfut_hook.log):
|
||
|
||
- **Our events reach FIFA's code and parse successfully.** `OnlineStatus.deser`
|
||
(FIFA23.exe+0x28a4d0) and `Login.deser` (+0x287a30) each fired 44× (once per 2s
|
||
re-push) and returned success (0x1 / valid ptr). The push works at the deepest
|
||
level — not a delivery problem.
|
||
- **`GoOnline` (anadius64.dll+0x2bb90) never fires.** The 2026-06-30 review saw FIFA
|
||
calling GoOnline and *retrying every ~7s*; with our bridge + events FIFA is not in
|
||
that retry loop at all. Our events moved it out of the retry state — but it does not
|
||
advance.
|
||
- **`GetInternetConnectedState` (anadius64.dll+0x27790) never fires** — FIFA asks it
|
||
over LSX (to our bridge) instead; anadius is fully bypassed for LSX.
|
||
|
||
**Gate localized to the game-side event consumer.** After the deserializer,
|
||
`OnlineStatusEventT::HandleMessage` delivers the value via a virtual call
|
||
(`call [rax+0x28]`, FIFA23.exe+0x274d4e2) to a polymorphic game listener. FIFA parses
|
||
"online + logged in" every cycle but the listener does not act (no GoOnline, no auth,
|
||
no Blaze). The concrete listener is a runtime vtable target — not statically pinnable.
|
||
|
||
Note vs. the old M2 finding: forcing anadius's in-process state flags to online left
|
||
FIFA *retrying*; pushing our events *stops* the retry but doesn't advance. Neither
|
||
path alone flips the game-side online state machine — the real gate is in FIFA's own
|
||
online orchestrator, reachable only by probing the runtime virtual listener or a
|
||
deeper RE of that state machine.
|
||
|
||
### Next gate: Blaze/online (M2)
|
||
|
||
"connecting to EA servers" is the Blaze layer. The hook log shows FIFA has made **no**
|
||
Blaze connect yet (nothing on 443/10041/42127) while spinning, so it is stalled at
|
||
pre-auth. Per the architecture, Blaze rides ProtoSSL in-process (a different transport
|
||
than LSX), so serving it is separate, larger work — the M2 effort, now actually reached.
|