87241acc1a
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>
180 lines
6.9 KiB
Rust
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);
|
|
}
|