Files
OpenFUT/fifa17-recon/tools/acct_error_trace.md
T
funman300 6ddd5e9d47 fifa17-recon: offline FUT squad-shell working + full card-system RE
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
2026-08-01 20:24:30 -07:00

15 KiB
Raw Blame History

"Unable to retrieve account information. Please try again." — full trace (FIFA 17)

Clean-room. Sources: live /proc/PID/mem of FIFA17.exe (PID 3362053, base 0x140000000), cached image dumps scratchpad/live_code.bin (0x140001000..0x144c23000) + live_high.bin (0x144ed3000..0x151359000), and scratchpad/navregion.bin (heap nav JSON). All VAs absolute, stable across runs. New tools: fx.py (offline image str/dis/xref/callers), memsearch2.py (ASCII+UTF-16 live search), ptrscan.py, livedis.py, imgstr.py.


0. Headline

The popup is not a login failure. It is the OSDK generic alert raised on a 60-second timeout while the OSDK login state machine waits for a step to complete. The loc id is OSDK_A_R30B. The two states that raise it are LoginStateConnect (waiting for all six Util::fetchClientConfig results) and LoginStateLoadIspAccountInfo (waiting for the account country/locale from the OSDK 'cnnc' service). Both sit upstream of LoginStatePCLogin, which is where GetAuthCode + Authentication::login live — which is exactly why neither is ever seen.


1. The string is not in the EXE

Live search (ASCII and UTF-16LE) over every readable mapping: 14 hits, all in anonymous heap, zero inside the image range 0x140000000..0x151359000. So it is a localized string loaded from data, resolved at runtime.

Representative heap copies (ref-counted [u16 refcnt=1][u16 len=0x39][u16 cap] header, MSVC debug heap cd/fe fill around them):

0x0000b7563b00  "Unable to retrieve account information. Please try again."
0x0000b79ad020  same
0x0000bc81ba70  same
0x00000db86f88  UTF-16LE render copy
0x000078f5380   entry in a 64-byte-slot string table (slots 0x78f5280,2c0,300,340,380,3c0,…)

2. Who owns the string — the popup record

Pointer-scan for holders of 0x78f5380 found an FE parameter record at 0x41abd7c0, a run of {begin,end,cap,alloc} string triples:

field value
0x41abd798 "OnlineLoginViewModel" (20 ch)
0x41abd7c0 "ALERT_POPUP" (11 ch)
0x41abd7e0 "Unable to retrieve account information. Please try again." (0x39 = 57 incl NUL)

=> the text is the ALERT_POPUP data-provider value of OnlineLoginViewModel.

Confirming strings in the image:

0x143b4d6c0  OnlineLoginViewModel
0x143b4d890  UIFrameworkService::UIFDataProvider::PopupDp
0x143b4d8c0  LOGIN_POPUP
0x143b4d8e0  ALERT_POPUP
0x143b4d8f0  processLoginAlert
0x143b4d908  processBootLoginFailure
0x143b4d920  processLoginFailure

3. Which FE action sets it

OnlineLoginViewModel action trampoline 0x147dc62c0 (this=rcx, actionId=edx, params=r8):

1471dc62dc: lea eax,[rdx-0x27a4]      ; actionId - 10148
1471dc62ea: cmp eax,0x6 ; ja  ->ret   ; only ids 0x27a4..0x27aa
1471dc6310: lea rdx,"StrParam" ; call [params->vt+0x28]   ; HasParam("StrParam")
1471dc6329: lea rdx,"StrParam" ; call [params->vt+0x20]   ; GetString("StrParam", &buf)
1471dc634d: call 0x147e03080          ; handler(this, id, buf)

Handler 0x147e03080 — one function, 7-way jump table (add ebx,[0x146b1d808] where the dword = -10148; table at 0x1429f9148):

case target does
0 0x147e03134 sets LOGIN_POPUP
1 0x147e03579 sets LOGIN_POPUP
2 0x147e03682 sets ALERT_POPUP (0x147e03690) + button OSDK_OK + callback processLoginAlert
3 0x147e03ac6 processBootLoginFailure
4 0x147e03b3e ALERT_POPUP + OSDK_OK + processLoginFailure
5 0x147e04015 popup hide
6 0x147e03fe4 popup show

Action-id ↔ name registration block (0x147de8330..0x147de8544, mov edx,<id> + lea rdx,<name> + call [vt+0x20]):

