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
|
||||
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**
|
||||
|
||||
+87
-14
@@ -13,6 +13,48 @@ use tracing::{debug, info, warn};
|
||||
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];
|
||||
|
||||
/// 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
|
||||
// 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;
|
||||
@@ -92,22 +134,53 @@ async fn handle(mut stream: TcpStream) -> anyhow::Result<()> {
|
||||
stream.write_all(accepted.as_bytes()).await?;
|
||||
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 {
|
||||
let raw = match read_msg(&mut stream).await {
|
||||
Ok(b) => b,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Dump the exact wire bytes so the session seed can be brute-forced
|
||||
// offline against this ciphertext (see docs/connection-gate-findings.md).
|
||||
debug!("LSX: session raw ({} bytes): {}", raw.len(), bytes_to_hex(&raw));
|
||||
tokio::select! {
|
||||
read = read_msg(&mut stream) => {
|
||||
let raw = match read {
|
||||
Ok(b) => b,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Dump the exact wire bytes so the session seed can be brute-forced
|
||||
// offline against this ciphertext (see docs/connection-gate-findings.md).
|
||||
debug!("LSX: session raw ({} bytes): {}", raw.len(), bytes_to_hex(&raw));
|
||||
|
||||
let out = process_frame(&raw, seed);
|
||||
if out.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if stream.write_all(&out).await.is_err() {
|
||||
break;
|
||||
let out = process_frame(&raw, seed);
|
||||
if out.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if stream.write_all(&out).await.is_err() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user