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
This commit is contained in:
@@ -0,0 +1,527 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user