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
528 lines
28 KiB
Markdown
528 lines
28 KiB
Markdown
# FIFA 17 Blaze — Authentication component (0x0001) reflection-reversed schema
|
||
|
||
**Date:** 2026-07-30 · **Target:** live `FIFA17.exe` PID 19517 (alive throughout, still at the
|
||
main menu afterwards) · **Method:** `/proc/<pid>/mem` reflection walk + targeted disassembly +
|
||
in-process function calls via gdb.
|
||
|
||
Companion doc: `fifa17-recon/tools/preauth_schema_reflection.md` (Util component / PreAuthResponse).
|
||
|
||
---
|
||
|
||
## 0. Clean-room provenance
|
||
|
||
Every fact below came from one of three allowed sources:
|
||
|
||
1. **The client's own runtime reflection metadata**, read out of the process we own. FIFA 17's
|
||
BlazeSDK ships full TDF type descriptors (class names, member names, wire tags, struct offsets)
|
||
in `.data`. This is the bulk of §3–§6.
|
||
2. **Disassembly of code in the binary we own** (`getCommandName` / `getErrorName` /
|
||
`getNotificationName` switches, TDF descriptor initialisers, notification dispatcher).
|
||
3. **Calling the client's own name-lookup functions in-process** (§2). This is reading the
|
||
binary's answer to its own question — no external artefact involved.
|
||
|
||
**No EA/FIFA leaked source was consulted at any point.** No third-party BlazeSDK reimplementation
|
||
was consulted for this document either (unlike the Fire2 header note in the preAuth doc, the
|
||
findings here are all first-party).
|
||
|
||
---
|
||
|
||
## 1. Headline result: the client sent `logout`, not `login`
|
||
|
||
The observed live frame
|
||
|
||
```
|
||
RX #9 Authentication::cmd:0x0046 msgType=MESSAGE msgNum=16 userIdx=0 payload=0B
|
||
```
|
||
|
||
**`Authentication` command `0x0046` (70) = `logout`.** (§2, verified twice.)
|
||
|
||
`logout` has **no request TDF and no response TDF** — there is no `Blaze::Authentication::LogoutRequest`
|
||
or `LogoutResponse` anywhere in the client's type index, which is exactly why the payload was 0 bytes.
|
||
Our empty REPLY was therefore *correct on the wire*.
|
||
|
||
This reframes the whole gate:
|
||
|
||
- The client **never sent `Authentication::login` (cmd 0x000A)**, never sent `getAuthToken` (0x0024),
|
||
never sent `listUserEntitlements2` (0x001D), never sent `listPersonas` (0x0064).
|
||
- It went `preAuth` → `ping` → 6× `fetchClientConfig` → **`logout`** → transport-ping loop.
|
||
- A client that logs out without ever logging in has decided *before touching Blaze auth* that it has
|
||
no credential to present. That is the Origin/LSX layer (layer 1 in the repack recon), not Blaze.
|
||
|
||
**Implication for the workflow:** implementing `Authentication::login` server-side is necessary but
|
||
**not sufficient and not the current blocker**. The client must first obtain an auth code from the
|
||
in-process Origin/LSX stub on `127.0.0.1:4216` (`GetAuthCode` → `AuthCodeResponse`, and
|
||
`OriginIsOnline` / `GetInternetConnectedState` must report ONLINE). Until that succeeds the client
|
||
will keep skipping straight to `logout`. See §7 for the ordering this implies.
|
||
|
||
---
|
||
|
||
## 2. Authentication component RPC table — **complete**
|
||
|
||
### How it was recovered
|
||
|
||
Each Blaze component has a 7-pointer `ComponentDescription`-style table in `.rdata`. Layout
|
||
established by diffing the Util table (whose command map we already knew) against Authentication's:
|
||
|
||
| Offset | Meaning | Util | Authentication |
|
||
|---|---|---|---|
|
||
| `+0x00` | shared helper | `0x146f022d0` | `0x146f022d0` |
|
||
| `+0x08` | `getComponentName()` | `0x146df7370` → `"UtilComponent"` | `0x146e0ec00` → `"AuthenticationComponent"` |
|
||
| `+0x10` | **`getCommandName(u16)`** | `0x146df6dc0` | **`0x146e0d2a0`** |
|
||
| `+0x18` | shared helper | `0x145bf3580` | `0x145bf3580` |
|
||
| `+0x20` | `getErrorName(u32)` | `0x146df7380` | `0x146e0ec20` |
|
||
| `+0x28` | `getRestResourceInfo(u16)` | `0x146f82070` (null stub) | `0x146e0f2e0` |
|
||
| `+0x30` | shared helper | `0x1465734f0` | `0x1465734f0` |
|
||
|
||
Table addresses: Util `0x143895820`, Authentication `0x14389d628`, plus one more at `0x143891940`
|
||
(Redirector) and six in `0x1438f5xxx` (FIFA-custom components).
|
||
|
||
Util's `getCommandName` is clean code — `movzx eax,dx; dec eax; cmp eax,0x1b; ja default;`
|
||
jump table of 28 image-relative RVAs at `0x141b17af4`. Decoding it statically **reproduces the
|
||
previously-known Util table exactly** (`fetchClientConfig=1, ping=2, preAuth=7, postAuth=8`, …),
|
||
which validates the whole approach.
|
||
|
||
**Authentication's `getCommandName` at `0x146e0d2a0` is Denuvo-mutated** — it begins
|
||
`push rcx; lea rcx,[rip+…]; jmp 0x14e183f34` into the protector arena. Consequently:
|
||
|
||
- the 33-name alphabetical string pool at `0x14389d690`–`0x14389d928` has **zero** references
|
||
anywhere in the address space (no `lea`, no qword pointer, no image-relative u32 — checked across
|
||
every readable region including the heap);
|
||
- so the command map cannot be recovered statically.
|
||
|
||
It was recovered instead by **calling the function in the live process**:
|
||
|
||
```
|
||
gdb --batch -p <pid> -ex 'p ((char*(*)(long,long,int))0x146e0d2a0)(0,0,<id>)'
|
||
```
|
||
|
||
Note the calling-convention detail: the target is **Windows x64** code (`this`→`rcx`, arg→`rdx`) but
|
||
gdb marshals with the **SysV** ABI (`rdi, rsi, rdx, …`), so the command id must be passed as the
|
||
**third** gdb argument to land in `rdx`. Validated against Util first (returned `preAuth` for 7,
|
||
`ping` for 2, `fetchClientConfig` for 1, `postAuth` for 8), then applied to Authentication.
|
||
|
||
Two independent cross-checks on the Authentication result:
|
||
- id `0x000B` → `"trustedLogin"`, which matches the static REST-binding struct at `0x143896a80`
|
||
(`{u16 component=0x0001, u16 command=0x000B, …, const char* name="trustedLogin", …, "GET"}`).
|
||
- every one of the 33 names in the static string pool is accounted for, with no leftovers.
|
||
|
||
### The table
|
||
|
||
Ids 1–320 were swept; everything not listed returns the empty default string.
|
||
|
||
| Cmd | RPC | Cmd | RPC |
|
||
|---|---|---|---|
|
||
| `0x000A` (10) | **`login`** | `0x003C` (60) | `expressLogin` |
|
||
| `0x000B` (11) | `trustedLogin` | **`0x0046` (70)** | **`logout`** ← *observed on the wire* |
|
||
| `0x0014` (20) | `updateAccount` | `0x005A` (90) | `getPersona` |
|
||
| `0x0015` (21) | `upgradeAccount` | `0x0064` (100) | `listPersonas` |
|
||
| `0x001D` (29) | `listUserEntitlements2` | `0x0065` (101) | `expressCreateAccount` |
|
||
| `0x001E` (30) | `getAccount` | `0x00E6` (230) | `createWalUserSession` |
|
||
| `0x001F` (31) | `grantEntitlement` | `0x00F1` (241) | `acceptLegalDocs` |
|
||
| `0x0020` (32) | `listEntitlements` | `0x00F2` (242) | `getEmailOptInSettings` |
|
||
| `0x0022` (34) | `getUseCount` | `0x00F6` (246) | `getTermsOfServiceContent` |
|
||
| `0x0023` (35) | `decrementUseCount` | `0x0104` (260) | `getOriginPersona` |
|
||
| `0x0024` (36) | `getAuthToken` | `0x010E` (270) | `checkEmail` |
|
||
| `0x0026` (38) | `getPasswordRules` | `0x0118` (280) | `getPersonaNameSuggestions` |
|
||
| `0x0027` (39) | `grantEntitlement2` | `0x0122` (290) | `guestLogin` |
|
||
| `0x002B` (43) | `modifyEntitlement2` | | |
|
||
| `0x002C` (44) | `consumecode` | | |
|
||
| `0x002D` (45) | `passwordForgot` | | |
|
||
| `0x002F` (47) | `getPrivacyPolicyContent` | | |
|
||
| `0x0030` (48) | `listPersonaEntitlements2` | | |
|
||
| `0x0033` (51) | `checkAgeReq` | | |
|
||
| `0x0034` (52) | `getOptIn` | | |
|
||
| `0x0035` (53) | `enableOptIn` | | |
|
||
| `0x0036` (54) | `disableOptIn` | | |
|
||
|
||
Confidence: **certain** for every row (the client itself produced these strings).
|
||
|
||
### Request / response type binding
|
||
|
||
From the reflection type index (`Blaze::Authentication::*`):
|
||
|
||
| Cmd | RPC | Request TDF | Response TDF |
|
||
|---|---|---|---|
|
||
| `0x000A` | `login` | `LoginRequest` | **`LoginResponse`** |
|
||
| `0x000B` | `trustedLogin` | `TrustedLoginRequest` | `LoginResponse` |
|
||
| `0x003C` | `expressLogin` | `ExpressLoginRequest` | `LoginResponse` |
|
||
| `0x0122` | `guestLogin` | *(none)* | `LoginResponse` |
|
||
| **`0x0046`** | **`logout`** | ***(none — empty)*** | ***(none — empty)*** |
|
||
| `0x0024` | `getAuthToken` | *(none)* | `GetAuthTokenResponse` |
|
||
| `0x001D` | `listUserEntitlements2` | `ListUserEntitlements2Request` | `Entitlements` |
|
||
| `0x0030` | `listPersonaEntitlements2` | `ListPersonaEntitlements2Request` | `Entitlements` |
|
||
| `0x0064` | `listPersonas` | *(none)* | `ListPersonasResponse` |
|
||
| `0x005A` | `getPersona` | *(none)* | `GetPersonaResponse` |
|
||
| `0x001E` | `getAccount` | *(none)* | `AccountInfo` |
|
||
| `0x0104` | `getOriginPersona` | `GetOriginPersonaRequest` | `PersonaInfo` |
|
||
|
||
`logout` having no TDFs on either side is confirmed by absence: there is no `LogoutRequest` /
|
||
`LogoutResponse` type anywhere in the client's index.
|
||
|
||
---
|
||
|
||
## 3. `Blaze::Authentication::LoginResponse` — the login reply (cmd `0x000A`)
|
||
|
||
Descriptor `0x14487d170`, member table `0x1448789c0`, 5 members. Serialise in **ascending packed-tag
|
||
order** (which is the order the table itself is sorted in).
|
||
|
||
| Tag | Member | TDF type | Wire type | Offset |
|
||
|---|---|---|---|---|
|
||
| `ANON` | `isAnonymous` | bool | `0x00` | `+0x12` |
|
||
| `NTOS` | `needsLegalDoc` | bool | `0x00` | `+0x13` |
|
||
| `SESS` | `userLoginInfo` | `Blaze::Authentication::UserLoginInfo` | `0x03` | `+0x18` |
|
||
| `SPAM` | `isOfLegalContactAge` | bool | `0x00` | `+0x10` |
|
||
| `UNDR` | `isUnderage` | bool | `0x00` | `+0x11` |
|
||
|
||
### `Blaze::Authentication::UserLoginInfo` (`SESS`) — 8 members, descriptor `0x14487cb00`
|
||
|
||
| Tag | Member | Type | Offset |
|
||
|---|---|---|---|
|
||
| `1CON` | `isFirstConsoleLogin` | bool | `+0x51` |
|
||
| `BUID` | `blazeUserId` | int64 | `+0x28` |
|
||
| `FRST` | `isFirstLogin` | bool | `+0x50` |
|
||
| `KEY ` | `sessionKey` | string | `+0x10` |
|
||
| `LLOG` | `lastLoginDateTime` | int64 | `+0x58` |
|
||
| `MAIL` | `email` | string | `+0x38` |
|
||
| `PDTL` | `personaDetails` | `PersonaDetails` (struct) | `+0x60` |
|
||
| `UID ` | `userId` | int64 | `+0x30` |
|
||
|
||
> Note the trailing-space tags: `KEY` and `UID` are 3-char tags. `heat2.py`'s `encode_tag`
|
||
> pads to 4 with the 6-bit "space" code, which is what the wire expects.
|
||
|
||
### `Blaze::Authentication::PersonaDetails` (`PDTL`) — 6 members, descriptor `0x14487cab0`
|
||
|
||
| Tag | Member | Type | Offset |
|
||
|---|---|---|---|
|
||
| `DSNM` | `displayName` | string | `+0x18` |
|
||
| `LAST` | `lastAuthenticated` | uint32 | `+0x3c` |
|
||
| `PID ` | `personaId` | int64 | `+0x10` |
|
||
| `PLAT` | `clientPlatform` | `Blaze::ClientPlatformType` (enum → int) | `+0x38` |
|
||
| `STAS` | `status` | `PersonaStatus::Code` (enum → int) | `+0x40` |
|
||
| `XREF` | `extId` | uint64 | `+0x30` |
|
||
|
||
### Request side (for completeness)
|
||
|
||
`Blaze::Authentication::LoginRequest` (`0x14487ca10`, 3 members):
|
||
|
||
| Tag | Member | Type |
|
||
|---|---|---|
|
||
| `AUTH` | `authCode` | string ← *the Origin/LSX auth code* |
|
||
| `EXTB` | `externalBlob` | blob |
|
||
| `EXTI` | `externalId` | uint64 |
|
||
|
||
`TrustedLoginRequest` (`0x14487ca60`): `ID ` id:string, `ITYP` idType:string, `TOKN` accessToken:string.
|
||
`ExpressLoginRequest` (`0x14487d0d0`): `MAIL` email:string, `PASS` password:string, `PNAM` personaName:string.
|
||
`StressLoginRequest` (`0x14487d120`): `MAIL` email:string, `NUID` nucleusId:uint64, `PNAM` personaName:string.
|
||
|
||
The `AUTH` field being a plain string is the whole hinge: **the client cannot fill it without a
|
||
working Origin `GetAuthCode`.**
|
||
|
||
---
|
||
|
||
## 4. What the server must return for the client to consider itself authenticated
|
||
|
||
**Caveat on method.** For `PreAuthResponse` we could read `ConnectionManager::onPreAuthResponse`
|
||
directly and prove field-by-field what is and isn't validated. The equivalent login-response handler
|
||
lives in the FIFA/OSDK layer, and that code is **Denuvo-mutated** — the same protection that hid
|
||
`getCommandName`. So the list below is derived from *structure* (what the types carry, what the
|
||
UserSessions notification duplicates, what the client must have in order to name itself), not from
|
||
reading the handler. Flagged accordingly.
|
||
|
||
| Field | Why it matters | Confidence it is required |
|
||
|---|---|---|
|
||
| `SESS.KEY ` `sessionKey` | The Blaze session credential. Every later component echoes it; a session with an empty key is not a session. Must be non-empty. | **high** |
|
||
| `SESS.BUID` `blazeUserId` | The client's own BlazeId. Used as the identity key in UserManager and in every later `lookupUser*`. Must be non-zero. | **high** |
|
||
| `SESS.UID ` `userId` | Nucleus user id. Should equal `blazeUserId` for a single-account offline emu unless you have a reason to split them. | **high** |
|
||
| `SESS.PDTL.PID ` `personaId` | **Must equal `33068179`** (the `stp-origin_emu.ini` `PersonaId`). A mismatch is exactly what trips `AUTH_ERR_INVALID_PERSONA` / `AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA` / `AUTH_ERR_PERSONA_NOT_FOUND`. | **high** |
|
||
| `SESS.PDTL.DSNM` `displayName` | **Must equal `CAGE`.** This is the name the UI renders and the value later persona lookups are matched against. | **high** |
|
||
| `SESS.PDTL.STAS` `status` | Persona status enum. Must be the "active" value; `AUTH_ERR_PERSONA_INACTIVE` (19) and `AUTH_ERR_PERSONA_BANNED` (32) exist for the other cases. Send `0`. | medium |
|
||
| `SESS.PDTL.PLAT` `clientPlatform` | Should match the `PLAT` you returned in `PreAuthResponse` (`pc`). | medium |
|
||
| `SESS.PDTL.XREF` `extId` | External (Origin) id. Safe to mirror `personaId`. | low |
|
||
| `SESS.MAIL` `email` | Cosmetic; any well-formed address. | low |
|
||
| `ANON` `isAnonymous` | **Must be `false`/0.** An anonymous session is precisely a not-really-logged-in session and will not enable online features. | **high** |
|
||
| `NTOS` `needsLegalDoc` | **Must be `false`/0**, otherwise the client will branch into the legal-doc flow (`acceptLegalDocs` = 241, `getTermsOfServiceContent` = 246) instead of proceeding. | **high** |
|
||
| `UNDR` `isUnderage` | **Must be `false`/0** — underage gates online play. | **high** |
|
||
| `SPAM` `isOfLegalContactAge` | Set `true`/1 for symmetry with `UNDR=false`. | medium |
|
||
| `FRST` / `1CON` / `LLOG` | First-login flags and timestamp. Cosmetic; `false/false/<now>` is fine. | low |
|
||
|
||
Consistency rule that spans both layers: **`personaId` and `personaName` returned over LSX,
|
||
returned in `LoginResponse.PDTL`, and pushed in the `UserAuthenticated` notification must be the
|
||
same triple** (`33068179` / `CAGE` / `en_US`). Every `AUTH_ERR_*_PERSONA*` code in §6 is a symptom
|
||
of these three disagreeing.
|
||
|
||
---
|
||
|
||
## 5. The login-success notification the server must push
|
||
|
||
Authentication (component `0x0001`) has **exactly one** notification. Its `getNotificationName`-slot
|
||
function `0x146e0f2e0` is a one-case switch:
|
||
|
||
```
|
||
cmp dx,0xb ; jne default ; lea rax,[0x143896a80] ; ret
|
||
```
|
||
|
||
and `0x143896a80` is **not** a notification name — it is the REST-binding struct for
|
||
`trustedLogin` (cmd `0x000B`). So Authentication publishes **no async notifications at all**; the
|
||
slot is `getRestResourceInfo`, not `getNotificationName`.
|
||
|
||
**The login-success notification lives on `UserSessions`, component `0x7802` (30722).**
|
||
Component id confirmed independently: that component's error table compares against
|
||
`(errorNumber<<16)|0x7802` (`0x17802`, `0x27802`, … `0x177802`).
|
||
|
||
`UserSessions::getNotificationName` at `0x146de19a0` is **clean, unmutated** code —
|
||
`movzx eax,dx; dec eax; cmp eax,0xb; ja default;` with a 12-entry image-relative jump table at
|
||
`0x141b03f70`. Decoded statically:
|
||
|
||
| Notification id | Name | Payload TDF |
|
||
|---|---|---|
|
||
| `0x0001` | `UserSessionExtendedDataUpdate` | `Blaze::UserSessionExtendedDataUpdate` |
|
||
| `0x0002` | `UserAdded` | `Blaze::UserData` |
|
||
| `0x0003` | `UserRemoved` | `Blaze::UserIdentification` *(or `UserStatus`)* |
|
||
| `0x0004` | *(unused)* | — |
|
||
| `0x0005` | `UserUpdated` | `Blaze::UserStatus` |
|
||
| `0x0006`–`0x0007` | *(unused)* | — |
|
||
| **`0x0008`** | **`UserAuthenticated`** | **`Blaze::UserSessionLoginInfo`** |
|
||
| `0x0009` | `UserUnauthenticated` | `Blaze::UserSessionLogoutInfo` |
|
||
| `0x000A`–`0x000B` | *(unused)* | — |
|
||
| `0x000C` | `ServerDraining` | — |
|
||
|
||
The **id → name** column is certain (decoded from the client's own jump table). The **payload TDF**
|
||
column is *inferred*: the notification dispatcher at `0x146de2803` (12-entry jump table at
|
||
`0x141b05258`, branches at `0x146de2bc3 / 289c / 2ae9 / 2dd8 / 29e0 / 2cf1 / 2825`) constructs its
|
||
TDFs on the stack rather than through the type-descriptor getters, so I could not bind each branch
|
||
to a descriptor by xref. The mapping above is a 1:1 name match against the only session types the
|
||
client links in, and `UserSessionLoginInfo` is the only type in the entire index that carries a
|
||
session key plus a persona — but treat the payload column as **high confidence, not verified**.
|
||
|
||
### `Blaze::UserSessionLoginInfo` — 16 members, descriptor `0x14486f920`
|
||
|
||
This is the notification body to push after a successful `login`. Note it is a **superset** of
|
||
`UserLoginInfo` with the persona fields flattened in rather than nested.
|
||
|
||
| Tag | Member | Type | Offset |
|
||
|---|---|---|---|
|
||
| `1CON` | `isFirstConsoleLogin` | bool | `+0x51` |
|
||
| `ALOC` | `accountLocale` | uint32 | `+0x60` |
|
||
| `BUID` | `blazeUserId` | int64 | `+0x28` |
|
||
| `CGID` | `connectionGroupObjectId` | ObjectId | `+0xb0` |
|
||
| `DSNM` | `displayName` | string | `+0x70` |
|
||
| `FRST` | `isFirstLogin` | bool | `+0x50` |
|
||
| `KEY ` | `sessionKey` | string | `+0x10` |
|
||
| `LAST` | `lastAuthenticated` | uint32 | `+0xac` |
|
||
| `LLOG` | `lastLoginDateTime` | int64 | `+0x58` |
|
||
| `MAIL` | `email` | string | `+0x38` |
|
||
| `NASP` | `personaNamespace` | string | `+0x88` |
|
||
| `PID ` | `personaId` | int64 | `+0x68` |
|
||
| `PLAT` | `clientPlatform` | enum | `+0xa8` |
|
||
| `UID ` | `userId` | int64 | `+0x30` |
|
||
| `USTP` | `userSessionType` | `Blaze::UserSessionType` (enum) | `+0xc0` |
|
||
| `XREF` | `extId` | uint64 | `+0xa0` |
|
||
|
||
`ALOC` (`accountLocale`) is a packed uint32 locale — use the same encoding the client sent in
|
||
`ClientData.LANG` in the PreAuth request, i.e. echo it back rather than inventing one. `NASP`
|
||
(`personaNamespace`) should match the `NASP` you returned in `PreAuthResponse`.
|
||
|
||
### Related UserSessions types (for the rest of the session bring-up)
|
||
|
||
```
|
||
Blaze::UserSessionLogoutInfo (0x14486f970, 2)
|
||
BID blazeId int64
|
||
USTP userSessionType enum
|
||
|
||
Blaze::SessionInfo (0x14486ffa0, 5)
|
||
BUID blazeUserId int64 DSNM displayName string
|
||
KEY sessionKey string MAIL email string
|
||
UID userId int64
|
||
|
||
Blaze::UserIdentification (0x14486ebc0, 9)
|
||
AID accountId int64 ALOC accountLocale uint32
|
||
EXBB externalBlob blob EXID externalId uint64
|
||
ID blazeId int64 NAME name string
|
||
NASP personaNamespace string ORIG originPersonaId uint64
|
||
PIDI pidId int64
|
||
|
||
Blaze::UserData (0x1448706b0, 3)
|
||
EDAT extendedData Blaze::UserSessionExtendedData
|
||
FLGS statusFlags Blaze::UserDataFlags (bitfield)
|
||
USER userInfo Blaze::UserIdentification
|
||
|
||
Blaze::UserStatus (0x14486ed10, 2)
|
||
FLGS statusFlags Blaze::UserDataFlags ID blazeId int64
|
||
|
||
Blaze::UserSessionExtendedData (0x144870390, 12)
|
||
ADDR address Blaze::NetworkAddress (union, 5 cases)
|
||
BPS bestPingSiteAlias string
|
||
CTY country string
|
||
CVAR clientData variable
|
||
DMAP dataMap map<int64,uint32>
|
||
HWFG hardwareFlags Blaze::HardwareFlags (bitfield)
|
||
ISP iSP string
|
||
PSLM latencyList list<int32>
|
||
QDAT qosData Blaze::Util::NetworkQosData
|
||
TZ timeZone string
|
||
UATT userInfoAttribute uint64
|
||
ULST blazeObjectIdList list<ObjectId>
|
||
|
||
Blaze::UserSessionExtendedDataUpdate (0x1448703e0, 3)
|
||
DATA extendedData Blaze::UserSessionExtendedData
|
||
SUBS subscribed bool
|
||
USID userId int64
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Authentication error codes — **complete**
|
||
|
||
Blaze error codes for this component are packed `(errorNumber << 16) | 0x0001`. Recovered by calling
|
||
`getErrorName` (`0x146e0ec20`, clean code) in-process across the range; the static `cmp edx,0xNN0001`
|
||
chain at `0x146e0ec20`+ corroborates.
|
||
|
||
| # | Name | # | Name |
|
||
|---|---|---|---|
|
||
| 1 | `AUTH_ERR_INVALID_TOKEN` | 72 | `AUTH_ERR_TOO_MANY_ENTITLEMENTS` |
|
||
| 2 | `AUTH_ERR_TOS_REQUIRED` | 73 | `AUTH_ERR_PAGESIZE_ZERO` |
|
||
| 6 | `AUTH_ERR_INVALID_SANDBOX_ID` | 74 | `AUTH_ERR_ENTITLEMENT_TAG_REQUIRED` |
|
||
| 10 | `AUTH_ERR_INVALID_COUNTRY` | 75 | `AUTH_ERR_PAGENO_ZERO` |
|
||
| 11 | `AUTH_ERR_INVALID_USER` | 76 | `AUTH_ERR_MODIFIED_STATUS_INVALID` |
|
||
| 12 | `AUTH_ERR_INVALID_PASSWORD` | 77 | `AUTH_ERR_USECOUNT_INCREMENT` |
|
||
| 14 | `AUTH_ERR_EXPIRED_TOKEN` | 78 | `AUTH_ERR_TERMINATION_INVALID` |
|
||
| 16 | `AUTH_ERR_TOO_YOUNG` | 79 | `AUTH_ERR_UNKNOWN_ENTITLEMENT` |
|
||
| 17 | `AUTH_ERR_NO_ACCOUNT` | 80 | `AUTH_ERR_EXCEEDS_PSU_LIMIT` |
|
||
| 19 | `AUTH_ERR_PERSONA_INACTIVE` | 81 | `AUTH_ERR_OPTIN_NAME_REQUIRED` |
|
||
| 20 | `AUTH_ERR_INVALID_PMAIL` | 82 | `AUTH_ERR_INVALID_OPTIN` |
|
||
| 21 | `AUTH_ERR_INVALID_FIELD` | 83 | `AUTH_ERR_OPTIN_MISMATCH` |
|
||
| 22 | `AUTH_ERR_INVALID_EMAIL` | 84 | `AUTH_ERR_NO_SUCH_OPTIN` |
|
||
| 23 | `AUTH_ERR_INVALID_STATUS` | 85 | `AUTH_ERR_AUTHID_REQUIRED` |
|
||
| 32 | `AUTH_ERR_PERSONA_BANNED` | 86 | `AUTH_ERR_PERSONA_EXTREFID_REQUIRED` |
|
||
| 33 | `AUTH_ERR_INVALID_PERSONA` | 87 | `AUTH_ERR_SOURCE_REQUIRED` |
|
||
| 34 | `AUTH_ERR_CURRENT_PASSWORD_REQUIRED` | 88 | `AUTH_ERR_APPLICATION_REQUIRED` |
|
||
| 41 | `AUTH_ERR_DEACTIVATED` | 89 | `AUTH_ERR_TOKEN_REQUIRED` |
|
||
| 43 | `AUTH_ERR_BANNED` | 90 | `AUTH_ERR_PARAMETER_TOO_LENGTH` |
|
||
| 44 | `AUTH_ERR_DISABLED` | 91 | `AUTH_ERR_NO_SUCH_PERSONA_REFERENCE` |
|
||
| 50 | `AUTH_ERR_NEED_PCCDKEY` | 93 | `AUTH_ERR_INVALID_SOURCE` |
|
||
| 51 | `AUTH_ERR_CODE_ALREADY_USED` | 94 | `AUTH_ERR_NO_SUCH_AUTH_DATA` |
|
||
| 52 | `AUTH_ERR_INVALID_CODE` | 101 | `AUTH_ERR_USER_INACTIVE` |
|
||
| 53 | `AUTH_ERR_CODE_ALREADY_DISABLED` | 102 | `AUTH_ERR_UNEXPECTED_ACTIVATION` |
|
||
| 54 | `AUTH_ERR_NO_ASSOCIATED_PRODUCT` | 103 | `AUTH_ERR_NAME_MISMATCH` |
|
||
| 55 | `AUTH_ERR_INVALID_MAPPING_ERROR` | 105 | `AUTH_ERR_INVALID_NAMESPACE` |
|
||
| 56 | `AUTH_ERR_NO_SUCH_GROUP_NAME` | 198 | `AUTH_ERR_FIELD_MIN_LOWER_CHARS` |
|
||
| 57 | `AUTH_ERR_MISSING_PERSONAID` | 199 | `AUTH_ERR_FIELD_MIN_UPPER_CHARS` |
|
||
| 58 | `AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA` | 200 | `AUTH_ERR_FIELD_MIN_DIGITS` |
|
||
| 59 | `AUTH_ERR_WHITELIST` | 201 | `AUTH_ERR_FIELD_INVALID_CHARS` |
|
||
| 60 | `AUTH_ERR_LINK_PERSONA` | 202 | `AUTH_ERR_FIELD_TOO_SHORT` |
|
||
| 61 | `AUTH_ERR_NO_SUCH_GROUP` | 203 | `AUTH_ERR_FIELD_TOO_LONG` |
|
||
| 63 | `AUTH_ERR_NO_SUCH_ENTITLEMENT` | 204 | `AUTH_ERR_FIELD_MUST_BEGIN_WITH_LETTER` |
|
||
| 64 | `AUTH_ERR_GROUP_NAME_DOES_NOT_MATCH` | 205 | `AUTH_ERR_FIELD_MISSING` |
|
||
| 66 | `AUTH_ERR_USECOUNT_ZERO` | 206 | `AUTH_ERR_FIELD_INVALID` |
|
||
| 67 | `AUTH_ERR_ENTITLEMETNTAG_EMPTY` *(sic)* | 207 | `AUTH_ERR_FIELD_NOT_ALLOWED` |
|
||
| 70 | `AUTH_ERR_GROUPNAME_REQUIRED` | 208 | `AUTH_ERR_FIELD_NEEDS_SPECIAL_CHARS` |
|
||
| 71 | `AUTH_ERR_GROUPNAME_INVALID` | 209 | `AUTH_ERR_FIELD_ALREADY_EXISTS` |
|
||
| | | 210 | `AUTH_ERR_FIELD_NEEDS_CONSENT` |
|
||
| | | 211 | `AUTH_ERR_FIELD_TOO_YOUNG` |
|
||
| | | 212 | `AUTH_ERR_ASSOCIATION_TOO_YOUNG` |
|
||
|
||
---
|
||
|
||
## 7. Supporting types for the post-login flow
|
||
|
||
```
|
||
Blaze::Authentication::GetAuthTokenResponse (0x14487d080, 1)
|
||
AUTH authToken string
|
||
|
||
Blaze::Authentication::GetUserAccessTokenResponse (0x14487db80, 2)
|
||
ATOK accessToken string CLID clientId string
|
||
|
||
Blaze::Authentication::ListPersonasResponse (0x14487d210, 1)
|
||
PINF list list<Blaze::Authentication::PersonaDetails>
|
||
|
||
Blaze::Authentication::GetPersonaResponse (0x14487d1c0, 2)
|
||
PINF personaInfo Blaze::Authentication::PersonaInfo
|
||
UID userId int64
|
||
|
||
Blaze::Authentication::PersonaInfo (0x14487c7c0, 7)
|
||
DSNM displayName string DTCR dateCreated string
|
||
LADT lastAuthenticated uint32 NSNM nameSpaceName string
|
||
PID personaId int64 STAS status enum
|
||
STRC statusReasonCode enum
|
||
|
||
Blaze::Authentication::AccountInfo (0x14487c810, 16)
|
||
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
|
||
|
||
Blaze::Authentication::Entitlements (0x14487d4e0, 1)
|
||
NLST entitlements list<Blaze::Authentication::Entitlement>
|
||
|
||
Blaze::Authentication::Entitlement (0x14487d490, 16)
|
||
DEVI deviceUri:string GDAY grantDate:string GNAM groupName:string
|
||
ID id:uint64 ISCO isConsumable:bool PID personaId:int64
|
||
PJID projectId:string PRCA productCatalog:enum PRID productId:string
|
||
STAT status:enum STRC statusReasonCode:enum TAG entitlementTag:string
|
||
TDAY terminationDate:string TYPE entitlementType:enum
|
||
UCNT useCount:uint32 VER version:uint32
|
||
|
||
Blaze::Authentication::UserProfileInfo (0x14487c860, 8)
|
||
CITY city:string CTRY country:string STAT state:string STRT street:string
|
||
ZIP zipCode:string GNDR gender:enum UID userId:int64
|
||
ELEM profileInfoElementsByCategory: map<string, map<string,string>>
|
||
(the binary's own map<> name string is written value-first — read it backwards,
|
||
same caveat as documented for QoS maps in the preAuth doc)
|
||
```
|
||
|
||
When `listUserEntitlements2` (`0x001D`) does eventually get called, the entitlement the retail exe
|
||
requires is `TAG = "ONLINE_ACCESS"` with `PRID` tied to offer id `1027460`, `STAT` = active, and
|
||
`PID` = `33068179`. `AUTH_ERR_NO_SUCH_ENTITLEMENT` (63) and `AUTH_ERR_ENTITLEMENT_TAG_REQUIRED` (74)
|
||
are the failure modes.
|
||
|
||
---
|
||
|
||
## 8. Recommended server behaviour, in order
|
||
|
||
1. **Fix layer 1 first.** Nothing in this document is reachable until the in-process Origin/LSX stub
|
||
on `127.0.0.1:4216` answers `OriginIsOnline` / `GetInternetConnectedState` with *online* and
|
||
`GetAuthCode` with a non-empty code. Right now the client's `logout` at cmd `0x0046` is the
|
||
observable proof that it never got one. This is the single highest-value next step.
|
||
2. Keep replying to `Util::fetchClientConfig` (9/1) — an empty `FetchConfigResponse { CONF: {} }` is
|
||
structurally valid, but see the preAuth doc for the tunables the client reads out of `CONF`.
|
||
3. When `Authentication::login` (`0x0001`/`0x000A`) finally arrives, reply `LoginResponse` per §3
|
||
with the §4 field values, keyed to persona `33068179` / `CAGE`.
|
||
4. Immediately after the login REPLY, push a **NOTIFICATION** frame (msgType=2) on component
|
||
`0x7802`, notification id `0x0008` (`UserAuthenticated`), payload `Blaze::UserSessionLoginInfo`
|
||
per §5 — same session key, same BlazeId, same persona triple.
|
||
5. Expect `Util::postAuth` (9/8) next; `PostAuthResponse` is documented in the preAuth doc.
|
||
6. Answer `logout` (`0x0046`) with an empty REPLY — which is already correct.
|
||
|
||
---
|
||
|
||
## 9. Tooling produced (all in the session scratchpad)
|
||
|
||
| Script | Purpose |
|
||
|---|---|
|
||
| `stubscan.py` | list every `lea rax,[rip+str]; ret` name stub in a VA window, in address order |
|
||
| `authcmd.py` | locate RPC-name strings and every reference to them |
|
||
| `refscan.py` | find all references (rip-rel lea / qword / image-relative u32) into a VA window |
|
||
| `heapref.py` | same, but across *every* readable region including the heap |
|
||
| `cmdinfo.py` | scan the metadata arena for `{u16 component, u16 command, …, name}` REST structs |
|
||
| `comptabs.py` | enumerate Blaze `ComponentDescription` 7-pointer tables |
|
||
| `qdump.py` / `pooldump.py` / `dumpregion.py` | annotated qword dump / string-pool dump / raw region dump |
|
||
| `jt.py` | decode an MSVC image-relative jump table of `lea/ret` name stubs |
|
||
| `rpcmap.py` / `factref.py` / `vt2.py` | descriptor-getter and factory xref mapping (all three came up empty for Authentication — recorded as negative results) |
|
||
| `authcmds*.gdb`, `autherr.gdb` | in-process `getCommandName` / `getErrorName` sweeps |
|
||
|
||
Reused from the previous pass: `memtool.py`, `reflect2.py`, `strsearch.py`, `all_types.txt`.
|
||
|
||
### Negative results worth recording
|
||
|
||
- The 33-name Authentication command-name pool at `0x14389d690` is **completely unreferenced** —
|
||
no lea, no pointer, no RVA, anywhere in the process. Do not waste time looking for its xrefs.
|
||
- `Blaze::Authentication::LoginRequest`'s vtable (`0x14389ac68`) is referenced only by its own
|
||
factory/ctor/dtor. No game code constructs one statically — the login call site is inside
|
||
Denuvo-mutated code.
|
||
- Every `Blaze::Authentication::*` type-descriptor getter has exactly one caller: the TDF factory
|
||
registration run at `0x1451cd0xx`. There is no per-command descriptor table to mine.
|