id name case
0x27a4 onlineLoginToEaPopup 0
0x27a5 onlineBootLoginToEaPopup 1
0x27a6 evt_onlineAlertPopup 2
0x27a7 evt_onlineBootLoginFailurePopup 3
0x27a8 evt_onlineLoginFailurePopup 4
0x27a9 onlineLoginPopupHide 5
0x27aa onlineLoginPopupShow 6

=> the popup is evt_onlineAlertPopup (0x27a6), message carried in param StrParam. It is explicitly not evt_onlineLoginFailurePopup / evt_onlineBootLoginFailurePopup.

4. The FE flow (/online/onlineLoginFlow.nav)

Recovered verbatim from navregion.bin (offset ~31456301); saved to scratchpad/onlineLoginFlow_raw.txt:

{ "name":"onlineLoginFlow"
, "onEnter":[["loadViewModel", ["OnlineLoginViewModel"]]]
, "states":[{ "transitions":[
      { "event":"evt_onlineAlertPopup",            "targets":["onlineAlertLoginPopup"] }
     ,{ "event":"evt_onlineBootLoginFailurePopup", "targets":["onlineFailureLoginPopup"] }
     ,{ "event":"evt_onlineLoginFailurePopup",     "targets":["onlineFailureLoginPopup"] }
     ,{ "event":"evt_online_disconnected",         "targets":["processLoginFailure"] } ...]
  ,"states":[
     { "name":"startLoginWithoutMultiplayerCheck"
      ,"onEnter":[["sendScreenEvent", ["OnlineLogin", "0"]]] }
    ,{ "name":"onlineAlertLoginPopup"
      ,"onEnter":[["sendAction", ["onlineLoginPopupShow"]]]
      ,"onExit" :[["sendAction", ["onlineLoginPopupHide", "ALERT_POPUP"]]]
      ,"states":[{ "name":"onlineAlertLoginIdle"
                  ,"transitions":[{"event":"processLoginAlert","targets":["processLoginAlert"]}]}
                ,{ "name":"processLoginAlert"
                  ,"onEnter":[["sendScreenEvent", ["ProcessLoginAlert"]]] }]} ...]}]}

Entry per nav/root.nav: launchFUTFlow (/online/origin.nav) →OriginIsOnlineTruefutBlazeLogin (/online/onlineLoginFlow.nav, entry startLoginWithoutMultiplayerCheck) → loginSuccessCheckFUTRostersfutFlow.

We are inside futBlazeLogin. origin.nav already emitted OriginIsOnlineTrue (our LSX work) — the wall is one layer deeper.

5. The C++ that raises it — OSDK login state machine

EA Online SDK (OSDK) 8.01.03.00-fifa.01; source paths embedded, e.g. E:/p4/fifafb/rl/empatch/TnT/Code/fifa/gamemodes/extern/OSDK/8.01.03.00-fifa.01/source/common/modulemanager.cpp.

States are classes whose vtable slot 1 is a GetName() thunk (lea rax,<name>; ret) in 0x14719b3xx; vtable slot 4 (+0x20) is the per-tick handler; slot 8 (+0x40) is "advance to next state".

state vtable handler (vt+0x20)
LoginStateIsp 0x14395bc78 0x1471b45f0
LoginStateConnect 0x14395be20 0x1471b4a40
LoginStateLoadIspAccountInfo 0x14395bd18 0x1471b4b40
LoginStatePCLogin 0x14395c180 0x1471b58e0

Shared alert builder 0x147190e80OSDK_RaiseError(this, Notification* out, int code, const char* locId):

147190e9b: mov byte [rdx],1          ; notif type = error
147190e9e: mov byte [rdx+2],1
147190ea2: mov [rdx+8],r8d           ; numeric code
147190ebd: lea rcx,[rdi+0xc] ; call 0x145e27ff0   ; strcpy(notif+0xc, locId)
147190ee0: mov byte [rdi+0x20b],0
147190f16: lea rdx,"error" ; lea rcx,"osdk" ; call 0x146ec02d0   ; telemetry osdk/error/%d

It has 16 call sites, all inside the login-state cluster 0x1471b4xxx..0x1471b7xxx — i.e. it is the OSDK "raise login alert with loc id" entry point. The notification carries the loc-id string, which the OSDK→FE bridge resolves to localized text and delivers as StrParam on evt_onlineAlertPopup.

Loc ids raised by neighbouring states (for contrast):

0x14395cdd8  OSDK_SIGNIN_REQUIRED            (LoginStateIsp, default branch)
0x14395ce48  OSDK_ONLINE_OUTDATED_PATCH      (LoginStateIsp, NetConn 4CC '-upp')
0x14395ce68  OSDK_ONLINE_DISABLED_PARENTAL   (LoginStateIsp, NetConn 4CC '-adu')
0x14395cdc8  OSDK_ERROR
0x14395c6b8  OSDK_A_R30B                     <<< our popup

