# CONNECT_PLAN — Origin-SDK connect + Blaze-login trigger (clean-room synthesis) Synthesis of four independent reverses (Origin connect writer, connect-wait guard, "service 0x1e8" dispatcher, Blaze LoginManager), plus my own live re-verification against **pid 13643** (`FIFA17.exe`, still up at time of writing). Every VA/byte below is from our decrypted dumps, `/proc/13643/mem`, `captures/lsx/full_origin_init_session.log`, `/tmp/lsx.log` or `/tmp/blaze_responder.log`. --- ## 0. HEADLINE — BRIEF3's two premises were both wrong, and the real cause is one line of our responder | BRIEF3 said | Truth (live-verified) | |---|---| | `0x1470dbfa0` = service/interface lookup; "service `0x1e8` returns NULL → not connected" | `0x1470dbfa0` is the **Origin SDK allocator thunk** (`jmp [0x144b7c778]`, live target `0x1470d9380` = aligned-alloc). `0x1e8` is an **allocation size** (488 = `sizeof(LSXRequest)`), and the neighbouring `0x1e6/0x1e8/0x1ea` are `__LINE__` args to the logger. **There is no service table and nothing to register.** | | `0xa2080000` comes from the auth path failing a service lookup | `0xa2080000` is the LSXRequest/EventHandler **"response failed to validate"** default, written at `0x1471192dd`→`[req+0x1e0]`. It means *we replied and the client threw our reply away.* | | Empty Blaze `fetchClientConfig` leaves `[cfg+0x750]` NULL and blocks login | `[cfg+0x750]` is live **non-NULL** = `0x43c47ff0` (verified: `*[0x144b86bf8]=0x43c46c70`, `+0x360=0x43c47330`, `+0x750=0x43c47ff0`). It is fed by **`Util::preAuth`'s CONF map**, not by `fetchClientConfig`. No `fetchClientConfig` payload can change it. | **The actual blocker is a one-attribute protocol mismatch in `lsx_responder_v2.py`.** Every LSX `` is accepted only if its `sender` attribute **byte-equals the `recipient` the client put on the matching ``**. FIFA sends `recipient=""` for everything after `GetConfig` (because our `GetConfigResponse` is empty, so its 34-entry service-name table is all empty strings), but we answer `GetProfile`, `GetAuthCode` and `QueryEntitlements` with `sender="EbisuSDK"`. Those three replies — and only those three — are discarded. `GetProfile` is the *sole* writer of `OriginSDK+0x3a0/+0x3a8`, so the whole chain (default user → auth code → OSDK login state → Blaze `Authentication::login`) never starts. --- ## 1. THE CONNECT MECHANISM — end to end ### 1.1 The writer is a synchronous statement, not an event `0x1470e5ad5` (the `+0x3a0` write) lives inside **`Origin::OriginSDK::Initialize`**, function start `0x1470e5770` (prologue `push rbp/rsi/rdi/r12/r13/r14/r15; sub rsp,0xf0`, epilogue `0x1470e5caf`). Its logger passes `# 0x143937c78` = `"Origin::OriginSDK::Initialize"` and `# 0x143937b70` = `"E:\p4\fifafb\rl\empatch\TnT\Code\External\EA\OriginSDK\src\impl\OriginSDKimpl.cpp"` (both read live). Sequence inside `Initialize`: ``` 0x1470e5959..96 connect loop: call 0x14712ca40(conn=sdk+0x168, …, port) on false -> Sleep(1000); inc ebx; cmp ebx,0x1e; jl => 30 x 1s (NOT 15 s) 0x1470e59fb version gate: cmp eax,0x9b00000; jb -> 0xa0020007 (passes today) 0x1470e5a2f ecx=0x1a8; call 0x1470dbfa0 <-- ALLOC 424 bytes for the GetConfig request 0x1470e5a46 lea rdx,[rip..] # 0x143937d58 = "EbisuSDK" <-- HARD-CODED recipient/sender ... SendSync(GetConfig) ... 0x1470e5a96 lea rdx,[rbx+0x80]; call 0x1470e0520 <-- consume GetConfigResponse -> fills serviceNames[0..33] 0x1470e5aa8 call 0x14710df80(sdk) <-- register ~19 event handlers, each keyed on serviceNames[i] 0x1470e5ab7..c7 lea r9,[rsp+0x70]; xor edx,edx; mov r8d,0x3a98(=15000ms); mov rcx,rdi call 0x147118d80 <-- GetProfileSync(idx=0) 0x1470e5ad5 mov rax,[rdi+0x80]; mov [rbp+0x3a0],rax <-- UserId 0x1470e5ae1 mov rax,[rdi+0x88]; mov [rbp+0x3a8],rax <-- PersonaId ``` So: **the only stimulus for the `+0x3a0` write is a well-formed `` to the `` FIFA already sends at boot (log id=3).** There is nothing else to serve, no event to push, no "connect" verb we are missing. `+0x3a0`/`+0x3a8` are **u64 scalars** (UserId / PersonaId), not object pointers — the deserializer `0x147136140` maps `UserId→resp+0x00`, `PersonaId→+0x08`, `Persona→+0x10`, `AvatarId→+0x30`, `Country→+0x50`, `IsUnderAge→+0x70`, `IsSubscriber→+0x71`, `GeoCountry→+0x78`, `CommerceCountry→+0x98`, `CommerceCurrency→+0xb8`, and the response sub-object sits at `request+0x80`. `0x147118d80` = `Origin::OriginSDK::GetProfileSync` (both exits converge on `lea rax,[rip..] # 0x143945d00` = `"Origin::OriginSDK::GetProfileSync"`). It is **not** a poll loop: it builds the request, calls the send-and-wait template `0x1471186f0`, which serializes ``, then blocks on a timed condvar (`0x145e318f0(&req+0x1e8,&req+0x230)`) until the reader thread sets `[req+0x228]=1`. Success is `[req+0x1e0]==0` (`0x147118851: cmp DWORD PTR [rbx+0x1e0],0x0; sete al`). ### 1.2 The rejection — verified byte-for-byte, live Response matcher `0x1471189b0` (disassembled from `/proc/13643/mem` this session): ``` 1471189c6 call [rax+0x10] ; reader ok? -> je FAIL 1471189d7 call [rax+0x58] ; root element name 1471189dd lea rdx,[rip..] # 0x143938024 ; "LSX" (4-byte compare loop) -> jne FAIL 147118a14 call [rax+0x18] rdx=[this+0xd8] ; child "Response" -> je FAIL 147118a2a lea rdx,[rip..] # 0x14355de00 = "id" ; optional -> atoi -> [this+0x138] 147118a5d lea rdx,[rip..] # 0x143938028 = "sender" 147118a7d call [rax+0x70] ; GetAttributeValue("sender") 147118a80 test rax,rax ; je 0x147118ad9 ; ATTRIBUTE MUST BE PRESENT 147118a85 lea r8,[rdi+0xf8] ; expected sender (std::string, SSO at +0x18) 147118a99..aae inline byte-wise strcmp ; MISMATCH -> jne FAIL (0x147118ad9) 147118ab3 lea rdx,[rip..] # 0x143945ce8 = "GetProfileResponse" -> je FAIL 147118acf tail-jump to deserializer ``` On FAIL, `HandleMessage 0x147119210` does `call 0x1470e2e30` (look for an `` node), and when there is none: ``` 1471192dd mov edx,0xc347a20f 1471192e4 lea edx,[rdx-0x213fa20f] ; = 0xA2080000 1471192ef mov DWORD PTR [rbx+0x1e0],edx 147119311 call 0x145e30d20 ; signal condvar (so it fails FAST, never times out) 147119316 mov BYTE PTR [rbx+0x228],1 ``` The expected string `[req+0xf8]` and the emitted `recipient` are **the same std::string**, taken from `sdk->serviceNames[facility]` via accessor `0x1470e4870` (`cmp edx,0x21; movsxd rax,edx; shl rax,5; add rax,[rcx+0x3b0]`), and used symmetrically by the request ctor `0x147117fe0` (`"Request"@0x1435a43b8` and `"Response"@0x143938198` both get arg2). Facilities per verb (from the `add rdx,0xNN` after `mov rdx,[sdk+0x3b0]`, `NN/0x20 = idx`): | Verb | site | facility | |---|---|---| | `GetConfig` | `0x1470e5a46` | **literal `"EbisuSDK"`** (not table-driven) | | `GetProfile` | `0x147118df2` (`lea edx,[r12+1]`) | 1 PROFILE | | `GetAuthCode` | `0x1470e6882` (`add rdx,0x120`) | 9 UTILITY | | `GetSetting` / `GetGameInfo` | `0x1470e4903` / `0x1470e3683` (no add) | 0 SDK | | `QueryUserId` | `0x1470e62b4` | 26 GET_USERID | | `IsProgressiveInstallationAvailable` / `SetDownloaderUtilization` | `0x147129ddb` / `0x14712b821` | 31 PROGRESSIVE_INSTALLATION *(inferred)* | ### 1.3 Live state of the table (re-verified myself, pid 13643) ``` OriginSDK = *[0x144b7c7a0] = 0x25c98c50 +0x1b8 = 0x1fc (conn+0x50 socket, ALIVE -> transport healthy) +0x1e8 = 0xffffffffffffffff (conn+0x80, a SECOND handle, still unset - see §5) +0x218 = 0x0 (pending-request map size) +0x270 = 0x1c (28 requests issued == our lsx.log last id 27 + 1) +0x3a0 = 0x0 <-- default user STILL NULL +0x3a8 = 0x0 +0x3b0 = 0x25cb93e0 .. +0x3b8 = 0x25cb9820 -> (0x440)/0x20 = 34 entries all 34 std::strings: size == 0 ("all-empty") ``` 34 entries allocated proves `Initialize` **did** reach and accept our `GetConfigResponse` (`0x1470e0520`: `lea rbp,[rcx+0x3b0]; lea edx,[rbx+0x22]; call 0x1470eadd0` resizes to 34) — and all empty proves it contained **zero `` children**, because we answer ``. ### 1.4 On the wire — the smoking gun `captures/lsx/full_origin_init_session.log`: ``` id=1 (plaintext handshake) id=2 -> we reply sender="EbisuSDK" MATCH ✔ id=3 -> we reply sender="EbisuSDK" MISMATCH ✘ id=4 -> we reply sender="" MATCH ✔ id=5 -> we reply sender="" MATCH ✔ … ids 6..14 all recipient="" ``` Independent live proof that an **empty** `sender` attribute really does validate (i.e. `vt[0x70]` returns a pointer to `""`, not NULL): FIFA's OSDK failure classifier `0x14717d5d0` only reaches its `cmp eax,0xa2000003` branch **after** `OriginGetGameInfoSync(id=0)` returns rc==0 **and** the buffer strncmp-equals `"true"@0x14354be6c`; otherwise it would set `TXT_ORIGIN_GAME_VERSION_OUT_OF_DATE`. Live `[0x43d189d8+0x80] = 0x14395ca10 = "OSDK_INVALID_USER"` and `[+0x260] = 16` — so our `GetGameInfo(UPTODATE) → GameInfo="true"` reply, sent with `sender=""`, **was accepted**. (It also pins `GameInfoId` enum 0 == `UPTODATE`.) ### 1.5 Events are broken by the same mechanism Event validator `0x1471015e0` (per report 3) requires root `"LSX"@0x143938024`, child literal `"Event"@0x14365d8fc` (len 5), optional `id`, then `call [rax+0x70]` for `"sender"@0x143938028` → `test rax,rax; je FAIL` → inline strcmp against the per-handler string `[handler+0x10]`. I disassembled the registrar `0x14710df80` live and confirmed **that string is `serviceNames[facility]`** — every registration is `mov edx,; call 0x1470e4870; mov rdx,rax; call `: ``` 0x14710df8a edx=0x0c IGO_EVENT -> 0x1470eb6a0 0x14710dfac edx=0x0f INVITE_EVENT -> 0x1470eb060 (0xe7ffa20f+0x18005e00 = 0x0f) 0x14710dfcd edx=0x0e LOGIN_EVENT -> 0x1470eb100 (0xc390050f+0x3c6ffaff = 0x0e) << ours 0x14710dfee edx=0x10 PROFILE_EVENT -> 0x1470eb1a0 0x14710e009 edx=0x11 PRESENCE_EVENT -> 0x1470eb240 0x14710e024 edx=0x12 FRIENDS_EVENT -> 0x1470eb2e0 0x14710e03f edx=0x13 COMMERCE_EVENT -> 0x1470eb380 0x14710e05a edx=0x15 DOWNLOAD_EVENT -> 0x1470eb420 0x14710e075 edx=0x19 BLOCKED_USER_EVENT -> 0x1470eb4c0 0x14710e090 edx=0x14 CHAT_EVENT -> 0x1470ebfc0 0x14710e0ab edx=0x1b ONLINE_STATUS_EVENT -> 0x1470eb560 << ours 0x14710e0c6 edx=0x1d ACHIEVEMENT_EVENT -> 0x1470eb600 0x14710e0e1 edx=0x0f INVITE_EVENT (2nd) -> 0x1470ebf20 … 0x1e BROADCAST_EVENT, 0x20 PROGRESSIVE_INSTALLATION_EVENT, plus repeats of 0x11/0x13 ``` **This corrects report 1's open question #5: `LOGIN_EVENT` (14) and `ONLINE_STATUS_EVENT` (27) handlers ARE registered here** — they exist and are listening. They just expect `sender == ""` right now, while we push `sender="LOGIN_EVENT"` / `"LOGIN"` / `"ONLINE_STATUS_EVENT"`. All 3219 pushed `` frames in `/tmp/lsx.log` have been silently dropped. ### 1.6 The connect story, one paragraph There is no separate "connect" handshake to serve. `Initialize` connects the socket (already alive, `[sdk+0x1b8]=0x1fc`), passes the version gate (`[sdk+0x360]="10,4,13,6637"`), does GetConfig (accepted, table sized to 34 but left empty), registers ~19 event handlers keyed on the now-empty names, and then calls `GetProfileSync`. **Our `GetProfileResponse` is thrown away for a `sender` mismatch**, so `+0x3a0`/`+0x3a8` stay 0, `OriginGetDefaultUser (0x1470da6d0)` keeps returning NULL, `OriginRequestAuthCodeSync (0x1470e67f0)` keeps failing its `test rdx,rdx` / `cmp rdx,[rcx+0x3a0]` guards with `0xa2000003`, and FIFA's OSDK classifier turns that into `OSDK_INVALID_USER`. --- ## 2. THE BLAZE-LOGIN TRIGGER ### 2.1 `login` is compiled in and is exactly `1/0x0A` — it is simply never invoked * Stub `0x146e15070` ends `mov r9d,0xa; movzx r8d,WORD PTR [rdi+0x10]; call 0x146df0e80` (generic `sendRequest`). Live component id: `WORD[0x7c17170+0x10] = 0x0001` = Authentication. **Our responder's `1/0x0A = login` mapping is correct.** * Its only caller `0x146e12d83` sits in `0x146e12d20`, which appears at **vtable slot +0xd0** of all four LoginState vtables (`0x14389f828/…938/…a70/…b98`). * Live: all four `LoginManagerImpl` (vptr `0x14389f5a0`) are still in **`LoginStateInit`** (`stateMachine+0x28 == +0x08 == stateInit`, `+0x30 stateId = 0xFFFFFFFF`). The state machine has never been asked to advance. * `Authentication::logout (1/0x46)` comes from `LoginStateBase::logout` = vt+0x48 (`0x146e15eb0` → RPC stub `0x146e109d0`). It is a **normal step** of FIFA's OSDK sequence (Connect → LoadConfig → **Logout** → VersionCheck → Login) and matches our wire log exactly (preAuth → 6× fetchClientConfig → logout, 37 occurrences in `/tmp/blaze_responder.log`). **It is not an error signal and our empty REPLY to it is correct.** ### 2.2 The precondition FIFA checks — it is an Origin check, not a Blaze one FIFA's own OSDK login state (`0x43d189d8`, vptr `0x14395c180`, Update `0x1471b58e0`, jump table `0x141e7f55c`): * case 0 → `mov rcx,[0x144b86bf0]; call [rax+0x60]` — live `vt[0x60] = 0x146f82070 = xor eax,eax; ret`, so it always falls to `0x1471b5b42`, sets `TXT_NOT_LOGIN_TO_EBISU@0x1439633e8`, `[state+0x260]=1`, and calls the classifier `0x14717d5d0`. * Classifier `0x14717d5d0`: `OriginGetGameInfoSync(0)` must be rc==0 and `"true"` (it is — see §1.4), then reads the Ebisu/Origin manager's last error via `[rsi]->vt[0x80]()` and `cmp eax,0xa2000003; jne`. On match → `[state+0x80] = "OSDK_INVALID_USER"@0x14395ca10`, `call 0x147190d50(state, 0xe)`. * **Live right now: `[0x43d189d8+0x260] = 16`, `[+0x80] = "OSDK_INVALID_USER"`.** `0xa2000003` is precisely `OriginRequestAuthCodeSync`'s "NULL/invalid user" return — `OriginSDK+0x3a0 == 0`. The dword `0xa2000003` is resident at `0x43d320f8`, adjacent to the state heap. ### 2.3 `fetchClientConfig` is a dead end for this gate — do not touch it * `[cfg+0x750]` is **live non-NULL** (`0x43c47ff0`), holding the Blaze ConnectionManager whose string map at `+0x1218` (16 entries, 0x30 bytes each, begin `0x43f56450`) is **byte-for-byte our `Util::preAuth` CONF** — `blazeSdkClientId=FIFA17PC`, `blazeServerClientId=FIFA17PC-SERVER`, `blazeSdkClientSecret=openfut-secret`, `identityRedirectUri=http://127.0.0.1/login_successful.html`, etc. `NUCLEUS_ADDED_URL`/`NUCLEUS_CREATE_URL` (which we only send via `fetchClientConfig(CFID=OSDK_NUCLEUS)`) are **absent**, proving the two containers are different. * The getter the Blaze auth fetchers use is `vt[0x48] = 0x146e1bc50`, a binary search over that same `[this+0x1218]` map; the keys it asks for are `"blazeServerClientId"@0x143972690`, `"blazeSdkClientId"@0x1439726a8`, `"blazeSdkClientSecret"@0x1439726c0`, `"identityRedirectUri"@0x1439726d8` — all present. * The class that owns `blaze_authfetch1/2` (vtable base **`0x143972480`**, not `0x143972560`) has **zero live instances** (full-memory scan for the vptr: 0 hits, while the same scanner finds exactly 1 hit for the config vtable `0x1438a0850`). That code never runs. Even if it did, it would bail on `call 0x1470da6d0` (GetDefaultUser) → `0x1470db3c0` → `0xa2000003`, storing it at `[this+0x950]`. **Conclusion: the Blaze-login trigger is `OriginSDK+0x3a0 != 0`.** Once GetProfile validates, the Origin default user exists, `OriginRequestAuthCodeSync` succeeds, the classifier stops writing `OSDK_INVALID_USER`, the OSDK machine advances past sub-state 16, and LoginState vt+0xd0 (`0x146e12d20`) fires `Authentication::login (1/0x0A)` with no Blaze-side change at all. --- ## 3. CONCRETE SERVER-SIDE CHANGES (ranked, minimal) ### RANK 1 — `lsx_responder_v2.py`: echo the request's `recipient` back as the response's `sender` ★ do this first This is the whole fix, and it is correct for both the current empty-table state **and** any future populated table. **Edits (line numbers as of today's file):** 1. **L418** — widen the request regex so we capture `recipient`: ```python REQ_RE = re.compile( r']*\brecipient="([^"]*)"[^>]*\bid="(\d+)"[^>]*>\s*<([A-Za-z]+)([^>]*)/?>') ``` (`recipient` precedes `id` in every captured frame — ids 1..14 all have ``. If you prefer robustness, keep the old `REQ_RE` and add a separate `RECIP_RE = re.compile(r']*\brecipient="([^"]*)"')`, defaulting to `""`.) 2. **L475-481** — thread it through: ```python recip, mid, name, rest = mm.group(1), mm.group(2), mm.group(3), mm.group(4) attrs = dict(ATTR_RE.findall(rest)) reply = build_reply(mid, name, attrs, conn, recip) ``` 3. **L312 / L316** — make it the default sender: ```python def resp(mid, body, sender=""): # unchanged return f'<{body}/>' def build_reply(mid, req_name, attrs, conn, recipient=""): ... # pass sender=recipient at every return site ``` 4. **DELETE the three `sender="EbisuSDK"` overrides** — **L352** (`GetAuthCode`), **L358** (`QueryEntitlements`), **L371** (`GetProfile`). These are the only three rejected verbs. **L400** (`GetConfig`) may stay as-is or become `sender=recipient`; with echo it is identical, since FIFA really does send `recipient="EbisuSDK"` there (its literal is hard-coded at `0x1470e5a46`). 5. **Always emit the attribute, even when empty.** `0x147118a80: test rax,rax; je FAIL` rejects a *missing* `sender`; an empty-valued one is fine (proven in §1.4). `resp()` already does this — don't "optimise" it away. 6. **L231-232 / L235** — events: with the table empty, the handler strings are empty too, so push `sender=""`: ```python LOGIN_EVENT_SENDERS = ("", "LOGIN_EVENT", "LOGIN") # "" first; the others are harmless no-ops ONLINE_EVENT_SENDERS = ("", "ONLINE_STATUS_EVENT") ``` Mismatched senders are dropped silently and cost nothing, so keeping the old candidates as a hedge is free. Optionally add `id="N"` to `` (read at `0x14710164c-72` → `handler+0x78`, optional). **Observables (restart FIFA; boot `GetProfile` is id=3):** | # | Signal | How to read | |---|---|---| | 1 | `Origin Error(a2080000)` disappears for GetProfile/GetAuthCode | `/tmp/lsx.log`, FIFA log | | 2 | **`OriginSDK+0x3a0` and `+0x3a8` become `0x1f89493` (33068179)** | `SDK=*[0x144b7c7a0]`, read `+0x3a0` — this is *the* pass/fail bit | | 3 | `[0x43d189d8+0x80]` stops being `"OSDK_INVALID_USER"`, `[+0x260]` leaves 16 | live read (state VA may move across restarts — re-find via vptr `0x14395c180`) | | 4 | FIFA issues `` **without** our forging anything | `/tmp/lsx.log` | | 5 | **`Authentication::login (1/0x0A)`** appears | `/tmp/blaze_responder.log` (currently: 37× `logout`, 0× `login`) | > Note: `[SDK+0x1e8]` (= `conn+0x80`, live `-1`) is **not** "service 0x1e8" and is *not* the success > signal BRIEF3 asked for — there is no service to become non-null. Use signal #2 instead. Whether > `+0x1e8` flips as a side effect is a bonus observation (see §5). --- ### RANK 2 — `lsx_responder_v2.py`: make `GetConfigResponse` actually populate the service-name table (EA-faithful; do AFTER rank 1 confirms) Not required once rank 1 is in (echo is correct for empty names), but it restores the real protocol, gives events non-empty senders, and de-risks any code path that dislikes empty names. Consumer chain, all verified: matcher `0x1470e304f` checks child `"GetConfigResponse"@0x143937b30` → deserializer `0x147135610` builds the child element name as prefix + `"Service"@0x14394dd78` (len 7; namespaced variant `":Service"@0x14394e1a8` with ns prefix `"lsx"@0x14394def0`) and loops `0x14713af50` per element → reads `"Name"@0x1435604fc` into `entry+0x00` and `"Facility"@0x14394e198` via `0x1471400a0`→`0x147140010`, which **linearly strcmps the value against the 34-pointer table at `0x144341420`** and writes the matched index to `entry+0x20` (init 0). Then `0x1470e0520` assigns `names[entry.index] = entry.name`, skipping `index > 0x21` and empty names. Records are 40 bytes (`{std::string name; int32 index;}`), which is why `0x1470e0520` divides by 0x28. **Facility enum strings — read live from `0x144341420` this session (case-exact, required):** ``` 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 ``` Reply (keep the envelope `sender` = echo of `recipient`, i.e. `"EbisuSDK"`): ```xml ...one line per facility, Name == the Facility enum name... ``` Choosing **`Name == Facility`** is deliberate: with rank 1's echo, every response stays correct automatically, and the pushed events' *original* senders (`"LOGIN_EVENT"`, `"ONLINE_STATUS_EVENT"`) become correct for the first time. `Name` is free-form as far as anything we found goes (we own both ends), but see §5. **Ordering hazard (important):** populating the table while any verb still hard-codes `sender="EbisuSDK"` — or while events still push `""` — breaks verbs that work today. **Rank 1 must land first (or in the same commit).** Once echo is in place, rank 2 is a pure superset. **Observable:** all 34 `std::string`s at `[SDK+0x3b0]` become non-empty (script in §5), and the `` push stops being dropped. --- ### RANK 3 — `blaze_responder_v3b.py`: change nothing that matters now; pre-stage the post-login RPCs * **DO NOT** enlarge `fetchClientConfig` hoping to fill `[cfg+0x750]` — it is already `0x43c47ff0` and is fed by `preAuth`. Keep `blazeSdkClientId` / `blazeServerClientId` / `blazeSdkClientSecret` / `identityRedirectUri` **exactly where they are, in `blazesdk_config()` (preAuth CONF)**; moving them would break getter `0x146e1bc50`. * Keep the empty REPLY to `Authentication::logout (1/0x46)` — normal, not a failure. **Fix the misleading log line (~L5935/6777/6947 pattern, emitted around `blaze_responder_v3b.py:1091`):** it currently blames "layer 1 or blazeSdkClientId". Replace with: *"logout is a normal OSDK step; the login gate is `OriginSDK+0x3a0 != 0` (LSX GetProfile), not Blaze config."* * **Pre-stage Authentication handlers** for the stubs actually compiled into this client (found by enumerating all 188 rel32 callers of `sendRequest 0x146df0e80` and reading the preceding `mov r9d,imm32`): `0x0A` login, `0x14`, `0x1E`, `0x26`, `0x2D`, `0x2F`, `0x46` logout, `0xF1`, `0xF2`, `0xF6`, `0x122`. `0x1E` is the one that will stall you next: callers `0x146e15a6e`/`0x146e15cbe` (vtable `0x14389fb98` slots +0xe8/+0xe0, discriminated by a bool arg) = `getTermsOfServiceContent` / `getPrivacyPolicyContent`; `LoginStateAuthenticated` caches the result at `state+0x68/+0x78` (strings `"LoginStateAuthenticated::mTermsOfServiceBuffer"@0x14389fe98`, `"…mPrivacypolicyBuffer"@0x14389fec8`) and will hang on an empty body. Serve non-empty text. * Expect `login`'s `LoginRequest.AUTH` to carry the auth code we hand out at `lsx_responder_v2.py:352` (`OPENFUT_AUTHCODE`, default `OPENFUT-000000000000000000000000`). --- ## 4. MEMORY-FORGE SHORTCUT (fallback only — server-side is strictly better) If rank 1 somehow does not take, you can force the same state. Note `+0x3a0` is a **u64 scalar** (UserId), not a pointer — ENQUEUE_PLAN's "point it at the SDK object" advice works only because the guards are `!=0` and `==[sdk+0x3a0]`; the *correct* value is our UserId. ```python # force_defaultuser.py (ptrace_scope=0) import struct pid = 13643 # pgrep -x FIFA17.exe f = open(f"/proc/{pid}/mem","r+b") def q(va): f.seek(va); return struct.unpack('` reply would still be rejected with `0xa2080000` unless rank 1 is also in — i.e. **the forge is only useful as a same-run A/B control** ("does `+0x3a0 != 0` really clear `OSDK_INVALID_USER`?"), not as a shipping path. Explicitly **not recommended any more**: forging the `FirstPartyAuthCodeFutureImpl` node (ENQUEUE_PLAN §1/§3a). With the matcher fixed, FIFA enqueues it itself, and the forged-node route leaves the OSDK state machine untouched — which is why the earlier forge produced a `` on the wire but never a Blaze `login`. --- ## 5. STILL NEEDS A LIVE EXPERIMENT 1. **The whole of §3 rank 1 needs a FIFA restart.** `GetProfile` has not recurred since log id=18 (line 1068 of 3385) and `+0x3a0` is still 0 in pid 13643 — the running instance will **not** self-heal. `Initialize` runs once. 2. **`Name` semantics in ``.** Nothing in `0x1470d0000-0x147160000` compares a service *Name* against a literal, but the search was not exhaustive. If some path does, `Name == Facility-enum-name` would break and real Origin-client Names would be needed. Cheap probe: rank 2 with `Name==Facility`; if a verb that worked under rank 1 starts failing, that is the culprit. 3. **Facility index for `GetInternetConnectedState`, `SetDownloaderUtilization`, `IsProgressiveInstallationAvailable`** is inferred (9 / 31 / 31). Echo-back makes this moot for rank 1; it only matters if you ever hard-code senders again. Confirm by reading the `add rdx,0xNN` after `mov rdx,[sdk+0x3b0]` at `0x1470e040a`, `0x14712b821`, `0x147129ddb`. 4. **`[SDK+0x1e8]` (= `conn+0x80`, live `-1`)** — a second connection handle, read by `0x14712cc00` and waited on by `0x1470e6b70` (condvar `[sdk+0xf8]`, alt-wake `[sdk+0x240]`, both live 0). Its *writer* is untraced; the conn object at `SDK+0x168` has vptr `0x14394c218` with 2 slots (`0x14712c9b0`, `0x14712ce00`) — disassemble those. Watch whether it flips after rank 1; if it stays `-1` and something blocks on `0x1470e6b70`, that is the next gate. Also unidentified: `0x14712cc30(SDK+0x168, &outString)`, called after that wait, whose result is inserted into the map at `SDK+0x1f8` (size `[SDK+0x218]`, live 0). 5. **Which of `UserId` / `PersonaId` lands in `+0x3a0` vs `+0x3a8`** is unobservable while we send the same number for both. Make them differ once, then read both slots. 6. **Does the fixed chain reach Blaze in one go**, or is there a further OSDK sub-state after 16? Watch `[0x43d189d8+0x260]` (re-find via vptr `0x14395c180`) walk off 16 and `LoginManagerImpl+0x148 → stateMachine+0x30` leave `0xFFFFFFFF`. 7. **After `login` fires**, `Authentication 0x1E` (ToS / privacy content) is the next likely stall — see rank 3. 8. **Post-fix event senders.** Confirm the `LOGIN_EVENT`/`ONLINE_STATUS_EVENT` handlers stop dropping frames: under rank 1 push `sender=""`; under rank 2 push `sender="LOGIN_EVENT"`. Note the earlier claim that `0xa2080000` in the log means failure is **weak** for events — with 73 EventHandler template instantiations, non-matching handlers log it as ordinary fan-out noise. Use `OriginMgr(*[0x1448acf50])+0x13` (`m_isLoggedIn`) flipping to 1 *by itself* as the event observable. ### Live verification snippet (run before and after) ```python #!/usr/bin/env python3 import struct, glob pid = next(int(d.split('/')[-1]) for d in glob.glob('/proc/[0-9]*') if open(d+'/comm').read().strip() == 'FIFA17.exe') f = open(f"/proc/{pid}/mem", "rb") def rd(va,n): f.seek(va); return f.read(n) def q(va): return struct.unpack('= 16 else s[:n]).decode()) ``` Baseline recorded today (pid 13643): `+0x3a0=0x0 +0x3a8=0x0 +0x1e8=0xffffffffffffffff +0x270=0x1c`, all 34 names empty, `[0x43d189d8+0x80]="OSDK_INVALID_USER"`, `[+0x260]=16`, `/tmp/blaze_responder.log`: 37× `logout`, 0× `login`.