# FIFA 17 OriginSDK — what makes the game consider the user LOGGED IN (LSX events) Clean-room RE. Sources: `/mnt/games/FIFA 17/FIFA17.exe` (fully symboled; `.text` is runtime-decrypted, so all code analysis was done on live `/proc/PID/mem` dumps taken from the running game — `live_code.bin` @0x140001000, `live_high.bin` @0x144ed3000, both in this scratchpad), plus our own LSX captures. No leaked material used. Helper tools written for this pass (scratchpad): `dumplive.py`, `disasm_helper.py`, `fndis.py` (disassemble + auto-annotate rip-relative string refs), `fnstart.py`, `callers.py`, `xref2.py`, `bulkxref.py`, `symmap.py` (turns the binary's own `Class::Method` log strings into a symbol map). --- ## VERDICT **The prime hypothesis is CORRECT. The OriginSDK has a distinct "user is logged in" state that is delivered ONLY as a server-PUSHED LSX `` event. Our request-only responder never sends it, so FIFA's Origin manager has had `m_isLoggedIn = false` for the entire session — independently of `GetInternetConnectedState connected="1"`.** Add this **pushed** message to `lsx_responder.py` (encrypted like any other post-handshake message: `hex(AES128-ECB(session_key, pkcs7(xml)))` + `\0`): ```xml ``` `sender="LOGIN_EVENT"` is **mandatory and exact** (proof below). `IsLoggedIn` is parsed as a bool by literal comparison against `"false"` — anything that is not the exact string `false` means true, so `"true"` or `"1"` both work; `"false"` / missing attribute means logged out. Strongly recommended companion (FIFA also subscribes to this one, and it is likewise push-only — never requestable): ```xml ``` Optional, keeps the profile consistent: ```xml ``` **When to send:** any time after the client's first post-handshake request (the event handler array is built inside `Origin::OriginSDK::Initialize`, which completes before the client issues `GetConfig`). Practical recipe: push `` right after we answer the first `GetProfile` (id=3), and push it again after answering `GetInternetConnectedState` (id=16/18) and after `GetGameInfo UPTODATE`. Re-sending is harmless — the handler is idempotent (it just sets a flag and broadcasts). --- ## Evidence chain ### 1. The SDK's LSX event surface (28 push-only messages) `Origin::OriginSDK::RegisterEventCallback` (`0x14710e710`) indexes a fixed array of pre-constructed handlers at `sdk + 0x278 + 8*eventEnum` (29 slots — `lea ebp,[rdx+0x1d]` in the unregister-all path). Every handler's matcher has the identical shape (e.g. `Origin::EventHandler::HandleMessage` @`0x14710c7d0`, matcher @`0x147102800`): ``` root element must be "LSX" enter element (name comes from handler, always "Event") optional attr id="..." (HasAttribute check — may be omitted) REQUIRED attr sender="..." (must be PRESENT and strcmp-equal to handler->sender) enter element / / ... (hardcoded per handler) then parse that element's attributes ``` So the wire form for every event is: ```xml ``` Full set of inner element names the SDK listens for (extracted from the 28 matchers in `0x147101000..0x14710a000`): ``` AchievementSets BlockListUpdated BroadcastEvent ChatMessageEvent ChunkStatus CoreContentUpdated CurrentUserPresenceEvent GameMessageEvent GetPresenceResponse GroupEnterEvent GroupEvent GroupInviteEvent GroupLeaveEvent IGOEvent IGOUnavailable Login MinimizeRequest MultiplayerInvite MultiplayerInvitePending OnlineStatusEvent PresenceVisibilityEvent ProfileEvent PurchaseEvent QueryEntitlementsResponse QueryFriendsResponse RestoreRequest UserInvitedEvent VoipStatusEvent ``` (plus the separate `LSXEvent` = the `` handshake event we already send.) ### 2. `sender` value = the SDK's service-name enum. Login ⇒ `LOGIN_EVENT` `0x14710df7f` is the SDK's "create all event handlers" routine. For each handler it does: ``` edx = ; rax = GetServiceName(sdk, edx) ; 0x1470e4870 -> [sdk+0x3b0] + 0x20*edx call (sdk, rax /*sender string*/, r8 = sdk+0x278 /*array*/) ``` `GetServiceName`'s runtime table is initialised from the static string-pointer table at **`0x144341420`**: ``` 0=SDK 1=PROFILE 2=PRESENCE 3=FRIENDS 4=COMMERCE 5=RECENTPLAYER 6=IGO 7=MISC 8=LOGIN 9=UTILITY 10=XMPP 11=CHAT 12=IGO_EVENT 13=EALS_EVENTS 14=LOGIN_EVENT 15=INVITE_EVENT 16=PROFILE_EVENT 17=PRESENCE_EVENT 18=FRIENDS_EVENT 19=COMMERCE_EVENT 20=CHAT_EVENT 21=DOWNLOAD_EVENT 22=PERMISSION 23=RESOURCES 24=BLOCKED_USERS 25=BLOCKED_USER_EVENT 26=GET_USERID 27=ONLINE_STATUS_EVENT 28=ACHIEVEMENT 29=ACHIEVEMENT_EVENT 30=BROADCAST_EVENT 31=PROGRESSIVE_INSTALLATION 32=PROGRESSIVE_INSTALLATION_EVENT 33=CONTENT ``` The handler's `sender` string lands at `handler+0x48`, which is `this+0x10` for the `IEventHandler` sub-object (secondary vtable at object+0x38) — exactly what `HandleMessage` reads (`mov rbx,[rcx+0x10]`) and strcmp's the `sender` attribute against. Resolved handler table (service enum → array slot → OriginEventT enum → element): | OriginEventT | array slot | service enum | `sender` string | element | |---|---|---|---|---| | 0 | +0x00 | 0x0c | `IGO_EVENT` | `IGOEvent` | | 1 | +0x08 | 0x0f | `INVITE_EVENT` | `MultiplayerInvite` | | **2** | **+0x10** | **0x0e** | **`LOGIN_EVENT`** | **`Login`** | | 3 | +0x18 | 0x10 | `PROFILE_EVENT` | `ProfileEvent` | | 4 | +0x20 | 0x11 | `PRESENCE_EVENT` | `GetPresenceResponse` | | 5 | +0x28 | 0x12 | `FRIENDS_EVENT` | `QueryFriendsResponse` | | 6 | +0x30 | 0x13 | `COMMERCE_EVENT` | `PurchaseEvent` | | 7 | +0x38 | 0x15 | `DOWNLOAD_EVENT` | `CoreContentUpdated` | | 8 | +0x40 | 0x19 | `BLOCKED_USER_EVENT` | `BlockListUpdated` | | **9** | **+0x48** | **0x1b** | **`ONLINE_STATUS_EVENT`** | **`OnlineStatusEvent`** | | 10 | +0x50 | 0x1d | `ACHIEVEMENT_EVENT` | `AchievementSets` | | 11 | +0x58 | 0x0f | `INVITE_EVENT` | `MultiplayerInvitePending` | | 12 | +0x60 | 0x14 | `CHAT_EVENT` | `ChatMessageEvent` | | 13 | +0x68 | 0x11 | `PRESENCE_EVENT` | `CurrentUserPresenceEvent` | | 14 | +0x70 | 0x1e | `BROADCAST_EVENT` | `BroadcastEvent` | | 15 | +0x78 | 0x11 | `PRESENCE_EVENT` | `PresenceVisibilityEvent` | | 16 | +0x80 | 0x13 | `COMMERCE_EVENT` | `QueryEntitlementsResponse` | | 17 | +0x88 | 0x20 | `PROGRESSIVE_INSTALLATION_EVENT` | `ChunkStatus` | | 18 | +0x90 | 0x0c | `IGO_EVENT` | `IGOUnavailable` | (13 independent service-enum/element pairings all agree with the table — the mapping is not a guess.) ### 3. `` carries exactly one attribute: `IsLoggedIn`, parsed as a bool Deserializer `0x147138640` (reached via `0x14712fd80` from the `Login` matcher) reads exactly one attribute name, `"IsLoggedIn"` (string @`0x14394e0f0`), then converts with `0x14713ffa0`: ``` 0x14713ffc8: lea rdx, "false" 0x14713ffd2: call strcmp 0x14713ffda: setne al ; value != "false" => true 0x14713ffdd: mov [rdi], al ; stored as a byte ``` Same helper is used for `` (`0x147139e6e`) and ``. `` carries `Changed` + `UserId`. ### 4. FIFA subscribes to the Login event, and it is the ONLY event that mutates ### FIFA's Origin login state FIFA registers 9 Origin event callbacks in a loop at `0x146f33e90`+ (call site `0x146f34068` → `OriginRegisterEventCallback` @`0x1470db310`), from a constant array: ``` xmm0 @0x143565970 = {0,1,2,3} xmm1 @0x143902060 = {4,5,6,7} plus 9 => enums {0,1,2,3,4,5,6,7,9} (all with the same callback 0x146f20c20) ``` Cross-referenced against the table above, FIFA subscribes to exactly: IGOEvent, MultiplayerInvite, **Login**, ProfileEvent, GetPresenceResponse, QueryFriendsResponse, PurchaseEvent, CoreContentUpdated, **OnlineStatusEvent**. `0x146f20c20` forwards to the FIFA Origin-manager dispatcher `0x146f1e060` (manager singleton via `0x146f28790`, global `[0x1448acf50]`). Its `case 2` is the only case that writes state: ``` 0x146f1e099: cmp edx, 2 ; eventEnum == 2 (Login) 0x146f1e09e: cmp DWORD PTR [r9], 1 ; converted IsLoggedIn == 1 ? 0x146f1e0a2: lea rbx,[rcx+0x80] ; listener list for event 2 0x146f1e0ab: mov BYTE PTR [rcx+0x13], 1 ; <== OriginMgr.m_isLoggedIn = TRUE 0x146f1e0af: mov DWORD PTR [rcx+0x14], 0 ; <== clear login error/reason (else) 0x146f1e0b8: mov BYTE PTR [rcx+0x13], 0 ; m_isLoggedIn = FALSE 0x146f1e116: broadcast to listener list ``` All other cases (0,1,3,4,5,6,7,9) only broadcast to their listener list. `OriginMgr+0x13` is written from exactly two places in the whole image: this Login-event case, and `0x146f20c80` (the async login/`CheckOnline` completion callback, which also clears +0x18). **There is no requestable LSX verb that sets it** — it is unreachable without a pushed ``. ### 5. Why this matters: GetAuthCode is enqueue-driven and has never been enqueued * FIFA-level auth-code issuer: `FifaOnline::FirstPartyAuthTokenRetriever::DoTick` @`0x146f199b9` → `OriginRequestAuthCodeSync` @`0x1470db3c0` → `Origin::OriginSDK::RequestAuthCodeSync` @`0x1470e67f0` → LSX ``. * `DoTick` walks a **2-slot request array** at `retriever+0x8` and does nothing when both slots are null (`0x146f199e0: mov rsi,[rbx]; test rsi,rsi; je `). * Enqueue path: `RequestAuthCode` @`0x146f5b8ab`, reached via thunk `0x146f57bf0` (`mgr = *[0x1448a3b20]` gated by byte `[0x1448a3ac3]`; retriever = `mgr + 0x4e98`). * **Live read of the stuck game (PID 3362053) confirmed:** `[0x1448a3ac3] = 1`, `mgr = 0x43dc3e70`, retriever = `0x43dc8d08`, **both slots = 0x0** — no auth-code request has ever been created. This is why `/tmp/openfut_authcode.txt` stays empty and `GetAuthCode` never appears in the LSX log. The SDK itself has no login gate inside `RequestAuthCodeSync`; the gate is entirely FIFA-side, upstream of the enqueue. ### 6. Corroborating: `LoginStatePCLogin` has an explicit "not logged in to Origin" abort FIFA's Blaze login state machine (`LoginStateMachineImpl`, `LoginStateBase` subclasses; per-state `GetName` thunks in `0x14719b360..0x14719b4a7`; states: ShowMaintenance, Isp, LoadIspAccountInfo, Connect, LoadConfig, Logout, VersionCheck, Login, **PCLogin**, VerifyAccount, UpgradeAccount, LoginComplete, Unsuspend, CheckUser, RecheckUser, WebOffer). `LoginStatePCLogin` vtable @`0x14395c188`; its driver is `0x1471b58e0` (a 0x19-case sub-state machine on `this+0x260`). Its very first sub-state does: ``` 0x1471b59a5: rcx = *[0x144b86bf0]; call [vt+0x60] ; get the Origin/Ebisu session object 0x1471b59b5: test rax,rax 0x1471b59b8: je 0x1471b5b42 ; NULL -> failure branch ... 0x1471b5b64: lea rax, "TXT_NOT_LOGIN_TO_EBISU" ; loc key stored at state+0x80 0x1471b5b72: mov DWORD PTR [r14+0x260], 1 ; -> error sub-state ``` ("Ebisu" is EA's internal codename for Origin; the sibling key `TXT_ORIGIN_GAME_VERSION_OUT_OF_DATE` is the "title version outdated" gate we already beat via `GetGameInfo UPTODATE`. Both live at `0x1439633e8` / `0x14395bfc0`-ish in the same loc-key block.) Also proven live: FIFA's separate "Origin is online" byte `[0x1448a3ac0]` was **1** during the stuck session, so the *internet/online* gate (fed by `GetInternetConnectedState`, callback `0x146f1e6ae`, broadcasts `FE::FIFA::OriginOnlineEvent`) is already satisfied. **Online ≠ logged in.** They are two different flags with two different feeds; we only ever fed the first one. --- ## Wire recipe for `lsx_responder.py` Same framing as the existing `` event we already push successfully (NUL-terminated; after the handshake everything is `hex(AES128-ECB(session_key, pkcs7(xml)))`): ```python def push_event(conn, key, xml): conn.sendall(lsx_encrypt(xml, key)) LOGIN_EVENT = '' ONLINE_EVENT = '' PROFILE_EVT = '' ``` Trigger points (after sending our normal ``): 1. after answering the first `GetProfile` → push `LOGIN_EVENT`, then `ONLINE_EVENT` 2. after answering `GetInternetConnectedState` → push `LOGIN_EVENT` again 3. after answering `GetGameInfo UPTODATE` → push `LOGIN_EVENT` + `ONLINE_EVENT` again Notes / gotchas: * `sender` must be **exactly** `LOGIN_EVENT` / `ONLINE_STATUS_EVENT`. A wrong or missing `sender` makes `HandleMessage` return false and the message is silently dropped (no error, no crash) — which is exactly the failure mode to watch for. * An `id` attribute on `` is optional. * Sending an event before `OriginSDK::Initialize` has built the handler array is a silent no-op, so never send before the client's first request. * If the game still doesn't call `GetAuthCode` after this, the next thing to instrument is `OriginMgr+0x13` (byte) and `OriginMgr+0x14` (dword) via `/proc/PID/mem` (`OriginMgr = *[0x1448acf50]`): +0x13 flipping 0→1 proves the event landed and moves the investigation downstream to `LoginStatePCLogin` / the Blaze `Authentication::logout` loop. ## Open / not proven * I could not statically locate the consumer that *reads* `OriginMgr+0x13` (no matching `[reg+0x13]` byte-read in the FIFA online region) — it is presumably an inlined accessor or a reaction to the event-2 broadcast. So "flag false ⇒ GetAuthCode never enqueued" is a strong inference from (a) the flag being login-specific, (b) `LoginStatePCLogin`'s `TXT_NOT_LOGIN_TO_EBISU` abort and (c) the live-confirmed empty auth-code slots — but the exact read site is unconfirmed. Instrumenting +0x13 live is the cheap way to close this. * The one identified event-2 listener list subscriber is the EAStore/DLC subsystem (registrar `0x14735077f`, listener `0x147350e30`), not the login flow — consistent with the login gate reading the flag rather than listening. * The Blaze side still ends in `Authentication::logout (1/0x46)` → disconnect →3s ping-reconnect loop (`/tmp/blaze_responder.log`). If the Login event does not change that, the second candidate is the FIFA `LoginStateMachine` transition out of `LoginStateLogout`, which is a separate (Blaze-side) investigation.