5a. LoginStateConnect — handler 0x1471b4a40 (STRONGEST CANDIDATE)

Sub-state in [this+0x20], deadline in [this+0x24].

; ---- sub-state 0 : kick off ----
1471b4b0d: mov rcx,[rbx+0x28]
1471b4b11: call 0x1471a8970          ; issue all client-config loads
1471b4b16: lea eax,[rdi+0xea60]      ; rdi = now(ms);  0xea60 = 60000 = 60 s
1471b4b1c: mov [rbx+0x20],1          ; -> polling
1471b4b23: mov [rbx+0x24],eax        ; deadline = now + 60 s

; ---- sub-state 1 : poll ----
1471b4ab4: mov rax,[rbx+0x28] ; xor edx,edx
1471b4aba: mov r8d,[rax+0x60]        ; entry count
1471b4abe: test r8d,r8d ; je -> sub-state 3      ; nothing pending => done
1471b4ac3: mov r9d,[rax+0x64]        ; stride
1471b4ac7: mov r10,[rax+0x58]        ; array base
1471b4acb: (loop) mov rcx,[base + idx*stride]
1471b4ad5:        cmp byte [rcx+0x60],0
1471b4ad9:        je  0x1471b4af4                ; <-- this entry NOT ready
1471b4ae2: mov [rbx+0x20],3          ; all ready -> success
1471b4af4: sub edi,[rbx+0x24]        ; now - deadline
1471b4af7: test edi,edi ; jle -> ret ; still inside the 60 s window: keep waiting
1471b4afb: mov [rbx+0x20],2          ; TIMEOUT

; ---- sub-state 2 : alert ----
1471b4a86: lea r9,"OSDK_A_R30B" ; xor r8d,r8d ; mov rdx,r10 ; mov rcx,rbx
1471b4a9b: call 0x147190e80

0x1471a8970 iterates the same array and for each entry calls 0x1471a89f0, which:

1471a8a20: mov byte [rcx+0x60],0     ; clear the "ready" flag the poll loop reads
1471a8a2c: lea r8,"LoadCfg:%s"       ; 0x143962390

LoadCfg:%s is Util::fetchClientConfig. The config-name list sits immediately after it, and matches the six CFIDs we observe on the wire byte-for-byte:

0x143962be8  OSDK_CORE
0x143962bf8  OSDK_CLIENT
0x143962c08  OSDK_NUCLEUS
0x143962c18  OSDK_WEBOFFER
0x143962c28  OSDK_ABUSE_REPORTING
0x143962c40  OSDK_TICKER

The entries are ResourceLoader objects (0x143962960..0x143962b60, states LOADED / LOADING / NOT_FOUND, ResourceLoader::ResourceFailure()). [entry+0x60] is the "loaded OK" flag. An empty / failed config reply leaves it 0.

5b. LoginStateLoadIspAccountInfo — handler 0x1471b4b40 (SECOND CANDIDATE)

Same 4-sub-state shape:

; sub-state 0 : GetService('cnnc')->[+0x38]()->[+0xb0]() ; -> sub-state 3
1471b4e9d: mov rcx,[0x144b86bf8] ; mov edx,0x636e6e63 ('cnnc')
1471b4eac: call [rax+0x60]       ; mgr->GetService('cnnc')
1471b4eb5: call [rdx+0x38]
1471b4ebe: call [rdx+0xb0]

; sub-state 1 : poll the same accessor
1471b4e6d: call [rdx+0xb0] -> eax
1471b4e73: test eax,eax ; je  -> timeout check      ; 0     = not resolved
1471b4e77: cmp eax,0x5a5a ; jne -> sub-state 3      ; 'ZZ'  = placeholder
1471b4e7e: sub ebx,[rdi+0x24] ; jle -> ret          ; inside deadline: keep waiting
1471b4e85: mov [rdi+0x20],2                         ; TIMEOUT

; sub-state 2 : alert
1471b4e1f: lea r9,"OSDK_A_R30B" ; call 0x147190e80

; sub-state 3 : SUCCESS — parse the account locale
1471b4b84: mov rsi,[0x144b86bf8] ; mov rdx,[rsi+0xb0]   ; char* country/locale
           ... packs 4 chars, lowercases pair 1 (|0x20), uppercases pair 2 (&0xdf)
