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:
@@ -13,6 +13,8 @@ windows-sys = { version = "0.59", features = [
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/// Hooks ws2_32!connect via inline detour (no iptables needed).
|
||||
/// Uses unhook/rehook pattern: restores original bytes, calls real function, re-installs hook.
|
||||
/// This avoids trampoline RIP-relocation issues entirely.
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
const AF_INET: u16 = 2;
|
||||
const PORT_HTTPS_NBO: u16 = 0xBB01; // 443 big-endian
|
||||
const PORT_BRIDGE_NBO: u16 = 0xFB20; // 8443 big-endian
|
||||
const PORT_BLAZE_REDIRECTOR_NBO: u16 = 0x3927; // 10041 big-endian
|
||||
const PORT_BLAZE_MAIN_NBO: u16 = 0x8FA4; // 42127 big-endian
|
||||
const ADDR_LOOPBACK_NBO: u32 = 0x0100_007F; // 127.0.0.1 big-endian
|
||||
|
||||
#[repr(C)]
|
||||
struct SockaddrIn {
|
||||
sin_family: u16,
|
||||
sin_port: u16,
|
||||
sin_addr: u32,
|
||||
sin_zero: [u8; 8],
|
||||
}
|
||||
|
||||
// Address of ws2_32!connect (set at hook installation)
|
||||
static CONNECT_ADDR: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
// Original 14 bytes saved before we overwrite them
|
||||
static mut ORIGINAL_BYTES: [u8; 14] = [0u8; 14];
|
||||
|
||||
// For WSAConnect IAT fallback
|
||||
type WsaConnectFn = unsafe extern "system" fn(
|
||||
s: usize, name: *const u8, namelen: i32,
|
||||
caller: *const (), callee: *const (),
|
||||
sqos: *const (), gqos: *const ()) -> i32;
|
||||
static REAL_WSA: OnceLock<WsaConnectFn> = OnceLock::new();
|
||||
pub fn set_real_wsa_connect(f: WsaConnectFn) { let _ = REAL_WSA.set(f); }
|
||||
|
||||
unsafe fn write_hook(target: *mut u8, dest: u64) {
|
||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||
let mut old: u32 = 0;
|
||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||
// FF 25 00 00 00 00 JMP [rip+0]
|
||||
target.write(0xFF); target.add(1).write(0x25);
|
||||
(target.add(2) as *mut u32).write(0u32);
|
||||
(target.add(6) as *mut u64).write(dest);
|
||||
VirtualProtect(target as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
unsafe fn restore_original(target: *mut u8) {
|
||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||
let mut old: u32 = 0;
|
||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||
core::ptr::copy_nonoverlapping(ORIGINAL_BYTES.as_ptr(), target, 14);
|
||||
VirtualProtect(target as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 16], i32)> {
|
||||
if namelen < 8 { return None; }
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
if sa.sin_family != AF_INET { return None; }
|
||||
|
||||
let orig = sa.sin_addr.to_le_bytes();
|
||||
let orig_port = u16::from_be(sa.sin_port);
|
||||
|
||||
let new_port_nbo = match sa.sin_port {
|
||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
crate::write_log(&format!(
|
||||
"connect_hook: {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
||||
orig[3], orig[2], orig[1], orig[0], orig_port,
|
||||
u16::from_be(new_port_nbo)
|
||||
));
|
||||
|
||||
let mut buf = [0u8; 16];
|
||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
||||
out.sin_family = AF_INET;
|
||||
out.sin_port = new_port_nbo;
|
||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
||||
Some((buf, 16))
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen: i32) -> i32 {
|
||||
let addr = CONNECT_ADDR.load(Ordering::Relaxed) as *mut u8;
|
||||
|
||||
// Log every call so we can confirm the hook fires at all
|
||||
if namelen >= 8 {
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
if sa.sin_family == AF_INET {
|
||||
let b = sa.sin_addr.to_le_bytes();
|
||||
let port = u16::from_be(sa.sin_port);
|
||||
// Log socket type (SOCK_STREAM=1, SOCK_DGRAM=2) to detect UDP connects
|
||||
let sock_type = {
|
||||
use windows_sys::Win32::Networking::WinSock::{getsockopt, SOL_SOCKET, SO_TYPE};
|
||||
let mut ty: i32 = -1;
|
||||
let mut len: i32 = 4;
|
||||
getsockopt(s, SOL_SOCKET as i32, SO_TYPE, &mut ty as *mut i32 as *mut u8, &mut len);
|
||||
ty
|
||||
};
|
||||
crate::write_log(&format!(
|
||||
"connect_hook: call {}.{}.{}.{}:{} sock_type={}\n",
|
||||
b[3], b[2], b[1], b[0], port, sock_type
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Port 3216 (EA App LSX) is now handled by the native openfut-bridge LSX
|
||||
// server — no in-process interception needed, just let connect() go through.
|
||||
|
||||
let (call_name, call_len) = if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
||||
restore_original(addr);
|
||||
let r = {
|
||||
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32
|
||||
= core::mem::transmute(addr);
|
||||
f(s, buf.as_ptr(), len)
|
||||
};
|
||||
write_hook(addr, hooked_connect as u64);
|
||||
return r;
|
||||
} else {
|
||||
(name, namelen)
|
||||
};
|
||||
|
||||
restore_original(addr);
|
||||
let r = {
|
||||
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32
|
||||
= core::mem::transmute(addr);
|
||||
f(s, call_name, call_len)
|
||||
};
|
||||
write_hook(addr, hooked_connect as u64);
|
||||
if namelen >= 8 {
|
||||
let sa = &*(call_name as *const SockaddrIn);
|
||||
if sa.sin_family == AF_INET {
|
||||
let err = if r != 0 {
|
||||
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
|
||||
WSAGetLastError()
|
||||
} else { 0 };
|
||||
crate::write_log(&format!("connect_hook: result={r} wsa_err={err}\n"));
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn hooked_wsa_connect(
|
||||
s: usize, name: *const u8, namelen: i32,
|
||||
caller: *const (), callee: *const (),
|
||||
sqos: *const (), gqos: *const (),
|
||||
) -> i32 {
|
||||
let real = REAL_WSA.get().copied().unwrap();
|
||||
if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
||||
real(s, buf.as_ptr(), len, caller, callee, sqos, gqos)
|
||||
} else {
|
||||
real(s, name, namelen, caller, callee, sqos, gqos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Install inline detour on ws2_32!connect.
|
||||
pub unsafe fn install_inline_connect_hook() -> bool {
|
||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||
|
||||
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
||||
if ws2.is_null() { return false; }
|
||||
let connect_fn = match GetProcAddress(ws2, b"connect\0".as_ptr()) {
|
||||
Some(f) => f as *mut u8,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
// Save original 14 bytes
|
||||
core::ptr::copy_nonoverlapping(connect_fn, ORIGINAL_BYTES.as_mut_ptr(), 14);
|
||||
CONNECT_ADDR.store(connect_fn as usize, Ordering::Relaxed);
|
||||
|
||||
// Overwrite first 14 bytes with absolute indirect JMP to our hook
|
||||
write_hook(connect_fn, hooked_connect as u64);
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/// Intercepts ConnectEx (EA/DirtySDK's preferred async connect API).
|
||||
///
|
||||
/// DirtySDK calls WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, WSAID_CONNECTEX) once at
|
||||
/// startup to get a ConnectEx function pointer, bypassing all IAT hooks. We hook WSAIoctl
|
||||
/// inline so that when it returns a ConnectEx pointer we swap it for our own wrapper.
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
use core::ffi::c_void;
|
||||
|
||||
const AF_INET: u16 = 2;
|
||||
const PORT_HTTPS_NBO: u16 = 0xBB01; // 443 big-endian
|
||||
const PORT_BRIDGE_NBO: u16 = 0xFB20; // 8443 big-endian
|
||||
const PORT_BLAZE_REDIRECTOR_NBO: u16 = 0x3927; // 10041 big-endian
|
||||
const PORT_BLAZE_MAIN_NBO: u16 = 0x8FA4; // 42127 big-endian
|
||||
const ADDR_LOOPBACK_NBO: u32 = 0x0100_007F; // 127.0.0.1 big-endian
|
||||
|
||||
// SIO_GET_EXTENSION_FUNCTION_POINTER
|
||||
const SIO_GET_EXT_FN: u32 = 0xC8000006;
|
||||
|
||||
// WSAID_CONNECTEX = {25A207B9-DDF3-4660-8EE9-76E58C74063E}
|
||||
const CONNECTEX_GUID: [u8; 16] = [
|
||||
0xB9, 0x07, 0xA2, 0x25,
|
||||
0xF3, 0xDD, 0x60, 0x46,
|
||||
0x8E, 0xE9, 0x76, 0xE5, 0x8C, 0x74, 0x06, 0x3E,
|
||||
];
|
||||
|
||||
#[repr(C)]
|
||||
struct SockaddrIn {
|
||||
sin_family: u16,
|
||||
sin_port: u16,
|
||||
sin_addr: u32,
|
||||
sin_zero: [u8; 8],
|
||||
}
|
||||
|
||||
// The real ConnectEx pointer, saved after WSAIoctl returns it
|
||||
static REAL_CONNECTEX: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
// ConnectEx function signature
|
||||
type ConnectExFn = unsafe extern "system" fn(
|
||||
s: usize,
|
||||
name: *const u8,
|
||||
namelen: i32,
|
||||
send_buf: *const c_void,
|
||||
send_data_len: u32,
|
||||
bytes_sent: *mut u32,
|
||||
overlapped: *mut c_void,
|
||||
) -> i32;
|
||||
|
||||
// WSAIoctl function address (for inline unhook/rehook)
|
||||
static WSAIOCTL_ADDR: AtomicUsize = AtomicUsize::new(0);
|
||||
static mut WSAIOCTL_ORIG: [u8; 14] = [0u8; 14];
|
||||
|
||||
type WsaIoctlFn = unsafe extern "system" fn(
|
||||
s: usize,
|
||||
code: u32,
|
||||
in_buf: *const c_void,
|
||||
in_len: u32,
|
||||
out_buf: *mut c_void,
|
||||
out_len: u32,
|
||||
bytes_ret: *mut u32,
|
||||
overlapped: *mut c_void,
|
||||
completion: *const c_void,
|
||||
) -> i32;
|
||||
|
||||
unsafe fn write_hook(target: *mut u8, dest: u64) {
|
||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||
let mut old: u32 = 0;
|
||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||
target.write(0xFF); target.add(1).write(0x25);
|
||||
(target.add(2) as *mut u32).write(0u32);
|
||||
(target.add(6) as *mut u64).write(dest);
|
||||
VirtualProtect(target as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
unsafe fn restore_wsaioctl(target: *mut u8) {
|
||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||
let mut old: u32 = 0;
|
||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||
core::ptr::copy_nonoverlapping(WSAIOCTL_ORIG.as_ptr(), target, 14);
|
||||
VirtualProtect(target as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
/// Our ConnectEx wrapper: redirects EA ports to 127.0.0.1
|
||||
unsafe extern "system" fn hooked_connectex(
|
||||
s: usize,
|
||||
name: *const u8,
|
||||
namelen: i32,
|
||||
send_buf: *const c_void,
|
||||
send_data_len: u32,
|
||||
bytes_sent: *mut u32,
|
||||
overlapped: *mut c_void,
|
||||
) -> i32 {
|
||||
let real_fn: ConnectExFn = core::mem::transmute(REAL_CONNECTEX.load(Ordering::Relaxed));
|
||||
|
||||
if namelen >= 8 {
|
||||
let sa = &*(name as *const SockaddrIn);
|
||||
if sa.sin_family == AF_INET {
|
||||
let o = sa.sin_addr.to_le_bytes();
|
||||
let orig_port = u16::from_be(sa.sin_port);
|
||||
let new_port_nbo = match sa.sin_port {
|
||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
||||
_ => 0,
|
||||
};
|
||||
if new_port_nbo != 0 {
|
||||
crate::write_log(&format!(
|
||||
"connectex_hook: {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
||||
o[3], o[2], o[1], o[0], orig_port,
|
||||
u16::from_be(new_port_nbo)
|
||||
));
|
||||
let mut redirect = [0u8; 16];
|
||||
let out = &mut *(redirect.as_mut_ptr() as *mut SockaddrIn);
|
||||
out.sin_family = AF_INET;
|
||||
out.sin_port = new_port_nbo;
|
||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
||||
return real_fn(s, redirect.as_ptr(), 16, send_buf, send_data_len, bytes_sent, overlapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
real_fn(s, name, namelen, send_buf, send_data_len, bytes_sent, overlapped)
|
||||
}
|
||||
|
||||
/// Our WSAIoctl hook: when ConnectEx is requested, save the real pointer and return ours
|
||||
pub unsafe extern "system" fn hooked_wsaioctl(
|
||||
s: usize,
|
||||
code: u32,
|
||||
in_buf: *const c_void,
|
||||
in_len: u32,
|
||||
out_buf: *mut c_void,
|
||||
out_len: u32,
|
||||
bytes_ret: *mut u32,
|
||||
overlapped: *mut c_void,
|
||||
completion: *const c_void,
|
||||
) -> i32 {
|
||||
let addr = WSAIOCTL_ADDR.load(Ordering::Relaxed) as *mut u8;
|
||||
|
||||
// Call the real WSAIoctl via unhook/rehook
|
||||
restore_wsaioctl(addr);
|
||||
let result = {
|
||||
let f: WsaIoctlFn = core::mem::transmute(addr);
|
||||
f(s, code, in_buf, in_len, out_buf, out_len, bytes_ret, overlapped, completion)
|
||||
};
|
||||
write_hook(addr, hooked_wsaioctl as u64);
|
||||
|
||||
// If this was a ConnectEx request that succeeded, swap the pointer
|
||||
if result == 0
|
||||
&& code == SIO_GET_EXT_FN
|
||||
&& in_len == 16
|
||||
&& !in_buf.is_null()
|
||||
{
|
||||
let guid = core::slice::from_raw_parts(in_buf as *const u8, 16);
|
||||
if guid == CONNECTEX_GUID
|
||||
&& out_len >= 8
|
||||
&& !out_buf.is_null()
|
||||
{
|
||||
let out_ptr = out_buf as *mut usize;
|
||||
let real_addr = *out_ptr;
|
||||
if REAL_CONNECTEX.compare_exchange(0, real_addr, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
|
||||
crate::write_log(&format!("connectex_hook: intercepted ConnectEx @ {real_addr:#x}\n"));
|
||||
}
|
||||
// Return our hook instead
|
||||
*out_ptr = hooked_connectex as usize;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub unsafe fn install_wsaioctl_hook() -> bool {
|
||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
||||
if ws2.is_null() { return false; }
|
||||
let fn_ptr = match GetProcAddress(ws2, b"WSAIoctl\0".as_ptr()) {
|
||||
Some(f) => f as *mut u8,
|
||||
None => return false,
|
||||
};
|
||||
core::ptr::copy_nonoverlapping(fn_ptr, WSAIOCTL_ORIG.as_mut_ptr(), 14);
|
||||
WSAIOCTL_ADDR.store(fn_ptr as usize, Ordering::Relaxed);
|
||||
write_hook(fn_ptr, hooked_wsaioctl as u64);
|
||||
true
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+43
-17
@@ -1,13 +1,13 @@
|
||||
use std::{ffi::CStr, sync::OnceLock};
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
sync::{
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use windows_sys::Win32::Networking::WinSock::{ADDRINFOA, getaddrinfo as sys_getaddrinfo};
|
||||
|
||||
const INTERCEPT: &[&str] = &[
|
||||
"fut.ea.com",
|
||||
"utas.mob.v4.fut.ea.com",
|
||||
"utas.s2.fut.ea.com",
|
||||
];
|
||||
|
||||
type GetaddrinfoFn = unsafe extern "system" fn(
|
||||
*const u8,
|
||||
*const u8,
|
||||
@@ -16,19 +16,35 @@ type GetaddrinfoFn = unsafe extern "system" fn(
|
||||
) -> i32;
|
||||
|
||||
static REAL: OnceLock<GetaddrinfoFn> = OnceLock::new();
|
||||
// Stored as a NUL-terminated byte string so the hook can pass it to getaddrinfo.
|
||||
static REDIRECT_IP: OnceLock<Vec<u8>> = OnceLock::new();
|
||||
|
||||
// Flipped to true the first time we successfully apply the runtime cert patch.
|
||||
// The patch is deferred to here (rather than DllMain) because EAWebKit.dll may
|
||||
// not be loaded yet when the hook DLL is injected.
|
||||
static CERT_PATCHED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn set_real(f: GetaddrinfoFn) {
|
||||
let _ = REAL.set(f);
|
||||
}
|
||||
|
||||
pub fn set_redirect_ip(ip: String) {
|
||||
let mut bytes = ip.into_bytes();
|
||||
bytes.push(0); // NUL-terminate for passing to getaddrinfo
|
||||
bytes.push(0);
|
||||
let _ = REDIRECT_IP.set(bytes);
|
||||
}
|
||||
|
||||
/// Returns true if `host` is an EA / EA-Sports domain that should be redirected
|
||||
/// to the local OpenFUT bridge.
|
||||
fn is_ea_host(host: &str) -> bool {
|
||||
let h = host.to_ascii_lowercase();
|
||||
h.ends_with(".ea.com")
|
||||
|| h == "ea.com"
|
||||
|| h.ends_with(".easports.com")
|
||||
|| h == "easports.com"
|
||||
|| h.ends_with(".ugc.footapi.com")
|
||||
|| h.ends_with(".footapi.com")
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn hooked_getaddrinfo(
|
||||
node_name: *const u8,
|
||||
service_name: *const u8,
|
||||
@@ -37,15 +53,25 @@ pub unsafe extern "system" fn hooked_getaddrinfo(
|
||||
) -> i32 {
|
||||
if !node_name.is_null() {
|
||||
if let Ok(host) = CStr::from_ptr(node_name as *const i8).to_str() {
|
||||
for target in INTERCEPT {
|
||||
if host.eq_ignore_ascii_case(target) {
|
||||
let redirect = REDIRECT_IP
|
||||
.get()
|
||||
.map(|v| v.as_ptr())
|
||||
.unwrap_or(b"127.0.0.1\0".as_ptr());
|
||||
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
|
||||
return real(redirect, service_name, hints, result);
|
||||
crate::write_log(&format!("openfut_hook: getaddrinfo({host})\n"));
|
||||
if is_ea_host(host) {
|
||||
// Apply the ProtoSSL cert-verify bypass the first time we see an EA
|
||||
// hostname — EAWebKit.dll must be loaded by now because it's calling us.
|
||||
if !CERT_PATCHED.load(Ordering::Relaxed) {
|
||||
if crate::ssl_patch::patch_eawebkit_cert_verify() {
|
||||
CERT_PATCHED.store(true, Ordering::Relaxed);
|
||||
crate::write_log("openfut_hook: ProtoSSL cert-verify patched (lazy, from getaddrinfo)\n");
|
||||
} else {
|
||||
crate::write_log("openfut_hook: ProtoSSL cert-verify patch FAILED in getaddrinfo\n");
|
||||
}
|
||||
}
|
||||
|
||||
let redirect = REDIRECT_IP
|
||||
.get()
|
||||
.map(|v| v.as_ptr())
|
||||
.unwrap_or(b"127.0.0.1\0".as_ptr());
|
||||
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
|
||||
return real(redirect, service_name, hints, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -39,7 +39,7 @@ struct ImageDataDirectory {
|
||||
#[repr(C)]
|
||||
struct ImageOptionalHeader64 {
|
||||
magic: u16,
|
||||
_pad: [u8; 106],
|
||||
_pad: [u8; 110],
|
||||
data_directory: [ImageDataDirectory; 16],
|
||||
}
|
||||
|
||||
@@ -71,6 +71,15 @@ pub unsafe fn patch_iat(original_fn: *const (), hook_fn: *const ()) -> usize {
|
||||
patch_module(module, original_fn, hook_fn)
|
||||
}
|
||||
|
||||
/// Patch the IAT of a specific already-loaded DLL (e.g. b"EAWebKit.dll\0").
|
||||
pub unsafe fn patch_iat_in(module_name: &[u8], original_fn: *const (), hook_fn: *const ()) -> usize {
|
||||
let module = GetModuleHandleA(module_name.as_ptr());
|
||||
if module.is_null() {
|
||||
return 0;
|
||||
}
|
||||
patch_module(module, original_fn, hook_fn)
|
||||
}
|
||||
|
||||
unsafe fn patch_module(
|
||||
module: HMODULE,
|
||||
original_fn: *const (),
|
||||
|
||||
+80
-20
@@ -1,6 +1,11 @@
|
||||
mod config;
|
||||
mod connect_hook;
|
||||
mod connectex_hook;
|
||||
mod hooks;
|
||||
mod iat;
|
||||
mod origin_spy;
|
||||
mod ssl_patch;
|
||||
mod tls_bypass;
|
||||
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{BOOL, HMODULE, TRUE},
|
||||
@@ -8,35 +13,90 @@ use windows_sys::Win32::{
|
||||
Networking::WinSock::ADDRINFOA,
|
||||
};
|
||||
|
||||
pub(crate) fn write_log(msg: &str) {
|
||||
use std::io::Write;
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true).append(true)
|
||||
.open(r"C:\openfut_hook.log")
|
||||
{ let _ = f.write_all(msg.as_bytes()); }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn DllMain(
|
||||
module: HMODULE,
|
||||
reason: u32,
|
||||
_reserved: *mut (),
|
||||
) -> BOOL {
|
||||
if reason == DLL_PROCESS_ATTACH {
|
||||
install_hooks(module);
|
||||
}
|
||||
pub unsafe extern "system" fn DllMain(module: HMODULE, reason: u32, _: *mut ()) -> BOOL {
|
||||
if reason == DLL_PROCESS_ATTACH { install_hooks(module); }
|
||||
TRUE
|
||||
}
|
||||
|
||||
unsafe fn install_hooks(module: HMODULE) {
|
||||
// Load redirect IP from openfut.cfg before patching
|
||||
write_log("openfut_hook: DllMain fired\n");
|
||||
let ip = config::read_redirect_ip(module);
|
||||
hooks::set_redirect_ip(ip);
|
||||
|
||||
let real_ptr = iat::resolve(b"ws2_32.dll\0", b"getaddrinfo\0");
|
||||
if real_ptr.is_null() {
|
||||
return;
|
||||
let ga = iat::resolve(b"ws2_32.dll\0", b"getaddrinfo\0");
|
||||
if !ga.is_null() {
|
||||
let f: unsafe extern "system" fn(*const u8,*const u8,*const ADDRINFOA,*mut *mut ADDRINFOA)->i32
|
||||
= std::mem::transmute(ga);
|
||||
hooks::set_real(f);
|
||||
let n = iat::patch_iat(ga, hooks::hooked_getaddrinfo as *const ());
|
||||
let m = iat::patch_iat_in(b"EAWebKit.dll\0", ga, hooks::hooked_getaddrinfo as *const ());
|
||||
write_log(&format!("openfut_hook: getaddrinfo IAT patched {n}+{m}\n"));
|
||||
}
|
||||
|
||||
let real_fn: unsafe extern "system" fn(
|
||||
*const u8,
|
||||
*const u8,
|
||||
*const ADDRINFOA,
|
||||
*mut *mut ADDRINFOA,
|
||||
) -> i32 = std::mem::transmute(real_ptr);
|
||||
if ssl_patch::patch_main_exe_cert_verify() { write_log("ssl: main exe cert-verify patched\n"); }
|
||||
else { write_log("ssl: main exe cert-verify NOT FOUND\n"); }
|
||||
if ssl_patch::patch_eawebkit_cert_verify() { write_log("ssl: EAWebKit cert-verify patched\n"); }
|
||||
else { write_log("ssl: EAWebKit cert-verify deferred\n"); }
|
||||
|
||||
hooks::set_real(real_fn);
|
||||
iat::patch_iat(real_ptr, hooks::hooked_getaddrinfo as *const ());
|
||||
if connect_hook::install_inline_connect_hook() { write_log("connect: inline-hooked\n"); }
|
||||
else { write_log("connect: hook FAILED\n"); }
|
||||
let wp = iat::resolve(b"ws2_32.dll\0", b"WSAConnect\0");
|
||||
if !wp.is_null() {
|
||||
let f: unsafe extern "system" fn(usize,*const u8,i32,*const(),*const(),*const(),*const())->i32
|
||||
= std::mem::transmute(wp);
|
||||
connect_hook::set_real_wsa_connect(f);
|
||||
iat::patch_iat(wp, connect_hook::hooked_wsa_connect as *const ());
|
||||
write_log("connect: WSAConnect IAT patched\n");
|
||||
}
|
||||
|
||||
if connectex_hook::install_wsaioctl_hook() { write_log("connectex: WSAIoctl inline-hooked\n"); }
|
||||
else { write_log("connectex: WSAIoctl hook FAILED\n"); }
|
||||
|
||||
// recv/send hooks removed — LSX is now handled by the native openfut-bridge
|
||||
// LSX server (port 3216), so in-process interception is no longer needed.
|
||||
|
||||
macro_rules! hook_iat {
|
||||
($dll:expr, $sym:expr, $setter:ident, $handler:expr, $ty:ty) => {{
|
||||
let ptr = iat::resolve($dll, $sym);
|
||||
if !ptr.is_null() {
|
||||
let f: $ty = std::mem::transmute(ptr);
|
||||
origin_spy::$setter(f);
|
||||
iat::patch_iat(ptr, $handler as *const ());
|
||||
"ok"
|
||||
} else { "miss" }
|
||||
}};
|
||||
}
|
||||
let ra = hook_iat!(b"advapi32.dll\0", b"RegQueryValueExA\0", set_real_reg_a,
|
||||
origin_spy::hooked_reg_query_a,
|
||||
unsafe extern "system" fn(isize,*const u8,*mut u32,*mut u32,*mut u8,*mut u32)->i32);
|
||||
let rw = hook_iat!(b"advapi32.dll\0", b"RegQueryValueExW\0", set_real_reg_w,
|
||||
origin_spy::hooked_reg_query_w,
|
||||
unsafe extern "system" fn(isize,*const u16,*mut u32,*mut u32,*mut u8,*mut u32)->i32);
|
||||
let ma = hook_iat!(b"kernel32.dll\0", b"OpenMutexA\0", set_real_mutex_a,
|
||||
origin_spy::hooked_open_mutex_a,
|
||||
unsafe extern "system" fn(u32,i32,*const u8)->isize);
|
||||
let mw = hook_iat!(b"kernel32.dll\0", b"OpenMutexW\0", set_real_mutex_w,
|
||||
origin_spy::hooked_open_mutex_w,
|
||||
unsafe extern "system" fn(u32,i32,*const u16)->isize);
|
||||
write_log(&format!("origin_spy: RegA={ra} RegW={rw} MutexA={ma} MutexW={mw}\n"));
|
||||
|
||||
let cv = iat::resolve(b"crypt32.dll\0", b"CertVerifyCertificateChainPolicy\0");
|
||||
if !cv.is_null() {
|
||||
let f: unsafe extern "system" fn(*const u8,*const(),*const(),*mut u32)->BOOL
|
||||
= std::mem::transmute(cv);
|
||||
tls_bypass::set_real(f);
|
||||
iat::patch_iat(cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
||||
iat::patch_iat_in(b"EAWebKit.dll\0", cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
||||
iat::patch_iat_in(b"winhttp.dll\0", cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
||||
iat::patch_iat_in(b"wininet.dll\0", cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
/// EA App LSX protocol emulator (port 3216).
|
||||
///
|
||||
/// FIFA 23 opens two concurrent connections to port 3216 (one for EbisuSDK,
|
||||
/// one for the login service). We track up to 4 sockets in LSX_POOL with
|
||||
/// independent state per connection.
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
// ─── per-connection slot ─────────────────────────────────────────────────────
|
||||
|
||||
struct LsxSlot {
|
||||
socket: AtomicUsize, // usize::MAX = empty
|
||||
state: AtomicUsize,
|
||||
seed: AtomicUsize,
|
||||
pending: Mutex<Option<Vec<u8>>>,
|
||||
}
|
||||
|
||||
const MAX_LSX: usize = 4;
|
||||
|
||||
macro_rules! empty_slot {
|
||||
() => { LsxSlot {
|
||||
socket: AtomicUsize::new(usize::MAX),
|
||||
state: AtomicUsize::new(0),
|
||||
seed: AtomicUsize::new(0),
|
||||
pending: Mutex::new(None),
|
||||
}};
|
||||
}
|
||||
|
||||
static POOL: [LsxSlot; MAX_LSX] = [
|
||||
empty_slot!(), empty_slot!(), empty_slot!(), empty_slot!(),
|
||||
];
|
||||
|
||||
fn find_slot(s: usize) -> Option<&'static LsxSlot> {
|
||||
POOL.iter().find(|sl| sl.socket.load(Ordering::Relaxed) == s)
|
||||
}
|
||||
|
||||
// ─── public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_lsx_socket(s: usize) {
|
||||
// Try to reuse an existing slot for this socket first
|
||||
if find_slot(s).is_some() { return; }
|
||||
// Find a free slot
|
||||
for sl in &POOL {
|
||||
if sl.socket.load(Ordering::Relaxed) == usize::MAX {
|
||||
sl.state.store(0, Ordering::Relaxed);
|
||||
sl.seed.store(0, Ordering::Relaxed);
|
||||
if let Ok(mut g) = sl.pending.lock() { *g = None; }
|
||||
sl.socket.store(s, Ordering::Relaxed);
|
||||
crate::write_log(&format!("lsx: socket registered s={s}\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// All slots full — evict the first one
|
||||
let sl = &POOL[0];
|
||||
sl.state.store(0, Ordering::Relaxed);
|
||||
sl.seed.store(0, Ordering::Relaxed);
|
||||
if let Ok(mut g) = sl.pending.lock() { *g = None; }
|
||||
sl.socket.store(s, Ordering::Relaxed);
|
||||
crate::write_log(&format!("lsx: socket registered s={s} (evicted old slot)\n"));
|
||||
}
|
||||
|
||||
pub fn is_lsx(s: usize) -> bool {
|
||||
find_slot(s).is_some()
|
||||
}
|
||||
|
||||
pub fn current_socket() -> usize {
|
||||
// Return any active LSX socket (used by select hook if needed)
|
||||
POOL.iter()
|
||||
.map(|sl| sl.socket.load(Ordering::Relaxed))
|
||||
.find(|&s| s != usize::MAX)
|
||||
.unwrap_or(usize::MAX)
|
||||
}
|
||||
|
||||
const GREETING_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb";
|
||||
const AES_KEY: [u8; 16] = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
|
||||
|
||||
pub unsafe fn on_recv(s: usize, buf: *mut u8, len: i32) -> i32 {
|
||||
let sl = match find_slot(s) { Some(x) => x, None => return -1 };
|
||||
let state = sl.state.load(Ordering::Relaxed);
|
||||
crate::write_log(&format!("lsx: recv s={s} state={state}\n"));
|
||||
|
||||
let payload: Vec<u8> = match state {
|
||||
0 => {
|
||||
let xml = 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>\0"
|
||||
);
|
||||
sl.state.store(1, Ordering::Relaxed);
|
||||
xml.into_bytes()
|
||||
}
|
||||
_ => {
|
||||
let mut guard = sl.pending.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match guard.take() {
|
||||
Some(pb) => pb,
|
||||
None => {
|
||||
// No pending data — return 0.
|
||||
// For the state-1 probe recv (FIFA checking if there is more
|
||||
// greeting data), 0 is the correct "no more data" signal and
|
||||
// FIFA proceeds to send the ChallengeResponse.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let n = payload.len().min(len as usize);
|
||||
core::ptr::copy_nonoverlapping(payload.as_ptr(), buf, n);
|
||||
crate::write_log(&format!("lsx: recv -> {n} bytes\n"));
|
||||
n as i32
|
||||
}
|
||||
|
||||
pub unsafe fn on_send(s: usize, buf: *const u8, len: i32) -> i32 {
|
||||
let sl = match find_slot(s) { Some(x) => x, None => return len };
|
||||
let state = sl.state.load(Ordering::Relaxed);
|
||||
let data = core::slice::from_raw_parts(buf, len as usize);
|
||||
let text = core::str::from_utf8(data).unwrap_or("(binary)");
|
||||
crate::write_log(&format!("lsx: send s={s} state={state} len={len} data={}\n",
|
||||
&text[..text.len().min(300)]));
|
||||
|
||||
let response = match state {
|
||||
1 => handle_challenge(sl, data),
|
||||
st => handle_request(sl, data, st),
|
||||
};
|
||||
|
||||
if let Some(payload) = response {
|
||||
let mut guard = sl.pending.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*guard = Some(payload);
|
||||
}
|
||||
sl.state.fetch_add(1, Ordering::Relaxed);
|
||||
len
|
||||
}
|
||||
|
||||
// ─── handshake ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn handle_challenge(sl: &LsxSlot, raw: &[u8]) -> Option<Vec<u8>> {
|
||||
let text = core::str::from_utf8(raw).unwrap_or("").trim_end_matches('\0');
|
||||
let parts: Vec<&str> = text.split('"').collect();
|
||||
let id = parts.get(3).copied().unwrap_or("1");
|
||||
let key = parts.get(7).copied().unwrap_or("");
|
||||
crate::write_log(&format!("lsx: challenge id={id} key={key}\n"));
|
||||
|
||||
let our_response = make_challenge_response(key);
|
||||
let seed = compute_seed(&our_response);
|
||||
sl.seed.store(seed as usize, Ordering::Relaxed);
|
||||
crate::write_log(&format!("lsx: response={our_response} seed={seed}\n"));
|
||||
|
||||
let xml = format!(
|
||||
"<LSX>\r\n <Response id=\"{id}\" sender=\"EALS\">\r\n <ChallengeAccepted response=\"{our_response}\" />\r\n </Response>\r\n</LSX>\0"
|
||||
);
|
||||
Some(xml.into_bytes())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn handle_request(sl: &LsxSlot, raw: &[u8], _state: usize) -> Option<Vec<u8>> {
|
||||
let seed = sl.seed.load(Ordering::Relaxed) as u16;
|
||||
let text = core::str::from_utf8(raw).unwrap_or("").trim_end_matches('\0');
|
||||
|
||||
let decrypted = lsx_decrypt(text, seed);
|
||||
crate::write_log(&format!("lsx: request decrypted={}\n", &decrypted[..decrypted.len().min(300)]));
|
||||
|
||||
let response_xml = dispatch_request(decrypted.trim());
|
||||
crate::write_log(&format!("lsx: response={}\n", &response_xml[..response_xml.len().min(300)]));
|
||||
|
||||
let encrypted = lsx_encrypt(&response_xml, seed);
|
||||
let payload = format!("{encrypted}\0");
|
||||
Some(payload.into_bytes())
|
||||
}
|
||||
|
||||
// ─── session dispatcher ───────────────────────────────────────────────────────
|
||||
|
||||
pub fn dispatch(xml: &str) -> String { dispatch_request(xml) }
|
||||
|
||||
fn dispatch_request(xml: &str) -> String {
|
||||
let parts: Vec<&str> = xml.split('"').collect();
|
||||
let id = parts.get(3).copied().unwrap_or("1");
|
||||
let req_type = parts.get(4).copied().unwrap_or("");
|
||||
crate::write_log(&format!("lsx: dispatch id={id} type={req_type}\n"));
|
||||
|
||||
match req_type {
|
||||
"><GetConfig version=" => get_config(id),
|
||||
"><GetAuthCode ClientId=" | "><GetAuthCode UserId=" => get_auth_code(id),
|
||||
"><GetInternetConnectedState version=" => get_internet_state(id),
|
||||
"><GetProfile index=" => get_profile(id),
|
||||
"><GetSetting SettingId=" => {
|
||||
let setting = parts.get(5).copied().unwrap_or("");
|
||||
get_setting(id, setting)
|
||||
}
|
||||
"><QueryEntitlements UserId=" => query_entitlements(id),
|
||||
"><RequestLicense UserId=" => request_license(id),
|
||||
"><QueryContent UserId=" => query_content(id),
|
||||
"><GetBlockList version=" => get_block_list(id),
|
||||
"><QueryFriends UserId=" => query_friends(id),
|
||||
"><QueryPresence UserId=" => query_presence(id),
|
||||
"><SetPresence UserId=" => set_presence(id),
|
||||
"><GetPresenceVisibility UserId=" => get_presence_visibility(id),
|
||||
"><GetWalletBalance UserId=" => get_wallet_balance(id),
|
||||
"><GetAllGameInfo version=" => get_all_game_info(id),
|
||||
_ => {
|
||||
crate::write_log(&format!("lsx: UNKNOWN type: {req_type}\n"));
|
||||
format!("<LSX><Response id=\"{id}\" sender=\"EbisuSDK\"><Ok /></Response></LSX>\0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── LSX response templates ───────────────────────────────────────────────────
|
||||
|
||||
fn get_config(id: &str) -> String {
|
||||
format!(r#"<LSX>
|
||||
<Response id="{id}" sender="EbisuSDK">
|
||||
<GetConfigResponse>
|
||||
<Service Facility="SDK" Name="EbisuSDK" />
|
||||
<Service Facility="PROFILE" Name="EbisuSDK" />
|
||||
<Service Facility="PRESENCE" Name="XMPP" />
|
||||
<Service Facility="FRIENDS" Name="XMPP" />
|
||||
<Service Facility="COMMERCE" Name="Commerce" />
|
||||
<Service Facility="RECENTPLAYER" Name="EbisuSDK" />
|
||||
<Service Facility="IGO" Name="EbisuSDK" />
|
||||
<Service Facility="MISC" Name="EbisuSDK" />
|
||||
<Service Facility="LOGIN" Name="EALS" />
|
||||
<Service Facility="UTILITY" Name="Utility" />
|
||||
<Service Facility="XMPP" Name="XMPP" />
|
||||
<Service Facility="CHAT" Name="XMPP" />
|
||||
<Service Facility="IGO_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="EALS_EVENTS" Name="EALS" />
|
||||
<Service Facility="LOGIN_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="INVITE_EVENT" Name="XMPP" />
|
||||
<Service Facility="PROFILE_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="PRESENCE_EVENT" Name="XMPP" />
|
||||
<Service Facility="FRIENDS_EVENT" Name="XMPP" />
|
||||
<Service Facility="COMMERCE_EVENT" Name="Commerce" />
|
||||
<Service Facility="CHAT_EVENT" Name="XMPP" />
|
||||
<Service Facility="DOWNLOAD_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="PERMISSION" Name="EbisuSDK" />
|
||||
<Service Facility="RESOURCES" Name="EbisuSDK" />
|
||||
<Service Facility="BLOCKED_USERS" Name="EbisuSDK" />
|
||||
<Service Facility="BLOCKED_USER_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="GET_USERID" Name="EbisuSDK" />
|
||||
<Service Facility="ONLINE_STATUS_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="ACHIEVEMENT" Name="EbisuSDK" />
|
||||
<Service Facility="ACHIEVEMENT_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="BROADCAST_EVENT" Name="EbisuSDK" />
|
||||
<Service Facility="PROGRESSIVE_INSTALLATION" Name="PI" />
|
||||
<Service Facility="PROGRESSIVE_INSTALLATION_EVENT" Name="PI" />
|
||||
<Service Facility="CONTENT" Name="EbisuSDK" />
|
||||
</GetConfigResponse>
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn get_auth_code(id: &str) -> String {
|
||||
format!(r#"<LSX>
|
||||
<Response id="{id}" sender="Utility">
|
||||
<AuthCode value="OpenFUT_fake_auth_code_v1" />
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn get_internet_state(id: &str) -> String {
|
||||
format!(r#"<LSX>
|
||||
<Response id="{id}" sender="Utility">
|
||||
<InternetConnectedState connected="1" />
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn get_profile(id: &str) -> String {
|
||||
format!(r#"<LSX>
|
||||
<Response id="{id}" sender="EbisuSDK">
|
||||
<GetProfileResponse PersonaId="1000000000001" Persona="OpenFUT_Player" Country="US" GeoCountry="US"
|
||||
UserIndex="0" IsTrialSubscriber="false" AvatarId="1"
|
||||
IsUnderAge="false" IsSubscriber="false" IsSteamSubscriber="false" SubscriberLevel="2"
|
||||
CommerceCurrency="USD" UserId="2000000000001" CommerceCountry="US" />
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn get_setting(id: &str, setting: &str) -> String {
|
||||
let value = match setting { "ENVIRONMENT" => "production", _ => "false" };
|
||||
format!(r#"<LSX>
|
||||
<Response id="{id}" sender="EbisuSDK">
|
||||
<GetSettingResponse Setting="{value}" />
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn query_entitlements(id: &str) -> String {
|
||||
format!(r#"<LSX>
|
||||
<Response id="{id}" sender="Commerce">
|
||||
<QueryEntitlementsResponse>
|
||||
<Entitlements ItemId="Origin.OFR.50.0004658" Type="ONLINE_ACCESS"
|
||||
EntitlementId="1021747550001" EntitlementTag="ONLINE_ACCESS"
|
||||
Group="FIFA23PC" ResourceId="" UseCount="0"
|
||||
Expiration="0000-00-00T00:00:00" GrantDate="2022-09-30T00:00:00"
|
||||
LastModifiedDate="2022-09-30T00:00:00" Version="0" />
|
||||
<Entitlements ItemId="Origin.OFR.50.0004658" Type="DEFAULT"
|
||||
EntitlementId="1021747550002" EntitlementTag="ONLINE_ACCESS"
|
||||
Group="FIFA23PC" ResourceId="" UseCount="0"
|
||||
Expiration="0000-00-00T00:00:00" GrantDate="2022-09-30T00:00:00"
|
||||
LastModifiedDate="2022-09-30T00:00:00" Version="0" />
|
||||
</QueryEntitlementsResponse>
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn request_license(id: &str) -> String {
|
||||
format!(r#"<LSX>
|
||||
<Response sender="EbisuSDK" id="{id}">
|
||||
<RequestLicenseResponse License="OpenFUT_fake_license_v1" />
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn query_content(id: &str) -> String {
|
||||
format!(r#"<LSX>
|
||||
<Response id="{id}" sender="EbisuSDK">
|
||||
<QueryContentResponse>
|
||||
<Content Gamestate="READY_TO_PLAY" progressValue="0"
|
||||
contentID="Origin.OFR.50.0004658"
|
||||
installedVersion="1.0.0.0" availableVersion="1.0.0.0"
|
||||
displayName="FIFA 23" />
|
||||
</QueryContentResponse>
|
||||
</Response>
|
||||
</LSX>"#)
|
||||
}
|
||||
|
||||
fn get_block_list(id: &str) -> String {
|
||||
format!(r#"<LSX><Response id="{id}" sender="EbisuSDK"><GetBlockListResponse /></Response></LSX>"#)
|
||||
}
|
||||
|
||||
fn query_friends(id: &str) -> String {
|
||||
format!(r#"<LSX><Response id="{id}" sender="XMPP"><QueryFriendsResponse /></Response></LSX>"#)
|
||||
}
|
||||
|
||||
fn query_presence(id: &str) -> String {
|
||||
format!(r#"<LSX><Response id="{id}" sender="XMPP"><QueryPresenceResponse UserId="2000000000001" PersonaId="1000000000001" /></Response></LSX>"#)
|
||||
}
|
||||
|
||||
fn set_presence(id: &str) -> String {
|
||||
format!(r#"<LSX><Response id="{id}" sender="XMPP"><SetPresenceResponse /></Response></LSX>"#)
|
||||
}
|
||||
|
||||
fn get_presence_visibility(id: &str) -> String {
|
||||
format!(r#"<LSX><Response id="{id}" sender="EbisuSDK"><GetPresenceVisibilityResponse Visibility="FRIENDS" /></Response></LSX>"#)
|
||||
}
|
||||
|
||||
fn get_wallet_balance(id: &str) -> String {
|
||||
format!(r#"<LSX><Response id="{id}" sender="Commerce"><GetWalletBalanceResponse Balance="0" Currency="USD" /></Response></LSX>"#)
|
||||
}
|
||||
|
||||
fn get_all_game_info(id: &str) -> String {
|
||||
format!(r#"<LSX><Response id="{id}" sender="EbisuSDK"><GetAllGameInfoResponse /></Response></LSX>"#)
|
||||
}
|
||||
|
||||
// ─── AES-128-ECB (pure Rust) ──────────────────────────────────────────────────
|
||||
|
||||
#[rustfmt::skip]
|
||||
const SBOX: [u8; 256] = [
|
||||
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
|
||||
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
|
||||
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
|
||||
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
|
||||
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
|
||||
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
|
||||
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
|
||||
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
|
||||
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
|
||||
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
|
||||
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
|
||||
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
|
||||
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
|
||||
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
|
||||
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
|
||||
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
|
||||
];
|
||||
|
||||
fn xtime(a: u8) -> u8 { if a & 0x80 != 0 { (a << 1) ^ 0x1b } else { a << 1 } }
|
||||
fn mul(mut a: u8, mut b: u8) -> u8 {
|
||||
let mut r = 0u8;
|
||||
while b > 0 { if b & 1 != 0 { r ^= a; } a = xtime(a); b >>= 1; }
|
||||
r
|
||||
}
|
||||
|
||||
fn sub_bytes(s: &mut [u8; 16]) { for b in s.iter_mut() { *b = SBOX[*b as usize]; } }
|
||||
fn shift_rows(s: &mut [u8; 16]) {
|
||||
let t = s[1]; s[1]=s[5]; s[5]=s[9]; s[9]=s[13]; s[13]=t;
|
||||
s.swap(2,10); s.swap(6,14);
|
||||
let t = s[15]; s[15]=s[11]; s[11]=s[7]; s[7]=s[3]; s[3]=t;
|
||||
}
|
||||
fn mix_col(s: &mut [u8; 16], c: usize) {
|
||||
let (a,b,c2,d) = (s[c],s[c+4],s[c+8],s[c+12]);
|
||||
s[c] = mul(2,a)^mul(3,b)^c2^d;
|
||||
s[c+4] = a^mul(2,b)^mul(3,c2)^d;
|
||||
s[c+8] = a^b^mul(2,c2)^mul(3,d);
|
||||
s[c+12] = mul(3,a)^b^c2^mul(2,d);
|
||||
}
|
||||
fn mix_columns(s: &mut [u8; 16]) { for c in 0..4 { mix_col(s,c); } }
|
||||
fn add_round_key(s: &mut [u8; 16], rk: &[u8; 16]) { for i in 0..16 { s[i] ^= rk[i]; } }
|
||||
|
||||
fn expand_key(key: &[u8; 16]) -> [[u8; 16]; 11] {
|
||||
let rcon: [u8; 10] = [0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36];
|
||||
let mut w = [[0u8; 4]; 44];
|
||||
for i in 0..4 { w[i] = [key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]]; }
|
||||
for i in 4..44 {
|
||||
let mut t = w[i-1];
|
||||
if i % 4 == 0 {
|
||||
t.rotate_left(1);
|
||||
for b in &mut t { *b = SBOX[*b as usize]; }
|
||||
t[0] ^= rcon[i/4-1];
|
||||
}
|
||||
w[i] = [w[i-4][0]^t[0], w[i-4][1]^t[1], w[i-4][2]^t[2], w[i-4][3]^t[3]];
|
||||
}
|
||||
let mut rk = [[0u8; 16]; 11];
|
||||
for r in 0..11 { for c in 0..4 { rk[r][4*c..4*c+4].copy_from_slice(&w[r*4+c]); } }
|
||||
rk
|
||||
}
|
||||
|
||||
fn aes_block_encrypt(block: &[u8; 16], rk: &[[u8; 16]; 11]) -> [u8; 16] {
|
||||
let mut s = *block;
|
||||
add_round_key(&mut s, &rk[0]);
|
||||
for r in 1..10 { sub_bytes(&mut s); shift_rows(&mut s); mix_columns(&mut s); add_round_key(&mut s, &rk[r]); }
|
||||
sub_bytes(&mut s); shift_rows(&mut s); add_round_key(&mut s, &rk[10]);
|
||||
s
|
||||
}
|
||||
|
||||
fn aes_ecb_pkcs7_encrypt(key: &[u8; 16], plaintext: &[u8]) -> Vec<u8> {
|
||||
let rk = expand_key(key);
|
||||
let pad = 16 - (plaintext.len() % 16);
|
||||
let mut padded = plaintext.to_vec();
|
||||
padded.resize(plaintext.len() + pad, pad as u8);
|
||||
let mut out = Vec::with_capacity(padded.len());
|
||||
for chunk in padded.chunks(16) {
|
||||
let mut b = [0u8; 16]; b.copy_from_slice(chunk);
|
||||
out.extend_from_slice(&aes_block_encrypt(&b, &rk));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const INV_SBOX: [u8; 256] = [
|
||||
0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
|
||||
0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
|
||||
0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
|
||||
0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
|
||||
0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
|
||||
0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
|
||||
0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
|
||||
0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
|
||||
0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
|
||||
0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
|
||||
0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
|
||||
0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
|
||||
0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
|
||||
0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
|
||||
0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
|
||||
0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d,
|
||||
];
|
||||
|
||||
fn inv_sub_bytes(s: &mut [u8; 16]) { for b in s.iter_mut() { *b = INV_SBOX[*b as usize]; } }
|
||||
fn inv_shift_rows(s: &mut [u8; 16]) {
|
||||
let t = s[13]; s[13]=s[9]; s[9]=s[5]; s[5]=s[1]; s[1]=t;
|
||||
s.swap(2,10); s.swap(6,14);
|
||||
let t = s[3]; s[3]=s[7]; s[7]=s[11]; s[11]=s[15]; s[15]=t;
|
||||
}
|
||||
fn inv_mix_col(s: &mut [u8; 16], c: usize) {
|
||||
let (a,b,c2,d) = (s[c],s[c+4],s[c+8],s[c+12]);
|
||||
s[c] = mul(0x0e,a)^mul(0x0b,b)^mul(0x0d,c2)^mul(0x09,d);
|
||||
s[c+4] = mul(0x09,a)^mul(0x0e,b)^mul(0x0b,c2)^mul(0x0d,d);
|
||||
s[c+8] = mul(0x0d,a)^mul(0x09,b)^mul(0x0e,c2)^mul(0x0b,d);
|
||||
s[c+12] = mul(0x0b,a)^mul(0x0d,b)^mul(0x09,c2)^mul(0x0e,d);
|
||||
}
|
||||
fn inv_mix_columns(s: &mut [u8; 16]) { for c in 0..4 { inv_mix_col(s,c); } }
|
||||
|
||||
fn aes_ecb_decrypt_nopad(key: &[u8; 16], data: &[u8]) -> Vec<u8> {
|
||||
let rk = expand_key(key);
|
||||
let mut out = Vec::with_capacity(data.len());
|
||||
for chunk in data.chunks(16) {
|
||||
if chunk.len() < 16 { break; }
|
||||
let mut b = [0u8; 16]; b.copy_from_slice(chunk);
|
||||
add_round_key(&mut b, &rk[10]);
|
||||
inv_shift_rows(&mut b); inv_sub_bytes(&mut b);
|
||||
for r in (1..10).rev() {
|
||||
add_round_key(&mut b, &rk[r]);
|
||||
inv_mix_columns(&mut b); inv_shift_rows(&mut b); inv_sub_bytes(&mut b);
|
||||
}
|
||||
add_round_key(&mut b, &rk[0]);
|
||||
out.extend_from_slice(&b);
|
||||
}
|
||||
if let Some(&pad) = out.last() {
|
||||
let pad = pad as usize;
|
||||
if pad <= 16 && out.len() >= pad { out.truncate(out.len() - pad); }
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ─── CRandom ─────────────────────────────────────────────────────────────────
|
||||
|
||||
struct CRandom { seed: u32 }
|
||||
impl CRandom {
|
||||
fn new() -> Self { Self { seed: 0 } }
|
||||
fn seed_with(&mut self, s: u32) { self.seed = s; }
|
||||
fn rand(&mut self) -> u32 {
|
||||
self.seed = self.seed.wrapping_mul(214013).wrapping_add(2531011);
|
||||
(self.seed >> 16) & 0xFFFF
|
||||
}
|
||||
}
|
||||
|
||||
fn get_lsx_key(seed: u16) -> [u8; 16] {
|
||||
let mut rng = CRandom::new();
|
||||
rng.seed_with(7);
|
||||
let next = rng.rand();
|
||||
rng.seed_with(next.wrapping_add(seed as u32));
|
||||
let mut k = [0u8; 16];
|
||||
for b in &mut k { *b = rng.rand() as u8; }
|
||||
k
|
||||
}
|
||||
|
||||
// ─── session encrypt/decrypt ─────────────────────────────────────────────────
|
||||
|
||||
fn hex_to_bytes(s: &str) -> Vec<u8> {
|
||||
let s: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
||||
if s.len() % 2 != 0 { return Vec::new(); }
|
||||
(0..s.len()/2).filter_map(|i| u8::from_str_radix(&s[2*i..2*i+2], 16).ok()).collect()
|
||||
}
|
||||
|
||||
fn bytes_to_hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
pub fn lsx_decrypt(hex_data: &str, seed: u16) -> String {
|
||||
let key = get_lsx_key(seed);
|
||||
let ct = hex_to_bytes(hex_data);
|
||||
if ct.is_empty() { return String::new(); }
|
||||
let plain = aes_ecb_decrypt_nopad(&key, &ct);
|
||||
String::from_utf8_lossy(&plain).trim_matches('\0').to_string()
|
||||
}
|
||||
|
||||
pub fn lsx_encrypt(text: &str, seed: u16) -> String {
|
||||
let key = get_lsx_key(seed);
|
||||
bytes_to_hex(&aes_ecb_pkcs7_encrypt(&key, text.as_bytes()))
|
||||
}
|
||||
|
||||
pub fn make_challenge_response(key: &str) -> String {
|
||||
bytes_to_hex(&aes_ecb_pkcs7_encrypt(&AES_KEY, key.as_bytes()))
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/// Hooks RegQueryValueExA/W and OpenMutexA/W to log what the Origin SDK is checking.
|
||||
use std::sync::OnceLock;
|
||||
|
||||
type RegQueryValueExAFn = unsafe extern "system" fn(
|
||||
hkey: isize,
|
||||
lpvaluename: *const u8,
|
||||
lpreserved: *mut u32,
|
||||
lptype: *mut u32,
|
||||
lpdata: *mut u8,
|
||||
lpcbdata: *mut u32,
|
||||
) -> i32;
|
||||
|
||||
type RegQueryValueExWFn = unsafe extern "system" fn(
|
||||
hkey: isize,
|
||||
lpvaluename: *const u16,
|
||||
lpreserved: *mut u32,
|
||||
lptype: *mut u32,
|
||||
lpdata: *mut u8,
|
||||
lpcbdata: *mut u32,
|
||||
) -> i32;
|
||||
|
||||
type OpenMutexAFn = unsafe extern "system" fn(u32, i32, *const u8) -> isize;
|
||||
type OpenMutexWFn = unsafe extern "system" fn(u32, i32, *const u16) -> isize;
|
||||
|
||||
static REAL_REG_A: OnceLock<RegQueryValueExAFn> = OnceLock::new();
|
||||
static REAL_REG_W: OnceLock<RegQueryValueExWFn> = OnceLock::new();
|
||||
static REAL_MUTEX_A: OnceLock<OpenMutexAFn> = OnceLock::new();
|
||||
static REAL_MUTEX_W: OnceLock<OpenMutexWFn> = OnceLock::new();
|
||||
|
||||
pub fn set_real_reg_a(f: RegQueryValueExAFn) { let _ = REAL_REG_A.set(f); }
|
||||
pub fn set_real_reg_w(f: RegQueryValueExWFn) { let _ = REAL_REG_W.set(f); }
|
||||
pub fn set_real_mutex_a(f: OpenMutexAFn) { let _ = REAL_MUTEX_A.set(f); }
|
||||
pub fn set_real_mutex_w(f: OpenMutexWFn) { let _ = REAL_MUTEX_W.set(f); }
|
||||
|
||||
fn narrow_to_string(p: *const u8) -> String {
|
||||
if p.is_null() { return "(null)".into(); }
|
||||
let bytes = unsafe { std::ffi::CStr::from_ptr(p as *const i8) };
|
||||
bytes.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn wide_to_string(p: *const u16) -> String {
|
||||
if p.is_null() { return "(null)".into(); }
|
||||
let mut len = 0usize;
|
||||
unsafe { while *p.add(len) != 0 { len += 1; } }
|
||||
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, len) })
|
||||
}
|
||||
|
||||
fn is_interesting(name: &str) -> bool {
|
||||
name.contains("LSX") || name.contains("Origin") || name.contains("EAL") ||
|
||||
name.contains("Client") || name.contains("lsx") || name.contains("Port") ||
|
||||
name.contains("EA") || name.contains("Connection")
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn hooked_reg_query_a(
|
||||
hkey: isize,
|
||||
lpvaluename: *const u8,
|
||||
lpreserved: *mut u32,
|
||||
lptype: *mut u32,
|
||||
lpdata: *mut u8,
|
||||
lpcbdata: *mut u32,
|
||||
) -> i32 {
|
||||
let name = narrow_to_string(lpvaluename);
|
||||
let real = REAL_REG_A.get().copied().unwrap();
|
||||
let ret = real(hkey, lpvaluename, lpreserved, lptype, lpdata, lpcbdata);
|
||||
if is_interesting(&name) {
|
||||
crate::write_log(&format!("origin_spy: RegQueryValueExA({name}) → {ret}\n"));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn hooked_reg_query_w(
|
||||
hkey: isize,
|
||||
lpvaluename: *const u16,
|
||||
lpreserved: *mut u32,
|
||||
lptype: *mut u32,
|
||||
lpdata: *mut u8,
|
||||
lpcbdata: *mut u32,
|
||||
) -> i32 {
|
||||
let name = wide_to_string(lpvaluename);
|
||||
let real = REAL_REG_W.get().copied().unwrap();
|
||||
let ret = real(hkey, lpvaluename, lpreserved, lptype, lpdata, lpcbdata);
|
||||
if is_interesting(&name) {
|
||||
crate::write_log(&format!("origin_spy: RegQueryValueExW({name}) → {ret}\n"));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn hooked_open_mutex_a(
|
||||
dwdesiredaccess: u32,
|
||||
binherithandle: i32,
|
||||
lpmutexname: *const u8,
|
||||
) -> isize {
|
||||
let name = narrow_to_string(lpmutexname);
|
||||
let real = REAL_MUTEX_A.get().copied().unwrap();
|
||||
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
||||
crate::write_log(&format!("origin_spy: OpenMutexA({name}) → {}\n",
|
||||
if handle == 0 { "NOT_FOUND" } else { "FOUND" }));
|
||||
handle
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn hooked_open_mutex_w(
|
||||
dwdesiredaccess: u32,
|
||||
binherithandle: i32,
|
||||
lpmutexname: *const u16,
|
||||
) -> isize {
|
||||
let name = wide_to_string(lpmutexname);
|
||||
let real = REAL_MUTEX_W.get().copied().unwrap();
|
||||
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
||||
crate::write_log(&format!("origin_spy: OpenMutexW({name}) → {}\n",
|
||||
if handle == 0 { "NOT_FOUND" } else { "FOUND" }));
|
||||
handle
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/// Inline hooks on ws2_32!recv and ws2_32!send only.
|
||||
///
|
||||
/// WSARecv/WSASend are NOT hooked — their prologues contain RIP-relative
|
||||
/// (short conditional jump) instructions that would break trampolines.
|
||||
/// FIFA's LSX client uses plain recv/send, which is confirmed by prior logs.
|
||||
///
|
||||
/// Trampolines allow multiple threads to call the original function
|
||||
/// concurrently without locks or unhook/rehook races.
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
unsafe fn write_jmp(target: *mut u8, dest: u64) {
|
||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||
let mut old: u32 = 0;
|
||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||
target.write(0xFF); target.add(1).write(0x25);
|
||||
(target.add(2) as *mut u32).write(0);
|
||||
(target.add(6) as *mut u64).write(dest);
|
||||
VirtualProtect(target as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
unsafe fn make_trampoline(orig: *mut u8, name: &str) -> Option<usize> {
|
||||
use windows_sys::Win32::System::Memory::{
|
||||
VirtualAlloc, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE,
|
||||
};
|
||||
// Log prologue so we can diagnose if trampolines misbehave
|
||||
let bytes: [u8; 14] = core::array::from_fn(|i| *orig.add(i));
|
||||
let hex: String = bytes.iter().map(|b| format!("{b:02x} ")).collect();
|
||||
crate::write_log(&format!("recv_hook: {name} prologue {hex}\n"));
|
||||
|
||||
// Walk instruction boundaries to find relative branches.
|
||||
// Byte-by-byte scanning mis-identifies immediate operands (e.g. `sub rsp, 0x70`)
|
||||
// as jump opcodes, so we must parse properly.
|
||||
if has_rip_relative_branch(&bytes) {
|
||||
crate::write_log(&format!("recv_hook: {name} has relative branch in prologue, skipping trampoline\n"));
|
||||
return None;
|
||||
}
|
||||
|
||||
let mem = VirtualAlloc(
|
||||
core::ptr::null_mut(), 32,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE,
|
||||
);
|
||||
if mem.is_null() { crate::write_log("recv_hook: VirtualAlloc failed\n"); return None; }
|
||||
|
||||
let t = mem as *mut u8;
|
||||
core::ptr::copy_nonoverlapping(orig, t, 14);
|
||||
// JMP [RIP+0] → orig+14
|
||||
let cont = (orig as u64) + 14;
|
||||
t.add(14).write(0xFF); t.add(15).write(0x25);
|
||||
(t.add(16) as *mut u32).write(0);
|
||||
(t.add(20) as *mut u64).write(cont);
|
||||
Some(t as usize)
|
||||
}
|
||||
|
||||
/// Walk x86-64 instruction boundaries and return true if any relative branch
|
||||
/// (JE/JNE/JCC rel8, JMP rel8, JMP/CALL rel32, Jcc rel32) is encountered.
|
||||
/// Correctly skips over immediate operands so `sub rsp, 0x70` doesn't trigger.
|
||||
fn has_rip_relative_branch(bytes: &[u8]) -> bool {
|
||||
let mut pos = 0;
|
||||
while pos < bytes.len() {
|
||||
let (len, branch) = decode_instr_len(&bytes[pos..]);
|
||||
if branch { return true; }
|
||||
if len == 0 { break; } // unknown/truncated — stop safely
|
||||
pos += len;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn modrm_extra(modrm: u8) -> usize {
|
||||
let md = (modrm >> 6) & 3;
|
||||
let rm = modrm & 7;
|
||||
match md {
|
||||
0 => if rm == 5 { 4 } else if rm == 4 { 1 } else { 0 },
|
||||
1 => if rm == 4 { 2 } else { 1 },
|
||||
2 => if rm == 4 { 5 } else { 4 },
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns (instruction_length_in_bytes, is_rip_relative_branch).
|
||||
/// Returns (0, false) for unknown/truncated.
|
||||
fn decode_instr_len(b: &[u8]) -> (usize, bool) {
|
||||
if b.is_empty() { return (0, false); }
|
||||
let mut i = 0;
|
||||
// Legacy prefixes
|
||||
while let Some(&p) = b.get(i) {
|
||||
if matches!(p, 0x66 | 0x67 | 0xF0 | 0xF2 | 0xF3) { i += 1; } else { break; }
|
||||
}
|
||||
// REX prefix (40–4F)
|
||||
if b.get(i).copied().map(|x| (0x40..=0x4F).contains(&x)).unwrap_or(false) { i += 1; }
|
||||
|
||||
let op = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
||||
i += 1;
|
||||
|
||||
match op {
|
||||
// push/pop reg (50-5F): no extra bytes
|
||||
0x50..=0x5F => (i, false),
|
||||
// nop
|
||||
0x90 => (i, false),
|
||||
// Short Jcc (70-7F): 1 byte operand, IS a relative branch
|
||||
x if (0x70..=0x7F).contains(&x) => (i + 1, true),
|
||||
// JMP rel8, JMP rel32, CALL rel32
|
||||
0xEB => (i + 1, true),
|
||||
0xE9 | 0xE8 => (i + 4, true),
|
||||
// 0F prefix
|
||||
0x0F => {
|
||||
let op2 = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
||||
i += 1;
|
||||
if (0x80..=0x8F).contains(&op2) { return (i + 4, true); } // Jcc rel32
|
||||
// Most 0F XX: ModRM
|
||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
||||
(i + 1 + modrm_extra(modrm), false)
|
||||
}
|
||||
// Instructions with ModRM only (no immediate)
|
||||
0x85 | 0x87 | 0x88 | 0x89 | 0x8A | 0x8B | 0x8C | 0x8D | 0x8E | 0x8F |
|
||||
0x01 | 0x03 | 0x09 | 0x0B | 0x11 | 0x13 | 0x21 | 0x23 | 0x29 | 0x2B |
|
||||
0x31 | 0x33 | 0x39 | 0x3B | 0xD3 | 0xFF | 0xF7 => {
|
||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
||||
(i + 1 + modrm_extra(modrm), false)
|
||||
}
|
||||
// ModRM + imm8
|
||||
0x6B | 0x80 | 0x83 | 0xC0 | 0xC1 | 0xC6 => {
|
||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
||||
(i + 1 + modrm_extra(modrm) + 1, false)
|
||||
}
|
||||
// ModRM + imm32
|
||||
0x69 | 0x81 | 0xC7 => {
|
||||
let modrm = match b.get(i) { Some(&x) => x, None => return (0, false) };
|
||||
(i + 1 + modrm_extra(modrm) + 4, false)
|
||||
}
|
||||
// MOV reg, imm8/imm32
|
||||
0xB0..=0xB7 => (i + 1, false),
|
||||
0xB8..=0xBF => (i + 4, false),
|
||||
// PUSH imm
|
||||
0x6A => (i + 1, false),
|
||||
0x68 => (i + 4, false),
|
||||
// RET
|
||||
0xC2 => (i + 2, false),
|
||||
0xC3 => (i, false),
|
||||
_ => (0, false), // unknown — stop
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_fn(dll: &[u8], sym: &[u8]) -> Option<*mut u8> {
|
||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||
let h = GetModuleHandleA(dll.as_ptr());
|
||||
if h.is_null() { return None; }
|
||||
GetProcAddress(h, sym.as_ptr()).map(|f| f as *mut u8)
|
||||
}
|
||||
|
||||
// ─── recv ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
static RECV_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flags: i32) -> i32 {
|
||||
if crate::lsx::is_lsx(s) {
|
||||
return crate::lsx::on_recv(s, buf, len);
|
||||
}
|
||||
let t = RECV_TRAMPOLINE.load(Ordering::Relaxed);
|
||||
if t == 0 { return -1; }
|
||||
let f: unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32 = core::mem::transmute(t);
|
||||
f(s, buf, len, flags)
|
||||
}
|
||||
|
||||
pub unsafe fn install_recv_hook() -> bool {
|
||||
let ptr = match get_fn(b"ws2_32.dll\0", b"recv\0") { Some(p) => p, None => return false };
|
||||
match make_trampoline(ptr, "recv") {
|
||||
Some(t) => { RECV_TRAMPOLINE.store(t, Ordering::Relaxed); }
|
||||
None => { crate::write_log("recv_hook: recv trampoline failed, hook skipped\n"); return false; }
|
||||
}
|
||||
write_jmp(ptr, hooked_recv as u64);
|
||||
true
|
||||
}
|
||||
|
||||
// ─── send ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
static SEND_TRAMPOLINE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub unsafe extern "system" fn hooked_send(s: usize, buf: *const u8, len: i32, flags: i32) -> i32 {
|
||||
if crate::lsx::is_lsx(s) {
|
||||
return crate::lsx::on_send(s, buf, len);
|
||||
}
|
||||
let t = SEND_TRAMPOLINE.load(Ordering::Relaxed);
|
||||
if t == 0 { return -1; }
|
||||
let f: unsafe extern "system" fn(usize, *const u8, i32, i32) -> i32 = core::mem::transmute(t);
|
||||
f(s, buf, len, flags)
|
||||
}
|
||||
|
||||
pub unsafe fn install_send_hook() -> bool {
|
||||
let ptr = match get_fn(b"ws2_32.dll\0", b"send\0") { Some(p) => p, None => return false };
|
||||
match make_trampoline(ptr, "send") {
|
||||
Some(t) => { SEND_TRAMPOLINE.store(t, Ordering::Relaxed); }
|
||||
None => { crate::write_log("recv_hook: send trampoline failed, hook skipped\n"); return false; }
|
||||
}
|
||||
write_jmp(ptr, hooked_send as u64);
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Runtime in-memory patch for ProtoSSL's certificate verification function inside
|
||||
// EAWebKit.dll. Rather than patching the DLL on disk (offset-dependent, fragile),
|
||||
// we scan the loaded module for the function's unique byte prologue and overwrite the
|
||||
// first six bytes with `mov eax, 1; ret` — making every cert-chain validation call
|
||||
// immediately return success.
|
||||
//
|
||||
// Why this is safe: the patched function (`ProtoSSL_VerifyCert` at VA 0x180a85570 in
|
||||
// the shipped binary) is only used by ProtoSSL's TLS state machine to validate the
|
||||
// server's certificate chain. Always returning 1 is equivalent to trusting all certs,
|
||||
// which is the behaviour we want for the local self-signed bridge certificate.
|
||||
|
||||
use windows_sys::Win32::{
|
||||
System::{
|
||||
LibraryLoader::GetModuleHandleA,
|
||||
Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE},
|
||||
},
|
||||
};
|
||||
|
||||
// Unique 22-byte prologue of ProtoSSL's cert-verify function.
|
||||
// Confirmed present in the EA-shipped EAWebKit.dll (June 2023 build).
|
||||
const PROLOGUE: &[u8] = &[
|
||||
0x44, 0x89, 0x44, 0x24, 0x18, // mov [rsp+0x18], r8d
|
||||
0x48, 0x89, 0x54, 0x24, 0x10, // mov [rsp+0x10], rdx
|
||||
0x56, // push rsi
|
||||
0x57, // push rdi
|
||||
0x41, 0x55, // push r13
|
||||
0x41, 0x56, // push r14
|
||||
0x41, 0x57, // push r15
|
||||
0x48, 0x83, 0xec, 0x30, // sub rsp, 0x30
|
||||
];
|
||||
|
||||
// Return 0 (PROTOSSL_ERROR_NONE = success). ProtoSSL convention: 0 = ok, negative = error.
|
||||
// The function sets r15d = 0xFFFFFFFF (-1) for its own error returns, confirming 0 = success.
|
||||
const PATCH: &[u8] = &[
|
||||
0x31, 0xc0, // xor eax, eax (eax = 0 = PROTOSSL_ERROR_NONE)
|
||||
0xc3, // ret
|
||||
0x90, 0x90, 0x90, // nop padding
|
||||
];
|
||||
|
||||
fn patch_module(module: isize, scan_bytes: usize) -> bool {
|
||||
if module == 0 { return false; }
|
||||
let base = module as usize;
|
||||
let image: &[u8] = unsafe { core::slice::from_raw_parts(base as *const u8, scan_bytes) };
|
||||
let offset = match image.windows(PROLOGUE.len()).position(|w| w == PROLOGUE) {
|
||||
Some(o) => o,
|
||||
None => return false,
|
||||
};
|
||||
let target = (base + offset) as *mut u8;
|
||||
let mut old_prot: u32 = 0;
|
||||
unsafe {
|
||||
VirtualProtect(target as *const core::ffi::c_void, PATCH.len(), PAGE_EXECUTE_READWRITE, &mut old_prot);
|
||||
core::ptr::copy_nonoverlapping(PATCH.as_ptr(), target, PATCH.len());
|
||||
VirtualProtect(target as *const core::ffi::c_void, PATCH.len(), old_prot, &mut old_prot);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Patch ProtoSSL cert-verify in EAWebKit.dll (call when EAWebKit is loaded).
|
||||
pub unsafe fn patch_eawebkit_cert_verify() -> bool {
|
||||
let module = GetModuleHandleA(b"EAWebKit.dll\0".as_ptr()) as isize;
|
||||
// EAWebKit.dll is ~22 MB
|
||||
patch_module(module, 24 * 1024 * 1024)
|
||||
}
|
||||
|
||||
/// Patch ProtoSSL cert-verify compiled into FIFA23.exe itself (DirtySDK's copy).
|
||||
/// The main exe is ~100 MB; confirmed present at file offset 0xf0c850.
|
||||
pub unsafe fn patch_main_exe_cert_verify() -> bool {
|
||||
let module = GetModuleHandleA(core::ptr::null()) as isize;
|
||||
// Scan first 110 MB — the function is near offset 0xf0c850 (~15 MB in)
|
||||
patch_module(module, 110 * 1024 * 1024)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::sync::OnceLock;
|
||||
use windows_sys::Win32::Foundation::BOOL;
|
||||
|
||||
// CERT_CHAIN_POLICY_STATUS.dwError offset 0 = u32 error code; 0 = success.
|
||||
// We use raw pointers to avoid pulling in the full Cryptography struct tree.
|
||||
type CertVerifyChainPolicyFn = unsafe extern "system" fn(
|
||||
*const u8, // pszPolicyOID
|
||||
*const (), // pChainContext
|
||||
*const (), // pPolicyPara
|
||||
*mut u32, // &mut pPolicyStatus.dwError (first field)
|
||||
) -> BOOL;
|
||||
|
||||
static REAL: OnceLock<CertVerifyChainPolicyFn> = OnceLock::new();
|
||||
|
||||
pub fn set_real(f: CertVerifyChainPolicyFn) {
|
||||
let _ = REAL.set(f);
|
||||
}
|
||||
|
||||
/// Hooked CertVerifyCertificateChainPolicy — always reports success.
|
||||
/// This allows the bridge's self-signed TLS cert to be accepted by the game.
|
||||
pub unsafe extern "system" fn hooked_cert_verify_chain_policy(
|
||||
psz_policy_oid: *const u8,
|
||||
p_chain_context: *const (),
|
||||
p_policy_para: *const (),
|
||||
p_policy_status: *mut u32,
|
||||
) -> BOOL {
|
||||
if let Some(real) = REAL.get().copied() {
|
||||
real(psz_policy_oid, p_chain_context, p_policy_para, p_policy_status);
|
||||
}
|
||||
// Clear the error field of CERT_CHAIN_POLICY_STATUS regardless
|
||||
if !p_policy_status.is_null() {
|
||||
*p_policy_status = 0;
|
||||
}
|
||||
1 // TRUE = verified OK
|
||||
}
|
||||
+2
-2
@@ -518,7 +518,7 @@ impl LauncherApp {
|
||||
changed |= ui.text_edit_singleline(&mut self.config.core_data_dir).changed();
|
||||
ui.end_row();
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.label("");
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
|
||||
@@ -546,7 +546,7 @@ impl LauncherApp {
|
||||
changed |= ui.checkbox(&mut self.config.bridge_tls_enabled, "").changed();
|
||||
ui.end_row();
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.label("");
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user