Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28773e7cf1 | |||
| 3ae5587a38 | |||
| 70a64e3709 |
@@ -27,3 +27,6 @@ __pycache__/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Frozen baseline archives / inspects / manifests
|
||||||
|
/docker-backups/
|
||||||
|
|||||||
Generated
-6480
File diff suppressed because it is too large
Load Diff
-10
@@ -1,10 +0,0 @@
|
|||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = [
|
|
||||||
"openfut-core",
|
|
||||||
"openfut-bridge",
|
|
||||||
"openfut-launcher",
|
|
||||||
"openfut-launcher/openfut-hook",
|
|
||||||
"fifa-blaze/crates/blaze-proto",
|
|
||||||
"fifa-blaze/crates/server",
|
|
||||||
]
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
# OpenFUT — project handoff
|
|
||||||
|
|
||||||
Self-contained briefing for an assistant with **no access to this repo or machine**.
|
|
||||||
Everything needed to understand the project and reason about its open problems is here.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. What this is
|
|
||||||
|
|
||||||
**OpenFUT** is a clean-room, fully offline re-implementation of the server backend for
|
|
||||||
**FIFA 17 Ultimate Team (FUT)**. EA's servers for FIFA 17 are long dead. The goal is to
|
|
||||||
make the retail game's FUT mode fully playable again — open packs, build squads, use the
|
|
||||||
transfer market, play matches and earn rewards — by emulating every server the client
|
|
||||||
talks to, on localhost.
|
|
||||||
|
|
||||||
Nothing is decompiled *into* the project. The game's binaries are read to learn the
|
|
||||||
**wire format** (which JSON keys, of which types, each response must contain), and the
|
|
||||||
servers are written from scratch in Python against that spec.
|
|
||||||
|
|
||||||
The client is unmodified retail FIFA 17 running under Wine/Proton on Linux.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Architecture — four independent servers
|
|
||||||
|
|
||||||
FIFA 17 does not talk to one backend. It talks to four, on different protocols, and all
|
|
||||||
four must be satisfied in sequence before FUT loads.
|
|
||||||
|
|
||||||
```
|
|
||||||
FIFA 17 (Wine/Proton)
|
|
||||||
│
|
|
||||||
├─ LSX / Origin :4216 XML over TCP. Local Origin client emulation.
|
|
||||||
│ Login, entitlements, persona.
|
|
||||||
├─ Blaze :42127 redirector (TLS) → :42130 game server, :42131 nucleus
|
|
||||||
│ EA's binary "Fire2/TDF" RPC protocol. Session, auth,
|
|
||||||
│ and — critically — the CLIENT-CONFIG STORE.
|
|
||||||
├─ UTAS / RS4 :8099 The FUT REST API. JSON over HTTP. ~45 endpoints.
|
|
||||||
│ Club, squads, packs, market, matches. The bulk of it.
|
|
||||||
└─ POW / EASFC :8094 (+ :8080 content) A third HTTP API, discovered late.
|
|
||||||
Online status bar, level, EASFC credits, catalogue.
|
|
||||||
```
|
|
||||||
|
|
||||||
Plus two helpers: a **roster** server (:8081) serving a roster-update XML the FUT
|
|
||||||
loading screen blocks on, and **autopatch**, which patches the running process's
|
|
||||||
ProtoSSL certificate verification so the client accepts our self-signed TLS.
|
|
||||||
|
|
||||||
### How the client is redirected
|
|
||||||
|
|
||||||
Three mechanisms, in decreasing order of preference:
|
|
||||||
|
|
||||||
1. **Blaze client-config keys** — the cleanest. Blaze serves a key/value config store
|
|
||||||
and the client reads its own service URLs from it. `FUT_RS4_APIURL_<MODULE>` and
|
|
||||||
`FUT_RS4_URL_<CALL>` point the FUT API at `127.0.0.1:8099`; `FIFA_POW_URL` points
|
|
||||||
the EASFC layer at `127.0.0.1:8094`. **No root, no DNS games.**
|
|
||||||
2. **`/etc/hosts`** — for hosts baked into the binary (`easw.easports.com`,
|
|
||||||
`gosredirector.ea.com`).
|
|
||||||
3. **iptables DNAT** — for a hardcoded IP (`159.153.51.20` → the Blaze redirector).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. The wire format, and why it is unforgiving
|
|
||||||
|
|
||||||
FUT responses are JSON, but the client does **not** use a general JSON object model. Each
|
|
||||||
response class has a hand-written SAX-style deserializer that walks tokens and dispatches
|
|
||||||
on a **hashed key id** (an "atom"). This has three consequences that dominate the project:
|
|
||||||
|
|
||||||
**Atoms.** Every JSON key name maps to a 16-bit atom id via FNV-1a. There is a recovered
|
|
||||||
table of ~900 (id → name). A response is really "which atoms does deserializer X read,
|
|
||||||
and of what type".
|
|
||||||
|
|
||||||
**Type fidelity is fatal.** Feeding a scalar where the parser expects an object or array
|
|
||||||
does not error — it **desyncs the token reader and the game hard-freezes** in a busy loop
|
|
||||||
at `0x1801c7f1a`. This is the single most common way to break the game, and it has bitten
|
|
||||||
this project repeatedly. Arrays must be arrays; nested objects must be objects.
|
|
||||||
|
|
||||||
**Unknown keys are usually skipped safely** — most deserializers route an unrecognised
|
|
||||||
atom to a value-skip handler (`FUN_180135ff0`), so extra fields are inert. This document
|
|
||||||
previously named `FutMoveCard` (`0x180128600`) as an exception with no skip handler at
|
|
||||||
all. **That was wrong**, and the retraction is in §5a: it has two skip-handler call sites
|
|
||||||
and parses seven atoms. No deserializer in this project is currently known to lack one.
|
|
||||||
Treat any future "this class has no skip handler" claim as unproven until the search is
|
|
||||||
shown to have covered the whole function.
|
|
||||||
|
|
||||||
### Working method
|
|
||||||
|
|
||||||
For any endpoint: find the response class's deserializer, extract the atom ids it
|
|
||||||
compares against, map them to names, note the getter used for each (int / string / bool /
|
|
||||||
nested), and build the minimal body. Omit nested members unless their shape is known —
|
|
||||||
omission is skip-safe, a wrong shape freezes the game.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. What works today (live-verified)
|
|
||||||
|
|
||||||
| Area | Status |
|
|
||||||
|---|---|
|
|
||||||
| Boot to the FUT hub | ✅ |
|
|
||||||
| Club identity, coins, W/D/L record | ✅ |
|
|
||||||
| Active squad — renders 11 real players, chemistry links | ✅ |
|
|
||||||
| Squad building — `PUT /squad/<id>` fires, persists across relaunch | ✅ |
|
|
||||||
| Squad roster ("MY SQUADS") | ✅ |
|
|
||||||
| Transfer market — browse, bid, buy-now, sell, watchlist | ✅ |
|
|
||||||
| Packs — buy, reveal, Send to Club | ✅ (the workaround is retired, see §5a) |
|
|
||||||
| Quick sell — destroys cards, credits coins | ✅ |
|
|
||||||
| Match loop — create/ready/play/destroy + coin rewards | ✅ implemented, **never played in-game** |
|
|
||||||
| Online/EASFC status bar (no "servers unreachable") | ✅ when enabled |
|
|
||||||
| Seasons / tournaments / leaderboards / champions | ⚠️ routed with reversed schemas, never live-tested |
|
|
||||||
|
|
||||||
Current save state: 99 club items, 8,400 coins, 0-0-0 record.
|
|
||||||
|
|
||||||
**Design convention:** every risky change ships behind an environment flag, default set
|
|
||||||
to whatever is live-proven (`FUT_MASSINFO`, `FUT_USERINFO`, `FUT_MODES`,
|
|
||||||
`FUT_PACK_AUTOCLUB`, `FUT_POW`, `FUT_STORE_GROUPS`, …). This exists because two working
|
|
||||||
screens were broken by shipping "corrections" on by default.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Open problems
|
|
||||||
|
|
||||||
### 5a. "Send to Club" kills the FUT session — SOLVED 2026-08-04
|
|
||||||
|
|
||||||
Opening a pack shows the cards correctly; choosing **Send to Club** used to produce
|
|
||||||
*"there has been an error connecting to FIFA 17 Ultimate Team"* and a logout, seven
|
|
||||||
attempts running. The cards always moved server-side; only the acknowledgement was
|
|
||||||
rejected.
|
|
||||||
|
|
||||||
The cause was the response body. `PUT ut/%s/item` returns per-item **verdict** records,
|
|
||||||
not an acknowledgement, and the completion handler raises
|
|
||||||
`EVENT_CARDS_MOVE_CARD_FAILURE` when the record vector is empty or when `success != 1`.
|
|
||||||
Every body the project returned, `{}` included, reported the move as failed. The fix is
|
|
||||||
the real shape:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"itemData":[{"id":100000125,"pile":"club","success":true}, ...]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Live: five cards to the club, session survived, cards persisted, no `ut/delete/auth`.
|
|
||||||
The autoclub workaround is retired.
|
|
||||||
|
|
||||||
Two corrections this closed, both worth carrying forward:
|
|
||||||
|
|
||||||
- The repo's claim that this deserializer had **no skip handler** and parsed only two
|
|
||||||
keys was false. It came from searching a truncated decompile. It implied the body
|
|
||||||
could not be at fault, which is what sent seven attempts after client-side state.
|
|
||||||
Never conclude an absence from a truncated or unverified-length extraction.
|
|
||||||
- The quick-sell asymmetry was not evidence of client state. Quick sell's callbacks
|
|
||||||
read only the transport status code and never touch the body.
|
|
||||||
|
|
||||||
### 5b. "MY CLUB" counter always reads 0 — UNSOLVED
|
|
||||||
|
|
||||||
The hub tab bar shows `MY CLUB 0` despite the club holding 99 items.
|
|
||||||
|
|
||||||
**Eliminated by live test:**
|
|
||||||
- **Not the item list** — the user opened MY CLUB, the client fetched the club and
|
|
||||||
**displayed all 99 players correctly**, and the counter still read 0.
|
|
||||||
- **Not `pileSizeClientData`** — the massinfo member that carries pile sizes as
|
|
||||||
`{"entries":[{"key":int,"value":int}]}`. A probe sent 16 entries with uniquely
|
|
||||||
identifiable values; the counter stayed 0.
|
|
||||||
- **Not lazy loading** — the club endpoint was fetched 6 times that session.
|
|
||||||
|
|
||||||
**Unexplored contrast:** the `ACTIVE SQUAD` tab in the same bar correctly shows `11/23`.
|
|
||||||
So some counters work. Whatever differs between that one and the club one is likely the
|
|
||||||
answer.
|
|
||||||
|
|
||||||
### 5c. Store tiles render "unknown" — DIAGNOSED, NOT FIXED
|
|
||||||
|
|
||||||
Pack tiles show `unknown` with zero item counts. The `"unknown"` string is an
|
|
||||||
unconditional **default** in a string constructor — the field simply never gets written.
|
|
||||||
The store renders *display groups*, and `displayGroup` is parsed **recursively by the same
|
|
||||||
element parser**. Sending it populated **froze the store** (the type-desync busy loop), so
|
|
||||||
it is behind a flag, default off. Doing it properly needs the group's own field set worked
|
|
||||||
out rather than a self-referential copy of the pack.
|
|
||||||
|
|
||||||
### 5d. Seasons and Draft refuse — UNSOLVED, and not obviously server-side
|
|
||||||
|
|
||||||
Selecting **single-player Seasons** raises *"There was a problem communicating with the
|
|
||||||
FIFA Ultimate Team servers"* while making **zero requests to any layer**. UTAS, Blaze and
|
|
||||||
POW logs show only pings and one census subscription across the whole failure window. No
|
|
||||||
response can be wrong because no request was made. POW is eliminated (same failure with it
|
|
||||||
enabled and disabled).
|
|
||||||
|
|
||||||
**Online Draft** hangs the client rather than crashing it (process alive, no dump). The
|
|
||||||
one suspicious thing on the wire is `GET ut/%s/squad/mode/draft/state`, which our generic
|
|
||||||
`/squad` route answers with a full active-squad object: 23 slots, nested `itemData`, a
|
|
||||||
33-integer formation string. The real class wants `roundsInfo` plus a state enum, so this
|
|
||||||
is a textbook type-desync candidate and the timing matches. **Nothing has isolated it**;
|
|
||||||
it is a suspect, not a cause.
|
|
||||||
|
|
||||||
Both matter beyond themselves, because they are the only two routes into a match, and the
|
|
||||||
`/match` request shape has therefore never been captured.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Notable reverse-engineering findings
|
|
||||||
|
|
||||||
- **Class → deserializer resolution.** A response class's name literal is preceded by a
|
|
||||||
**4-byte header**, and the constructing factory's `lea` points at *the header*, not the
|
|
||||||
text. Lookups must use `name_address - 4`. Six attempts failed on this off-by-four;
|
|
||||||
four of them returned zero results and nearly got recorded as "this class has no
|
|
||||||
deserializer".
|
|
||||||
- **The request-template table is a floor, not a ceiling.** Several real endpoints are
|
|
||||||
built by the caller appending a suffix and therefore never appear in the binary's URL
|
|
||||||
table: `squad/list`, `user/club`, `club/stats/*`, `clientdata/<key>`. Only live traffic
|
|
||||||
reveals them. This has caught the project three separate times.
|
|
||||||
- **The documentation lies.** The project's own `ENDPOINT_MAP.md` (~1,360 lines, ~100
|
|
||||||
reversed structs) has been wrong repeatedly: it claimed `FutMoveCard` parses
|
|
||||||
`chemistry` (it does not); it called seven store pack fields "skipped no-ops" (all are
|
|
||||||
parsed); it gave price-object keys as `amount`/`currency` (the parsers read
|
|
||||||
`externalPriceId`). **Verify against the decompiler before relying on any row.**
|
|
||||||
- **The online layer was hiding in an unpacked DLL.** "EA FC servers are unreachable"
|
|
||||||
comes from a third HTTP API implemented in a *loose, unpacked, string-rich* library —
|
|
||||||
not the packed executable, and not any layer previously emulated. It is redirectable
|
|
||||||
purely through the Blaze config store.
|
|
||||||
- **The main executable is Denuvo-packed.** Its code exists only in a live process. Live
|
|
||||||
memory is readable via `/proc/PID/mem` (the PE is mapped flat), which is how a crash
|
|
||||||
site was disassembled. Any logic living there cannot be reversed statically.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Tooling built
|
|
||||||
|
|
||||||
- A **PyGhidra harness** with helpers for decompiling, xrefs, vtables, byte scanning and
|
|
||||||
class→deserializer resolution. (Ghidra's Java/OSGi scripting is broken on this machine;
|
|
||||||
PyGhidra bypasses it entirely.)
|
|
||||||
- A **minidump reader** — exception record, fault-time registers, module map, and a stack
|
|
||||||
walk that recovers a usable backtrace.
|
|
||||||
- A **live code grabber** that reads and disassembles unpacked code out of a running
|
|
||||||
process.
|
|
||||||
- A **read-only live probe** pattern for polling client model state while playing.
|
|
||||||
- **Two test suites**: 380 live contract checks (freeze-safety: asserts every response
|
|
||||||
field's type against the reversed schema) and 51 pure unit checks for match rewards.
|
|
||||||
- A **traffic-replay audit** that diffs current responses against a known-good session —
|
|
||||||
this is what proves a change did not alter what the client sees.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Documentation in-repo
|
|
||||||
|
|
||||||
| File | Contents |
|
|
||||||
|---|---|
|
|
||||||
| `ENDPOINT_MAP.md` | ~100 reversed response structs, atoms, types, freeze risks |
|
|
||||||
| `FUT_RESPONSE_REBUILD_PLAN.md` | Squad family, massinfo, the service-layer call graph |
|
|
||||||
| `REBUILD_RESEARCH.md` | Complete API surface, gap analysis, POW layer, and every eliminated hypothesis with its evidence |
|
|
||||||
| `CARD_SYSTEM.md` | How card identity resolves locally from the game's own database |
|
|
||||||
| `REPACK_INTEL.md` | Origin/LSX emulation notes |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Where help would be most valuable
|
|
||||||
|
|
||||||
1. **The MY CLUB counter (§5b).** Given the client demonstrably *has* the items and
|
|
||||||
*renders* them, what else could a tab counter read from? Note that a sibling counter in
|
|
||||||
the same bar works correctly.
|
|
||||||
2. **Seasons refusing with zero requests to any server (§5d).** The client raises a
|
|
||||||
"problem communicating with the FIFA Ultimate Team servers" without contacting
|
|
||||||
anything. Nothing on the wire can be wrong because nothing went on the wire.
|
|
||||||
3. **Whether the remaining problems are fixable server-side at all**, or whether the
|
|
||||||
deciding logic lives in the Denuvo-packed executable and only live instrumentation can
|
|
||||||
settle it. Note that this question was asked about `Send to Club` too, and there the
|
|
||||||
answer turned out to be a plain wire fix, so treat "it must be client-side" as a
|
|
||||||
hypothesis needing evidence rather than a fallback explanation.
|
|
||||||
|
|
||||||
Useful framing: this project's failures have almost always come from proposing a fix
|
|
||||||
before testing the assumption under it. Hypotheses that come with a cheap way to
|
|
||||||
disconfirm them are worth far more than plausible ones.
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
# OpenFUT — Project Report
|
|
||||||
|
|
||||||
**Goal:** make FIFA 17 Ultimate Team fully playable offline, forever, by re-implementing
|
|
||||||
every server the game talks to.
|
|
||||||
|
|
||||||
**Status:** FUT boots, loads, and is playable. Packs, squads, the transfer market, coins
|
|
||||||
and progression all work. Two cosmetic/flow problems remain open.
|
|
||||||
|
|
||||||
**Timeline:** 2026-06-25 → 2026-08-04 · 44 commits · ~12,900 lines of Python across 39
|
|
||||||
tools · ~3,600 lines of reverse-engineering documentation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. What the project is
|
|
||||||
|
|
||||||
EA shut down FIFA 17's servers years ago, which kills Ultimate Team — the mode is entirely
|
|
||||||
server-driven. Your club, squads, packs, market and progression all live server-side, so
|
|
||||||
without a backend the mode is dead even though the game still installs and runs.
|
|
||||||
|
|
||||||
OpenFUT replaces that backend with local servers. The game is **unmodified retail
|
|
||||||
FIFA 17** running under Wine/Proton on Linux; nothing is patched into the game except a
|
|
||||||
single runtime tweak so it accepts our TLS certificate.
|
|
||||||
|
|
||||||
This is **clean-room work**. No EA code is copied or redistributed. The game's own
|
|
||||||
binaries are read to learn the *wire format* — which JSON keys, of which types, each
|
|
||||||
response must carry — and the servers are written from scratch against that specification.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Project history
|
|
||||||
|
|
||||||
The project changed target twice before finding its footing. That arc matters, because
|
|
||||||
each pivot was driven by hitting a hard wall.
|
|
||||||
|
|
||||||
**Phase 1 — FIFA 23 (June 2026).** Began as an offline FUT backend for FIFA 23: a Rust
|
|
||||||
core (`openfut-core`, Axum + SQLite), a protocol bridge (`openfut-bridge`), and a GUI
|
|
||||||
launcher. All three built and passed tests. The architecture was sound but the client
|
|
||||||
never got far enough to exercise it.
|
|
||||||
|
|
||||||
**Phase 2 — the FIFA 23 wall.** FIFA 23 refused to go online at all. Extensive reverse
|
|
||||||
engineering of the connection state machine, live-memory probing, and forcing the
|
|
||||||
"go online" gate directly all failed — the client's internal coherence checks were the
|
|
||||||
wall, not any single flag. Documented as a dead end rather than fought.
|
|
||||||
|
|
||||||
**Phase 3 — the FIFA 17 pivot (late July).** FIFA 17 turned out to be a far better target:
|
|
||||||
its network library is **unprotected and fully symboled**, exposing 979 RPC names. The
|
|
||||||
insight was to crack FIFA 17 first and port the understanding back.
|
|
||||||
|
|
||||||
That worked, quickly:
|
|
||||||
- **TLS pinning defeated** — the client's certificate verification is patched at runtime
|
|
||||||
in memory, so a self-signed cert is accepted.
|
|
||||||
- **Origin/LSX emulation** — the local Origin client protocol was reverse engineered,
|
|
||||||
including the repack's own crypto layer, beating "log in to Origin" and
|
|
||||||
"title version outdated".
|
|
||||||
- **Blaze cracked end to end** — EA's binary RPC protocol: redirector, second-hop
|
|
||||||
handshake, and the encoded pre-auth exchange. This was the big one.
|
|
||||||
- **Full FUT API mapped** — ~100 response structures reverse engineered to field level.
|
|
||||||
|
|
||||||
**Phase 4 — building the FUT backend (August).** With the protocol understood, the work
|
|
||||||
became making FUT actually *play*: card rendering, packs, the store, the transfer market,
|
|
||||||
squads, and match rewards. This is where the project stands.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Architecture
|
|
||||||
|
|
||||||
FIFA 17 does not talk to one backend. It talks to **four**, on different protocols, and
|
|
||||||
all four must be satisfied in sequence before FUT loads.
|
|
||||||
|
|
||||||
```
|
|
||||||
FIFA 17 (Wine/Proton)
|
|
||||||
│
|
|
||||||
├─ LSX / Origin :4216 XML/TCP — local Origin client emulation
|
|
||||||
│ login, entitlements, persona
|
|
||||||
├─ Blaze :42127 redirector (TLS) → :42130 game, :42131 nucleus
|
|
||||||
│ EA's binary Fire2/TDF RPC
|
|
||||||
│ session, auth, and the CLIENT-CONFIG STORE
|
|
||||||
├─ UTAS / RS4 :8099 the FUT REST API — JSON/HTTP, ~45 endpoints
|
|
||||||
│ club, squads, packs, market, matches
|
|
||||||
└─ POW / EASFC :8094 a third HTTP API (+ :8080 content)
|
|
||||||
online status, level, credits, catalogue
|
|
||||||
```
|
|
||||||
|
|
||||||
Plus **roster** (:8081), serving an XML file the FUT loading screen blocks on, and
|
|
||||||
**autopatch**, which patches certificate verification in the running process.
|
|
||||||
|
|
||||||
### Redirection, in order of preference
|
|
||||||
|
|
||||||
1. **Blaze client-config keys** — the client reads its own service URLs from a key/value
|
|
||||||
store that Blaze serves. Pointing FUT and EASFC at localhost needs **no root and no
|
|
||||||
DNS manipulation**. This is the clean mechanism and most redirection uses it.
|
|
||||||
2. **`/etc/hosts`** — for hostnames baked into the binary.
|
|
||||||
3. **iptables DNAT** — for one hardcoded IP address.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. The wire format — and why it is unforgiving
|
|
||||||
|
|
||||||
FUT responses are JSON, but the client does not use a general JSON object model. Each
|
|
||||||
response class has a hand-written SAX-style deserializer that walks tokens and dispatches
|
|
||||||
on a **hashed key id** ("atom"). Three consequences dominate the project:
|
|
||||||
|
|
||||||
**Atoms.** Every JSON key maps to a 16-bit id via FNV-1a. A recovered table of ~900
|
|
||||||
id→name pairs is the Rosetta stone. A response spec is really "which atoms does this
|
|
||||||
deserializer read, of what type".
|
|
||||||
|
|
||||||
**Type fidelity is fatal.** A scalar where an object or array is expected does not error —
|
|
||||||
it **desyncs the reader and hard-freezes the game** in a busy loop. This is the primary
|
|
||||||
failure mode of the entire project.
|
|
||||||
|
|
||||||
**Unknown keys are usually skipped — but not always.** Most deserializers route
|
|
||||||
unrecognised atoms to a skip handler, making extra fields inert. At least one does not,
|
|
||||||
so any unexpected key desyncs it.
|
|
||||||
|
|
||||||
**Working method:** locate the deserializer, extract its atom set and per-atom getter
|
|
||||||
types, build the minimal body, and omit nested members whose shape isn't known — omission
|
|
||||||
is safe, a wrong shape freezes the game.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. What works
|
|
||||||
|
|
||||||
| Capability | State |
|
|
||||||
|---|---|
|
|
||||||
| Boot: Origin → Blaze → FUT hub | ✅ |
|
|
||||||
| Club identity, coins, W/D/L record | ✅ |
|
|
||||||
| Active squad — 11 real players, ratings, chemistry | ✅ |
|
|
||||||
| Squad building — saves and survives relaunch | ✅ |
|
|
||||||
| Squad roster ("MY SQUADS") | ✅ |
|
|
||||||
| Transfer market — browse, bid, buy-now, list, watchlist | ✅ |
|
|
||||||
| Store — buy packs | ✅ |
|
|
||||||
| Packs — cards land in the club | ✅ via workaround (§6a) |
|
|
||||||
| Quick sell — credits coins | ✅ |
|
|
||||||
| Match loop — create/ready/play/destroy + rewards | ⚠️ built, **never requested by the client** (see below) |
|
|
||||||
| Online/EASFC status bar | ✅ when enabled |
|
|
||||||
| Seasons, tournaments, leaderboards, champions | ⚠️ routed, **never requested by the client** (see below) |
|
|
||||||
| Card identity (names, faces, ratings) | ✅ resolves from the game's own local database |
|
|
||||||
|
|
||||||
### "Untested" is two different things, and the difference matters
|
|
||||||
|
|
||||||
The server log records the User-Agent of every request. The real client identifies as
|
|
||||||
`ProtoHttp`; this project's own curl and Python probes do not. Separating them shows that
|
|
||||||
several endpoints previously filed as "built but untested" have in fact **never been
|
|
||||||
requested by the game at all**, and everything recorded against them was self-inflicted
|
|
||||||
traffic:
|
|
||||||
|
|
||||||
| endpoint | client requests | project probes |
|
|
||||||
|---|---|---|
|
|
||||||
| `/leaderboards/options` | 5 | 1 |
|
|
||||||
| `/clientdata/userHubData` | 13 | 2 |
|
|
||||||
| `/user/accountinfo` | 23 | 49 |
|
|
||||||
| `/season`, `/season/user` | **0** | 2 |
|
|
||||||
| `/tournament`, `/tournament/user` | **0** | 3 |
|
|
||||||
| `/leaderboards` (bare) | **0** | 2 |
|
|
||||||
| `/champion` | **0** | 2 |
|
|
||||||
| `/match` | **0** | 2 |
|
|
||||||
| `/clubUser` | **0** | 93 |
|
|
||||||
| `/user/list` | **0** | 180 |
|
|
||||||
| `/sbs` (SBC), `/draft/mode` | **0** | 0 |
|
|
||||||
|
|
||||||
`/clubUser` and `/user/list` are the starkest: 273 requests between them, none from the
|
|
||||||
game. Work was done on both on the assumption the client wanted them.
|
|
||||||
|
|
||||||
A zero in the client column does **not** mean the client never wants that endpoint. In
|
|
||||||
most cases it means **nobody has navigated to that part of the game yet**. It does mean no
|
|
||||||
claim about those endpoints has been tested against the client, and any analysis that does
|
|
||||||
not apply this filter is misleading by default.
|
|
||||||
|
|
||||||
**Requirement:** every capture and analysis tool in this project should apply the
|
|
||||||
User-Agent split by default rather than as an afterthought.
|
|
||||||
|
|
||||||
**Design convention.** Every risky change ships behind an environment flag whose default
|
|
||||||
is whatever is live-proven. This exists because shipping "corrections" on by default broke
|
|
||||||
two working screens — once freezing the store outright.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Open problems
|
|
||||||
|
|
||||||
### 6a. "Send to Club" ends the FUT session — SOLVED 2026-08-04
|
|
||||||
|
|
||||||
Opening a pack displayed the cards correctly, but choosing **Send to Club** produced
|
|
||||||
*"there has been an error connecting to FIFA 17 Ultimate Team"* and a logout, seven
|
|
||||||
attempts running. The cards always moved correctly server-side; only the
|
|
||||||
acknowledgement was rejected.
|
|
||||||
|
|
||||||
**It was the response body all along.** `PUT ut/%s/item` does not parse an
|
|
||||||
acknowledgement, it builds per-item **verdict** records, and the completion handler
|
|
||||||
raises `EVENT_CARDS_MOVE_CARD_FAILURE` when the record vector is empty or when
|
|
||||||
`success != 1`. Every body this project returned, `{}` included, therefore told the
|
|
||||||
client the move had failed, and the client ended the FUT session because that is what
|
|
||||||
that event does. Serving the real shape fixed it in one launch:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"itemData":[{"id":100000125,"pile":"club","success":true}, ...]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Live result: five cards sent to the club, session survived, cards persisted, no
|
|
||||||
`ut/delete/auth` logout. Both flags are now defaults and the autoclub workaround is
|
|
||||||
retired.
|
|
||||||
|
|
||||||
**Why it took seven attempts,** which is the part worth keeping: the project had
|
|
||||||
recorded that this deserializer had *no skip handler* and parsed only two keys. That
|
|
||||||
was false, produced by searching a **truncated** decompile (the first 4,000 characters
|
|
||||||
of a 6,193-character function). It implied "the body cannot be the problem", which is
|
|
||||||
what redirected the investigation to client-side state. The quick-sell asymmetry that
|
|
||||||
seemed to confirm it has a mundane explanation: quick sell's callbacks read only the
|
|
||||||
transport status code and never touch the body, so its tolerance of `{}` said nothing
|
|
||||||
about this endpoint.
|
|
||||||
|
|
||||||
The general lesson, now a standing rule: **never conclude an absence from a truncated
|
|
||||||
or unverified-length extraction**, and treat every negative claim in the endpoint docs
|
|
||||||
as weaker than the corresponding positive one.
|
|
||||||
|
|
||||||
### 6b. "MY CLUB" counter reads 0
|
|
||||||
|
|
||||||
The hub shows `MY CLUB 0` despite 99 items. Not the item list (the client *displays* all
|
|
||||||
99), not the pile-size data (a 16-entry probe changed nothing), not lazy loading (fetched
|
|
||||||
6 times). Unexplored: the neighbouring `ACTIVE SQUAD` counter works correctly — the
|
|
||||||
difference between them is likely the answer.
|
|
||||||
|
|
||||||
### 6c. Store tiles read "unknown"
|
|
||||||
|
|
||||||
`"unknown"` is an unconditional default in a string constructor — the field is never
|
|
||||||
written. The store renders *display groups*, and that member is parsed recursively by the
|
|
||||||
same parser; sending it populated **froze the store**, so it is flagged off pending a
|
|
||||||
correct group schema.
|
|
||||||
|
|
||||||
### 6d. Large parts of the game have never been opened
|
|
||||||
|
|
||||||
Distinct from 6a to 6c, which are things that misbehave. Per the User-Agent table in
|
|
||||||
section 5, entire modes have never issued a single client request: Seasons, Tournaments,
|
|
||||||
FUT Champions, Draft, SBC, and the match loop itself. Their endpoints are routed and their
|
|
||||||
schemas are reversed, but no claim about any of them has been tested against the game.
|
|
||||||
|
|
||||||
This is not a bug list. It is unmeasured surface, and it is the cheapest information
|
|
||||||
available to the project because most of it costs nothing but navigating menus. It is
|
|
||||||
recorded here because "routed from a reversed schema" reads like a stronger claim than it
|
|
||||||
is, and section 7 warns that this project's notes have described things differently from
|
|
||||||
what is true.
|
|
||||||
|
|
||||||
*A multi-agent investigation into 6a and 6b is currently running.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Notable findings
|
|
||||||
|
|
||||||
- **Class → deserializer resolution.** A response class's name literal is preceded by a
|
|
||||||
4-byte header and the factory points at *the header*. Six attempts failed on that
|
|
||||||
off-by-four; four returned nothing and were nearly recorded as "no deserializer exists".
|
|
||||||
- **The URL table is a floor, not a ceiling.** Several real endpoints are built by
|
|
||||||
appending a suffix at the call site and never appear in the binary's template table.
|
|
||||||
Only live traffic reveals them — this caught the project three separate times.
|
|
||||||
- **The project's own documentation has been wrong repeatedly** — fields described as
|
|
||||||
inert turned out to be parsed, and documented key names didn't match the parsers.
|
|
||||||
Verify against the decompiler, not the notes.
|
|
||||||
- **The online layer was hiding in plain sight.** "EA FC servers unreachable" comes from a
|
|
||||||
third HTTP API in a *loose, unpacked, string-rich* library — not the protected
|
|
||||||
executable, and not any previously emulated layer. Redirectable purely by config.
|
|
||||||
- **The main executable is Denuvo-packed**, so its code exists only in a live process.
|
|
||||||
Live memory is readable, which is how a crash site was disassembled — but logic living
|
|
||||||
there cannot be reverse engineered statically.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Tooling and quality
|
|
||||||
|
|
||||||
- **PyGhidra harness** with decompile / xref / vtable / byte-scan / class-resolution
|
|
||||||
helpers (Ghidra's own Java scripting is broken on this machine).
|
|
||||||
- **Minidump reader** — exception record, fault-time registers, module map, stack walk.
|
|
||||||
- **Live code grabber** — reads and disassembles unpacked code from a running process.
|
|
||||||
- **Read-only live model probes** for watching client state while playing.
|
|
||||||
- **Test suites:** 380 live contract checks (type/freeze safety per reversed schema) plus
|
|
||||||
51 pure unit checks. Both green.
|
|
||||||
- **Traffic-replay audit** — diffs current responses against a known-good session to prove
|
|
||||||
a change didn't alter what the client sees.
|
|
||||||
- **~3,600 lines of RE documentation** across six files, including every eliminated
|
|
||||||
hypothesis with its supporting evidence.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Roadmap
|
|
||||||
|
|
||||||
**Immediate (free, no code):** play a match — the reward loop is built and unit-tested but
|
|
||||||
has never run in-game. Enable the game-mode endpoints and see whether four more modes
|
|
||||||
light up.
|
|
||||||
|
|
||||||
**Near term:** close the two open problems, most likely via live instrumentation rather
|
|
||||||
than more static analysis. Implement SBC and Draft (schemas already recovered). Fix the
|
|
||||||
store display groups properly.
|
|
||||||
|
|
||||||
**Longer term:** the stated destination is porting this into the Rust `openfut-core`
|
|
||||||
behind a FIFA-17 bridge. Everything currently lives in Python prototypes; the
|
|
||||||
documentation is now good enough to write the port against.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Honest assessment
|
|
||||||
|
|
||||||
**What went well.** The FIFA 17 pivot was the decisive call — recognising that an
|
|
||||||
unprotected binary was worth more than persisting against a hardened one. Blaze, Origin
|
|
||||||
and the FUT API were all cracked end to end. The freeze-safety test suite has repeatedly
|
|
||||||
caught regressions before they reached the game.
|
|
||||||
|
|
||||||
**What went badly.** Progress has been slowest where fixes were proposed before the
|
|
||||||
assumption under them was tested. Both open problems absorbed many attempts built on
|
|
||||||
plausible but unverified theories; several were disproved in a single measurement that
|
|
||||||
could have been taken first. Two working screens were broken by shipping unverified
|
|
||||||
"corrections" on by default — which is precisely why the flag convention exists now.
|
|
||||||
|
|
||||||
**The most reliable technique** has been comparing a working case against a failing one:
|
|
||||||
diffing live traffic against a known-good session, and contrasting a succeeding endpoint
|
|
||||||
with its failing sibling. That has produced more answers than any amount of decompilation.
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"reportDate": "2026-07-28",
|
|
||||||
"codebaseName": "OpenFUT",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"submodulesCovered": [
|
|
||||||
"openfut-core",
|
|
||||||
"openfut-bridge",
|
|
||||||
"openfut-launcher"
|
|
||||||
],
|
|
||||||
"language": "Rust",
|
|
||||||
"framework": "Axum + SQLite"
|
|
||||||
},
|
|
||||||
"vulnerabilities": [
|
|
||||||
{
|
|
||||||
"severity": "critical",
|
|
||||||
"category": "authentication",
|
|
||||||
"file": "openfut-core/src/services/profile.rs",
|
|
||||||
"line": 8,
|
|
||||||
"cwe": "CWE-287",
|
|
||||||
"title": "Missing Authentication on All Endpoints",
|
|
||||||
"description": "No authentication or authorization checks on any API endpoint. The system uses single-profile design with get_active_profile() returning the first row (LIMIT 1) without any token validation, session management, or per-user isolation. In a networked context, any HTTP client can access all endpoints without credentials.",
|
|
||||||
"impact": "Complete compromise of data confidentiality and integrity. Any attacker can view, modify, or delete all user data without authentication.",
|
|
||||||
"exploitPath": "curl http://127.0.0.1:8080/clubs - accesses club data without any auth headers or tokens",
|
|
||||||
"recommendation": "Implement stateless JWT tokens or session-based authentication. Add middleware to validate tokens on all endpoints. Implement per-user authorization checks in services."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "critical",
|
|
||||||
"category": "injection",
|
|
||||||
"file": "openfut-core/src/routes/auth.rs",
|
|
||||||
"line": 87,
|
|
||||||
"cwe": "CWE-89",
|
|
||||||
"title": "SQL Injection via String Interpolation",
|
|
||||||
"description": "SQL table names are interpolated using string formatting: sqlx::query(&format!(\"DELETE FROM {table}\")). Although currently hardcoded in a loop, this violates parameterized query principles and creates a risk if the table list ever becomes user-controlled or the pattern is copied elsewhere.",
|
|
||||||
"impact": "Potential remote code execution via database manipulation. If extended to user input, attackers could modify arbitrary tables or drop the database.",
|
|
||||||
"exploitPath": "Currently mitigated by hardcoded table names, but the pattern is dangerous and violates secure coding practices.",
|
|
||||||
"recommendation": "Use SQLx's dynamic query builders or identifier types that properly escape table/column names. Replace format! string interpolation with sqlx::query_builder for dynamic identifiers."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "high",
|
|
||||||
"category": "configuration",
|
|
||||||
"file": "openfut-bridge/src/proxy.rs",
|
|
||||||
"line": 44,
|
|
||||||
"cwe": "CWE-295",
|
|
||||||
"title": "TLS Certificate Validation Disabled",
|
|
||||||
"description": "HTTP client explicitly disables TLS certificate validation: .danger_accept_invalid_certs(true). This bypasses all certificate pinning, expiration, and hostname verification, making the bridge vulnerable to man-in-the-middle attacks.",
|
|
||||||
"impact": "Attacker positioned between bridge and upstream can intercept, modify, or read all traffic. Compromises confidentiality and integrity of requests to Core and external services.",
|
|
||||||
"exploitPath": "MITM attack between openfut-bridge and openfut-core or upstream services. ARP spoofing on localhost subnet would redirect traffic.",
|
|
||||||
"recommendation": "Remove .danger_accept_invalid_certs(true) in production. If testing requires it, gate behind a development-only environment variable with strong warning. Use proper certificate management (CA bundles, cert pinning)."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "high",
|
|
||||||
"category": "dos",
|
|
||||||
"file": "openfut-core/src/services/season.rs",
|
|
||||||
"line": 23,
|
|
||||||
"cwe": "CWE-248",
|
|
||||||
"title": "Unguarded expect() Causes Denial of Service",
|
|
||||||
"description": "Multiple unchecked expect() calls that will panic and crash the server if database queries fail or return unexpected results: Ok(fetch(pool, profile_id).await?.expect(\"just inserted\"))",
|
|
||||||
"impact": "Denial of service. A single database inconsistency or race condition crashes the entire server, making the application unavailable.",
|
|
||||||
"exploitPath": "Trigger race conditions during concurrent requests (e.g., rapid profile deletion + season fetch). Database corruption or migration failure crashes the service immediately.",
|
|
||||||
"recommendation": "Replace expect() with proper error handling (Result types, error logging, graceful degradation). Handle database query failures without panicking. Add integration tests for race conditions."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "high",
|
|
||||||
"category": "dos",
|
|
||||||
"file": "openfut-core/src/services/season.rs",
|
|
||||||
"line": 69,
|
|
||||||
"cwe": "CWE-248",
|
|
||||||
"title": "Unguarded expect() in season fetch",
|
|
||||||
"description": "let season = fetch(pool, profile_id).await?.expect(\"season must exist\"); Panics if season is not found.",
|
|
||||||
"impact": "Server crash on missing or deleted season records.",
|
|
||||||
"exploitPath": "Delete a season via concurrent requests, then call /seasons endpoint. Server panics.",
|
|
||||||
"recommendation": "Return proper error (AppError::NotFound) instead of panicking."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "high",
|
|
||||||
"category": "dos",
|
|
||||||
"file": "openfut-core/src/services/season.rs",
|
|
||||||
"line": 144,
|
|
||||||
"cwe": "CWE-248",
|
|
||||||
"title": "Unguarded expect() in season update",
|
|
||||||
"description": "let updated = fetch(pool, profile_id).await?.expect(\"season must exist\");",
|
|
||||||
"impact": "Server crash on concurrent season modifications.",
|
|
||||||
"exploitPath": "Rapid concurrent season updates that fail race conditions.",
|
|
||||||
"recommendation": "Handle missing records gracefully."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "high",
|
|
||||||
"category": "cors",
|
|
||||||
"file": "openfut-core/src/app.rs",
|
|
||||||
"line": 257,
|
|
||||||
"cwe": "CWE-346",
|
|
||||||
"title": "Permissive CORS Configuration Allows All Origins",
|
|
||||||
"description": ".layer(CorsLayer::permissive()) enables CORS for all origins (*), methods, and headers. Any website can make cross-origin requests to the API and access/modify data.",
|
|
||||||
"impact": "Cross-site request forgery (CSRF) attacks. Malicious websites can issue API requests on behalf of users. Data exfiltration via JavaScript from any origin.",
|
|
||||||
"exploitPath": "Attacker website:\n <img src=\"http://127.0.0.1:8080/clubs\" />\n Fetch API calls to delete profiles, modify squads, etc.",
|
|
||||||
"recommendation": "Restrict CORS to specific origins (e.g., localhost:3000 for web UI, or the game process if exposed). Use CorsLayer::very_restrictive() as default and explicitly allowlist origins."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "high",
|
|
||||||
"category": "dos",
|
|
||||||
"file": "openfut-bridge/src/proxy.rs",
|
|
||||||
"line": 47,
|
|
||||||
"cwe": "CWE-248",
|
|
||||||
"title": "HTTP Client Construction Panic",
|
|
||||||
"description": ".expect(\"failed to build HTTP client\") will panic if the HTTP client fails to initialize, crashing the entire proxy service on startup.",
|
|
||||||
"impact": "Service unavailability. Bridge cannot start if HTTP client configuration is invalid.",
|
|
||||||
"exploitPath": "Invalid system configuration or missing TLS libraries causes HTTP client build to fail, crashing bridge during startup.",
|
|
||||||
"recommendation": "Return Result<ProxyState, Error> from new() and handle construction errors. Use anyhow::Context for better error messages."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "medium",
|
|
||||||
"category": "information-disclosure",
|
|
||||||
"file": "openfut-core/src/error.rs",
|
|
||||||
"line": 54,
|
|
||||||
"cwe": "CWE-209",
|
|
||||||
"title": "Error Messages Leak Implementation Details",
|
|
||||||
"description": "JSON parsing errors are returned directly to clients: format!(\"json parse error: {e}\"). Exposes serde_json parser internals and syntax details useful for crafting attacks.",
|
|
||||||
"impact": "Information disclosure. Attackers learn the JSON parser implementation and can tailor payloads to bypass validation or find parser-specific quirks.",
|
|
||||||
"exploitPath": "Send malformed JSON to any endpoint. Response includes parser error details (e.g., 'expected `,` at line 2 col 5') that aid in crafting exploits.",
|
|
||||||
"recommendation": "Return generic error message to clients: 'invalid request format'. Log detailed errors internally with tracing for debugging."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "medium",
|
|
||||||
"category": "information-disclosure",
|
|
||||||
"file": "openfut-core/src/error.rs",
|
|
||||||
"line": 40,
|
|
||||||
"cwe": "CWE-215",
|
|
||||||
"title": "Database Errors Logged with Full Details",
|
|
||||||
"description": "Database errors are logged with full SQL/query details: tracing::error!(\"Database error: {e}\"). If logs are exposed or compromised, schema, query patterns, and data structure are revealed.",
|
|
||||||
"impact": "Information disclosure in logs. Compromised log files expose database schema and query logic useful for SQL injection or data exfiltration planning.",
|
|
||||||
"exploitPath": "Access server logs (via log aggregation service, file access, etc.) and extract database schema and query patterns.",
|
|
||||||
"recommendation": "Log only error type and ID to clients. Sanitize logs before exporting. Use structured logging with field masking for queries."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "medium",
|
|
||||||
"category": "input-validation",
|
|
||||||
"file": "openfut-core/src/routes/auth.rs",
|
|
||||||
"line": 17,
|
|
||||||
"cwe": "CWE-1025",
|
|
||||||
"title": "Hardcoded Default Credentials",
|
|
||||||
"description": "Default username 'Player 1' is hardcoded with no unique identifier enforcement. Multiple profiles can be created with identical usernames, and weak defaults are used.",
|
|
||||||
"impact": "Weak account creation, potential for account confusion or conflicts. No strong identity guarantees.",
|
|
||||||
"exploitPath": "Multiple users create profiles with default 'Player 1' username. No way to distinguish profiles programmatically.",
|
|
||||||
"recommendation": "Require explicit username on profile creation. Use UUIDs as primary identifiers. Validate username uniqueness and minimum length."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "medium",
|
|
||||||
"category": "input-validation",
|
|
||||||
"file": "openfut-core/src/services/",
|
|
||||||
"line": 0,
|
|
||||||
"cwe": "CWE-400",
|
|
||||||
"title": "Missing Input Length Validation",
|
|
||||||
"description": "No maximum length checks on string fields (usernames, club names, squad names, etc.). Large inputs can cause database bloat, memory exhaustion, or DoS.",
|
|
||||||
"impact": "Denial of service via large payloads. Database bloat. Memory exhaustion. While DefaultBodyLimit::max(256KB) provides some protection, field-level validation is missing.",
|
|
||||||
"exploitPath": "POST /auth/local with username = 256KB string. Database receives bloated data. Repeated calls exhaust storage.",
|
|
||||||
"recommendation": "Add input validation for all user-submitted strings. Set maximum lengths (e.g., username: 50 chars, club name: 100 chars). Validate at route handler level."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "medium",
|
|
||||||
"category": "configuration",
|
|
||||||
"file": "openfut-core/src/db.rs",
|
|
||||||
"line": 13,
|
|
||||||
"cwe": "CWE-315",
|
|
||||||
"title": "Unencrypted SQLite Database on Disk",
|
|
||||||
"description": "SQLite database file (openfut.db) is stored unencrypted on disk. All user data, profiles, squads, cards, etc., are readable by anyone with filesystem access.",
|
|
||||||
"impact": "Data breach if server filesystem is compromised. No protection against:local file access, stolen backups, forensic recovery.",
|
|
||||||
"exploitPath": "Attacker gains filesystem access (compromised server, stolen disk). Reads openfut.db directly. All game data is readable without authentication.",
|
|
||||||
"recommendation": "Use SQLite encryption (e.g., sqlcipher crate) or migrate to PostgreSQL with TLS. Implement file-level encryption. Use restrictive filesystem permissions (0600)."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "medium",
|
|
||||||
"category": "rate-limiting",
|
|
||||||
"file": "openfut-core/src/app.rs",
|
|
||||||
"line": 0,
|
|
||||||
"cwe": "CWE-770",
|
|
||||||
"title": "No Rate Limiting on Endpoints",
|
|
||||||
"description": "No per-IP or per-user rate limiting. Endpoints like POST /auth/reset can be called repeatedly without restriction, allowing attackers to repeatedly wipe all data.",
|
|
||||||
"impact": "Denial of service and data destruction. Attacker can spam /auth/reset to destroy user data or exhaust server resources.",
|
|
||||||
"exploitPath": "for i in 1..1000: POST /auth/reset with confirm='reset'. All data wiped repeatedly.",
|
|
||||||
"recommendation": "Implement rate limiting middleware using tower_governor or similar. Add per-IP limits (e.g., 10 requests/min) and per-endpoint limits. Use exponential backoff."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "low",
|
|
||||||
"category": "audit-logging",
|
|
||||||
"file": "openfut-core/src/services/",
|
|
||||||
"line": 0,
|
|
||||||
"cwe": "CWE-778",
|
|
||||||
"title": "Missing Audit Logging",
|
|
||||||
"description": "No audit trail of user actions (profile creation, data deletion, squad modifications). Cannot detect unauthorized access, data tampering, or compliance violations.",
|
|
||||||
"impact": "Incident response and forensics are impossible. Cannot determine who did what and when. Compliance risks (GDPR, etc.).",
|
|
||||||
"exploitPath": "Attacker deletes all profiles, modifies squads. No audit log shows what happened or who did it.",
|
|
||||||
"recommendation": "Add audit logging for all data mutations. Log: timestamp, user (profile) ID, action, resource affected, before/after state. Store in separate immutable table."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "low",
|
|
||||||
"category": "dependencies",
|
|
||||||
"file": "openfut-bridge/Cargo.toml",
|
|
||||||
"line": 0,
|
|
||||||
"cwe": "CWE-1035",
|
|
||||||
"title": "Older Dependency Versions (reqwest, rustls)",
|
|
||||||
"description": "openfut-bridge uses reqwest 0.11 (latest is 0.12) and rustls 0.21 (latest is 0.23). Intentional for version matching, but creates a larger surface area for known CVEs.",
|
|
||||||
"impact": "Potential vulnerabilities in older dependencies. Delayed access to security patches.",
|
|
||||||
"exploitPath": "Known CVE in reqwest 0.11 or rustls 0.21 could be exploited. Combined with danger_accept_invalid_certs, TLS bypass becomes easier.",
|
|
||||||
"recommendation": "Upgrade dependencies to latest versions when possible. Monitor CVE databases (CVE, RustSec) for the versions in use. Pin versions and set up automated dependency updates."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"severity": "low",
|
|
||||||
"category": "error-handling",
|
|
||||||
"file": "openfut-core/src/app.rs",
|
|
||||||
"line": 256,
|
|
||||||
"cwe": "CWE-248",
|
|
||||||
"title": "Body Size Limit Without Per-Field Validation",
|
|
||||||
"description": "DefaultBodyLimit::max(256KB) limits the entire request body, but individual fields are not validated. A single large field can consume most of the limit.",
|
|
||||||
"impact": "Mild DoS. Large field values cause database bloat. Not a critical issue due to body limit, but field-level validation would be better.",
|
|
||||||
"exploitPath": "POST /auth/local with 250KB club_name field. Database receives bloated data.",
|
|
||||||
"recommendation": "Add per-field validation in addition to body limits. Validate and sanitize fields before database insertion."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"riskScore": 82,
|
|
||||||
"riskCategory": "CRITICAL",
|
|
||||||
"riskSummary": "OpenFUT has critical security issues that would make it unsafe for production or networked deployment. The most severe are the complete absence of authentication/authorization and the SQL injection pattern in the auth.rs module. The system is designed as single-player (single-profile) with no multi-tenant isolation, which is dangerous if exposed to the network.",
|
|
||||||
"recommendations": [
|
|
||||||
{
|
|
||||||
"priority": "CRITICAL",
|
|
||||||
"area": "Authentication & Authorization",
|
|
||||||
"recommendation": "Implement JWT-based or session-based authentication on all endpoints. Add middleware to validate auth tokens on every request. Implement per-profile authorization checks. Currently any HTTP client can access all endpoints.",
|
|
||||||
"effort": "High",
|
|
||||||
"impact": "Blocks all data breaches from unauthenticated access"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "CRITICAL",
|
|
||||||
"area": "SQL Injection Prevention",
|
|
||||||
"recommendation": "Replace sqlx::query(&format!(...)) in auth.rs:87 with proper parameterized identifiers. Use sqlx::query_builder for dynamic table/column names instead of string interpolation.",
|
|
||||||
"effort": "Low",
|
|
||||||
"impact": "Prevents SQL injection even if pattern is copied to user input"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "HIGH",
|
|
||||||
"area": "TLS & Transport Security",
|
|
||||||
"recommendation": "Remove .danger_accept_invalid_certs(true) from proxy.rs:44. If development requires it, gate behind an environment variable (e.g., DEV_SKIP_TLS_VERIFICATION) with strong warnings in logs.",
|
|
||||||
"effort": "Low",
|
|
||||||
"impact": "Prevents MITM attacks on bridge-to-core communication"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "HIGH",
|
|
||||||
"area": "Error Handling",
|
|
||||||
"recommendation": "Replace all expect() calls with proper Result handling. Use anyhow::Context or custom error types. Add logging for debugging but return generic errors to clients.",
|
|
||||||
"effort": "Medium",
|
|
||||||
"impact": "Prevents DoS via server panics"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "HIGH",
|
|
||||||
"area": "CORS",
|
|
||||||
"recommendation": "Replace CorsLayer::permissive() with CorsLayer::very_restrictive() or explicit allowlist. For single-player use, restrict to localhost and the game process only.",
|
|
||||||
"effort": "Low",
|
|
||||||
"impact": "Prevents CSRF and cross-origin attacks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "HIGH",
|
|
||||||
"area": "Rate Limiting",
|
|
||||||
"recommendation": "Add per-IP rate limiting using tower_governor or similar. Implement limits on destructive endpoints (e.g., POST /auth/reset: 1 request per hour per IP).",
|
|
||||||
"effort": "Medium",
|
|
||||||
"impact": "Prevents DoS and repeated data destruction"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "MEDIUM",
|
|
||||||
"area": "Input Validation",
|
|
||||||
"recommendation": "Add maximum length validation for all string fields (username, club_name, squad_name, etc.). Enforce at route handler level. Example: username max 50 chars, club_name max 100 chars.",
|
|
||||||
"effort": "Medium",
|
|
||||||
"impact": "Prevents database bloat and data validation failures"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "MEDIUM",
|
|
||||||
"area": "Data Encryption",
|
|
||||||
"recommendation": "Use SQLite encryption (sqlcipher) or migrate to PostgreSQL with TLS. Set restrictive filesystem permissions (0600) on openfut.db.",
|
|
||||||
"effort": "High",
|
|
||||||
"impact": "Protects data at rest from filesystem access"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "MEDIUM",
|
|
||||||
"area": "Error Message Handling",
|
|
||||||
"recommendation": "Return generic error messages to clients. Log detailed errors internally. Example: client sees 'invalid request', server logs 'JSON parse error: expected `,` at line 2'.",
|
|
||||||
"effort": "Low",
|
|
||||||
"impact": "Reduces information disclosure"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "MEDIUM",
|
|
||||||
"area": "Audit Logging",
|
|
||||||
"recommendation": "Add audit trail for all data mutations (create, update, delete). Log timestamp, profile ID, action, resource, and before/after state. Store in immutable audit_log table.",
|
|
||||||
"effort": "Medium",
|
|
||||||
"impact": "Enables incident response and forensics"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "LOW",
|
|
||||||
"area": "Dependency Management",
|
|
||||||
"recommendation": "Upgrade reqwest to 0.12 and rustls to 0.23 when possible. Set up Dependabot or RustSec monitoring for CVEs. Regularly audit dependencies.",
|
|
||||||
"effort": "Low",
|
|
||||||
"impact": "Reduces attack surface from known CVEs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"priority": "LOW",
|
|
||||||
"area": "Default Values",
|
|
||||||
"recommendation": "Remove hardcoded default username 'Player 1'. Require explicit username on profile creation. Use UUIDs for profile identification.",
|
|
||||||
"effort": "Low",
|
|
||||||
"impact": "Improves account identity and prevents confusion"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"securityDesignNotes": {
|
|
||||||
"intendedUse": "OpenFUT is designed for single-player offline use. Single-profile design is intentional for local FIFA 23 emulation.",
|
|
||||||
"deploymentContext": "Localhost only (127.0.0.1:8080). Not intended for networked or multi-user deployment.",
|
|
||||||
"implicationForSecurity": "Many security issues (no auth, permissive CORS) are acceptable for localhost-only use. However, the code structure lacks security boundaries, so if ever exposed to the network, it would be completely unsecured. Recommend adding security gates now rather than retrofitting later.",
|
|
||||||
"suggestedDefensiveApproach": "Even for single-player use, add security layers (basic auth, CORS restrictions, rate limiting) to prevent accidental misuse if deployed in an unsafe context."
|
|
||||||
},
|
|
||||||
"positiveFindingsAndStrengths": [
|
|
||||||
"✓ SQLx is used throughout with parameterized queries (except auth.rs:87)",
|
|
||||||
"✓ Foreign key constraints are enforced in SQLite",
|
|
||||||
"✓ UUIDs are used for entity IDs instead of sequential IDs (reduces enumeration attacks)",
|
|
||||||
"✓ Request body size is limited to 256KB (prevents large payload DoS)",
|
|
||||||
"✓ Concurrency is limited to 256 concurrent requests",
|
|
||||||
"✓ Sensitive tokens (X-UT-SID, X-UT-PHISHING-TOKEN) are stripped from captures",
|
|
||||||
"✓ Logging is structured using tracing crate (good for audit trails)",
|
|
||||||
"✓ Services layer properly encapsulates database access"
|
|
||||||
],
|
|
||||||
"testingRecommendations": [
|
|
||||||
"Add integration tests for authentication bypass (attempt to access endpoints without tokens)",
|
|
||||||
"Test SQL injection payloads in auth.rs:87 pattern (if table names become dynamic)",
|
|
||||||
"Test CORS with cross-origin requests from external origins",
|
|
||||||
"Test rate limiting with rapid concurrent requests to /auth/reset",
|
|
||||||
"Test input validation with oversized strings (100MB+ usernames)",
|
|
||||||
"Test panic handling with corrupted database state",
|
|
||||||
"Test TLS MITM scenarios (certificate pinning validation)",
|
|
||||||
"Add fuzz testing for JSON parsing to find edge cases"
|
|
||||||
],
|
|
||||||
"complianceNotes": {
|
|
||||||
"gdpr": "No explicit data handling policy. If user data is processed, GDPR requires consent, data retention limits, and audit trails. Not currently implemented.",
|
|
||||||
"dataProtection": "Unencrypted database at rest violates most data protection frameworks.",
|
|
||||||
"logging": "Audit logging is missing, violating compliance requirements."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
Submodule fifa-blaze updated: d2a9a01ec9...eccd46f52b
@@ -9,5 +9,3 @@
|
|||||||
*.log
|
*.log
|
||||||
__pycache__/
|
__pycache__/
|
||||||
captures/
|
captures/
|
||||||
staging/
|
|
||||||
tools/fifa17_profile.json
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# Raw /proc/PID/mem captures: regenerate with tools/db_dump.py, never commit.
|
|
||||||
# One of these directories reached 2.3GB.
|
|
||||||
data/memdump/*.bin
|
|
||||||
data/memdump/*.raw
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
state/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Copy to .env in this directory. Required for remote deployment.
|
||||||
|
#
|
||||||
|
# OPENFUT_ADVERTISE — the address of THIS host as seen from the game machine
|
||||||
|
# (105). The responders advertise it to the client for every next hop (Blaze,
|
||||||
|
# roster, UTAS, POW). Compose refuses to start without it.
|
||||||
|
OPENFUT_ADVERTISE=10.10.0.120
|
||||||
|
|
||||||
|
# OPENFUT_BIND — address the listeners bind inside the container.
|
||||||
|
# Defaults to 0.0.0.0 (container-facing); the original all-on-localhost flow
|
||||||
|
# uses the loopback default baked into the responders when unset.
|
||||||
|
OPENFUT_BIND=0.0.0.0
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# OpenFUT FIFA-17 FUT backend — Python migration deployment (fifa17-python/).
|
||||||
|
#
|
||||||
|
# Runs the 5 network responders (LSX / Blaze / roster / UTAS / POW) that FIFA 17
|
||||||
|
# dials to reach the FUT hub. Pure-Python; the only third-party dep is
|
||||||
|
# pycryptodome (LSX AES handshake). autopatch.py is intentionally NOT run here —
|
||||||
|
# it patches the game process memory and belongs on the client (105).
|
||||||
|
#
|
||||||
|
# Build context is this directory (fifa17-python/): tools/ and data/ are the
|
||||||
|
# authoritative deployment sources, staged from the frozen baseline image
|
||||||
|
# openfut-fut-backend:python-baseline-2026-08-10 (see docs/BASELINE-*.md). A
|
||||||
|
# SHA256SUMS.txt is baked into the image so any running backend can be matched
|
||||||
|
# to the exact dataset it was built from.
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir pycryptodome==3.20.0
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY tools/ /app/tools/
|
||||||
|
COPY data/ /app/data/
|
||||||
|
|
||||||
|
# Redirector TLS cert (CN/SAN = winter15.gosredirector.ea.com). ProtoSSL
|
||||||
|
# cert-verify is patched client-side, so a self-signed cert is fine. The staged
|
||||||
|
# pair is git-ignored (*.pem/*.key); regenerate if absent so a fresh checkout
|
||||||
|
# builds without extra steps.
|
||||||
|
RUN if [ ! -s tools/redir_cert.pem ] || [ ! -s tools/redir_key.pem ]; then \
|
||||||
|
apt-get update && apt-get install -y --no-install-recommends openssl && \
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||||
|
-keyout tools/redir_key.pem -out tools/redir_cert.pem \
|
||||||
|
-days 3650 -subj "/CN=winter15.gosredirector.ea.com" \
|
||||||
|
-addext "subjectAltName=DNS:winter15.gosredirector.ea.com,DNS:*.gosredirector.ea.com,DNS:*.ea.com" && \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bake a dataset manifest so every image is self-identifying.
|
||||||
|
RUN find /app/tools /app/data -type f | LC_ALL=C sort | xargs sha256sum > /app/SHA256SUMS.txt
|
||||||
|
|
||||||
|
COPY entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
# LSX 4216 | Blaze redir/main/nucleus 42127/42130/42131 | roster 8081 | UTAS 8099 | POW 8094/8080
|
||||||
|
EXPOSE 4216 42127 42130 42131 8081 8099 8094 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# OpenFUT FIFA-17 — CLIENT-side arming (runs on the GAME machine, e.g. 105).
|
||||||
|
#
|
||||||
|
# Companion to the dev container on the SERVER (120). The server runs the heavy
|
||||||
|
# responders (Blaze / UTAS / roster / POW). Two pieces are inherently local to
|
||||||
|
# the game and therefore stay here:
|
||||||
|
#
|
||||||
|
# * autopatch.py — patches FIFA17.exe process memory (ProtoSSL cert-verify).
|
||||||
|
# Must run where the game runs; cannot be containerised.
|
||||||
|
# * lsx_responder — the Origin/EADesktop emulator the game dials on the
|
||||||
|
# hardcoded loopback 127.0.0.1:4216. Loopback IPC can't be
|
||||||
|
# cleanly redirected to a remote host, so it lives here.
|
||||||
|
#
|
||||||
|
# Everything the game reaches by a routable address is redirected to the server:
|
||||||
|
# * winter15.gosredirector.ea.com (hardcoded EA IP 159.153.51.20) -> SERVER:42127
|
||||||
|
# * easw.easports.com (dead hardcoded UTAS host) -> SERVER (:8099)
|
||||||
|
#
|
||||||
|
# The server's responders were started with OPENFUT_ADVERTISE=<SERVER_IP>, so
|
||||||
|
# after these first redirected contacts the game is handed <SERVER_IP> for every
|
||||||
|
# later hop (Blaze main, roster, UTAS, telemetry) and dials the server directly.
|
||||||
|
#
|
||||||
|
# Usage: sudo OPENFUT_SERVER=10.10.0.120 ./client_arm.sh
|
||||||
|
# (re-run after every reboot; the sysctl/iptables state is volatile)
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVER="${OPENFUT_SERVER:?set OPENFUT_SERVER to the backend host IP, e.g. 10.10.0.120}"
|
||||||
|
GOS_EA_IP="159.153.51.20" # winter15.gosredirector.ea.com (hardcoded in FIFA17)
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "!! must run as root (sudo). Re-run: sudo OPENFUT_SERVER=$SERVER $0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[client_arm] backend server = $SERVER"
|
||||||
|
|
||||||
|
# 1) allow /proc/PID/mem writes (autopatch's ProtoSSL cert-verify patch)
|
||||||
|
sysctl -q kernel.yama.ptrace_scope=0
|
||||||
|
|
||||||
|
# 2) Redirect the hardcoded Blaze redirector IP to the server's redirector.
|
||||||
|
# (Replace any stale rule first so re-runs and IP changes are clean.)
|
||||||
|
while iptables -t nat -D OUTPUT -p tcp -d "$GOS_EA_IP" -j DNAT \
|
||||||
|
--to-destination "$SERVER:42127" 2>/dev/null; do :; done
|
||||||
|
iptables -t nat -A OUTPUT -p tcp -d "$GOS_EA_IP" -j DNAT --to-destination "$SERVER:42127"
|
||||||
|
|
||||||
|
# 2b) DNAT from OUTPUT to a REMOTE host needs a matching source-NAT on the way
|
||||||
|
# out, or the server's replies (from its own IP) won't match the game's
|
||||||
|
# conntrack entry. MASQUERADE the redirected flow so it is SNAT'd to this
|
||||||
|
# host's outbound IP. (Harmless duplicate-guarded like the DNAT above.)
|
||||||
|
while iptables -t nat -D POSTROUTING -p tcp -d "$SERVER" --dport 42127 \
|
||||||
|
-j MASQUERADE 2>/dev/null; do :; done
|
||||||
|
iptables -t nat -A POSTROUTING -p tcp -d "$SERVER" --dport 42127 -j MASQUERADE
|
||||||
|
|
||||||
|
# 3) Point the dead hardcoded UTAS host at the server. The port (8099) is carried
|
||||||
|
# in the game's own URL, so only the name needs redirecting. Remove any prior
|
||||||
|
# OpenFUT-managed line (loopback or other server) and write the current one.
|
||||||
|
sed -i '/[[:space:]]easw\.easports\.com\b.*# openfut$/d' /etc/hosts
|
||||||
|
printf '%s\teasw.easports.com\t# openfut\n' "$SERVER" >> /etc/hosts
|
||||||
|
|
||||||
|
echo "[client_arm] --- armed ---"
|
||||||
|
sysctl kernel.yama.ptrace_scope
|
||||||
|
iptables -t nat -L OUTPUT -n | grep -i "$GOS_EA_IP" || echo " (DNAT missing!)"
|
||||||
|
grep 'easw.easports.com' /etc/hosts && echo " /etc/hosts ok" || echo " (/etc/hosts easw missing!)"
|
||||||
|
echo
|
||||||
|
echo "[client_arm] Next: start the LOCAL pieces (LSX + autopatch) with client_local.sh,"
|
||||||
|
echo " ensure the container is up on $SERVER, then launch FIFA 17."
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user