6ddd5e9d47
Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).
Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
reads identity/rating/face from a resolved record at item+0x10, filled
by a lookup (0x18011cca0) in the FUT item-definition std::map at
CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
(JSON fields routed to the skip handler). Owned items don't auto-trigger
a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
ready; the fetch trigger lives in the packed FIFA17.exe.
New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
545 lines
32 KiB
Markdown
545 lines
32 KiB
Markdown
# FIFA 17 — "account information" retrieval path, end to end
|
||
|
||
**Date:** 2026-07-30 · **Target:** live `FIFA17.exe` (base `0x140000000`, Wine flat PE map),
|
||
plus the saved live-string dump `modstrings.txt` after the process exited mid-analysis.
|
||
|
||
**Clean-room provenance:** every fact below comes from (a) static/dynamic analysis of binaries we
|
||
own (`FIFA17.exe` as mapped in our own process, `stp-origin_emu.dll`), (b) our own client's observed
|
||
LSX/Blaze wire traffic (`/tmp/lsx.log`, `/tmp/blaze_responder.log`), and (c) the client's own
|
||
runtime reflection/metadata. **No 2021 EA/FIFA leak material was used or consulted.**
|
||
|
||
Builds on `auth_schema_reflection.md`, `auth_statemachine.md`, `origin_nucleus.md`.
|
||
|
||
---
|
||
|
||
## 0. Verdict (short)
|
||
|
||
**Account information is carried on the Blaze channel, but it is *gated* by the LSX/Origin channel.
|
||
The Nucleus/account HTTP channel is not part of the PC login path at all.**
|
||
|
||
* **LSX/Origin (`127.0.0.1:4216`)** supplies the *identity* and the *credential*:
|
||
`GetProfile` → the OriginSDK's default **user id / persona id**; `QueryEntitlements` →
|
||
`ONLINE_ACCESS`; **`GetAuthCode` → the one-time Origin auth code**.
|
||
* **Blaze (Fire2, `:42130`)** supplies the *account record*: `Authentication::login (1/0x0A)` takes
|
||
the auth code in `LoginRequest.AUTH` and returns `LoginResponse.SESS` (session key, blazeUserId,
|
||
userId, email, persona details) — this **is** "account information" for the login flow. The
|
||
explicit account RPCs (`getAccount 1/0x1E → AccountInfo`, `getPersona 1/0x5A`,
|
||
`listPersonas 1/0x64`, `listUserEntitlements2 1/0x1D`) all live *behind* that login.
|
||
* **Nucleus/account HTTP (`:42131`)** is unused **by design of this code path**, for two independent
|
||
reasons proven below (§4). It is not a missing piece; it is a dead end for PC/Origin login.
|
||
|
||
**The exact missing piece:** `LoginRequest.AUTH` (`authCode`, a plain string) can only be filled by
|
||
LSX `GetAuthCode`, and the client never issues it. No auth code → no `Authentication::login` → no
|
||
`LoginResponse` → nothing that can answer "account information" → popup. Everything downstream
|
||
(`getAccount`, `listPersonas`, FUT `user/accountinfo`) is unreachable.
|
||
|
||
---
|
||
|
||
## 1. Channel 1 — LSX / Origin on `127.0.0.1:4216`
|
||
|
||
### 1.1 Complete OriginSDK LSX surface (from the client's own template instantiations)
|
||
|
||
`Origin::LSXRequest<>` / `LSXEnumeration<>` instantiations present in the image:
|
||
|
||
| Request | Response | Notes |
|
||
|---|---|---|
|
||
| `GetConfigT` | `GetConfigResponseT` | |
|
||
| `GetProfileT` | `GetProfileResponseT` | **sets the default user/persona — see §1.2** |
|
||
| `GetSettingT` | `GetSettingResponseT` | |
|
||
| `GetGameInfoT` | `GetGameInfoResponseT` | `UPTODATE` gate |
|
||
| `GetInternetConnectedStateT` | `InternetConnectedStateT` | the online gate |
|
||
| **`GetAuthCodeT`** | **`AuthCodeT`** | handler `0x1439385b0`, callback `0x143938660` |
|
||
| `QueryEntitlementsT` | `QueryEntitlementsResponseT` → `OriginItemT` | enumeration, `0x1439484e0` |
|
||
| `GetUserProfileByEmailorEAIDT` | `…ResponseT` → `OriginFriendT` | |
|
||
| `IsProgressiveInstallationAvailableT`, `AreChunksInstalledT`, `QueryChunkStatusT`, `SetDownloaderUtilizationT`, `QueryOffersT`, `GetWalletBalanceT`, `CheckoutT`, `ConsumeEntitlementT`, `GetPresenceT`, `SetPresenceT`, `QueryFriendsT`, `GetBlockListT`, `SendInviteT`, `ShowIGOT`, `GrantAchievementT`, `ExtendTrialT`, `SelectStoreT` | | not on the login path |
|
||
|
||
### 1.2 `GetProfile` is what defines "who is logged in" — **proven**
|
||
|
||
`OriginGetDefaultUser()` @ `0x1470da6d0` and `OriginGetDefaultPersona()` @ `0x1470da680` are bare
|
||
field reads:
|
||
|
||
```
|
||
1470da6d0 call 0x1470e2840 ; SDK-ready predicate
|
||
1470da6f4 call 0x1470e3560 ; get impl singleton
|
||
1470da6f9 mov rax,[rax+0x3a0] ; <<< default USER id
|
||
1470da680 ... mov rax,[rax+0x3a8] ; <<< default PERSONA id
|
||
```
|
||
|
||
Both fields are zeroed in the SDK constructor (`0x1470deb2f` / `0x1470deb36`, `rsi = 0`) and are
|
||
written in exactly **one** place — `Origin::OriginSDK::Initialize` @ ~`0x1470e5a30`:
|
||
|
||
```
|
||
1470e5ac7 call 0x147118d80 ; sync GetProfile(index=0), 15000 ms timeout (0x3a98)
|
||
1470e5acc test eax,eax
|
||
1470e5ace jne ... ; on failure, leave 0
|
||
1470e5ad0 mov rax,[rsp+0x70]
|
||
1470e5ad5 mov [rdi+0x3a0],rax ; <<< userId from GetProfileResponse
|
||
1470e5adc mov rax,[rsp+0x78]
|
||
1470e5ae1 mov [rdi+0x3a8],rax ; <<< personaId from GetProfileResponse
|
||
```
|
||
|
||
`0x147118d80` builds the request through `0x147117fe0` with `index = 0` and waits via `0x1471186f0`.
|
||
The service name used is **`"EbisuSDK"`** (`0x143937d58`), which is why our responder must answer
|
||
`GetProfile` with `sender="EbisuSDK"` — it already does.
|
||
|
||
> **Consequence:** our `GetProfileResponse` with `UserId="33068179" PersonaId="33068179"` *is* the
|
||
> mechanism that populates the SDK's identity. That part is working; the client asked `GetProfile`
|
||
> three times (ids 3, 10, 17) and we answered correctly each time.
|
||
|
||
### 1.3 The auth-code request path — fully mapped
|
||
|
||
`FifaOnline::FirstPartyAuthTokenRetriever::DoTick` @ **`0x146f199c0`**:
|
||
|
||
```
|
||
rbx = this+8 ; rbp = 2 ; two request slots: this+0x08, this+0x10
|
||
loop:
|
||
rsi = [rbx]; if (!rsi) goto next ; nothing pending -> nothing happens, silently
|
||
[rsp+0x60] = 0 ; out: auth-code buffer
|
||
[rsp+0x58] = 0 ; out: auth-code length
|
||
rax = call 0x1470da6d0 ; OriginGetDefaultUser() -> sdk[+0x3a0]
|
||
r9 = &len ; r8 = &buf ; rdx = rsi+0x18 ; rdx = ClientId string from the request object
|
||
rcx = rax ; user handle
|
||
eax = call 0x1470db3c0 ; Origin::OriginSDK::RequestAuthCodeSync
|
||
if (eax != 0) -> 0x146f19aab ; log "[%s] Origin Error(%d)" (0x1438f5e18)
|
||
if (buf == 0 || len == 0) -> 0x146f19a7e ; log "[%s] Invalid authcode" (0x1438f5e00)
|
||
; success: store the code at rsi+0xd8, set rsi+0xe8 = 1
|
||
```
|
||
|
||
`Origin::OriginSDK::RequestAuthCodeSync` @ **`0x1470db3c0`**:
|
||
|
||
```
|
||
1470db3da lea rdx,[0x143936158] ; trace "OriginRequestAuthCodeSync entered"
|
||
1470db3f2 call 0x1470dbf30 ; trace
|
||
1470db3f7 call 0x1470e2840 ; <<< SDK-ready predicate
|
||
1470db3fe je 0x1470db424 ; NOT ready -> error 0xa0010000, NO LSX TRAFFIC AT ALL
|
||
1470db400 call 0x1470e3560 ; impl
|
||
1470db41d call 0x1470e67f0 ; impl->RequestAuthCodeSync(user, clientId, &buf, &len, 0)
|
||
```
|
||
|
||
**Signature (recovered):** `OriginRequestAuthCodeSync(OriginUserT user, const char* clientId,
|
||
char** outBuf, size_t* outLen, ...)`. `clientId` comes from the request object at `+0x18` and is
|
||
what lands in the LSX `<GetAuthCode ClientId="…"/>` attribute (attribute name `ClientId`
|
||
@ `0x14394e098`).
|
||
|
||
**Note the wire-silent failure path:** if `0x1470e2840` returns false, `RequestAuthCodeSync` logs and
|
||
returns `0xa0010000` **without ever touching the socket**. That is a failure mode that looks exactly
|
||
like our symptom (nothing in the LSX log). It is, however, unlikely here, because the same predicate
|
||
guards `OriginGetDefaultUser`, `OriginCheckOnline`, `OriginGetProfile` etc., all of which demonstrably
|
||
worked. Ranked below in §5.
|
||
|
||
### 1.4 Pushed LSX **Events** — the SDK does support them, including `<Login>`
|
||
|
||
Our responder is request-driven only and never pushes. The client's OriginSDK *does* register
|
||
event handlers. Complete inventory of `Origin::EventHandler<lsx::…T, X>::HandleMessage`:
|
||
|
||
| Element (name table `0x14393cfd8`–`0x14393d208`) | Payload type |
|
||
|---|---|
|
||
| **`Login`** @ `0x14393d0ac` | `unsigned int` |
|
||
| `OnlineStatusEvent` @ `0x14393d120` | `bool` |
|
||
| `ProfileEvent` @ `0x14393d0b8` | `OriginProfileChangeT` |
|
||
| `CurrentUserPresenceEvent`, `PresenceVisibilityEvent`, `BroadcastEvent`, `IGOEvent`, `IGOUnavailable`, `MinimizeRequest`, `RestoreRequest`, `MultiplayerInvite`, `MultiplayerInvitePending`, `UserInvitedEvent`, `PurchaseEvent`, `ChatMessageEvent`, `GameMessageEvent`, `CoreContentUpdated`, `BlockListUpdated`, `AchievementSets`, `ChunkStatus`, `GroupEvent`, `GroupEnterEvent`, `GroupLeaveEvent`, `GroupInviteEvent`, `VoipStatusEvent` | various |
|
||
|
||
`Origin::LSXEvent<>` (the *handshake* event path) is instantiated for `ChallengeT` **only** — that
|
||
is the `<Event sender="EALS"><Challenge …/></Event>` we already send. Everything in the table above
|
||
goes through the `EventHandler`/`EventEnumerator` dispatch, whose element-name match is at
|
||
`0x1471028ee` (it first compares the `sender` attribute — `sender` @ `0x143938028` — then the element
|
||
name).
|
||
|
||
**Relevant attribute names in the pool** (`0x14394de00`+) that belong to this family:
|
||
`SessionInformation` `0x14394e0d8`, `IsLoggedIn` `0x14394e0f0`, `Changed` `0x14394e100`,
|
||
`userid` `0x14394e108`, `isOnline` `0x14394e180`, `initial` `0x14394e0c8`, `from` `0x14394e0d0`.
|
||
|
||
So hypothesis (a) from the brief is **structurally possible** — a `<Login>` event exists and the
|
||
client can consume it. It is *not* proven to be required (see §5).
|
||
|
||
---
|
||
|
||
## 2. Channel 2 — Blaze (Fire2 on `:42130`) — **this is where account info actually lives**
|
||
|
||
Schemas already reflected out in `auth_schema_reflection.md`; the load-bearing parts:
|
||
|
||
```
|
||
Authentication::LoginRequest (0x14487ca10, 3) AUTH authCode:string EXTB externalBlob:blob
|
||
EXTI externalId:uint64
|
||
Authentication::LoginResponse (0x14487d170, 5) ANON NTOS SESS SPAM UNDR
|
||
SESS = UserLoginInfo (0x14487cb00, 8) KEY_ sessionKey BUID blazeUserId UID_ userId
|
||
MAIL email PDTL personaDetails LLOG FRST 1CON
|
||
Authentication::AccountInfo (0x14487c810, 16) <- reply of getAccount (1/0x1E), request is EMPTY
|
||
AMU anonymousUser:bool ASRC authenticationSource:string CO country:string DOB dOB:string
|
||
DTCR dateCreated:string GOPT globalOptin:int8 LATH lastAuth:string
|
||
LN language:string MAIL email:string PML parentalEmail:string RC reasonCode:enum
|
||
STAS status:enum STAT emailStatus:enum TPOT thirdPartyOptin:int8
|
||
UDU underageUser:bool UID userId:int64
|
||
Authentication::Entitlements (0x14487d4e0, 1) NLST list<Entitlement> <- listUserEntitlements2 (1/0x1D)
|
||
Authentication::Entitlement (0x14487d490, 16) TAG entitlementTag PRID productId STAT status
|
||
PID personaId GDAY grantDate UCNT useCount …
|
||
```
|
||
|
||
Command ids (recovered by calling the client's own `getCommandName`, cross-checked against the
|
||
static REST binding at `0x143896a80` → `trustedLogin = 0x0B`):
|
||
`login 0x0A · trustedLogin 0x0B · listUserEntitlements2 0x1D · getAccount 0x1E · getAuthToken 0x24 ·
|
||
listPersonaEntitlements2 0x30 · expressLogin 0x3C · logout 0x46 · getPersona 0x5A · listPersonas 0x64 ·
|
||
getOriginPersona 0x104`.
|
||
|
||
**Observed wire behaviour (14:46 session):**
|
||
`Util::preAuth (9/0x07)` ✓ → `Util::ping` ✓ → 6× `Util::fetchClientConfig` (`OSDK_CORE`,
|
||
`OSDK_CLIENT`, `OSDK_NUCLEUS`, `OSDK_WEBOFFER`, `OSDK_ABUSE_REPORTING`, `OSDK_XMS_ABUSE_REPORTING`) ✓
|
||
→ **`Authentication::logout (1/0x46)`, empty payload** → disconnect → 3-second transport-ping
|
||
reconnect loop forever.
|
||
|
||
`logout` has no request and no response TDF (confirmed by absence of `LogoutRequest`/`LogoutResponse`
|
||
anywhere in the client's type index). It is the OSDK `LoginStateLogout` state — i.e. **the login state
|
||
machine aborted between `LoadConfig` and `Login`.**
|
||
|
||
### 2.1 The OSDK login state machine (recovered state list)
|
||
|
||
`LoginStateMachineImpl` states, from the client's `GetStateName` thunks at `0x14719b360`+:
|
||
|
||
```
|
||
LoginStateShowMaintenance LoginStateIsp LoginStateLoadIspAccountInfo
|
||
LoginStateConnect LoginStateLoadConfig LoginStateVersionCheck
|
||
LoginStateLogin LoginStatePCLogin LoginStateVerifyAccount
|
||
LoginStateUpgradeAccount LoginStateLoginComplete LoginStateUnsuspend
|
||
LoginStateCheckUser LoginStateRecheckUser LoginStateWebOffer
|
||
LoginStateLogout
|
||
```
|
||
|
||
Mapped to observed traffic: `Connect` = `preAuth`; `LoadConfig` = the 6 `fetchClientConfig` calls;
|
||
then it should proceed `VersionCheck → PCLogin → Login → VerifyAccount → LoadIspAccountInfo →
|
||
LoginComplete`. It went to `Logout` instead.
|
||
|
||
Associated OSDK events (`0x143984000`+): `EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE` /
|
||
`…_SUCCESS`, `EVENT_LOGIN_FAILURE`, `EVENT_LOGIN_ABORTED`, `EVENT_LOGIN_QUEUED`,
|
||
`EVENT_LOGIN_TOS_NOT_ACCEPTED`, `EVENT_LOGIN_TOLLBOOTH`, …
|
||
Associated operation names (`0x1439623c0`+): **`FetchAccountInfo`**, `UpdateAccountInfo`,
|
||
`LookupOriginPersona`, `FetchOriginPersona`, `CreateOriginPersona`.
|
||
|
||
`LoginStateVersionCheck` reads three keys out of the Blaze client config —
|
||
`SV_ENABLE_SERVER_VERSIONING` `0x14395d148`, `SV_CLIENT_CHANGELIST` `0x14395d168`,
|
||
`SV_SERVER_VERSION` `0x14395d180` — and on mismatch emits
|
||
`"Client/server version mismatch! Client is at version (%08d). Server is at version (%08d).%s"`
|
||
(`0x14395d1d0`). **We currently return none of these keys.** Absent ⇒ almost certainly treated as
|
||
disabled, but see §6 for the cheap belt-and-braces fix.
|
||
|
||
---
|
||
|
||
## 3. Channel 3 — the FUT web API (UTAS) — real, but strictly post-login
|
||
|
||
This is a *fourth* surface the brief did not list, and it is the one that literally serves a thing
|
||
called "account info". It is **not** the current blocker (it is unreachable without a Blaze session),
|
||
but it *will* be the next wall, so it is documented here.
|
||
|
||
```
|
||
0x1438dbe88 FUT_RS4_BASE_URL <- config key; NO hardcoded host anywhere in the image
|
||
0x1438dbe58 ut/game/%s/ <- path prefix, %s = sku ("fifa17", 0x1438dac20)
|
||
0x1438db6c0 user/accountinfo <- THE FUT account-info endpoint
|
||
0x1438db810 users/club?sku= <- POST body {"idList":[%I64d,…]}
|
||
0x1438db8f8 utStats?sku=
|
||
0x1438dbe48 FutServerCall / FutServerCall::RequestBuffer
|
||
0x1438dc378 FutGetUserAccountInfoServerCallConfig
|
||
0x1438dc3a0 FutGetUtStatsServerCallConfig
|
||
0x1438dc3c0 FutGetUsersClubInfoServerCallConfig
|
||
```
|
||
|
||
Request headers for `user/accountinfo` (contiguous at `0x1438db6e0`–`0x1438db731`):
|
||
|
||
```
|
||
Accept: application/json
|
||
Content-Type: application/json
|
||
Accept-Encoding: gzip
|
||
Easw-Session-Data-Nucleus-Id: %lld <- the nucleus/user id, i.e. 33068179
|
||
```
|
||
|
||
Response JSON keys the client parses (contiguous at `0x1438db760`–`0x1438db8b0`):
|
||
`userAccountInfo` · `personas` · `userClubList` · `clubName` · `established` · `assetId` ·
|
||
`returningUser` · `userPersonaInfos` · `divisionOnline`.
|
||
|
||
A separate OSDK "SportsWorld"/EASFC HTTP module uses its own header set
|
||
(`0x14396f8b0`+): `EASW-Version: 2.0.5.0`, `EASW-Token:`, `EASW-Session:`,
|
||
`EASW-Nucleus-Persona:`, `EASW-Userid:`, `EASW-Request-Signature:`, `EASW-Content-Signature:`.
|
||
|
||
Because `FUT_RS4_BASE_URL` has no hardcoded default, **we can point the entire FUT web API at our own
|
||
HTTP server purely by serving that config key** — no DNS/DNAT needed. Worth banking now.
|
||
|
||
---
|
||
|
||
## 4. Why the `nucleusConnect` stub on `127.0.0.1:42131` is unused — answered
|
||
|
||
Two independent reasons, both proven.
|
||
|
||
### 4.1 `nucleusConnect` / `nucleusConnectTrusted` are **server-supplied config keys**, and we never send them
|
||
|
||
`0x14389fef8 = "nucleusConnect"` has exactly one reader, `0x147237862`:
|
||
|
||
```
|
||
147237830 sub rsp,0x28
|
||
147237834 call 0x1471995b0 ; get service locator
|
||
14723783f call [rdx+0x188] ; -> Blaze connection/config object
|
||
147237845 mov rcx,[rax+0x750]
|
||
147237855 test rcx,rcx ; je -> return 0
|
||
147237862 lea rdx,[0x14389fef8] ; "nucleusConnect"
|
||
147237869 call [rax+0x48] ; getConfigString(key, &out)
|
||
14723786c mov rax,[rsp+0x38] ; return the string
|
||
```
|
||
|
||
`[vtbl+0x48]` is the Blaze config-string getter. The value therefore comes from the **server** —
|
||
the `CONF` map in our `PreAuthResponse`, or a `fetchClientConfig` section. Our `PreAuthResponse`
|
||
`CONF` map contains only timing values (`pingPeriod` etc.) and our six `fetchClientConfig` replies
|
||
are fabricated key sets that contain no `nucleusConnect*`. **The client therefore gets an empty
|
||
string and never dials anything.** Zero requests on `:42131` is the expected, correct outcome of
|
||
what we are currently serving.
|
||
|
||
### 4.2 The Nucleus path that *does* exist is S2S/client-cert-only, and belongs to `trustedLogin`
|
||
|
||
`nucleusConnectTrusted` (`0x14389fdf8`) is read at `0x146e1658b` and immediately feeds a URL builder:
|
||
|
||
```
|
||
146e1658b lea rdx,[0x14389fdf8] ; "nucleusConnectTrusted"
|
||
146e16595 call [rax+0x48] ; getConfigString
|
||
14e1659d lea r8,[0x14389fe10] ; "%s/connect/token"
|
||
146e165ae call 0x146dc0950 ; snprintf(buf, 0x400, "%s/connect/token", url)
|
||
146e165db movups xmm0,[0x14389fe28] ; "grant_type=client_credentials"
|
||
```
|
||
|
||
Surrounding literals, contiguous, all in the BlazeSDK `LoginStateMachine` block:
|
||
|
||
```
|
||
0x14389fd90 Content-Type: application/x-www-form-urlencoded
|
||
0x14389fdc1 enable-client-cert-auth: true
|
||
0x14389fde0 LoginStateMachine
|
||
0x14389fdf8 nucleusConnectTrusted
|
||
0x14389fe10 %s/connect/token
|
||
0x14389fe28 grant_type=client_credentials
|
||
0x14389fe50 NEXUS_S2S
|
||
0x14389fe60 "access_token" : "
|
||
0x14389fef8 nucleusConnect
|
||
```
|
||
|
||
So the shape, if it were ever used, is:
|
||
|
||
```
|
||
POST {nucleusConnectTrusted}/connect/token
|
||
Content-Type: application/x-www-form-urlencoded
|
||
enable-client-cert-auth: true
|
||
Authorization: NEXUS_S2S <credential>
|
||
|
||
grant_type=client_credentials
|
||
→ 200 {"access_token" : "…"}
|
||
```
|
||
|
||
…and the token then goes into `TrustedLoginRequest {ID_ id, ITYP idType, TOKN accessToken}` =
|
||
**`Authentication::trustedLogin (1/0x0B)`**, whose REST binding at `0x143896a80` confirms the shape
|
||
(`GET`, headers `Authorization: accessToken`, `X-Forwarded-UserType: idType`,
|
||
`X-Forwarded-UserId: id`).
|
||
|
||
**This is the console/dedicated-server trusted path. It requires a client certificate
|
||
(`enable-client-cert-auth: true`) and is not what the PC/Origin build uses.** The PC build uses
|
||
`login (1/0x0A)` with `AUTH = <Origin auth code>`.
|
||
|
||
### 4.3 The hardcoded EA account hosts are for the web UI, not for login
|
||
|
||
```
|
||
0x143b8b528 https://accounts.int.ea.com/ 0x143b8b588 https://accounts.ea.com/
|
||
0x143b8b548 https://gateway.int.ea.com/ 0x143b8b5a8 https://gateway.ea.com/
|
||
0x143b8b568 https://signin.int.ea.com/ 0x143b8b5c0 https://signin.ea.com/
|
||
0x143b8b700 Nucleus::gNucleusLocale 0x143b8b718 Nucleus::gNucleusBaseUrl
|
||
0x143b8b738 Nucleus::gNucleusBaseProxyUrl 0x143b8b758 Nucleus::gNucleusBasePortalUrl
|
||
0x143b8b778 Nucleus::gNucleusClientSideRedirectUri
|
||
```
|
||
|
||
These back the EAWebKit account-management pages, reached through the OSDK WebOffer config keys
|
||
`NUCLEUS_CREATE_URL` / `NUCLEUS_ADDED_URL` / `NUCLEUS_INCOMPLETE_URL` / `NUCLEUS_CREATE_INFO_URL` /
|
||
`NUCLEUS_DUPACCT_INFO_URL` / `NUCLEUS_DEACTIVATED_INFO_URL` (`0x14395eb60`–`0x14395ec18`, sitting in
|
||
the middle of the `WEB_OFFER_URL` / `NEWS_URL` / `FAQ_URL` / `TOSA_URL` block) — i.e. the
|
||
`OSDK_NUCLEUS` `fetchClientConfig` section is **a set of account-web-page URLs**, not login plumbing.
|
||
That the game does not dial `accounts.ea.com` is therefore *correct behaviour*, not a symptom.
|
||
|
||
> **Bottom line on channel 3:** do not build a Nucleus HTTP server. If you want the `:42131` stub to
|
||
> ever receive traffic you would have to (a) advertise `nucleusConnectTrusted` in the Blaze config
|
||
> and (b) satisfy `enable-client-cert-auth` over TLS — and that would put you on the `trustedLogin`
|
||
> path, which is *not* the path this build's login state machine takes. Serve the auth code instead.
|
||
|
||
---
|
||
|
||
## 5. The dependency chain, and where it actually breaks
|
||
|
||
```
|
||
LSX GetInternetConnectedState connected="1" ✅ done
|
||
-> g_originOnline @0x1443337f8 (writer 0x146f1e6b0) + FE::FIFA::OriginOnlineEvent
|
||
-> origin.nav emits OriginIsOnlineTrue -> startFutBlazeLogin
|
||
LSX GetGameInfo UPTODATE="true" ✅ done
|
||
LSX GetConfig / GetProfile ✅ done -> sdk[+0x3a0]=UserId, [+0x3a8]=PersonaId
|
||
--------------------------------------------------------------------------------
|
||
OSDK LoginStateConnect -> Blaze Util::preAuth ✅ answered
|
||
OSDK LoginStateLoadConfig-> Blaze Util::fetchClientConfig × 6 ✅ answered (fabricated data)
|
||
OSDK LoginStateVersionCheck -> SV_* keys from config ⚠️ keys absent
|
||
OSDK LoginStatePCLogin -> FirstPartyAuthTokenRetriever::DoTick
|
||
-> OriginGetDefaultUser()
|
||
-> LSX GetAuthCode(ClientId) ❌ NEVER SENT <<< BREAK
|
||
-> Blaze Authentication::login AUTH=<code> ❌ never sent
|
||
OSDK LoginStateVerifyAccount / LoadIspAccountInfo
|
||
-> Blaze getAccount 1/0x1E -> AccountInfo ❌ unreachable
|
||
-> QueryEntitlements / listUserEntitlements2 ❌ unreachable
|
||
OSDK LoginStateLoginComplete ❌ unreachable
|
||
--------------------------------------------------------------------------------
|
||
FUT: CheckFUTRosters -> {FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo ❌ unreachable
|
||
```
|
||
|
||
Observed instead: `LoadConfig` → **`LoginStateLogout`** → `Authentication::logout (1/0x46)` →
|
||
disconnect → reconnect ping loop. In the most recent run the client did not even re-reach `preAuth`;
|
||
it sat in a bare PING/close loop every 3 s, i.e. the connection manager gave up.
|
||
|
||
### Ranked hypotheses for "no `GetAuthCode`", with the discriminating test for each
|
||
|
||
| # | Hypothesis | Evidence for | Evidence against | Discriminating probe |
|
||
|---|---|---|---|---|
|
||
| **H1** | The login state machine aborts *before* `LoginStatePCLogin` — i.e. nothing is ever enqueued into `DoTick`'s two slots, so `RequestAuthCodeSync` is never called. | `logout` arrives immediately after the last `fetchClientConfig`, with **no** intervening RPC and no LSX traffic. `QueryEntitlements` is *also* never issued — consistent with "the whole PC-login step never ran", not with "the auth-code call failed". | — | Breakpoint `0x146f199c0` (`DoTick`) and `0x1470db3c0`. If `DoTick` runs but both slots are null → H1 confirmed. **Do this first.** |
|
||
| **H2** | Our fabricated `fetchClientConfig` payloads are wrong/insufficient, so `LoadConfig` "succeeds" but a required key is missing and the next state fails. | All six replies contain keys we invented. `SV_ENABLE_SERVER_VERSIONING` / `SV_CLIENT_CHANGELIST` / `SV_SERVER_VERSION` are read by `LoginStateVersionCheck` and we send none. `OSDK_NUCLEUS` should be six `NUCLEUS_*_URL` keys; we send four invented `OSDK_NUCLEUS_*` keys. | Missing keys usually degrade to `""`/0 in OSDK. | Serve the corrected config (§6.1) and re-run; watch whether the client advances past `LoadConfig`. Cheap, no RE needed. |
|
||
| **H3** | The OriginSDK needs a *pushed* `<Login>` / `<OnlineStatusEvent>` event before it considers a user authenticated. | `Origin::EventHandler<lsx::LoginT, unsigned int>` exists (`0x14393f900`), element `Login` @ `0x14393d0ac`; attributes `IsLoggedIn` / `SessionInformation` / `userid` exist in the pool. Our responder never pushes anything. | The Steampunks emu never pushed events either, yet the game still got as far as `preAuth`. `OriginGetDefaultUser` is fed by `GetProfile`, *not* by the Login event (§1.2) — so the SDK's notion of "who" does not depend on it. | Push `<LSX><Event sender="EbisuSDK"><Login userid="33068179" IsLoggedIn="1"/></Event></LSX>` (encrypted, right after `ChallengeAccepted`) and see whether behaviour changes. Cheap; try alongside H2. |
|
||
| **H4** | `RequestAuthCodeSync` early-outs at the SDK-ready predicate `0x1470e2840` and returns `0xa0010000` without wire traffic. | It is the only *silent* failure path inside the auth-code call. | The same predicate guards `OriginGetDefaultUser` / `OriginGetProfile` / `OriginCheckOnline`, all of which demonstrably worked in this session. | Breakpoint `0x1470db3f7`; check `al` after `0x1470e2840`. Only worth doing if H1's `DoTick` probe shows the call *is* being made. |
|
||
|
||
H1 is by far the most likely. H2 is the cheapest thing that could plausibly cause H1.
|
||
|
||
---
|
||
|
||
## 6. What we must serve
|
||
|
||
### 6.1 Blaze `Util::fetchClientConfig` — replace the invented keys
|
||
|
||
Serve authentic key *names* (values can be conservative). Recovered key names, by section:
|
||
|
||
* **`OSDK_CORE`** — include the version-check trio explicitly so nothing is ambiguous:
|
||
`SV_ENABLE_SERVER_VERSIONING = 0`, `SV_CLIENT_CHANGELIST = 0`, `SV_SERVER_VERSION = 0`.
|
||
Keep `OSDK_PRESENCE_DELAY = 5`, `OSDK_PRESENCE_POLL = 60` (real key names, `0x143962848`/`0x143962860`).
|
||
* **`OSDK_NUCLEUS`** — the *real* keys are web-page URLs, not the `OSDK_NUCLEUS_*` we invented:
|
||
`NUCLEUS_CREATE_URL`, `NUCLEUS_ADDED_URL`, `NUCLEUS_INCOMPLETE_URL`, `NUCLEUS_CREATE_INFO_URL`,
|
||
`NUCLEUS_DUPACCT_INFO_URL`, `NUCLEUS_DEACTIVATED_INFO_URL` (`0x14395eb60`–`0x14395ec18`).
|
||
Empty strings are fine and honest; invented `OSDK_NUCLEUS_ENABLED=1` is not.
|
||
* **`OSDK_WEBOFFER`** — real keys: `WEB_OFFER_URL`, `NEWS_URL`, `NEWS_TIME_STAMP_URL`, `FAQ_URL`,
|
||
`TOSA_URL`, `TOSAC_URL`, `MENU_ESPN_URL`, `MENU_WEBGM0_URL`…`MENU_WEBGM2_URL`.
|
||
* **`OSDK_ABUSE_REPORTING` / `OSDK_XMS_ABUSE_REPORTING`** — `OSDK_ABUSE_REPORTING_ENABLED = 0`,
|
||
`OSDK_ABUSE_NUM_TYPES = 0` (already correct).
|
||
* **`OSDK_CLIENT`** — the `OSDK_CLUBS_*` limits we already send are real key names.
|
||
* Consider adding **`FUT_RS4_BASE_URL = http://127.0.0.1:<port>/`** — see §6.4.
|
||
|
||
### 6.2 LSX `GetAuthCode` — be ready the instant it is asked
|
||
|
||
```xml
|
||
<LSX><Response id="N" sender="EbisuSDK">
|
||
<AuthCode Code="QUXbLm3…opaque…" Return="QUXbLm3…opaque…"/>
|
||
</Response></LSX>
|
||
```
|
||
|
||
Emit **both** `Code` and `Return` until the log shows which one the client consumes (the attribute
|
||
pool position between `ClientId` and `connected` is suffix-shared, so `Code` is the strong
|
||
candidate). The code is opaque — we author both ends — but it **must be echoed verbatim into
|
||
`LoginRequest.AUTH`** and accepted there. Log the incoming `ClientId` value; that tells us which EA
|
||
client id FIFA 17 presents and is worth recording.
|
||
|
||
### 6.3 LSX `QueryEntitlements` — pre-stage the answer
|
||
|
||
```xml
|
||
<LSX><Response id="N" sender="EbisuSDK"><QueryEntitlementsResponse>
|
||
<OriginItem ItemId="ONLINE_ACCESS" EntitlementId="1" ResourceId="1027460" OfferId="1027460"
|
||
GrantDate="2016-09-01T00:00:00Z" bIsOwned="true" Uses="0"/>
|
||
</QueryEntitlementsResponse></Response></LSX>
|
||
```
|
||
|
||
### 6.4 Blaze `Authentication` — the account-info replies to have ready
|
||
|
||
| Cmd | Reply | Must contain |
|
||
|---|---|---|
|
||
| `login 0x0A` | `LoginResponse` | `ANON=0 NTOS=0 UNDR=0 SPAM=1`; `SESS.KEY_` non-empty; `SESS.BUID = SESS.UID_ = 33068179`; `SESS.PDTL.PID_ = 33068179`; `SESS.PDTL.DSNM = "CAGE"`; `SESS.PDTL.STAS = 0`; `SESS.PDTL.PLAT` = the `PLAT` from `PreAuthResponse` |
|
||
| `getAccount 0x1E` | `AccountInfo` (empty request) | `UID = 33068179`, `CO = "US"`, `LN = "en_US"`, `MAIL` = any well-formed address, `STAS` = active, `STAT` = verified, `UDU = false`, `AMU = false`, `ASRC = "cem_ea_id"` (matches the `NASP` we already return in `PreAuthResponse`), `DTCR` / `LATH` ISO-8601 |
|
||
| `listPersonas 0x64` | `ListPersonasResponse` | one persona: id `33068179`, name `CAGE`, status active |
|
||
| `getPersona 0x5A` | `GetPersonaResponse` | same single persona |
|
||
| `listUserEntitlements2 0x1D` | `Entitlements` | one `Entitlement`: `TAG = "ONLINE_ACCESS"`, `PRID` tied to offer `1027460`, `PID = 33068179`, `STAT` active |
|
||
| `logout 0x46` | empty REPLY | already correct — but treat its arrival as the failure signal it is |
|
||
|
||
Also serve `UserSessions` notification `UserAuthenticated (0x08)` after login (notification ids
|
||
already decoded: `0x141b03f70`).
|
||
|
||
### 6.5 FUT web (`{FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo`) — the next wall, pre-built
|
||
|
||
```
|
||
GET {FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo
|
||
Accept: application/json
|
||
Content-Type: application/json
|
||
Accept-Encoding: gzip
|
||
Easw-Session-Data-Nucleus-Id: 33068179
|
||
|
||
200 {"userAccountInfo": {
|
||
"personas": [{
|
||
"personaId": 33068179, "personaName": "CAGE",
|
||
"returningUser": 0,
|
||
"userClubList": [{"clubName":"…","established":<epoch>,"assetId":<id>}],
|
||
"userPersonaInfos": [], "divisionOnline": 0
|
||
}]
|
||
}}
|
||
```
|
||
|
||
Because `FUT_RS4_BASE_URL` has no hardcoded default, serving it as a config key is enough to point
|
||
the whole FUT web API at us — plain HTTP on `127.0.0.1` is fine, no TLS or DNAT needed.
|
||
|
||
---
|
||
|
||
## 7. Immediate next actions (ordered)
|
||
|
||
1. **Probe H1.** Relaunch, breakpoint `0x146f199c0` (`FirstPartyAuthTokenRetriever::DoTick`) and
|
||
`0x1470db3c0` (`RequestAuthCodeSync`). Whether `DoTick` runs at all, and whether its two slots are
|
||
null, decides H1 vs H4 in one shot.
|
||
2. **Ship the corrected `fetchClientConfig` (§6.1)** — real key names, explicit `SV_*` trio. Cheapest
|
||
possible fix for H2 and it removes a class of ambiguity permanently.
|
||
3. **Add `GetAuthCode` + `QueryEntitlements` responses (§6.2/6.3)** to `lsx_responder.py` so the
|
||
moment the client asks, it is answered — and log the `ClientId` it presents.
|
||
4. **Try the pushed `<Login>` event (§H3)** — one extra encrypted frame after `ChallengeAccepted`.
|
||
Low cost, and it is the only untested structural difference between us and a real Origin client.
|
||
5. Keep the Nucleus HTTP stub on `:42131` parked. It is on the `trustedLogin`/client-cert path and is
|
||
not what this build uses.
|
||
|
||
---
|
||
|
||
## Appendix — addresses
|
||
|
||
| What | VA |
|
||
|---|---|
|
||
| `FifaOnline::FirstPartyAuthTokenRetriever::DoTick` | `0x146f199c0` |
|
||
| `Origin::OriginSDK::RequestAuthCodeSync` | `0x1470db3c0` |
|
||
| … its SDK-ready predicate (wire-silent bail) | `call 0x1470e2840` @ `0x1470db3f7` |
|
||
| … its impl call | `call 0x1470e67f0` @ `0x1470db41d` |
|
||
| `OriginGetDefaultUser()` → `sdk[+0x3a0]` | `0x1470da6d0` |
|
||
| `OriginGetDefaultPersona()` → `sdk[+0x3a8]` | `0x1470da680` |
|
||
| `Origin::OriginSDK::Initialize` — writes `+0x3a0`/`+0x3a8` from `GetProfile` | `0x1470e5ad5` / `0x1470e5ae1` |
|
||
| sync `GetProfile(index=0)` helper (timeout `0x3a98` = 15 s) | `0x147118d80` |
|
||
| OriginSDK ctor zeroing `+0x3a0`/`+0x3a8` | `0x1470deb2f` / `0x1470deb36` |
|
||
| `"[%s] Invalid authcode"` / `"[%s] Origin Error(%d)"` | `0x1438f5e00` / `0x1438f5e18` |
|
||
| `"OriginRequestAuthCodeSync entered"` | `0x143936158` |
|
||
| `nucleusConnect` (config key) / its only reader | `0x14389fef8` / `0x147237862` |
|
||
| `nucleusConnectTrusted` / its reader | `0x14389fdf8` / `0x146e1658b` |
|
||
| `%s/connect/token` · `grant_type=client_credentials` · `enable-client-cert-auth: true` · `NEXUS_S2S ` | `0x14389fe10` · `0x14389fe28` · `0x14389fdc1` · `0x14389fe50` |
|
||
| `trustedLogin` REST binding (`GET`, `Authorization`/`X-Forwarded-*`) | `0x143896a80` |
|
||
| LSX **event** element-name table (incl. `Login` @ `0x14393d0ac`) | `0x14393cfd8`–`0x14393d208` |
|
||
| `Origin::EventHandler<lsx::LoginT,unsigned int>::HandleMessage` | `0x14393f900` |
|
||
| `Origin::EventHandler<lsx::OnlineStatusEventT,bool>::HandleMessage` | `0x143940050` |
|
||
| LSX event dispatch / element-name compare | `0x1471028ee` |
|
||
| LSX request element table / attribute pool | `0x14394dc00` / `0x14394de00` |
|
||
| `"EbisuSDK"` / `"EALS"` service names | `0x143937d58` / `0x143937c60` |
|
||
| OSDK login-state name thunks | `0x14719b360`+ |
|
||
| `SV_ENABLE_SERVER_VERSIONING` / `SV_CLIENT_CHANGELIST` / `SV_SERVER_VERSION` | `0x14395d148` / `0x14395d168` / `0x14395d180` |
|
||
| version-mismatch message | `0x14395d1d0` |
|
||
| OSDK config section names (`OSDK_CORE`…`OSDK_TICKER`) | `0x143962be8`–`0x143962c40` |
|
||
| OSDK operations `FetchAccountInfo` / `UpdateAccountInfo` | `0x1439623c0` / `0x1439623d8` |
|
||
| `EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE` / `_SUCCESS` | `0x1439840b8` / `0x1439840e0` |
|
||
| `NUCLEUS_*_URL` config keys (OSDK_NUCLEUS section) | `0x14395eb60`–`0x14395ec18` |
|
||
| FUT `FUT_RS4_BASE_URL` / `ut/game/%s/` / `user/accountinfo` | `0x1438dbe88` / `0x1438dbe58` / `0x1438db6c0` |
|
||
| FUT `Easw-Session-Data-Nucleus-Id: %lld` | `0x1438db731` |
|
||
| FUT response JSON keys (`userAccountInfo`…`divisionOnline`) | `0x1438db760`–`0x1438db8b0` |
|
||
| EASW/SportsWorld header block | `0x14396f8b0`–`0x14396fa50` |
|
||
| hardcoded `accounts/gateway/signin.ea.com` (web UI only) | `0x143b8b528`–`0x143b8b5c0` |
|
||
| `Nucleus::gNucleusBaseUrl` & friends | `0x143b8b700`–`0x143b8b778` |
|
||
| Blaze `AccountInfo` / `LoginRequest` / `LoginResponse` descriptors | `0x14487c810` / `0x14487ca10` / `0x14487d170` |
|
||
|
||
Tools written this pass (all in `…/scratchpad/`): `acct_probe1.py` (CommandInfo neighbourhood dump),
|
||
`strdump.py` (string run at a VA), `wsearch.py` (ASCII+UTF-16 phrase search), `navdump.py`
|
||
(nav-flow JSON extractor). Reused: `memtool.py`, `strsearch.py`, `xref.py`, `dis.sh`, `cmdinfo.py`.
|