LSX: push OnlineStatusEvent + Login to drive FIFA online (M2 probe)
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>
This commit is contained in:
@@ -592,6 +592,175 @@ from it during online-init. With the service absent, the handle was null → the
|
|||||||
registry Name). This is the first change that targets the *actual* crash address, so
|
registry Name). This is the first change that targets the *actual* crash address, so
|
||||||
it is the real test of the loading-screen gate.
|
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)
|
### Next gate: Blaze/online (M2)
|
||||||
|
|
||||||
"connecting to EA servers" is the Blaze layer. The hook log shows FIFA has made **no**
|
"connecting to EA servers" is the Blaze layer. The hook log shows FIFA has made **no**
|
||||||
|
|||||||
+75
-2
@@ -13,6 +13,48 @@ use tracing::{debug, info, warn};
|
|||||||
const GREETING_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb";
|
const GREETING_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb";
|
||||||
const AES_KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
const AES_KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||||
|
|
||||||
|
/// The async `ONLINE_STATUS_EVENT` push FIFA waits for after `GoOnline`.
|
||||||
|
///
|
||||||
|
/// FIFA calls GoOnline (handled in-process by EbisuSDK/anadius), gets success,
|
||||||
|
/// then blocks waiting for an unsolicited `OnlineStatusEvent` push on the LSX
|
||||||
|
/// socket — the M2 wall. Now that our bridge owns FIFA's LSX socket we can send
|
||||||
|
/// it. Format RE'd from FIFA23.exe's OriginSDK deserializer (authoritative):
|
||||||
|
/// element `OnlineStatusEvent` (RTTI `Origin::EventHandler<lsx::OnlineStatusEventT,
|
||||||
|
/// bool>`, parser FIFA23.exe+0x274d4ae), single bool attribute `isOnline`
|
||||||
|
/// (deserializer FIFA23.exe+0x28a4d0). See docs/connection-gate-findings.md.
|
||||||
|
///
|
||||||
|
/// The value is the string literal `true`/`false` (not `1`/`0`): FIFA's bool
|
||||||
|
/// parser (FIFA23.exe+0x28fb50) string-compares the attribute against `"false"`,
|
||||||
|
/// matching the OriginSDK convention every other bool in our LSX responses uses.
|
||||||
|
/// An initial `isOnline="1"` push was delivered but parsed as not-online (FIFA
|
||||||
|
/// kept polling), which pinned the encoding. Re-push cadence below still
|
||||||
|
/// TODO/CONFIRM (single push may suffice once value is correct).
|
||||||
|
const ONLINE_STATUS_EVENT: &str =
|
||||||
|
r#"<LSX><Event sender="EbisuSDK"><OnlineStatusEvent isOnline="true"/></Event></LSX>"#;
|
||||||
|
|
||||||
|
/// The authenticated-session `Login` event. OnlineStatusEvent alone set FIFA's
|
||||||
|
/// *connectivity* (presence went INGAME) but not an authenticated session, so FIFA
|
||||||
|
/// never started the GetAuthCode→Nucleus→Blaze chain. FIFA's Origin event set has a
|
||||||
|
/// separate `Login` event (`Origin::EventHandler<lsx::LoginT,OriginLoginT>`) that
|
||||||
|
/// carries the login state. Format RE'd from FIFA23.exe's LoginT deserializer
|
||||||
|
/// (0x142787a30): fields `UserIndex` (int, primary user 0), `IsLoggedIn` (bool,
|
||||||
|
/// parsed by the same true/false helper as isOnline), `LoginReasonCode` (int/enum).
|
||||||
|
///
|
||||||
|
/// Pushed BEFORE OnlineStatusEvent each cycle (log in, then go online).
|
||||||
|
/// TODO/CONFIRM `LoginReasonCode` — 0 is the natural "no error / normal" value; if
|
||||||
|
/// FIFA switches on a specific success code, RE the consumer and adjust.
|
||||||
|
const LOGIN_EVENT: &str =
|
||||||
|
r#"<LSX><Event sender="EbisuSDK"><Login UserIndex="0" IsLoggedIn="true" LoginReasonCode="0"/></Event></LSX>"#;
|
||||||
|
|
||||||
|
/// Events pushed on the LSX socket each interval, in order, to drive FIFA online.
|
||||||
|
const ONLINE_PUSH_EVENTS: &[&str] = &[LOGIN_EVENT, ONLINE_STATUS_EVENT];
|
||||||
|
|
||||||
|
/// Delay before the first `OnlineStatusEvent` push (and the re-push interval).
|
||||||
|
/// Long enough that FIFA has finished LSX bootstrap and subscribed its event
|
||||||
|
/// listeners + issued GoOnline (GetInternetConnectedState was seen ~2s in), short
|
||||||
|
/// enough to fire while FIFA is still polling and waiting.
|
||||||
|
const ONLINE_PUSH_INTERVAL: std::time::Duration = std::time::Duration::from_millis(2000);
|
||||||
|
|
||||||
// Account identity + locale that anadius's in-process LSX emu reports (recovered
|
// Account identity + locale that anadius's in-process LSX emu reports (recovered
|
||||||
// from anadius.cfg — the config the emu reads). FIFA carries these values into
|
// from anadius.cfg — the config the emu reads). FIFA carries these values into
|
||||||
// its online/loading state, so our bridge must report the SAME ones anadius does;
|
// its online/loading state, so our bridge must report the SAME ones anadius does;
|
||||||
@@ -92,9 +134,18 @@ async fn handle(mut stream: TcpStream) -> anyhow::Result<()> {
|
|||||||
stream.write_all(accepted.as_bytes()).await?;
|
stream.write_all(accepted.as_bytes()).await?;
|
||||||
info!("LSX: handshake complete");
|
info!("LSX: handshake complete");
|
||||||
|
|
||||||
// 4. Session loop
|
// 4. Session loop — multiplexes request handling with an unsolicited
|
||||||
|
// OnlineStatusEvent push. A single task keeps owning the stream; the
|
||||||
|
// `select!` services reads and lets a timer fire the push, so no socket
|
||||||
|
// split / shared writer is needed.
|
||||||
|
let mut push_timer = tokio::time::interval(ONLINE_PUSH_INTERVAL);
|
||||||
|
// interval fires immediately on the first tick — consume it so the first
|
||||||
|
// push lands after ONLINE_PUSH_INTERVAL (giving FIFA time to subscribe).
|
||||||
|
push_timer.tick().await;
|
||||||
loop {
|
loop {
|
||||||
let raw = match read_msg(&mut stream).await {
|
tokio::select! {
|
||||||
|
read = read_msg(&mut stream) => {
|
||||||
|
let raw = match read {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
};
|
};
|
||||||
@@ -110,6 +161,28 @@ async fn handle(mut stream: TcpStream) -> anyhow::Result<()> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ = push_timer.tick() => {
|
||||||
|
// Push the Login (authenticated session) then OnlineStatusEvent
|
||||||
|
// (connectivity) so FIFA leaves the "connecting" wait and starts the
|
||||||
|
// GetAuthCode→Nucleus→Blaze chain. Each is encrypted + null-terminated
|
||||||
|
// like any session frame. Re-pushed each interval to cover the
|
||||||
|
// subscribe-timing race until FIFA advances (or the socket closes).
|
||||||
|
let mut push_err = false;
|
||||||
|
for ev in ONLINE_PUSH_EVENTS {
|
||||||
|
let mut frame = lsx_encrypt(ev, seed).into_bytes();
|
||||||
|
frame.push(0);
|
||||||
|
debug!("LSX: pushing event: {ev}");
|
||||||
|
if stream.write_all(&frame).await.is_err() {
|
||||||
|
push_err = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if push_err {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
debug!("LSX: client disconnected");
|
debug!("LSX: client disconnected");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user