# FIFA 17 Blaze `Util::preAuth` — reflection-reversed schema **Date:** 2026-07-30 · **Method:** live `/proc//mem` reflection walk of `FIFA17.exe` (PID 7618, alive throughout) + targeted disassembly of the response handler. ## Clean-room provenance Everything below came from one of three sources, all allowed: 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 the findings. 2. **Disassembly of code in the binary we own** (`ConnectionManager::onPreAuthResponse` and the Util component's `getCommandName` switch). 3. **Our own captured wire bytes** (`fifa17-recon/captures/blaze/*.bin`). One item — the Fire2 header field layout — was **cross-checked** against three independent third-party clean-room BlazeSDK-15.x reimplementations cloned in the scratchpad (`pamplona-future`, `catalyst-mitm`, `grid-blaze`, all Mirror's Edge Catalyst / Mass Effect era). That is flagged inline. **No EA/FIFA leaked source was consulted at any point.** --- ## 0. Correction: the Fire2 header layout we were using is wrong Our working assumption was `[10:12]=u16 error/msgId, [12]=u8 msgType`. That is **incorrect**, and it matters: a reply built with it would put the message type in the wrong byte and drop the sequence number, so the client would never match the reply to its pending request. Correct 16-byte header (all big-endian): | Bytes | Field | |---|---| | `[0:4]` | u32 payload length | | `[4:6]` | u16 metadata length (always 0 observed) | | `[6:8]` | u16 component | | `[8:10]` | u16 command | | `[10:13]` | **u24 msgNum** (3 bytes, not 2) | | `[13]` | **msgType << 5** | | `[14]` | options | | `[15]` | reserved | `msgType`: `0=MESSAGE 1=REPLY 2=NOTIFICATION 3=ERROR_REPLY 4=PING 5=PING_REPLY`. **Evidence this is right, independent of the reference repos:** byte `[12]` across our 8 captured frames takes the values `0,1,2,3,4,5,6,7` — a monotonic counter, alternating `preAuth`(even) / `ping`(odd). Under the old reading that would be eight different "message types", which is nonsense. Under the new reading every captured frame is `msgNum = 0..7, msgType = 0 (MESSAGE)` — i.e. all eight are client *requests*, which is exactly what we expect since we never replied. This also retires the earlier "ping/pong msgType 0x01/0x03" note: both 16-byte frames are plain `Util::ping` **requests** (command 0x0002, see §2), not a ping/pong pair. `heat2.py`'s `build_fire2_frame` / `parse_fire2_frame` still encode the old layout and should be replaced by the versions in `build_preauth_response.py`. Its **TDF value encoding is fine** — it round-trips the 219-byte capture byte-for-byte. --- ## 1. `Blaze::Util::PreAuthResponse` — 14 members Descriptor at VA `0x144875600`, member table `0x144874a90`, count 14. Members are listed and must be **serialized in ascending packed-tag order** (which is what the table itself is sorted by, and what the captured request does). | Tag | Member name | TDF type | Wire type | Struct offset | Confidence | |---|---|---|---|---|---| | `ASRC` | `authenticationSource` | string | `0x01` | +0x1d8 | certain | | `CIDS` | `componentIds` | list\ | `0x04` | +0x070 | certain | | `CLID` | `clientId` | string | `0x01` | +0x058 | certain | | `CONF` | `config` | `Blaze::Util::FetchConfigResponse` | `0x03` | +0x0b8 | certain | | `ESRC` | `entitlementSource` | string | `0x01` | +0x228 | certain | | `INST` | `serviceName` | string | `0x01` | +0x040 | certain | | `MAID` | `machineId` | uint32 | `0x00` | +0x240 | certain | | `MINR` | `underageSupported` | bool | `0x00` | +0x220 | certain | | `NASP` | `personaNamespace` | string | `0x01` | +0x1c0 | certain | | `PILD` | `legalDocGameIdentifier` | string | `0x01` | +0x208 | certain | | `PLAT` | `platform` | string | `0x01` | +0x028 | certain | | `QOSS` | `qosSettings` | `Blaze::QosConfigInfo` | `0x03` | +0x120 | certain | | `RSRC` | `registrationSource` | string | `0x01` | +0x1f0 | certain | | `SVER` | `serverVersion` | string | `0x01` | +0x010 | certain | Tag decoding is validated: the request's four member tags round-trip exactly (`0x8e4874`→`CDAT`, `0x8e9ba6`→`CINF`, `0x9a38f2`→`FCCR`, `0xb21924`→`LADD`), matching the capture. ### Nested types ``` Blaze::Util::FetchConfigResponse (CONF, 1 member) CONF config map Blaze::QosConfigInfo (QOSS, 4 members) BWPS bandwidthPingSiteInfo Blaze::QosPingSiteInfo (struct) LNP numLatencyProbes uint16 LTPS pingSiteInfoByAliasMap map TIME timeout TimeValue (int, microseconds) Blaze::QosPingSiteInfo (2 members) PSA address string PSP port uint16 ``` Note `CONF` nests a struct whose single member is *also* tagged `CONF` — the outer is the `FetchConfigResponse` struct, the inner is the map. Easy to get wrong. **Map key/value order:** the key descriptor is at `+0x28`, the value at `+0x30`. The binary's own `map` *name string* is written **value-first**, so it reads backwards — don't trust it. Confirmed against two maps whose semantics are unambiguous: `pingSiteLatencyByAliasMap` is named `map` but is really `map`; `permissionsByComponent` is named `map,string>` but is really `map permissions>`. So `LTPS` = `map`. ### Request side, for reference `Blaze::Util::PreAuthRequest` (4 members) — matches our capture exactly, which is what validated the whole descriptor-walking method: | Tag | Member | Type | |---|---|---| | `CDAT` | `clientData` | `Blaze::Util::ClientData` | | `CINF` | `clientInfo` | `Blaze::ClientInfo` (10 members) | | `FCCR` | `fetchClientConfig` | `Blaze::Util::FetchClientConfigRequest` { `CFID` configSection: string } | | `LADD` | `localAddress` | uint32 | `Blaze::Util::ClientData` = { `IITO` ignoreInactivityTimeout: bool, `LANG` locale: uint32, `SVCN` serviceName: string, `TYPE` clientType: enum }. --- ## 2. Util component RPC table — **complete** Component id **0x0009**, confirmed directly in the binary: the Util notification dispatcher compares against `0x00c80009`, `0x00980009`, `0x00640009`, `0x00960009`, `0x00970009` — i.e. `(notificationId << 16) | 9`. Recovered by locating the compiler-generated `getCommandName(commandId)` switch: 21 `lea rax,[rip+name]; ret` stubs emitted in alphabetical order at RVA `0x1b17a43 + 8k`, plus a jump table of 28 entries at VA `0x146df72f4` indexed by `commandId - 1`. | Command | RPC | Command | RPC | |---|---|---|---| | `0x0001` | `fetchClientConfig` | `0x000f` | `userSettingsLoadMultiple` | | `0x0002` | **`ping`** ✅ | `0x0010`–`0x0013` | *(unused)* | | `0x0003` | `setClientData` | `0x0014` | `filterForProfanity` | | `0x0004` | `localizeStrings` | `0x0015` | `fetchQosConfig` | | `0x0005` | `getTelemetryServer` | `0x0016` | `setClientMetrics` | | `0x0006` | `getTickerServer` | `0x0017` | `setConnectionState` | | `0x0007` | **`preAuth`** ✅ | `0x0018` | *(unused)* | | `0x0008` | `postAuth` | `0x0019` | `getUserOptions` | | `0x0009` | *(unused)* | `0x001a` | `setUserOptions` | | `0x000a` | `userSettingsLoad` | `0x001b` | `suspendUserPing` | | `0x000b` | `userSettingsSave` | `0x001c` | `setClientState` | | `0x000c` | `userSettingsLoadAll` | | | | `0x000d` | *(unused)* | | | | `0x000e` | `deleteUserSettings` | | | **This table is self-validating**: it independently reproduces both values we observed on the wire — `ping = 0x0002` and `preAuth = 0x0007`. Confidence: high. ### Request/response type mapping (from the reflection type index) | Command | Request | Response | |---|---|---| | `0x0002` ping | *(empty)* | `Blaze::Util::PingResponse` { `STIM` serverTime } | | `0x0007` preAuth | `PreAuthRequest` | `PreAuthResponse` | | `0x0008` postAuth | `PostAuthRequest` { `DSUI` dirtySockUserIndex: int32, `UDID` uniqueDeviceId: string } | `PostAuthResponse` | | `0x0001` fetchClientConfig | `FetchClientConfigRequest` { `CFID` } | `FetchConfigResponse` { `CONF` map } | | `0x0015` fetchQosConfig | *(empty)* | `QosConfigInfo` | `Blaze::Util::PostAuthResponse` (3 members) — **the next thing we'll need**: ``` TELE telemetryServer Blaze::Util::GetTelemetryServerResponse (15 members) ADRS address:string ANON isAnonymous:bool DISA disable:string EDCT enableDisconnectTelemetry:bool FILT filter:string LOC locale:uint32 MINR underage:bool NOOK noToggleOk:string PORT port:uint32 SDLY sendDelay:uint32 SESS sessionID:string SKEY key:string SPCT sendPercentage:uint32 STIM useServerTime:string SVNM telemetryServiceName:string TICK tickerServer Blaze::Util::GetTickerServerResponse ADRS address:string PORT port:uint32 SKEY key:string UROP userOptions Blaze::Util::UserOptions TMOP telemetryOpt:enum UID userId:int64 ``` ### Component ids (recovered from each component's notification dispatcher) `Authentication=1`, `GameManager=4`, `Redirector=5`, `Util=9`, `GameReporting=28`, `UserSessions=30722`. FIFA-custom components (`CoopSeason`, `EaAccess`, `Easfc`, `FifaCups`, `SponsoredEvents`, `VProSPManagement`, `OSDKSettings`, `OSDKTournaments`, `OsdkArena`) live in the `2069/2070/2077`… range; I did not separate them individually (their dispatchers are adjacent and my scan window overlapped). `Stats`, `Clubs`, `Messaging`, `Mail`, `AssociationLists`, `GpsContentController`, `CensusData` are also linked in. --- ## 3. What the client actually *does* with the response `ConnectionManager::onPreAuthResponse(this, PreAuthResponse* r /*rdx*/, errorCode /*r8d*/)` at **VA `0x146e1cf10`**. Signature confirmed because every `[r14+offset]` it touches matches a `PreAuthResponse` member offset from §1. The function performs **no validation whatsoever** — on the success path it only copies fields out and then advances. So the practical requirement is: *reply with a well-formed `REPLY` frame carrying the right `msgNum`.* Nothing in the body is checked for a specific value. Field-by-field: | Field | What happens | Mandatory? | |---|---|---| | `CONF` config | whole map copied into ConnectionManager `+0x11b8` | **effectively yes** — see the tunables below | | `CIDS` componentIds | list copied to `+0x1240`; this is the client's view of which components the server has | **yes, practically** — later components look themselves up here | | `SVER` serverVersion | `memcpy` → `+0x1288` (512-byte buf) | no, free-form | | `PLAT` platform | `memcpy` → `+0x1488` (512-byte buf) | no, free-form | | `INST` serviceName | `memcpy` → `+0x1688` (512-byte buf) | no — echo the request's `SVCN` (`fifa-2017-pc`) | | `CLID` clientId | `memcpy` → `+0x1888` (512-byte buf) | no | | `NASP` personaNamespace | `memcpy` → `+0x1a88`, **capped at 32 bytes** | no here, but auth will care | | `RSRC` registrationSource | `memcpy` → `+0x1a?8` | no | | `ASRC` authenticationSource | `memcpy` → `+0x1ae8` | no | | `PILD` legalDocGameIdentifier | `memcpy` → `+0x1b28` | no | | `MINR` underageSupported | byte → `+0x1b6c` | no | | `ESRC` entitlementSource | `memcpy` → `+0x1b6d` | no | | `MAID` machineId | passed to `0x146124610` | no | | `QOSS` qosSettings | passed to QoS manager init `0x146e1c3f0` | see below | Then it calls `0x146e1e460` and `0x146e1c3f0` (QoS start) and returns. ### The `CONF` map keys the client reads Read at `0x146e1d0a5`–`0x146e1d1a2`, all via `ConnectionManager` vtable getters (`+0x50` = uint32, `+0x58` = TimeValue/int64, both returning "found"): | Key | Parsing | Behaviour if absent | |---|---|---| | `pingPeriod` | value ÷ 1000 (so the config value is in **microseconds** → stored as ms); if result < 1000 ms, clamped to 15000 | **defaults to 15000 ms** (`0x3a98`) | | `defaultRequestTimeout` | ÷1000 → ms, stored `+0x278` | left unchanged | | `connIdleTimeout` | ÷1000 → ms, stored `+0xd28` | left unchanged | | `autoReconnectEnabled` | `!= 0` → bool `+0x11b7` | left unchanged | | `maxReconnectAttempts` | uint32 → `+0xc5c` | left unchanged | **None of these are strictly required** — every one has a fallback. But `pingPeriod` decides how fast the client starts hammering `Util::ping` (cmd `0x0002`), so set it deliberately. ### Turning off the QoS probes — important for an offline emulator Separate helper at `0x146e1bbc0` reads two more keys and starts from flags `= 3` (both tests on): | Key | Effect | |---|---| | `enableQosFirewallTest` | if the value string equals **`"false"`** exactly (string at VA `0x14354be74`), clears bit 0 | | `enableQosBandwidthTest` | same → clears bit 1 | So putting `enableQosFirewallTest=false` and `enableQosBandwidthTest=false` in `CONF` stops the client trying to reach real QoS ping servers. Combined with an **empty `LTPS` map**, the QoS manager has nothing to probe. Recommended for our first working response. --- ## 4. Practical recipe `build_preauth_response.py` implements this and self-verifies (correct header layout, all 14 fields present in tag order, decode round-trip). It reuses `heat2.py`'s validated TDF value encoders and **overrides** its Fire2 framing. ``` reply = fire2(component=0x0009, command=0x0007, msg_num=, msg_type=REPLY(1), payload=preauth_response(...)) ``` Produced frame for `msgNum=2`: 344 bytes, header `00 00 01 48 | 00 00 | 00 09 | 00 07 | 00 00 02 | 20 | 00 | 00`. Note byte 13 = `0x20` = `REPLY << 5`. Body sent (all fields present; strings the client only stores are left empty or placeholder): - `CIDS` = `[1, 4, 5, 7, 9, 15, 25, 28, 30722]` - `CONF.CONF` = `pingPeriod=20000000`, `defaultRequestTimeout=30000000`, `connIdleTimeout=90000000`, `enableQosFirewallTest=false`, `enableQosBandwidthTest=false` - `INST` = `fifa-2017-pc` (echo of the request's `CDAT/SVCN`) - `QOSS` = `{ BWPS:{PSA:"127.0.0.1", PSP:17502}, LNP:10, LTPS:{}, TIME:5000000 }` - `MAID=0`, `MINR=0`, remaining strings empty / placeholder Right after this the client will start sending `Util::ping` (`0x0009/0x0002`) on `pingPeriod`; reply with `msgType=REPLY` and a `PingResponse { STIM: serverTime }`. `ping_reply_frame()` in the same module does that. ### Residual uncertainty - **`NASP` / `PLAT` / `SVER` values are placeholders, not reverse-engineered.** `onPreAuthResponse` doesn't validate them, but `Authentication` (component 1) almost certainly will care about `NASP`. Expect to revisit. - **Group-inside-list/map framing is unverified.** Our capture never exercises it, and the third-party crates disagree with some Blaze versions about a `0x02` group-start marker. This is why the recommended `LTPS` is empty — it sidesteps the question. If we later need populated `LTPS`, verify that encoding first. - The `2069/2070/2077` FIFA-custom component ids were not individually attributed to component names. - Varint sign convention (bit `0x40` of the first byte) is unverified; nothing in `PreAuthResponse` is signed, so it doesn't bite here. ## Tooling produced | File | Purpose | |---|---| | `reflect2.py` | recursive TDF type-descriptor walker (`raw` / `walk` / `byname` / `index`) | | `build_preauth_response.py` | corrected Fire2 framer + PreAuthResponse/PingResponse builders, self-testing | ### Reflection metadata layout (for reuse) Type descriptor, 64 bytes: ``` +0x00 u32 typeEnum +0x04 u32 nameHash +0x08 ptr fullName +0x10 ptr shortName (interior) +0x18 ptr auxTypeInfo +0x20 ptr runtimeInstance +0x28 ptr shortName2 +0x30 ptr memberTable +0x38 u64 memberCount ``` Member entry, 48 bytes: `+0x00` member type descriptor · `+0x08` member name string · `+0x20` **u32 wire tag, 6-bit-packed in the top 3 bytes** · `+0x28` u64 byte offset within the C++ struct. `typeEnum`: `2`=map `3`=list `4`=float `5`=enum `6`=string `7`=variable `9`=blob `10`=union `11`=**struct/class** `12`=ObjectType `13`=ObjectId `14`=TimeValue `15`=bool `16`=int8 `17`=uint8 `19`=uint16 `20`=int32 `21`=uint32 `22`=int64 `23`=uint64. Containers keep their element types at `+0x28` (list elem / map key) and `+0x30` (map value). Blaze reflection strings live around VA `0x143888000`–`0x1438a1000`; descriptors around `0x144860000`–`0x144890000`.