1471b4df0: call [rax+0x188] ; mov [rax+0x52c],ebx       ; store packed "enUS"-style u32
1471b4e1c: jmp [this->vt+0x40]                          ; advance to next login state

0x5a5a is literally "ZZ" — the unknown-country placeholder. So this state waits for a real 2-letter account country code from the OSDK 'cnnc' (connection / Nucleus adaptor) service and alerts OSDK_A_R30B if it never arrives.

Operation names registered in the same module manager, right beside LoadCfg:%s:

0x1439623c0  FetchAccountInfo        <<< semantic match for the popup text
0x1439623d8  UpdateAccountInfo
0x1439623f0  LookupOriginPersona
0x143962408  FetchOriginPersona
0x143962420  CreateOriginPersona
0x143962598  "Operation timed out.  Name = [%s], Handle = [%u]"

6. Why this matches the observed behaviour exactly

observation explanation
preAuthping6× fetchClientConfig → then popup LoginStateConnect sub-state 0 issues exactly these 6 LoadCfg: requests
popup appears after a wait, not instantly hard-coded 60 000 ms deadline (lea eax,[rdi+0xea60])
we answer all 6 fetchClientConfig with empty replies ResourceLoader never reaches LOADED, [entry+0x60] stays 0 → poll never satisfied → timeout
popup is the alert, not the login-failure popup evt_onlineAlertPopup (0x27a6), not 0x27a7/0x27a8 — the flow never reported a login failure, it timed out earlier
GetAuthCode never requested Origin::OriginSDK::RequestAuthCodeSync / LSXRequest<lsx::GetAuthCodeT,…> is driven from LoginStatePCLogin, which is downstream and never entered
Authentication::login (1/0x0A) never sent same reason — the state machine aborts before it
client sends Authentication 1/0x46 then reconnect-loops the OSDK teardown path after the alert (LoginStateLogout)
nucleusConnect stub on :42131 gets zero requests the client gets its Nucleus config from fetchClientConfig OSDK_NUCLEUS, which we return empty — so it never learns a Nucleus URL to dial

The last row is the key causal loop: OSDK_NUCLEUS is where the Nucleus/account endpoints come from. Returning it empty both (a) fails the LoginStateConnect ready check and (b) guarantees the account-info fetch can never resolve, which is why both OSDK_A_R30B sites are armed.


7. What was NOT proven

  1. OSDK_A_R30B → that exact English text is inferred, not read. The loc DB lives in compressed Frostbite superbundles (Data/Win32/loc/en.toc + .sb, chunked); the plain text is not greppable on disk and the game exited before I could read the live loc table. Confidence is high (it is the only alert id raised by the two "waiting for account data" states, and every other login alert id has a clearly different meaning), but it is one unverified link.
  2. Which of the two sites fired (LoginStateConnect vs LoginStateLoadIspAccountInfo). LoginStateConnect is the stronger candidate because the observed Blaze traffic stops exactly at its wait condition.

Cheap runtime confirmations (next live run)

  • Read [state+0x20] / [state+0x24] while the spinner is up — sub-state 2 = timeout hit.
  • Breakpoint / patch-log 0x147190e80 and dump r9 (the loc-id string) — this names the alert with zero ambiguity and works for every login alert.
  • Grep /tmp/lsx.log-style logging for the OSDK telemetry event osdk / error / %d emitted at 0x147190f24.

8. Actionable conclusion

Layer implicated: Blaze Util::fetchClientConfig (9/0x01) — not LSX, not Nucleus HTTP.

The Origin/LSX layer is already satisfied (origin.nav emitted OriginIsOnlineTrue). The wall is the first Blaze step after preAuth: the client demands non-empty client configs for all six CFIDs and blocks for 60 s, then alerts.

Fix order:

  1. Return real, non-empty fetchClientConfig maps for OSDK_CORE, OSDK_CLIENT, OSDK_NUCLEUS, OSDK_WEBOFFER, OSDK_ABUSE_REPORTING, OSDK_TICKER (note: OSDK_TICKER, per the in-binary list at 0x143962c40 — earlier notes recorded OSDK_XMS_ABUSE_REPORTING as the 6th; verify against the live capture). OSDK_NUCLEUS must carry auth/account URLs pointed at our stub.
  2. That should let LoginStateConnect reach sub-state 3 and advance to LoginStateLoadIspAccountInfo, which then needs the account country code to come back as a real 2-letter code (not 0, not "ZZ") from the 'cnnc' service.
  3. Only then does LoginStatePCLogin run — and that is where LSX GetAuthCode will finally be requested, followed by Blaze Authentication::login (1/0x0A).