750d6c2e18
The launcher spawned `python3 lsx_responder_v2.py` and `python3 autopatch.py`. Both are
now Rust workspace crates, and the launcher spawns the binaries (gitlink 1cd4f18).
openfut-lsx (2244 lines, 57 tests) — EA Origin LSX emulator on loopback 4216.
Dependency-light on purpose: `aes` for the one security-shaped primitive, parking_lot
per the project lock rule. AES-128-ECB is the whole cipher requirement, so the
surrounding framing (PKCS7, lowercase hex, NUL-termination) stays explicit and separate
because it is protocol, not cryptography.
openfut-autopatch (43 tests) — ProtoSSL cert gates plus the CardsDLL store patches,
applied over /proc/<pid>/mem. Deliberately dependency-free: a tool that writes another
process's memory should be auditable end to end without a dependency tree. std has no
getuid and no local-time formatting, so it carries a small TZif reader rather than
pulling in chrono to reproduce Python's strftime('%H:%M:%S').
The Python remains in fifa17-recon/tools. It is NOT dead: the docker entrypoint,
client_arm.sh, the runbooks and test_autopatch_guard.py still use it. Only the
launcher's dependency on Python is gone, which is what was asked for; deleting the
recon toolchain's implementation would have broken unrelated workflows.
VERIFICATION — the ports are checked against the Python, not against themselves:
* Crypto parity across THREE implementations. The Rust tests assert the Rust's own
constants, which proves consistency, not parity, and the Python cannot run here
(pycryptodome absent) with the client host unreachable. So the LCG and key derivation
were transcribed from the Python and run as plain arithmetic, and every AES value came
from the openssl CLI. All agree: msvcr_rand(7)==61, _TAIL_CONST
954f64f2e4e86e9eee82d20216684899, the 96-hex emu challenge shape, the derived session
key 6a9da3e78615153cc2f10eec25ae6382, the framing rule at both boundaries (an aligned
payload gains a whole block), and the port's pinned 4-block login-frame ciphertext.
* LSX end to end on the real port. 4216 here is a docker forward into the production
netns, so the smoke test runs under `unshare -n` — the real binary on the port the
client actually dials, with no port-override hack and no risk to production. A
hand-written client read the unprompted <Challenge>, completed the handshake, and
decrypted the GetProfileResponse (PersonaId 33068179, Persona CAGE) with a session key
derived INDEPENDENTLY of the Rust, then observed the Login pushes across all three
candidate senders.
* autopatch behaviourally. The startup banner, the --launcher-pid watchdog exiting with
the exact Python message, dual stdout+logfile output, and a missing value rejected
with Python's own "invalid --launcher-pid". The subagent additionally cross-checked
every constant by executing the Python module and drove the binary against a synthetic
client (correct comm, a CardsDLL mapping, gates mmapped at their absolute VAs),
confirming all eleven patches byte-exact in table order.
* The `[store-guard] verified capability …` line is byte-identical to openfut-launcher's
own parser fixture, so backend capability registration still works.
Workspace builds; openfut-lsx 57, openfut-autopatch 43, openfut-launcher 74 tests green.
110 lines
4.6 KiB
Rust
110 lines
4.6 KiB
Rust
//! OpenFUT clean-room LSX responder for FIFA 17 -- Rust port of
|
|
//! `fifa17-recon/tools/lsx_responder_v2.py` (v2, EVENT-PUSHING).
|
|
//!
|
|
//! v1 (`lsx_responder.py`) was REQUEST-DRIVEN ONLY. It answered every verb the
|
|
//! client asked for and never sent an unsolicited frame. That is exactly why the
|
|
//! client never issued GetAuthCode and never sent Blaze Authentication::login.
|
|
//!
|
|
//! THE ORIGIN SDK HAS TWO INDEPENDENT FLAGS, FED BY TWO DIFFERENT MECHANISMS:
|
|
//!
|
|
//! 1. "internet is reachable" -> OriginMgr online byte [0x1448a3ac0],
|
|
//! fed by the REQUEST verb `GetInternetConnectedState -> connected="1"`
|
|
//! (v1 already beat this; live-confirmed == 1).
|
|
//! 2. "a user is LOGGED IN" -> `OriginMgr.m_isLoggedIn` [OriginMgr+0x13],
|
|
//! fed ONLY by a server-PUSHED `<Event sender="LOGIN_EVENT"><Login/>`.
|
|
//! There is NO request verb that can set it.
|
|
//!
|
|
//! v1 fed (1) and never fed (2), so `m_isLoggedIn` was 0 for the whole session,
|
|
//! FIFA never enqueued an auth-code request into FirstPartyAuthTokenRetriever
|
|
//! (both request slots live-read as 0x0), DoTick @0x146f199c0 exited immediately,
|
|
//! OriginRequestAuthCodeSync @0x1470db3c0 was never called, LoginRequest.AUTH
|
|
//! could never be filled -> no Blaze login -> "Unable to retrieve account
|
|
//! information."
|
|
//!
|
|
//! The binary evidence for the push (dispatcher @0x146f1e060 case 2, the `<Login>`
|
|
//! matcher @0x147102880 and its silent sender strcmp, the service-name tables at
|
|
//! 0x144341420 / sdk+0x3b0, the `strcmp(v,"false")` truthiness of `IsLoggedIn`)
|
|
//! lives in the Python's module docstring; the specific facts that constrain code
|
|
//! are repeated at the site that depends on them. Nothing here is derived from
|
|
//! the 2021 EA/FIFA leak: every constant came from our own static+dynamic
|
|
//! analysis of binaries we own plus traffic we captured ourselves.
|
|
//!
|
|
//! Transport: TCP `127.0.0.1:4216`, every message NUL-terminated (send strlen+1).
|
|
|
|
pub mod crypto;
|
|
pub mod events;
|
|
pub mod identity;
|
|
pub mod protocol;
|
|
|
|
/// Loopback port the Steampunks Origin emulator stub binds; we must own it
|
|
/// BEFORE FIFA 17 starts so the stub's own `bind()` fails.
|
|
pub const LSX_PORT: u16 = 4216;
|
|
|
|
/// Emu's own advertised challenge (any 32 hex chars work; the client echoes it
|
|
/// back).
|
|
pub const CHALLENGE_KEY: &str = "2b8ee7faea76e8a34f5f5d20e5328e32";
|
|
pub const BUILD: &str = "release";
|
|
pub const VERSION: &str = "10,4,13,6637";
|
|
|
|
/// Success-signal files the run's watch steps poll. Written only from a REAL
|
|
/// GetAuthCode on a live connection.
|
|
pub const AUTHCODE_FILE: &str = "/tmp/openfut_authcode.txt";
|
|
pub const CLIENTID_FILE: &str = "/tmp/openfut_lsx_clientid.txt";
|
|
|
|
/// How the modules read environment knobs. Indirected through a closure so the
|
|
/// tests can pin an environment without mutating the process (env vars are
|
|
/// process-global and `cargo test` runs threads in parallel).
|
|
pub type Env<'a> = &'a dyn Fn(&str) -> Option<String>;
|
|
|
|
/// The real environment. Present-but-empty is Some(""), which matters: the
|
|
/// Python's `os.environ.get(...)` returns "" for `FOO=`, and "" is not None, so
|
|
/// an empty setting WINS over the built-in default.
|
|
pub fn os_env(name: &str) -> Option<String> {
|
|
std::env::var_os(name).map(|v| v.to_string_lossy().into_owned())
|
|
}
|
|
|
|
/// The Python's recurring `os.environ.get(NAME, "0") != "0"` rule: ANY value
|
|
/// other than the literal "0" enables the knob (including the empty string).
|
|
pub fn env_flag(env: Env<'_>, name: &str, default_on: bool) -> bool {
|
|
match env(name) {
|
|
Some(v) => v != "0",
|
|
None => default_on,
|
|
}
|
|
}
|
|
|
|
/// An env knob was set to something the Python's own `int()`/`float()` would
|
|
/// reject. The Python dies at import in that case; we refuse to start for the
|
|
/// same reason -- a silently different persona, or a silently absent heartbeat,
|
|
/// is worse than a loud failure.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ConfigError {
|
|
pub var: &'static str,
|
|
pub value: String,
|
|
pub expected: &'static str,
|
|
}
|
|
|
|
impl std::fmt::Display for ConfigError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"{} must be {} (got {:?})",
|
|
self.var, self.expected, self.value
|
|
)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ConfigError {}
|
|
|
|
/// Every diagnostic line carries the `[lsx] ` prefix and is flushed per line:
|
|
/// the launcher pipes our stdout+stderr into its log buffer and parses some of
|
|
/// these lines, so the formats are a contract.
|
|
#[macro_export]
|
|
macro_rules! log {
|
|
($($arg:tt)*) => {{
|
|
use std::io::Write;
|
|
let mut out = std::io::stdout().lock();
|
|
let _ = writeln!(out, "[lsx] {}", format_args!($($arg)*));
|
|
let _ = out.flush();
|
|
}};
|
|
}
|