# FIFA 17 Blaze Recon (Rosetta Stone for FIFA 23)

Clean-room reverse engineering: all findings derive from observing our own running
FIFA 17 client + static disassembly of the shipped binary we own. **No leaked EA
source is used or referenced.**

> ## ✅ WORKING: FIFA 17 Ultimate Team, 100% offline
> The full online + FUT stack is emulated. **Quick start → [`FUT-RUNBOOK.md`](FUT-RUNBOOK.md):**
> ```bash
> cd tools && ./openfut-fut.sh start     # arm host + start all servers (re-run after reboot)
> ~/Desktop/launch-fifa17.sh             # then launch FIFA FRESH and select Ultimate Team
> ```
> Proven end-to-end 2026-08-01: auth → Blaze login → device-trust → the FUT hub. The rest of this
> file is the reverse-engineering history that got there (see also `login_dump/*.md`, `docs/*.md`).

## Breakthrough — 2026-07-30: ProtoSSL cert pin DEFEATED, redirector handshake captured

FIFA 17 dials the **secure** Blaze redirector `winter15.gosredirector.ea.com` over
TLS 1.2 (RSA-kx). We MITM it with a self-signed cert and defeated DirtySDK/ProtoSSL's
cert pinning with two live `/proc/PID/mem` patches, then captured the **plaintext**
first-hop handshake.

### Key architectural finding
The secure redirector is **HTTPS + XML (ProtoHttp)**, NOT raw Fire2/Heat2:
```
POST /redirector/getServerInstance HTTP/1.1
Host: winter15.gosredirector.ea.com:42230
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
Content-Type: application/xml
<serverinstancerequest>...</serverinstancerequest>
```
Fire2/Heat2 binary is the **second hop** — the redirector replies with a
`<serverinstance>` XML naming a Blaze server IP:port; the client then connects THERE
for the binary protocol. Full request body in `captures/getServerInstance_request.http`.

## Reproduce (after reboot — all live state is volatile)

Binary maps flat at base `0x140000000` under Wine/Proton (UMU-Proton-10.0-4,
prefix `~/Games/umu/fifa17`). VAs below are stable across launches.

### 1. Root arm (scratchpad/root_arm.sh via pkexec)
- `sysctl kernel.yama.ptrace_scope=0`   (enables /proc/mem WRITES)
- `sysctl net.ipv4.conf.lo.route_localnet=1`
- `iptables -t nat -A OUTPUT -p tcp -d 159.153.51.20 -j DNAT --to-destination 127.0.0.1:42127`
  (winter15 resolves to 159.153.51.20; a /etc/hosts entry for winter15 would
   short-circuit the DNAT and must be ABSENT)

### 2. TLS capture server
`scratchpad/blaze_tls_capture.py` on 127.0.0.1:42127, presents `redir_cert.pem`
(self-signed, CN+SAN=winter15.gosredirector.ea.com), ciphers `ALL:@SECLEVEL=0`.

### 3. The two cert-verify patches (via scratchpad/memtool.py)
The cert handler lives at ~`0x14613252x`. Two gates:

| VA | Role | Patch |
|---|---|---|
| `0x146132548` | Gate 1: `jne 0x1461326c4` (UNKNOWN_CA branch after chain-verify `call 0x146136410`) | 6 bytes → `90 90 90 90 90 90` (NOP) |
| `0x1461361b0` | **Gate 2: the real pin** — cert verify helper; returned `-51 (0xffffffcd)` live | 3 bytes → `31 c0 c3` (`xor eax,eax; ret`) |

Gate 2 (`0x1461361b0`) is the decisive one — a shared verify helper (also called from
`0x146131f86`). Forcing it to return 0 makes `r12d=0`, the `je 0x14613262d` at
`0x14613256c` is taken, and the accept path at `0x146132675` is reached (skips the
UNKNOWN_CA alert send at `0x146135250`).

NB: last session's patch of `0x146136410` (chain-verify callee) did NOT work — it was
not the function returning the live failure. gdb breakpoint on `0x1461361b0` proved
Gate 2 was the wall (`eax=0xffffffcd`).

## Breakthrough #2 — 2026-07-30: BOTH HOPS DEFEATED, Fire2/Heat2 decoded

Built `tools/blaze_responder.py`: answers `getServerInstance` over TLS with a
`<serverinstanceinfo>` that redirects the client to a local plain Blaze port, and
captures the second-hop Fire2 binary. The client **accepted the redirect and connected**,
sending its `Util::preAuth` handshake in binary Heat2. `tools/decode_fire2.py` decodes it.

