Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28773e7cf1 | |||
| 3ae5587a38 | |||
| 70a64e3709 |
@@ -27,3 +27,6 @@ __pycache__/
|
||||
# OS
|
||||
.DS_Store
|
||||
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
|
||||
__pycache__/
|
||||
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