# FIFA 17 Blaze `Util::preAuth` — reflection-reversed schema

**Date:** 2026-07-30 · **Method:** live `/proc/<pid>/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\<uint16\> | `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<string,string>

Blaze::QosConfigInfo                        (QOSS, 4 members)
  BWPS  bandwidthPingSiteInfo  Blaze::QosPingSiteInfo (struct)
  LNP   numLatencyProbes       uint16
  LTPS  pingSiteInfoByAliasMap map<string, Blaze::QosPingSiteInfo>
  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<A,B>` *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<int32_t,string>` but is really `map<string alias, int32 latency>`; `permissionsByComponent` is named `map<list<string>,string>` but is really `map<string component, list<string> permissions>`. So `LTPS` = `map<string, QosPingSiteInfo>`.

### 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=<echo from the request>, 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`.