### getServerInstance response schema (the redirect)
`ServerInstanceInfo.address` is a `ServerAddress` **union**; Heat2 XML encodes a union as
`<field member="N"><valu>...</valu></field>`. Working response (member=0 = ipAddress variant):
```xml
<serverinstanceinfo>
  <address member="0"><valu>
    <hostname>127.0.0.1</hostname><ip>2130706433</ip><port>42130</port>
  </valu></address>
  <secure>0</secure>
  <trialservicename></trialservicename>
  <defaultdnsaddress>0</defaultdnsaddress>
</serverinstanceinfo>
```
`<ip>` is a **decimal uint32** host-order (2130706433 = 127.0.0.1). `<secure>` 0/1 picks
plaintext vs TLS for the Blaze connection. (Schema cross-confirmed clean-room vs MEC
Catalyst private-server projects; response types reversed from the client's own TDF
reflection tables at ~0x143891xxx / 0x144873xxx.)

### Fire2 frame header (16 bytes, big-endian)
```
[0:4] u32 payloadLength   [6:8] u16 component   [8:10] u16 command
[10:12] u16 error/msgId    [12] u8 msgType       [13:16] reserved
```
First RPC observed: **component 0x0009 = Util, command 0x0007 = preAuth, msgType 0x02**.
Ping/pong keep-alives: Util command 0x0002, empty payload, msgType 0x01/0x03.

### Heat2 TDF encoding (decoded in decode_fire2.py)
Per field: 3-byte tag (4 chars, 6-bit packed, char = v?v+0x20:' ') + 1 type byte + value.
Types: 0x00 int(varint, first byte 6 data bits + continue@0x80), 0x01 string(varint len incl
null + bytes), 0x02 blob, 0x03 struct(nested, 0x00 terminator), 0x04 list, 0x05 map, 0x06 union.

### preAuth codebook (Util::preAuth PreAuthRequest) — captures/blaze/preauth_decoded.txt
```
CDAT{ IITO:int LANG:int SVCN:str='fifa-2017-pc' TYPE:int }
CINF{ BSDK='15.1.1.3.0' BTIM='Jun  9 2017 16:15:40' CLNT='FIFA17' CPFT:int=4
      CSKU='FIFAPC' CVER='3175939' DSDK='15.1.2.1.0' ENV='prod' LOC:int PTVR='1.1' }
FCCR{ CFID='BlazeSDK' }
LADD:int
```
Same fields as the XML getServerInstance request → XML and Fire2 are the two encodings of
the same TDFs (the Rosetta mapping).

(Fire2 header was later CORRECTED: byte[12] is the low octet of a 24-bit msgNum, not msgType;
msgType lives in byte[13] high bits = (msgType<<5)|userIndex. REPLY=1→0x20, NOTIFICATION=2→0x40.
metadataLen is u16 at [4:6]. See tools/heat2.py / blaze_responder_v3b.py.)

## Breakthrough #3 — Origin/LSX layer defeated (PreAuthResponse + login flow work)
`tools/blaze_responder_v3b.py` answers preAuth, ping, fetchClientConfig, login (1/0x0A),
getAccount(1/0x1E)=AccountInfo, getPersona/listPersonas, and pushes UserAuthenticated (0x7802/8).
But Blaze isn't the online gate — **Origin is**, via its own in-process LSX layer:

- The Steampunks `stp-origin_emu.dll` serves **LSX** (length-prefixed, NUL-terminated XML) IN-PROCESS
  on 127.0.0.1:4216. It's a blind fixed-script replayer that reports OFFLINE. **Replace it**:
  bind 4216 BEFORE launching FIFA (`tools/lsx_responder_v2.py`; the stub has no SO_REUSEADDR and
  stands down cleanly), serve real request-driven LSX.
- **LSX crypto (reversed + verified byte-exact):** server sends `<Challenge key="<32hex>">`; client
  replies `<ChallengeResponse response="<96hex>" key="<32hex>">`; **H = hex(AES128-ECB(K=000102..0f,
  PKCS7pad16(clientKey_ascii)))** (32 ASCII → 48 bytes/3 blocks); server sends `<ChallengeAccepted
  response="H">`; session key = srand(7) LCG of H; later msgs = hex(AES-ECB(pkcs7(xml)))+NUL.
- **LSX verbs to answer:** GetProfile(PersonaId=33068179 Persona=CAGE US), GetSetting UPPERCASE
  (ENVIRONMENT→"production", LANGUAGE→"en_US", else "false"), GetGameInfo (LANGUAGES→locales,
  **UPTODATE→"true"** [else "title version outdated"], FREETRIAL→"false"),
  **GetInternetConnectedState→connected="1"** [the online gate], etc.
- Gates cleared this way: "log in to Origin" ✓ and "title version outdated" ✓.

