Files
openfut-launcher/openfut-hook/src/ea_stub.rs
T
funman300 87241acc1a feat(hook): expand IAT hook coverage with TLS bypass, connect/recv hooks, and logging
Adds connect_hook, connectex_hook, recv_hook, ssl_patch, tls_bypass, lsx, ea_stub,
and origin_spy modules to intercept EA's TLS and socket layers in addition to
getaddrinfo. Adds DLL-level logging to C:\openfut_hook.log for debugging. Also
patches windows-sys feature flags to include Cryptography and Threading APIs needed
by the new hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 10:36:17 -07:00

180 lines
6.9 KiB
Rust

/// In-process LSX server (port 3216 / EA App Local Services Exchange).
///
/// Runs in a background thread inside FIFA's process so Wine's wineserver
/// routes FIFA's connect() directly here without needing any external process.
///
/// Protocol: server speaks first (sends XML greeting with challenge key),
/// then both sides do an AES-128-ECB challenge/response handshake, then
/// all subsequent messages are AES-128-ECB encrypted.
use windows_sys::Win32::Networking::WinSock::{
WSAStartup, WSACleanup, socket, bind, listen, accept, recv, send,
closesocket, setsockopt,
WSADATA, SOCKADDR, SOCKET, SOCKET_ERROR, INVALID_SOCKET,
AF_INET, SOCK_STREAM, IPPROTO_TCP, SOMAXCONN,
SO_REUSEADDR, SOL_SOCKET,
};
const PORT: u16 = 3216;
const GREETING_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb";
fn server_loop() {
unsafe {
let mut wsa = core::mem::zeroed::<WSADATA>();
if WSAStartup(0x0202, &mut wsa) != 0 {
crate::write_log("ea_stub: WSAStartup failed\n");
return;
}
let srv = socket(AF_INET as i32, SOCK_STREAM, IPPROTO_TCP as i32);
if srv == INVALID_SOCKET {
crate::write_log("ea_stub: socket() failed\n");
WSACleanup();
return;
}
let yes: i32 = 1;
setsockopt(srv, SOL_SOCKET as i32, SO_REUSEADDR, &yes as *const i32 as *const u8, 4);
// sockaddr_in: sin_family(u16-LE) + sin_port(u16-BE) + sin_addr(u32) + padding
let mut addr = [0u8; 16];
let family = AF_INET as u16;
addr[0] = (family & 0xFF) as u8;
addr[1] = (family >> 8) as u8;
addr[2] = (PORT >> 8) as u8;
addr[3] = (PORT & 0xFF) as u8;
if bind(srv, addr.as_ptr() as *const SOCKADDR, addr.len() as i32) == SOCKET_ERROR {
crate::write_log("ea_stub: bind() failed — port 3216 in use\n");
closesocket(srv);
WSACleanup();
return;
}
listen(srv, SOMAXCONN as i32);
crate::write_log("ea_stub: listening on port 3216\n");
loop {
crate::write_log("ea_stub: calling accept...\n");
let client = accept(srv, core::ptr::null_mut(), core::ptr::null_mut());
if client == INVALID_SOCKET {
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
let e = WSAGetLastError();
crate::write_log(&format!("ea_stub: accept FAILED wsa_err={e}\n"));
break;
}
crate::write_log("ea_stub: connection accepted\n");
handle_lsx(client);
}
closesocket(srv);
WSACleanup();
}
}
unsafe fn lsx_send(sock: SOCKET, msg: &str) -> bool {
// LSX messages are null-terminated
let mut buf = msg.as_bytes().to_vec();
buf.push(0);
let n = send(sock, buf.as_ptr(), buf.len() as i32, 0);
if n == SOCKET_ERROR {
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
let e = WSAGetLastError();
crate::write_log(&format!("ea_stub: send FAILED wsa_err={e}\n"));
false
} else {
crate::write_log(&format!("ea_stub: sent {n} bytes\n"));
true
}
}
unsafe fn lsx_recv(sock: SOCKET) -> Option<String> {
let mut buf = vec![0u8; 8192];
let n = recv(sock, buf.as_mut_ptr(), buf.len() as i32, 0);
if n <= 0 {
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
let e = WSAGetLastError();
crate::write_log(&format!("ea_stub: recv returned {n} wsa_err={e}\n"));
return None;
}
let text = String::from_utf8_lossy(&buf[..n as usize])
.trim_matches('\0')
.to_string();
crate::write_log(&format!("ea_stub: recv {n} bytes: {}\n", &text[..text.len().min(300)]));
Some(text)
}
unsafe fn handle_lsx(sock: SOCKET) {
// ── 1. Send greeting (server speaks first) ────────────────────────────
let greeting = format!(
"<LSX>\r\n <Event sender=\"EALS\">\r\n <Challenge build=\"release\" key=\"{GREETING_KEY}\" version=\"10,5,30,15625\" />\r\n </Event>\r\n</LSX>"
);
crate::write_log("ea_stub: sending LSX greeting\n");
if !lsx_send(sock, &greeting) {
closesocket(sock);
return;
}
// ── 2. Receive FIFA's ChallengeResponse ───────────────────────────────
let challenge_xml = match lsx_recv(sock) {
Some(s) => s,
None => { closesocket(sock); return; }
};
// Parse: split on '"' — EAappEmulater style
// <Request id="N" ...><ChallengeResponse ... response="HEX" key="HEX">
let parts: Vec<&str> = challenge_xml.split('"').collect();
let id = parts.get(3).copied().unwrap_or("1");
let key = parts.get(7).copied().unwrap_or("");
crate::write_log(&format!("ea_stub: challenge id={id} key={key}\n"));
let our_response = crate::lsx::make_challenge_response(key);
let seed = compute_seed(&our_response);
crate::write_log(&format!("ea_stub: our_response={our_response} seed={seed}\n"));
// ── 3. Send ChallengeAccepted ─────────────────────────────────────────
let accepted = format!(
"<LSX>\r\n <Response id=\"{id}\" sender=\"EALS\">\r\n <ChallengeAccepted response=\"{our_response}\" />\r\n </Response>\r\n</LSX>"
);
crate::write_log("ea_stub: sending ChallengeAccepted\n");
if !lsx_send(sock, &accepted) {
closesocket(sock);
return;
}
// ── 4. Session loop ───────────────────────────────────────────────────
loop {
let encrypted = match lsx_recv(sock) {
Some(s) => s,
None => break,
};
if encrypted.trim().is_empty() { continue; }
let request = crate::lsx::lsx_decrypt(&encrypted, seed);
crate::write_log(&format!("ea_stub: request: {}\n", &request[..request.len().min(300)]));
if request.trim().is_empty() {
crate::write_log("ea_stub: empty decrypted request — skipping\n");
continue;
}
let response_xml = crate::lsx::dispatch(request.trim());
crate::write_log(&format!("ea_stub: response: {}\n", &response_xml[..response_xml.len().min(300)]));
let encrypted_resp = crate::lsx::lsx_encrypt(&response_xml, seed);
if !lsx_send(sock, &encrypted_resp) { break; }
}
closesocket(sock);
crate::write_log("ea_stub: client disconnected\n");
}
fn compute_seed(hex: &str) -> u16 {
let b0 = u8::from_str_radix(&hex[..2.min(hex.len())], 16).unwrap_or(0);
let b1 = u8::from_str_radix(&hex[2..4.min(hex.len())], 16).unwrap_or(0);
((b0 as u16) << 8) | (b1 as u16)
}
pub fn start() {
std::thread::spawn(server_loop);
}