edab23f04a
Emulates FIFA 17's full online + Ultimate Team stack against an offline,
clean-room backend (no EA servers). Proven end-to-end 2026-08-01:
Origin login -> Blaze login -> device-trust -> the FUT hub.
Package:
- tools/openfut-fut.sh one-command orchestrator (start/stop/status/restart)
- tools/root_arm.sh idempotent host arm (sysctls, DNAT, /etc/hosts easw)
- tools/{lsx_responder_v2,blaze_responder_v3b,roster_server,utas_server,autopatch}.py
the 5 servers (Origin LSX :4216, Blaze :42127/42130/42131, roster :8081,
FUT/UTAS :8099) + heat2.py (Fire2/Heat2 TDF codec)
- FUT-RUNBOOK.md runbook + gate-ladder troubleshooting
- docs/, tools/login_dump/*.md the reverse-engineering write-ups
All findings are clean-room, from binaries we own; nothing from any leak.
The wire protocol maps 1:1 to FIFA 23.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
400 lines
42 KiB
Markdown
400 lines
42 KiB
Markdown
# FIFA 17 Steampunks repack — consolidated RE intel
|
||
|
||
**Provenance:** every claim below derives ONLY from `stp_emu_unpacked.dll` (PE base
|
||
`0x180000000`) and `f17_loader_unpacked.exe` (PE base `0x140000000`) in this directory, plus
|
||
arithmetic re-derivation of constants. No leaked EA source was used or consulted. Where a
|
||
statement comes from our earlier *live* FIFA17.exe recon rather than these binaries, it is
|
||
tagged **[LIVE]** and must not be treated as repack-confirmed.
|
||
|
||
**Synthesis note:** this document adjudicates five independent sub-reports. Two of them
|
||
disagreed on the ChallengeAccepted construction and several loader VAs were wrong. All
|
||
conflicts were re-verified against the binaries by the synthesis pass; the adjudications are
|
||
recorded in-line in §0 so the wrong versions do not get re-adopted later.
|
||
|
||
Section-header ground truth (from `objdump -h`, used for every VA↔file-offset mapping here):
|
||
|
||
| Binary | Section | VMA | File off | Mapping |
|
||
|---|---|---|---|---|
|
||
| emu | .text | `0x180001000` | `0x400` | VA = 0x180001000 + (off − 0x400) |
|
||
| emu | .rdata | `0x180004000` | `0x2e00` | VA = 0x180004000 + (off − 0x2e00) |
|
||
| emu | .data | `0x180005000` | `0x3e00` | VA = 0x180005000 + (off − 0x3e00) |
|
||
| loader | .text | `0x140001000` | `0x400` | VA = 0x140001000 + (off − 0x400) |
|
||
| loader | .rdata | `0x140010000` | `0xf000` | VA = 0x140010000 + (off − 0xf000) |
|
||
| loader | .data | `0x140018000` | `0x16200` | VA = 0x140018000 + (off − 0x16200) |
|
||
| loader | .stp0 | `0x14001d000` | `0x18600` | still packed — disassembles as garbage |
|
||
|
||
---
|
||
|
||
## 0. Conflict adjudications (read this before trusting any single sub-report)
|
||
|
||
| # | Dispute | Verdict | Proof |
|
||
|---|---|---|---|
|
||
| A | ChallengeAccepted = 64 hex (2 blocks, no pad) **or** 96 hex (3 blocks, PKCS7)? | **96 hex. Our v2 is correct — do NOT truncate to 64.** One sub-report read only the two `movaps`/encrypt pairs and missed the trailing `strcat_s`. | `0x1800020a9 lea r8,[rsp+0x90]` / `mov edx,0x200` / `mov rcx,rbp` / `call [rip+0x1fa9] # 0x180004068` (strcat_s). `rsp+0x90` = response-buffer(`rsp+0x50`) + `0x40` = `clientResponse[64:]`. Emu emits 64 computed + 32 echoed = 96. |
|
||
| B | Is our 3-block PKCS7 formula equivalent to the emu's 2-block+echo? | **Yes, numerically identical**, because the echoed tail is the client's own 3rd block and the client PKCS7-pads. Verified today. | `AES(K_FIXED, b'\x10'*16) = 954f64f2e4e86e9eee82d20216684899`; for client key `18a70055a3541fb27ab8e0f47afad18c`, 3-block hex `[:64]` == 2-block hex and `[64:]` == that constant. |
|
||
| C | Session-key seed = `bx + rand()` or `bx + (rand()==61)`? | **`bx + rand()`, integer 61.** The BRIEF's `r0=(rand()==61)` was a mis-transcription of a comment. `lsx_responder_v2.py:169` is already correct. | `0x1800020de call rand` → eax; `0x1800020e4 movzx ecx,bx`; `0x1800020e7 add ecx,eax`; `0x1800020e9 call srand`. MSVCR LCG: 7·214013+2531011 = 0x3D7BAE, `>>16 & 0x7fff` = 61. |
|
||
| D | Loader `jsHym…` base64 VA | **`0x140015b10`** (file `0x14b10`), not `0x140014b10`. One sub-report was systematically 0x1000 low. | `.rdata` maps file `0xf000`→VA `0x140010000`. |
|
||
| E | Loader License XML template VA | **`.data 0x140019620`** (file `0x17820`), not `0x140017820` (that VA is in the gap between `.rdata` end `0x140017114` and `.data` start `0x140018000` — it does not exist). | Template dumped verbatim below. |
|
||
| F | "xor'd fragment containing blaZe" in the loader | **False lead — no cipher.** It is raw 24-bit RGB pixel data inside the keygen's 602×408 BMP resource. | Resource type 2 BITMAP at RVA `0x10a388`; `BITMAPINFOHEADER` biWidth=602 biHeight=408 biBitCount=24 biSizeImage=737666. Offset `0x110fff` falls ~row 26 of pixel data; the bytes already read `blaZe` untransformed. |
|
||
| G | Loader S-box / Rcon VAs | **`0x1400155b0` / `0x1400154b0` — correct as reported.** | Byte search confirms file `0x145b0` / `0x144b0`. |
|
||
|
||
---
|
||
|
||
## 1. LSX CONTRACT (definitive)
|
||
|
||
### 1.1 Transport & framing — CONFIRMED
|
||
|
||
* Server: IPv4/TCP `127.0.0.1:4216`. `WSAStartup(0x202)` → `getaddrinfo("127.0.0.1","4216", {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP})` → socket → bind → `listen(s, 0x7fffffff)` → `accept(s, NULL, NULL)`.
|
||
Strings: `"4216"` @ file `0x33f4`, `"127.0.0.1"` @ file `0x3400`. WS2_32 ordinals from the IAT: `0x180004198`=1 accept, `0x180004168`=2 bind, `0x180004180`=3 closesocket, `0x180004190`=13 listen, `0x180004160`=16 recv, `0x180004188`=19 send, `0x1800041a0`=22 shutdown, `0x180004170`=23 socket.
|
||
* **Exactly one connection, ever.** There is exactly **one** `accept` call site in the whole image (`grep -c '# 0x180004198' emu.asm` → 1), not in a loop; the listener is closed immediately after (`0x1800022bb closesocket`). Terminal path `shutdown(sock,1)` @ `0x180002e89` → `WSACleanup` @ `0x180002e9d` → thread returns.
|
||
* **Framing: NUL-terminated, and the NUL is INCLUDED in the send length.** Every send is `send(s, buf, strlen(buf)+1, 0)`. There is **no** length prefix, no newline, no XML-close sentinel scan.
|
||
Idiom repeated before each send: `or rax,-1` / `cmp BYTE PTR [rdx+rax*1+0x1],0` / `lea rax,[rax+1]` / `jne` (inlined strlen) then `lea r8d,[rax+0x1]` / `xor r9d,r9d` / `call [0x180004188]`. First instance `0x1800022c8`; identical at `0x18000235f`, `0x1800023ed`, `0x18000249d` … `0x180002e1b`.
|
||
* **Receive: one blocking `recv(sock, buf, 0x1000, 0)` per message**, into a 0x1000 stack buffer at `rbp+0x320`. No accumulation, no partial-frame reassembly. The shipped emu simply *assumes* one whole message per recv.
|
||
Thread buffer map: `rbp+0x120` = 16-byte session key, `rbp+0x320` = 0x1000 recv/format/decrypt buffer, `rbp+0x1320` = 0x200 ChallengeAccepted hex buffer.
|
||
* Message-size envelope: the emu's plaintext buffers are 0x1000. Replies should stay **under 4095 bytes** if we want to remain inside the envelope the shipped emu proved safe (`QueryEntitlements` is the one at risk of growing).
|
||
|
||
### 1.2 Crypto — CONFIRMED, with one hardening note
|
||
|
||
Primitive is genuine, hand-inlined **AES-128** (no CryptoAPI/BCrypt/OpenSSL; imports are only MSVCR120 / KERNEL32 / WS2_32):
|
||
|
||
| Item | VA | Bytes / detail |
|
||
|---|---|---|
|
||
| S-box | `0x180004330` | `63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76` |
|
||
| Inv S-box | `0x180004430` | `52 09 6a d5 30 36 a5 38 …` |
|
||
| Rcon | `0x180004230` | `8d 01 02 04 08 10 20 40 80 1b 36 6c` |
|
||
| Key expansion | `0x180001000` | loop `cmp r9d,0x2c` = 44 words = AES-128, 10 rounds |
|
||
| Encrypt block | `0x180001710` | MixColumns `0x1800011d0` (xtime `mov al,0x1b`) |
|
||
| Decrypt block | `0x180001930` | InvMixColumns `0x1800012c0` |
|
||
| `K_FIXED` | `0x180005038` (file `0x3e38`) | **`000102030405060708090a0b0c0d0e0f`** — verified byte-exact |
|
||
| Key-schedule input ptrs | `0x180005fd0` (block), `0x180005fd8` (key) | set before each `call 0x180001000` |
|
||
|
||
**Handshake sequence:**
|
||
|
||
1. Server pushes the **plaintext** Challenge, verbatim from `.data 0x180005590` — it is a *fixed constant*, never computed or randomized, and is sent with no `sprintf` and no call to the encryptor:
|
||
```
|
||
<LSX><Event sender="EALS"><Challenge key="2b8ee7faea76e8a34f5f5d20e5328e32" build="release" version="10,4,13,6637"/></Event></LSX>
|
||
```
|
||
2. Client replies **plaintext** `<ChallengeResponse response="<96hex>" key="<32hex>"/>`.
|
||
3. Parse function `0x180001f10` does the *only* string parsing in the entire DLL — two `strstr` calls, both here (`0x180001f5a` for `response="` @`0x1800045e0`, +0xA; `0x180001fa6` for `key="` @`0x1800045ec`, +5), each terminated by the next `"` (bare quote literal @`0x1800045d8`), `memcpy`'d into two 0x200 stack buffers (`rsp+0x50` = response value, `rsp+0x250` = client key) and NUL-terminated. **Only the first occurrence of each is used, and the client's `response=` value is NEVER VERIFIED.**
|
||
4. `H` = `hex(AES128ECB(K_FIXED, clientKey[0:16]))` ‖ `hex(AES128ECB(K_FIXED, clientKey[16:32]))` ‖ `clientResponse[64:]`
|
||
— two `movaps`/expand/encrypt pairs at `0x180001fee` and `0x180002024`; hex loop `0x180002070`–`0x1800020a7` bounded by `cmp rbx,0x20` (32 bytes → 64 chars) via `sprintf_s(tmp,3,"%02x")` (**lowercase**, fmt @`0x1800045d0`) + `strcat_s`; then the echo `strcat_s` at `0x1800020a9`.
|
||
**Our PKCS7-3-block formula produces the identical 96 hex** (§0-B). Keep it; see the diff list for the hardening.
|
||
5. Server sends **plaintext** `ChallengeAccepted` (id=1). **Encryption begins only with the id=2 response**; the first inbound decrypt is the frame received *after* ChallengeAccepted. Proof: after `sprintf_s` at `0x180002343` the code goes straight to inlined strlen + send at `0x180002370` with no call to the encryptor `0x180001dc0`; first encrypt call is `0x1800023d6`, first decrypt `0x1800023a4`.
|
||
6. Session key (`0x1800020bf`–`0x180002101`), derived from the **ASCII characters** of `H`, not its binary bytes:
|
||
```
|
||
srand(7); r = rand(); // r == 61 under the MSVCR120 LCG
|
||
bx = (uint16)((H[0] << 8) + H[1]); // imul bx,ax is a 16-bit multiply
|
||
srand(bx + r); // movzx ecx,bx ; add ecx,eax
|
||
key[i] = (uint8)rand() for i = 0..15; // loop bounded by cmp rdi,0x10
|
||
```
|
||
|
||
**Post-handshake messages (both directions):** PKCS#7 pad → AES-128-ECB under the session key → **lowercase** hex → NUL-terminated.
|
||
* Encrypt `0x180001dc0`: `mov ecx,ebx; and ecx,0xf; mov eax,0x10; sub eax,ecx` then `rep stos BYTE PTR [rdi],al` — pad value == pad count, and **a full 16-byte block is appended when the length is already aligned** (true PKCS#7).
|
||
* Decrypt `0x180001ce0`: hex2bin `0x180001c50` via `sscanf_s("%02x")` (so **uppercase hex is accepted on input**), per-block decrypt, then a *validating* PKCS#7 strip at `0x180001d6e`–`0x180001d94` (`cmp al,0x10; ja skip` / `test al,al; je skip` / run-length check) that zeroes the pad bytes.
|
||
|
||
### 1.3 There is NO parser and NO dispatch table
|
||
|
||
This is the single most important structural fact about the shipped emu, and it changes how the response table below should be read.
|
||
|
||
The emu is a **blind, fully-unrolled, straight-line 18-step scripted conversation**. After the ChallengeResponse it never inspects another request: it `recv`s, `decrypt`s into `rbp+0x320`, and then immediately **overwrites that same buffer** with the next `sprintf_s` — the decrypted plaintext is never read. The region `0x180002325`–`0x180002d6f` is 18 literal `lea r8,[template]; mov r9d,<ordinal>; call sprintf_s` blocks with **zero conditional branches on message content**; the only `cmp`/`jne` present are the inlined strlen loops and the `send()==-1` check at `0x180002e2e`. From message 19 the tail loop `0x180002db0`–`0x180002e7b` replies `ErrorSuccess` forever and **does not even call the decrypt routine**.
|
||
|
||
Exact send/recv accounting (whole image): **20 send sites, 19 recv sites, 1 accept site** — i.e. 1 unsolicited Challenge + 18 scripted + 1 loop send, versus 18 scripted + 1 loop recv. **Exactly one frame in the entire binary is unsolicited, and it is the plaintext Challenge.** The emu never sends an encrypted Event.
|
||
|
||
### 1.4 Complete response table (byte-exact templates, verified verbatim)
|
||
|
||
`id` is a **hard-coded counter**, never echoed. Immediates: `0x180002333`=1, `0x1800023b7`=2, `0x180002462`=3, `0x1800024fa`=4, `0x18000258d`=5, `0x180002628`=6, `0x1800026bb`=7, `0x180002758`=8, `0x1800027eb`=9, `0x180002888`=0xa, `0x18000291b`=0xb, `0x1800029bf`=0xc, `0x180002a58`=0xd, `0x180002aeb`=0xe, `0x180002b88`=0xf, `0x180002c1b`=0x10, `0x180002cb8`=0x11, `0x180002d4b`=0x12; then `0x180002d6a mov esi,0x13` with `inc esi` at `0x180002dfd`.
|
||
|
||
The 12 templates are the **complete** XML surface of the 19,456-byte DLL (`.data` string region exhaustively enumerated):
|
||
|
||
| VA | Template (verbatim) |
|
||
|---|---|
|
||
| `0x180005020` | `.\stp-origin_emu.ini` |
|
||
| `0x180005050` | `<LSX><Response id="%d" sender=""><GetSettingResponse Setting="%s"/></Response></LSX>` |
|
||
| `0x1800050b0` | `<LSX><Response id="%d" sender=""><GetGameInfoResponse GameInfo="ar_SA,cs_CZ,da_DK,de_DE,en_US,es_ES,es_MX,fr_FR,it_IT,nl_NL,no_NO,pl_PL,pt_BR,pt_PT,ru_RU,sv_SE,tr_TR,zh_TW"/></Response></LSX>` |
|
||
| `0x180005170` | `<LSX><Response id="%d" sender=""><GetSettingResponse Setting="false"/></Response></LSX>` |
|
||
| `0x1800051d0` | `<LSX><Response id="%d" sender=""><ErrorSuccess Code="0" Description=""/></Response></LSX>` |
|
||
| `0x180005230` | `<LSX><Response id="%d" sender=""><IsProgressiveInstallationAvailableResponse ItemId="" Available="false"/></Response></LSX>` |
|
||
| `0x1800052b0` | `<LSX><Response id="%d" sender="EbisuSDK"><GetProfileResponse IsSubscriber="true" PersonaId="%llu" AvatarId="" Country="US" CommerceCountry="US" GeoCountry="US" UserId="%llu" Persona="%s" IsUnderAge="false" CommerceCurrency="USD"/></Response></LSX>` |
|
||
| `0x1800053b0` | `<LSX><Response id="%d" sender=""><InternetConnectedState connected="0"/></Response></LSX>` |
|
||
| `0x180005410` | `<LSX><Response id="%d" sender="EALS"><ChallengeAccepted response="%s"/></Response></LSX>` |
|
||
| `0x180005470` | `<LSX><Response id="%d" sender="EbisuSDK"><GetConfigResponse Config="false"/></Response></LSX>` |
|
||
| `0x1800054d0` | `<LSX><Response id="%d" sender=""><GetGameInfoResponse GameInfo="false"/></Response></LSX>` |
|
||
| `0x180005530` | `<LSX><Response id="%d" sender=""><GetSettingResponse Setting="production"/></Response></LSX>` |
|
||
| `0x180005590` | `<LSX><Event sender="EALS"><Challenge key="2b8ee7faea76e8a34f5f5d20e5328e32" build="release" version="10,4,13,6637"/></Event></LSX>` |
|
||
|
||
**Ordinal → template script.** Because the emu never reads the request, this is simultaneously (a) the emu's whole behaviour and (b) **a recording of FIFA 17's first 18 boot-time LSX requests**. The verb column is therefore an *inference from the answer*, not a parsed fact — but it is a high-value regression oracle.
|
||
|
||
| # | Template VA | Response | Inferred request |
|
||
|---|---|---|---|
|
||
| — | `0x180005590` | Challenge (**plaintext, pushed**) | — |
|
||
| 1 | `0x180005410` | ChallengeAccepted (**plaintext**) | ChallengeResponse |
|
||
| 2 | `0x180005470` | `GetConfigResponse Config="false"` | GetConfig |
|
||
| 3 | `0x1800052b0` | GetProfileResponse | GetProfile |
|
||
| 4 | `0x180005170` | `Setting="false"` | GetSetting |
|
||
| 5 | `0x1800054d0` | `GameInfo="false"` | GetGameInfo |
|
||
| 6 | `0x1800050b0` | locale list | GetGameInfo(LANGUAGES) |
|
||
| 7 | `0x180005530` | `Setting="production"` | GetSetting(ENVIRONMENT) |
|
||
| 8 | `0x180005170` | `Setting="false"` | GetSetting |
|
||
| 9 | `0x180005230` | IsProgressiveInstallationAvailableResponse | IsProgressiveInstallationAvailable |
|
||
| 10 | `0x1800052b0` | GetProfileResponse | GetProfile |
|
||
| 11 | `0x1800050b0` | locale list | GetGameInfo(LANGUAGES) |
|
||
| 12 | `0x180005050` | `Setting="%s"` ← ini Language | GetSetting(LANGUAGE) |
|
||
| 13 | `0x1800054d0` | `GameInfo="false"` | GetGameInfo |
|
||
| 14 | `0x1800051d0` | `ErrorSuccess` | **UNKNOWN — a verb with no meaningful response (Set*/Notify?). Biggest gap in this table.** |
|
||
| 15 | `0x180005530` | `Setting="production"` | GetSetting(ENVIRONMENT) |
|
||
| 16 | `0x1800054d0` | `GameInfo="false"` | GetGameInfo |
|
||
| 17 | `0x1800053b0` | `connected="0"` | GetInternetConnectedState |
|
||
| 18 | `0x1800052b0` | GetProfileResponse | GetProfile |
|
||
| ≥19 | `0x1800051d0` | `ErrorSuccess` forever | anything |
|
||
|
||
**Useful positive corollary:** FIFA 17 tolerates `<ErrorSuccess Code="0" Description=""/>` as the answer to arbitrary unknown requests, indefinitely, without dropping the LSX connection. An ErrorSuccess catch-all fallback is provably safe.
|
||
|
||
### 1.5 id / sender echo rules — CONFIRMED GROUND TRUTH
|
||
|
||
* **`id` is never echoed.** The emu emits its own contiguous counter 1,2,3,… and the shipped repack works. This *proves* FIFA 17's request ids are a contiguous integer sequence starting at 1 (otherwise the emu's blind counter would desynchronize). Echoing the client's id — what v2 does — is therefore equivalent and strictly safer.
|
||
* **`sender` is never echoed.** It is hard-coded per template, and only three values exist in the entire image:
|
||
* `EALS` → Challenge, ChallengeAccepted
|
||
* `EbisuSDK` → GetConfigResponse, GetProfileResponse
|
||
* `""` (empty) → GetSettingResponse, GetGameInfoResponse, InternetConnectedState, IsProgressiveInstallationAvailableResponse, ErrorSuccess
|
||
* **Attribute casing** (lock this in verbatim): `connected` is **lowercase** inside `InternetConnectedState` while every sibling attribute is PascalCase; `response=` and `key=` on the challenge exchange are lowercase; `build`, `version`, `id`, `sender` lowercase; everything else (`Setting`, `GameInfo`, `Config`, `ItemId`, `Available`, `Code`, `Description`, `IsSubscriber`, `PersonaId`, `AvatarId`, `Country`, `CommerceCountry`, `GeoCountry`, `UserId`, `Persona`, `IsUnderAge`, `CommerceCurrency`) as written above.
|
||
|
||
### 1.6 Emu identity config
|
||
|
||
Init `0x180001b60` makes exactly three ini reads from `.\stp-origin_emu.ini` `[Globals]` — **this is the entire configuration surface of the DLL**:
|
||
|
||
| Key | Default | Destination |
|
||
|---|---|---|
|
||
| `Language` | `"en_US"` (@`0x180004570`) | buffer `0x180005bc0`, **also** `SetEnvironmentVariableA("EAGameLocale", …)` @`0x180001c04` — a WRITE, not a read |
|
||
| `PersonaId` | `0x1f89493` = **33068179** | u64 @`0x180005dc0` |
|
||
| `PersonaName` | `"STEAMPUNKS"` (@`0x1800045a0`) | buffer `0x180005dd0` — **the shipped ini overrides this to `CAGE`** |
|
||
|
||
`GetProfileResponse` uses `PersonaId` for **both** `PersonaId=` and `UserId=` (same `%llu` twice, slots `[rsp+0x20]`/`[rsp+0x28]` both loaded from `[0x180005dc0]` at `0x18000245d`/`0x18000246d`).
|
||
|
||
**Emu bug worth banking as behavioural evidence:** the **third** GetProfileResponse (ordinal 18) emits a **garbage PersonaId** — the runtime address of the language buffer. Ordinal 3 sets all three vararg slots; ordinal 12 clobbers `[rsp+0x20]` with `lea rax,[rip+0x320f] # 0x180005bc0` (`0x1800029aa`/`0x1800029ca`); ordinal 18 (`0x180002d3d`–`0x180002d56`) sets only `rcx`/`edx`/`r8`/`r9d` and never restores it. So `PersonaId="%llu"` prints a pointer while `UserId` and `Persona` stay correct. **The repack still boots to a playable game — therefore FIFA 17 does not latch account identity from `GetProfileResponse`.** That is a genuine negative constraint on where the account-info gate lives.
|
||
|
||
### 1.7 Corrections to `lsx_responder_v2.py` — concrete diff list
|
||
|
||
Ranked by risk. **Most of the responder is confirmed correct**; the confirmations are listed because they *eliminate hypotheses* for the current failure.
|
||
|
||
> **CONFIRMED — NO CHANGE (these rule out root causes):**
|
||
> * `:159 derive_session_key()` — exactly right. `imul bx,ax` is a 16-bit multiply so `& 0xFFFF` is correct; `movzx ecx,bx; add ecx,eax` is a 32-bit add of the zero-extended 16-bit `bx`. `r0 = next(msvcr_rand(7))` is correct — the BRIEF's `(rand()==61)` was a mis-transcription (§0-C). Only the **docstring** should change.
|
||
> * `:185 lsx_encrypt()` — exactly right: PKCS7 with a full 16-byte block when aligned, lowercase hex, single trailing NUL, NUL counted in the length.
|
||
> * `:194 lsx_decrypt()` — `bytes.fromhex` is already case-insensitive, matching the emu's `sscanf_s("%02x")`.
|
||
> * `:257/:261 Conn.send_plain/send_enc` — both append exactly one `b"\0"` and `sendall` the whole buffer. **Framing is correct**, which rules out "our pushed Event was mis-framed and the reader stalled".
|
||
> * `:294 resp()` default `sender=""` — correct. Do NOT echo the request's sender.
|
||
> * `:422 resp(1, ChallengeAccepted, "EALS")` — matches the emu's hard-coded id=1 exactly.
|
||
> * `:108-110 PERSONA_ID/USER_ID = 33068179` — matches both the emu ini default `0x1f89493` **and** the decrypted `.dlf` `<UserId>`. `PERSONA_NAME = "CAGE"` matches the shipped ini (the *code* default is `STEAMPUNKS`; `CAGE` is right for this install).
|
||
> * `:122-124 CHALLENGE_KEY/BUILD/VERSION` — byte-exact against `0x180005590`.
|
||
> * The pushed-Event *shape* `<LSX><Event sender="X"><Y/></Event></LSX>` with no `id` and no `<Response>` wrapper is confirmed consumable — the working Challenge uses exactly it.
|
||
|
||
**C1 — `:173 challenge_response()` — harden to the emu's exact algorithm (MEDIUM).**
|
||
Currently a 3-block PKCS7 encrypt. Numerically identical **only while the client PKCS7-pads its third block**. The emu never computes block 3 at all; it echoes the client's.
|
||
```diff
|
||
-def challenge_response(client_key_ascii: str) -> str:
|
||
- b = client_key_ascii.encode()
|
||
- pad = 16 - (len(b) % 16)
|
||
- b += bytes([pad]) * pad
|
||
- return AES.new(K_FIXED, AES.MODE_ECB).encrypt(b).hex()
|
||
+def challenge_response(client_key_ascii: str, client_response_attr: str = "") -> str:
|
||
+ """Emu-exact (0x180001f10): TWO blocks computed from the client key, then
|
||
+ the client's own response[64:] appended verbatim (strcat_s @0x1800020a9)."""
|
||
+ two = AES.new(K_FIXED, AES.MODE_ECB).encrypt(client_key_ascii.encode()).hex()
|
||
+ if len(client_response_attr) >= 64:
|
||
+ tail = client_response_attr[64:]
|
||
+ # integrity check: FIFA's 3rd block must be AES(K_FIXED, 0x10*16)
|
||
+ assert tail == "954f64f2e4e86e9eee82d20216684899", f"unexpected tail {tail}"
|
||
+ return two + tail
|
||
+ return two + AES.new(K_FIXED, AES.MODE_ECB).encrypt(b"\x10" * 16).hex()
|
||
```
|
||
The assert converts a silent future breakage (a client build that randomizes the tail) into a loud one. **Do not truncate the output to 64 hex** — one sub-report recommended that; it is wrong (§0-A).
|
||
|
||
**C2 — `:414 serve()` — also extract `response="` (LOW, enables C1).**
|
||
The emu parses `response="` **before** `key="`. We currently regex only `key=`.
|
||
```diff
|
||
- m = re.search(r'key="([^"]*)"', data.decode(errors="replace"))
|
||
- client_key = m.group(1) if m else CHALLENGE_KEY
|
||
- h = challenge_response(client_key)
|
||
+ txt = data.decode(errors="replace")
|
||
+ mk = re.search(r'key="([^"]*)"', txt)
|
||
+ mr = re.search(r'response="([^"]*)"', txt)
|
||
+ client_key = mk.group(1) if mk else CHALLENGE_KEY
|
||
+ client_resp = mr.group(1) if mr else ""
|
||
+ h = challenge_response(client_key, client_resp)
|
||
```
|
||
|
||
**C3 — `:429` — buffer partial frames across `recv` (MEDIUM, latent).**
|
||
`data.split(b"\0")` silently drops a trailing partial frame. The emu never had to handle this (it does one 4096-byte recv per message), but our 65536 recv can straddle. Symptom would be an unexplained protocol stall that looks like a logic bug.
|
||
```diff
|
||
- for chunk in filter(None, data.split(b"\0")):
|
||
+ buf += data
|
||
+ *frames, buf = buf.split(b"\0")
|
||
+ for chunk in filter(None, frames):
|
||
```
|
||
(initialize `buf = b""` before the loop).
|
||
|
||
**C4 — `:263 push_login_state` / `:277 heartbeat` — the free-running timer is a wire pattern the client has never seen (MEDIUM).**
|
||
The emu is **strictly lockstep**: 20 sends / 19 recvs, one send per recv, no exceptions. A heartbeat push landing between the client's request and our response is unprecedented from the shipped emu's perspective. Gate pushes to fire only immediately after a Response is written (the `PUSH_AFTER` path already does this) and **drop the free-running timer**, or at minimum serialize it behind the request loop rather than just behind the send lock.
|
||
|
||
**C5 — `:220 login_event_frames()` docstring — weaken the structural claim (DOC).**
|
||
The claim "unsolicited `<Event>` frames are consumable: the LSX handshake itself is one" over-reaches. The emu's only Event is the Challenge, sent **in plaintext, before the session key exists** (`0x1800022e5`, no call to the encryptor). There is **zero** evidence in this binary that an *encrypted mid-session* Event is routed to the same parser. "Encrypted Events are dropped or routed elsewhere" remains a live hypothesis — see §4.
|
||
|
||
**C6 — flag unverified responses in comments (DOC).**
|
||
`GetAuthCode` (`sender="EbisuSDK"`), `QueryEntitlements`, and `GetGameInfo UPTODATE="true"` have **no template in this binary**. The emu only ever emits `GameInfo="false"` or the locale list; there is no `GameInfo="true"` string in the image, and no AuthCode template exists. These are **[LIVE]**-derived only. Keep them (the offline emu never trips the online-path checks they satisfy), but mark them as such so they are not later mistaken for repack-confirmed.
|
||
|
||
**C7 — BRIEF.md line 46 is wrong (DOC).**
|
||
There is no single `GetSettingResponse Setting="%s|production|false"` template. There are **three separate** templates (`0x180005050` `"%s"`, `0x180005170` `"false"`, `0x180005530` `"production"`) selected purely by script position. Our SettingId-based dispatch is consistent with the recorded ordering — keep it.
|
||
|
||
**C8 — add a boot-order regression oracle (NEW, high value).**
|
||
Diff our live request log against the §1.4 script. If our answers cause FIFA to diverge from that sequence **before #17**, we changed its path *before the online decision was even asked for*. Also assert id contiguity: a gap is the signature of a frame being consumed as the wrong thing.
|
||
|
||
**C9 — `:467 main()` — log loudly on a second connection (LOW).**
|
||
The emu closes its listener after the first accept and can never serve a reconnect. Our multi-connection responder is strictly better, but a 2nd connection is behaviour the reference implementation never had to handle — and would signal a state reset we should be reacting to rather than papering over.
|
||
|
||
---
|
||
|
||
## 2. REPACK INTEL (`f17_loader_unpacked.exe`)
|
||
|
||
### 2.1 What the loader is
|
||
|
||
A GUI **keygen + launcher**. `WinMain` → `0x140003cc0` (`0x14000484f`) → `lea r9,[0x1400037c0]` (dlgproc), `edx=0x65` (template id) → `DialogBoxParamW` @`0x1400102c0`. Two dialog commands: **GENERATE** (mint the Origin License) and **PLAY** (launch the game).
|
||
|
||
**Strings of interest (all cleartext — there is no string-deobfuscation routine in this binary; see §0-F):**
|
||
|
||
| VA | String |
|
||
|---|---|
|
||
| `0x1400157f0` | `dbdata.dll` |
|
||
| `0x140015800` | `getTableData` |
|
||
| `0x140015a20` | `FIFA17.exe` (wide) |
|
||
| `0x140015b10` | `jsHymMvuB34nUXoH80eHcw==` (base64 → 16 bytes `8ec1f298cbee077e27517a07f3478773`) |
|
||
| ~`0x140015acf` | `Unable to find dbdata.dll! Please put keygen into the game folder.` |
|
||
| ~`0x140015938` | `License file sucessfully generated. Press Play!` (sic) |
|
||
| `0x1400154b0` / `0x1400155b0` | AES Rcon / S-box |
|
||
| `0x1400195c0` | **License AES key** `4132722dd082efb0dc6457c57668ca09` |
|
||
| `0x1400195d0` | fixed 46-byte DER-shaped signature header |
|
||
| `0x140019620` | License XML template (below) |
|
||
| `0x1400157b0` | base64 charset `ABC…XYZabc…0123456789-_` (URL-safe) |
|
||
|
||
Negative scan: imports are **KERNEL32 / ADVAPI32 / GDI32 / SHELL32 / USER32 only — no WS2_32**, so no sockets. Whole-file keyword counts: `nucleus`=0, `gosredirector`=0, `entitlement`=0, `blaze`=1 (the BMP pixels), `ea.com`=1 (the License XML namespace), `auth`=1 (the `AuthenticAMD` CPUID vendor check ~`0x14000b393`, *not* authentication).
|
||
|
||
### 2.2 How licensing / GameToken works
|
||
|
||
**Flow.** GENERATE → `LoadLibraryA("dbdata.dll")` (`0x140003996` / `0x1400039b9`) → on success `call 0x140003120`, a virtualized wrapper that resolves and calls `dbdata.dll!getTableData` — **the actual keygen** — which returns the identity fields. The loader `sprintf`s the License XML from the `.data` template, AES-encrypts it (`call 0x140003f20`, virtualized, lives in `.stp0`), and writes it via `0x1400031f0`: `SHGetSpecialFolderPathW(CSIDL_COMMON_APPDATA)` → three `CreateDirectoryW` calls → `CreateFileW` (`0x1400033b2`) → `WriteFile` (`0x1400033df`) to
|
||
`%ProgramData%\Electronic Arts\EA Services\License\1027460.dlf`.
|
||
|
||
**Template (verbatim, `.data 0x140019620`, file `0x17820`):**
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><License xmlns="http://ea.com/license"><CipherKey>%s</CipherKey><MachineHash>2b8ee7faea76e8a34f5f5d20e5328e32</MachineHash><ContentId>%d</ContentId><UserId>%d</UserId><GameToken>%s</GameToken><GrantTime>2017-01-01T00:00:00Z</GrantTime><StartTime>2017-01-01T00:00:00Z</StartTime></License>
|
||
```
|
||
|
||
**On-disk `.dlf` format (recovered by decrypting the live file):**
|
||
```
|
||
[0x00 .. 0x2D] 46-byte fixed signature header, copied verbatim from loader .data 0x1400195d0:
|
||
302c0214 520de8c2b6fcab8ed27ab6ab24c8db27b5453570 0214 8cb3edcffd4d1a5740e25ab5fa2e75a9793b7628 0000
|
||
= ASN.1 SEQUENCE(0x2c){ INTEGER(20) r, INTEGER(20) s } -- a static DSA/ECDSA-SHA1
|
||
signature REUSED FOR EVERY LICENSE (content differs, signature does not)
|
||
[0x2E .. 0x40] zero pad
|
||
[0x41 .. ] AES-128-CBC ciphertext, IV = 0, key = 4132722dd082efb0dc6457c57668ca09
|
||
(loader .data 0x1400195c0, loaded at the very first .text instruction
|
||
0x140001014 `movzx eax,[rip -> 0x1400195c0]` feeding the key schedule at 0x140001000),
|
||
plaintext PKCS7-padded
|
||
```
|
||
|
||
**Decrypted payload fields:**
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| `CipherKey` | `jsHymMvuB34nUXoH80eHcw==` → `8ec1f298cbee077e27517a07f3478773` |
|
||
| `MachineHash` | `2b8ee7faea76e8a34f5f5d20e5328e32` |
|
||
| `ContentId` | `1027460` |
|
||
| `UserId` | **`33068179`** |
|
||
| `GameToken` | 1196-char base64url → 896 raw bytes, header `0100df00…` — Nucleus-format |
|
||
|
||
**The GameToken is NOT in any binary** (searched loader, emu, game — all `find == -1`). It is produced at runtime by `dbdata.dll!getTableData`.
|
||
|
||
**Three constants tie the loader and emu together into one coordinated identity:**
|
||
* `UserId` in the `.dlf` (**33068179**) == the emu's `PersonaId` default `0x1f89493` == the shipped ini `PersonaId`.
|
||
* `MachineHash` (**`2b8ee7fa…e32`**) == the emu's LSX `<Challenge key=…>`. A **fixed build constant**, not per-machine, not per-session.
|
||
* Note the two AES keys are **unrelated**: License `4132722d…` ≠ LSX `K_FIXED 000102…0f` ≠ License `CipherKey 8ec1f298…`. Do not conflate them; the License key does not derive the LSX session key.
|
||
|
||
### 2.3 Does the loader touch the game, or produce auth material?
|
||
|
||
**Touch: no.** PLAY launches the game via `CreateProcessW` (`0x14000361f`) from `0x1400034c0`: `GetCommandLineW` + `CommandLineToArgvW`, builds a command line around `"FIFA17.exe"` (`0x140015a20`), `lpApplicationName = NULL`, `bInheritHandles = 0`, `dwCreationFlags = 0x4000000` (`CREATE_DEFAULT_ERROR_MODE`) — **not** `0x4` `CREATE_SUSPENDED` — then `EndDialog`.
|
||
**There is no process-memory patching anywhere.** The import table has no `VirtualProtect` / `VirtualAllocEx` / `WriteProcessMemory` / `CreateRemoteThread` / `Nt*`; a binary search for those names finds nothing; and the only `GetProcAddress` loop (`0x1400063a8`/`0x1400063c8`) resolves CRT api-set helpers (`FlsAlloc`, `CreateThreadpoolTimer`, …), not FIFA addresses.
|
||
|
||
**Auth material: yes, but the wrong kind.** The `.dlf` carries a genuine Nucleus-format `GameToken` + `CipherKey` + identity. That is real auth material — and it is why the game clears its **ownership / entitlement** checks. But it is delivered **purely as an on-disk file** (no registry, env var, pipe, or shared memory) and is consumed by the **local license/entitlement path**, not by the LSX login state machine. **It will not flip `m_isLoggedIn`.**
|
||
|
||
**Caveat: `.stp0` is still packed.** This "unpacked" exe only had the outer UPX layer stripped. `.stp0` (VMA `0x14001d000`, size `0xec895`) disassembles as garbage (`movabs ds:0x6a04261ed6375c7b`, random `(bad)` opcodes), and the cleartext `.rdata`/`.data` strings above have **zero rip-relative xrefs** in the disassembled `.text` — meaning the code that consumes them lives inside that packed blob. Everything in §2 is therefore complete about *what the loader does*, but not about *how the virtualized helpers do it*.
|
||
|
||
---
|
||
|
||
## 3. LOGIN-GATE VERDICT
|
||
|
||
### **NO.** Nothing in this repack can flip `OriginMgr.m_isLoggedIn` or deliver account information.
|
||
|
||
This is a positive structural proof, not an absence-of-evidence argument. The emu is **offline by construction**, and the binary is small enough (19,456 bytes) to enumerate exhaustively:
|
||
|
||
1. **No login vocabulary exists.** `grep -aoib 'login|isloggedin|authcode|entitlement'` over the whole DLL returns **zero hits**. The `.data` string region is fully enumerated in §1.4 — the 12 templates plus the ini path are *all* of it. No `<Login>`, no `IsLoggedIn`, no `GetAuthCode`, no token, no session concept.
|
||
2. **`connected="0"` is a literal with no alternative.** The substrings `Internet` and `connected` occur at exactly two byte offsets in the entire file (`0x41d2`, `0x41e9`), both inside the single template at `0x1800053b0`. That template contains only one format specifier (`id="%d"`) — `connected="0"` is not parameterized. It is referenced by exactly **one** unconditional instruction (`0x180002caa lea r8,[rip+0x26ff]`), sitting in straight-line code with no `jcc` targeting it. There is no `connected="1"` string anywhere in the image.
|
||
3. **There is no code location where a Login event could ever be emitted.** The response path `0x180002325`–`0x180002d6f` is straight-line with **zero content-dependent branches** (§1.3). A dispatcher cannot make a decision it has no branch for.
|
||
4. **There is no configuration escape hatch.** The entire config surface is three ini keys (Language / PersonaId / PersonaName) and the only environment-variable call is a **write** (`SetEnvironmentVariableA("EAGameLocale", …)`).
|
||
5. **There is no hidden alternate template set.** A relocated 8-entry pointer table exists at `0x180004530` (file `0x3330`–`0x3368`: `0x1800050b0`, `0x1800051d0`, `0x1800052b0`, `0x180005170`, `0x180005530`, `0x1800054d0`, `0x1800052b0`, `0x1800054d0`) but it is **dead** — no instruction in `.text` references it, and even so it contains only the same offline templates.
|
||
6. **Exactly one unsolicited frame exists in the whole binary**, proven by exact call-site accounting (20 sends / 19 recvs / 1 accept), and it is the plaintext Challenge.
|
||
7. **The loader adds nothing.** No sockets (no WS2_32), no memory patching, no online/auth strings. Its `.dlf` GameToken is a local *entitlement* grant, not a session.
|
||
|
||
### The account-info failure is not what we assumed
|
||
|
||
`GetProfileResponse` is the only account-info feed in the protocol, and our v2 already matches the emu template **byte-for-byte** (§1.4). More decisively: the shipped emu emits a **garbage pointer** as `PersonaId` in its third GetProfileResponse (§1.6) and the repack still boots to a playable game. **FIFA 17 therefore does not latch account identity from `GetProfileResponse` at all.** "Unable to retrieve account information" is not a GetProfileResponse *shape* problem — and it is not a state the repack ever avoids, because the repack never enters the online path we are forcing FIFA down with `connected="1"`.
|
||
|
||
### What this binary DOES contribute: three eliminated root causes
|
||
|
||
The negative result is not worthless — it closes off three plausible explanations for why the 90× `<Login IsLoggedIn="true">` push did nothing:
|
||
|
||
* **Framing is confirmed correct.** `send(s, buf, strlen(buf)+1, 0)` — the NUL is transmitted. Our `send_plain`/`send_enc` do exactly this. "The event was mis-framed and FIFA's reader stalled" is **ruled out**.
|
||
* **The pushed-Event shape is confirmed correct.** The working Challenge is `<LSX><Event sender="EALS"><X/></Event></LSX>` with no `id` and no `<Response>` wrapper, and FIFA consumes it. Our push used exactly that shape. "Wrong element shape" is **ruled out**.
|
||
* **Timing/encryption ordering is confirmed correct.** Encryption begins only after ChallengeAccepted; anything pushed earlier must be plaintext. Our pushes are all encrypted post-handshake, which is right. "We encrypted something that should have been plaintext" is **ruled out**.
|
||
|
||
Combined, this narrows the failure to **either the `sender` name not matching FIFA's handler table, or handler-registration timing, or — the strongest remaining hypothesis — encrypted mid-session `<Event>` frames not being routed to the same parser as the plaintext handshake Event.** The emu never sends one, so this repack cannot adjudicate that last point.
|
||
|
||
### Where the mechanism actually lives
|
||
|
||
**Only in FIFA17.exe's live-decrypted code.** Concretely, our existing **[LIVE]** recon already names the machinery: the Origin event dispatcher at `0x146f1e060`, whose **case 2 @`0x146f1e0ab` sets `m_isLoggedIn`, clears `loginError`, and rebroadcasts on the FE bus**, and the sender `strcmp` at `0x147102880`. That is the target. These repack binaries contain **no** part of it and never will — their residual value is exactly as a byte-exact oracle for crypto, framing, sender attributes, template shapes, and FIFA's boot request ordering.
|
||
|
||
### The next LIVE experiment
|
||
|
||
**Instrument the dispatcher instead of guessing at the wire.** We have pushed 90 frames blind and learned nothing because we cannot see whether they even arrive. Convert this from guessing to observation:
|
||
|
||
> Using the `/proc/PID/mem` live-probe path (Wine maps the PE flat at `0x140000000`, no relaunch needed), breakpoint or trace-patch the Origin event dispatcher at `0x146f1e060` and the sender `strcmp` at `0x147102880`. Then push one `<Login IsLoggedIn="true">` Event and answer exactly three questions, in order:
|
||
> 1. **Does the frame reach the dispatcher at all?** If not, the failure is *below* the dispatcher — decryption, routing, or (most likely) encrypted-Event handling — and no amount of sender-name guessing will help.
|
||
> 2. **If it reaches, what sender string does the `strcmp` at `0x147102880` compare against?** Dump the handler table. That converts `LOGIN_EVENT_SENDERS` from a guessed list into a read one.
|
||
> 3. **If the sender matches, does case 2 at `0x146f1e0ab` execute, and does `m_isLoggedIn` change?** If it executes but the flag reverts, something re-clears it — find the writer.
|
||
|
||
This single experiment discriminates between all three surviving hypotheses in one run, which no wire-level A/B can do.
|
||
|
||
---
|
||
|
||
## 4. ACTIONABLE NEXT STEPS (ranked)
|
||
|
||
1. **[LIVE, highest value] Trace the Origin event dispatcher `0x146f1e060` + sender `strcmp` `0x147102880` while pushing one Login Event.** Answers the three-question ladder above and discriminates all remaining hypotheses in one run. Everything else is secondary to this. Method: `/proc/PID/mem` live probe (flat map at `0x140000000`, no relaunch).
|
||
2. **[LIVE, cheap, do alongside #1] A/B the encrypted-vs-plaintext Event hypothesis.** The repack proves only that a **plaintext, pre-session-key** Event is consumed. Push the Login (a) as a `<Response>`-shaped frame, and (b) in **plaintext immediately after ChallengeAccepted, before the session goes encrypted**. If (b) works and our current push does not, the whole 90× failure was "encrypted Events are dropped" — a hypothesis this binary explicitly cannot rule out.
|
||
3. **[Code, low risk] Apply corrections C1–C3** (emu-exact `challenge_response` + tail assert; extract `response="`; buffer partial frames). Small, mechanical, and C1's assert turns a future silent breakage into a loud one.
|
||
4. **[Code, medium] Apply C4** — drop the free-running heartbeat timer and gate pushes strictly to post-Response. The emu is provably lockstep (20 sends / 19 recvs); an interleaved push is a wire pattern FIFA has demonstrably never seen from the shipped emu, and is a plausible source of divergence we introduced ourselves.
|
||
5. **[Instrumentation, high value/low cost] Implement C8, the boot-order oracle.** Diff our live request log against the §1.4 script and assert id contiguity. This immediately answers three open questions at once: it names the unknown verb at ordinal 14, pins which `SettingId` maps to `production`/locale/`false`, and — most importantly — tells us whether our answers push FIFA off its normal path **before** the online decision at ordinal 17 is even asked for.
|
||
6. **[Tooling] Build a `.dlf` regenerator.** AES-128-CBC, IV=0, key `4132722dd082efb0dc6457c57668ca09`, prepend the fixed 46-byte header `302c0214…762800` + zero-pad to `0x41`, PKCS7-pad the XML. Lets us mint/patch licenses directly instead of depending on the Steampunks loader GUI — and lets us test whether the game verifies that static DER signature at all (it is byte-identical across licenses whose content differs, so either it is ignored or verification is bypassed).
|
||
7. **[Experiment, one line] Corrupt one hex char of `H`** and see whether the session still works. The emu never verifies the client's `response=`, which hints the challenge exists only to establish the session key. If FIFA does not verify either, we can stop treating the handshake as load-bearing. Same class of test: change the `Challenge key` from `2b8ee7fa…e32` and see whether anything notices — that determines whether we may safely hardcode it across installs.
|
||
8. **[Recon, if #1 stalls] Obtain `dbdata.dll`.** Not in this directory. It is the keygen that mints the 896-byte Nucleus `GameToken`. Learning whether that token is a fixed blob, derived from `UserId`, or structurally parseable would tell us whether any of its fields are reusable for Blaze `preAuth`.
|
||
9. **[Recon, low priority] Trial-decrypt `.stp0`** (`0x14001d000`, `0xec895`) with the `CipherKey` `8ec1f298cbee077e27517a07f3478773`. Long shot, but if it unpacks it exposes the virtualized encrypt fn `0x140003f20` and the real `getTableData` call sequence, removing the last inference in §2.2.
|
||
10. **[Hygiene] Fix BRIEF.md** — line 46 (three separate GetSetting templates, not one alternation), line 56 (`r0 = rand()`, not `rand()==61`), and strike the "xor'd fragment containing blaZe" lead from line 16 as a false positive (BMP pixel data).
|
||
|
||
---
|
||
|
||
## Open questions (consolidated, deduplicated)
|
||
|
||
* **What verb occupies ordinal 14?** Answered with `ErrorSuccess`, so it is a verb needing no meaningful response (a `Set*`/`Notify`?). Single biggest unknown in the boot-order table; a live log fills it in for free (step 5).
|
||
* **Is an *encrypted mid-session* `<Event>` routed to the same handler as the plaintext handshake Challenge?** The repack gives zero evidence either way. Most likely explanation for the 90 no-op pushes (steps 1–2).
|
||
* **Does FIFA validate the `Response` `id` against the pending request, or just pop a queue head?** The emu proves ids happen to line up 1..N but cannot distinguish. Determines whether our id-less pushed Events desynchronize request/response pairing.
|
||
* **Does FIFA verify the ChallengeAccepted `response`?** The emu never verifies the client's side and blindly echoes 32 of 96 hex chars (step 7).
|
||
* **Is `2b8ee7fa…e32` build-fixed across all Steampunks FIFA 17 installs, or regenerated per repack?** Determines whether we may safely hardcode it (step 7).
|
||
* **Why three GetProfileResponses (ordinals 3, 10, 18)?** Given the ordinal-18 pointer bug, FIFA clearly does not latch identity from them — but instrumenting *which* GetProfile is the last one before the error screen is still worth doing.
|
||
* **Does the game verify the `.dlf`'s 46-byte DER header against an embedded EA public key?** It is byte-identical for every license, so either it is ignored or the check is bypassed. Affects whether we can freely edit `.dlf` contents (step 6).
|
||
* **What does EbisuSDK do with `CipherKey` `8ec1f298…`?** Likely an inner key to decrypt/validate the GameToken. Confirm from FIFA17.exe's license-parse to know whether a mismatched GameToken/CipherKey pair would be rejected.
|
||
* **Does FIFA 17 ever reconnect to 4216?** The emu could never serve a reconnect, so probably not — but our multi-connection responder may be papering over a reconnect that signals a state reset we should react to (C9).
|