## Breakthrough #4 — 2026-07-30: repack fully reversed (LSX contract is a byte-exact oracle)
The Steampunks repack ships two UPX-packed helpers; we unpacked and clean-room reversed BOTH
(multi-agent workflow, adversarially verified — full report `docs/REPACK_INTEL.md`, emu disasm
`docs/emu.asm`). Unpack recipe: `upx -d stp-origin_emu.dll` and `upx -d _fifa17.exe` (emu base
0x180000000, loader base 0x140000000; both are NORMAL PEs — objdump works, unlike the encrypted
FIFA17.exe). Findings that matter:
- **`stp-origin_emu.dll` = the reference LSX server, offline BY CONSTRUCTION.** It is a blind
  18-step straight-line script with NO parser and NO dispatch branch; its ONLY unsolicited frame is
  the plaintext Challenge; it hardcodes `connected="0"` and has NO `<Login>` event / no auth vocab
  anywhere in its 19,456 bytes. **Structural proof (not absence-of-evidence): nothing in the repack
  can flip `m_isLoggedIn`.** The login mechanism lives ONLY in FIFA17.exe's live-decrypted code.
- **Our `lsx_responder_v2.py` is CONFIRMED byte-exact** on framing (NUL-terminated, NUL counted in
  send len), crypto (AES-128 K_FIXED=000102..0f, PKCS7, srand(7)→61 session-key LCG), event shape,
  sender values (EALS / EbisuSDK / ""), and encryption timing (plaintext through ChallengeAccepted
  id=1, encrypted from id=2). Applied hardening C1–C3 (emu-exact `challenge_response` + tail assert,
  extract `response="`, partial-frame buffering). Selftest still green (session key unchanged).
- The loader is an offline keygen/launcher (no WS2_32, no injection, no Blaze/Nucleus strings); its
  `.dlf` GameToken is a local ENTITLEMENT grant, not a session — will not help login. Shared build
  constants: UserId/PersonaId **33068179**, MachineHash == LSX Challenge key **2b8ee7fa…e32** (fixed).

## CURRENT WALL — "Unable to retrieve account information" (m_isLoggedIn stays 0)
FIFA has two Origin flags — "internet reachable" (fed by GetInternetConnectedState, DONE) and
**"user LOGGED IN" = OriginMgr.m_isLoggedIn @[OriginMgr+0x13]**, whose only setter is dispatcher
case-2 @0x146f1e0ab, driven by a server-PUSHED `<Event sender="LOGIN_EVENT"><Login IsLoggedIn="true"/>`.
**Pushing it 90× did NOT flip the flag.** Breakthrough #4 RULED OUT three causes: framing, event
shape, and encryption timing are all confirmed correct. **Surviving hypotheses, narrowed:**
(1) **encrypted mid-session Events are dropped** — the emu's only Event is plaintext+pre-key, so
there is zero evidence FIFA routes an *encrypted* Event to the same parser (STRONGEST); (2) `sender`
name mismatch; (3) handler-registration timing. Deeper residual: LoginStatePCLogin @0x1471b58e0 may
gate on a session OBJECT [0x144b86bf8]->vtbl+0x60, not the flag.

### THE decisive next experiment (observe, don't guess) — new tooling ready
1. Relaunch harness+game (below), run responder with an UNBOUNDED heartbeat so pushes stay in flight:
   `OPENFUT_LSX_EVENT_COUNT=100000 python3 -u tools/lsx_responder_v2.py`
2. `bash tools/trace_login.sh` — attaches gdb, traces the sender matcher (0x147102880), the <Login>
   parser (0x147138660), and dispatcher case-2 (0x146f1e09e / set-1 0x146f1e0ab / set-0 0x146f1e0b8).
   Answers the 3-question ladder in ONE run: does the frame reach the matcher? what sender does it
   strcmp against (dumps the table entry)? does case-2 run and the flag flip?
3. If the trace shows the ENCRYPTED frame never reaches the matcher → run the A/B:
   `OPENFUT_LSX_LOGIN_PLAINTEXT=1 …` pushes the Login Event in plaintext right after ChallengeAccepted.
4. `tools/dump_login_code.py` — dumps + disassembles the decrypted login machinery at true VAs for a
   follow-up static pass if the trace points below the dispatcher.

## How to resume (rebuild the volatile harness)
1. `pkexec sh tools/../scratchpad/root_arm.sh` (ptrace_scope=0, route_localnet, DNAT 159.153.51.20→42127).
2. `python3 -u tools/lsx_responder_v2.py` — bind :4216 BEFORE launching FIFA.
3. `python3 -u tools/blaze_responder_v3b.py` — :42127 (redir TLS) / :42130 (blaze) / :42131 (nucleus).
4. `python3 tools/autopatch.py` — re-applies the two ProtoSSL cert patches to any relaunched FIFA17.exe.
5. Launch FIFA via `~/Desktop/launch-fifa17.sh`; go Online.
6. Watch /tmp/lsx.log (LSX) + /tmp/blaze_responder.log (Blaze); use tools/origin_login_probe.py to read
   m_isLoggedIn. Everything is volatile across reboot; VAs are stable (base 0x140000000).

## Live-state note
Volatile across reboot: cert patches, responders, DNAT, ptrace_scope. `tools/autopatch.py`
re-applies both cert patches automatically to any relaunched FIFA17.exe (VAs are stable).
Everything ported here (framing, Heat2, LSX crypto, tags) applies to FIFA 23 (identical wire format).
