309 lines
11 KiB
Rust
309 lines
11 KiB
Rust
/// 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;
|
|
|
|
#[repr(C)]
|
|
struct SockaddrIn {
|
|
sin_family: u16,
|
|
sin_port: u16,
|
|
sin_addr: u32,
|
|
sin_zero: [u8; 8],
|
|
}
|
|
|
|
const AF_INET6: u16 = 23; // Windows AF_INET6 value (we run under the Win ABI in Wine)
|
|
|
|
/// Win32 `sockaddr_in6`. `sin6_port` is network byte order; `sin6_addr` is 16 raw
|
|
/// address bytes in network order. 28 bytes total.
|
|
#[repr(C)]
|
|
struct SockaddrIn6 {
|
|
sin6_family: u16,
|
|
sin6_port: u16,
|
|
sin6_flowinfo: u32,
|
|
sin6_addr: [u8; 16],
|
|
sin6_scope_id: u32,
|
|
}
|
|
|
|
// 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];
|
|
|
|
/// Restores the real WinSock call's thread-local last error after detour repair,
|
|
/// logging, and other instrumentation have run. Callers inspect this value after
|
|
/// `SOCKET_ERROR`; leaking a logger/VirtualProtect error changes connect semantics.
|
|
struct WsaLastErrorGuard(i32);
|
|
|
|
impl WsaLastErrorGuard {
|
|
unsafe fn capture() -> Self {
|
|
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
|
|
Self(WSAGetLastError())
|
|
}
|
|
|
|
fn value(&self) -> i32 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Drop for WsaLastErrorGuard {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
use windows_sys::Win32::Networking::WinSock::WSASetLastError;
|
|
WSASetLastError(self.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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(core::ptr::addr_of!(ORIGINAL_BYTES) as *const u8, target, 14);
|
|
VirtualProtect(target as _, 14, old, &mut old);
|
|
}
|
|
|
|
/// The armed redirect target, resolved once from `openfut.cfg` via `openfut-common`.
|
|
/// When set, `redirect_if_ea` rewrites matched EA connections to this configured
|
|
/// server; when unset, matched connections are left untouched (no redirect).
|
|
static REDIRECT: OnceLock<openfut_common::ResolvedServer> = OnceLock::new();
|
|
|
|
/// Arm the config-driven redirect (FIFA17). Idempotent: the first call wins.
|
|
pub fn set_redirect(server: openfut_common::ResolvedServer) {
|
|
let _ = REDIRECT.set(server);
|
|
}
|
|
|
|
/// If `name` is a matched EA connect target, return a rewritten sockaddr pointing
|
|
/// at the configured OpenFUT server (plus its meaningful byte length: 16 for v4,
|
|
/// 28 for v6). The target is armed once from `openfut.cfg` via `set_redirect`;
|
|
/// when unset — or when the port is not a known EA route — the connection is left
|
|
/// untouched. Shared by the connect / WSAConnect / ConnectEx detours.
|
|
pub(crate) unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 28], i32)> {
|
|
if namelen < 8 || name.is_null() {
|
|
return None;
|
|
}
|
|
redirect_configured(REDIRECT.get()?, name, namelen)
|
|
}
|
|
|
|
/// FIFA17 config-driven rewrite. Destination host+port come from `openfut.cfg`
|
|
/// through `openfut-common`, so the hook and the launcher agree by construction.
|
|
/// Matching is by EA source-port signature only (see `openfut_common::ea_ports`),
|
|
/// so a hardcoded EA IP (e.g. the `159.153.51.20:42230` redirector) and a
|
|
/// DNS-resolved one both land on the configured — possibly remote — server. An
|
|
/// unrecognised port returns `None` (connection left untouched). Never corrupts
|
|
/// the sockaddr: it only writes into a fresh 28-byte buffer.
|
|
unsafe fn redirect_configured(
|
|
server: &openfut_common::ResolvedServer,
|
|
name: *const u8,
|
|
namelen: i32,
|
|
) -> Option<([u8; 28], i32)> {
|
|
let family = *(name as *const u16);
|
|
let mut buf = [0u8; 28];
|
|
match family {
|
|
AF_INET => {
|
|
let sa = &*(name as *const SockaddrIn);
|
|
let redir = server.redirect_for_ea_port(sa.sin_port)?;
|
|
crate::write_log(&format!(
|
|
"connect_hook: v4 :{} → {}:{}\n",
|
|
u16::from_be(sa.sin_port),
|
|
redir.redirect_ip,
|
|
u16::from_be(redir.port_nbo)
|
|
));
|
|
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
|
out.sin_family = AF_INET;
|
|
out.sin_port = redir.port_nbo;
|
|
out.sin_addr = redir.addr_nbo;
|
|
Some((buf, 16))
|
|
}
|
|
AF_INET6 => {
|
|
if namelen < 28 {
|
|
return None;
|
|
}
|
|
let sa6 = &*(name as *const SockaddrIn6);
|
|
let redir = server.redirect_for_ea_port(sa6.sin6_port)?;
|
|
// ::ffff:<redirect_ip> — a v4-mapped v6 target so a v6 socket sends
|
|
// real IPv4 packets to the configured server.
|
|
let o = redir.redirect_ip.octets();
|
|
let mut v4mapped = [0u8; 16];
|
|
v4mapped[10] = 0xff;
|
|
v4mapped[11] = 0xff;
|
|
v4mapped[12..16].copy_from_slice(&o);
|
|
crate::write_log(&format!(
|
|
"connect_hook: v6 :{} → ::ffff:{}:{}\n",
|
|
u16::from_be(sa6.sin6_port),
|
|
redir.redirect_ip,
|
|
u16::from_be(redir.port_nbo)
|
|
));
|
|
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn6);
|
|
out.sin6_family = AF_INET6;
|
|
out.sin6_port = redir.port_nbo;
|
|
out.sin6_flowinfo = 0;
|
|
out.sin6_addr = v4mapped;
|
|
out.sin6_scope_id = 0;
|
|
Some((buf, 28))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
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,
|
|
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)
|
|
};
|
|
// Named binding held until `return r`: its Drop restores the WSA error after `write_hook`.
|
|
let _last_error = WsaLastErrorGuard::capture();
|
|
write_hook(addr, hooked_connect as *const () 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)
|
|
};
|
|
let last_error = WsaLastErrorGuard::capture();
|
|
write_hook(addr, hooked_connect as *const () as u64);
|
|
if namelen >= 8 {
|
|
let sa = &*(call_name as *const SockaddrIn);
|
|
if sa.sin_family == AF_INET {
|
|
let logged_error = if r != 0 { last_error.value() } else { 0 };
|
|
crate::write_log(&format!(
|
|
"connect_hook: result={r} wsa_err={logged_error}\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(c"ws2_32.dll".as_ptr().cast());
|
|
if ws2.is_null() {
|
|
return false;
|
|
}
|
|
let connect_fn = match GetProcAddress(ws2, c"connect".as_ptr().cast()) {
|
|
Some(f) => f as *mut u8,
|
|
None => return false,
|
|
};
|
|
|
|
// Save original 14 bytes
|
|
core::ptr::copy_nonoverlapping(
|
|
connect_fn,
|
|
core::ptr::addr_of_mut!(ORIGINAL_BYTES) as *mut u8,
|
|
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 *const () as u64);
|
|
true
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::WsaLastErrorGuard;
|
|
use windows_sys::Win32::Networking::WinSock::{
|
|
WSAGetLastError, WSASetLastError, WSAEWOULDBLOCK,
|
|
};
|
|
|
|
#[test]
|
|
fn restores_winsock_last_error_after_instrumentation() {
|
|
unsafe {
|
|
WSASetLastError(WSAEWOULDBLOCK);
|
|
{
|
|
let guard = WsaLastErrorGuard::capture();
|
|
assert_eq!(guard.value(), WSAEWOULDBLOCK);
|
|
WSASetLastError(0);
|
|
}
|
|
assert_eq!(WSAGetLastError(), WSAEWOULDBLOCK);
|
|
}
|
|
}
|
|
}
|