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
28 KiB
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:
- 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. - Disassembly of code in the binary we own (
getCommandName/getErrorName/getNotificationNameswitches, TDF descriptor initialisers, notification dispatcher). - 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 sentgetAuthToken(0x0024), never sentlistUserEntitlements2(0x001D), never sentlistPersonas(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–0x14389d928has zero references anywhere in the address space (nolea, 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 at0x143896a80({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:
KEYandUIDare 3-char tags.heat2.py'sencode_tagpads 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
- Fix layer 1 first. Nothing in this document is reachable until the in-process Origin/LSX stub
on
127.0.0.1:4216answersOriginIsOnline/GetInternetConnectedStatewith online andGetAuthCodewith a non-empty code. Right now the client'slogoutat cmd0x0046is the observable proof that it never got one. This is the single highest-value next step. - Keep replying to
Util::fetchClientConfig(9/1) — an emptyFetchConfigResponse { CONF: {} }is structurally valid, but see the preAuth doc for the tunables the client reads out ofCONF. - When
Authentication::login(0x0001/0x000A) finally arrives, replyLoginResponseper §3 with the §4 field values, keyed to persona33068179/CAGE. - Immediately after the login REPLY, push a NOTIFICATION frame (msgType=2) on component
0x7802, notification id0x0008(UserAuthenticated), payloadBlaze::UserSessionLoginInfoper §5 — same session key, same BlazeId, same persona triple. - Expect
Util::postAuth(9/8) next;PostAuthResponseis documented in the preAuth doc. - 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
0x14389d690is 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 at0x1451cd0xx. There is no per-command descriptor table to mine.