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>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
/// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user