From 4e44a3792f79150c423b998f08807f13416f9c20 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 13 Aug 2026 15:10:12 +0000 Subject: [PATCH] wip(hook): preserve local FIFA17 configurable-server redirect + hook refactors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserved uncommitted openfut-hook WIP from the feat/launcher-arming checkout so the canonical launcher can move to the reconciled merge ca7ce26 without losing it. This is WORK-IN-PROGRESS (not production-polished): a configurable OpenFUT-server redirect for the injected hook — new src/server.rs (server::set / sin_addr / dest_port_nbo_from_source_nbo, referenced by lib.rs + connect_hook + connectex_hook) plus supporting changes to config.rs, connect_hook.rs, connectex_hook.rs, hooks.rs, iat.rs, lib.rs, origin_spy.rs, ssl_patch.rs, tls_bypass.rs, Cargo.{toml,lock}. NOTE: this overlaps (same files, different approach) the SBC-tracing lineage's openfut-hook changes now merged in ca7ce26; reconciling the two hook variants is a separate, deliberate task. This commit only PRESERVES the local WIP on a dedicated branch. --- openfut-hook/Cargo.lock | 5 + openfut-hook/Cargo.toml | 4 + openfut-hook/src/config.rs | 28 +++--- openfut-hook/src/connect_hook.rs | 133 +++++++++++++++++-------- openfut-hook/src/connectex_hook.rs | 101 +++++++++++-------- openfut-hook/src/hooks.rs | 55 +++++++---- openfut-hook/src/iat.rs | 19 ++-- openfut-hook/src/lib.rs | 153 ++++++++++++++++++++++------- openfut-hook/src/origin_spy.rs | 53 +++++++--- openfut-hook/src/server.rs | 45 +++++++++ openfut-hook/src/ssl_patch.rs | 44 +++++---- openfut-hook/src/tls_bypass.rs | 15 ++- 12 files changed, 464 insertions(+), 191 deletions(-) create mode 100644 openfut-hook/src/server.rs diff --git a/openfut-hook/Cargo.lock b/openfut-hook/Cargo.lock index f02d3a6..c75ff37 100644 --- a/openfut-hook/Cargo.lock +++ b/openfut-hook/Cargo.lock @@ -2,10 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "openfut-common" +version = "0.1.0" + [[package]] name = "openfut-hook" version = "0.1.0" dependencies = [ + "openfut-common", "windows-sys", ] diff --git a/openfut-hook/Cargo.toml b/openfut-hook/Cargo.toml index fa73019..6425e82 100644 --- a/openfut-hook/Cargo.toml +++ b/openfut-hook/Cargo.toml @@ -7,6 +7,10 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] +# Shared, dependency-free source of truth for the OpenFUT destination +# (host + ports) and the sockaddr byte-order helpers. Keeps all three hooks +# consistent and keeps this logic host-testable outside WinSock. +openfut-common = { path = "../openfut-common" } windows-sys = { version = "0.59", features = [ "Win32_Foundation", "Win32_System_LibraryLoader", diff --git a/openfut-hook/src/config.rs b/openfut-hook/src/config.rs index f50dc3b..1fa6388 100644 --- a/openfut-hook/src/config.rs +++ b/openfut-hook/src/config.rs @@ -1,20 +1,18 @@ -/// Reads openfut.cfg from the same directory as this DLL. -/// -/// The file contains a single line: the IP the hook should redirect EA -/// hostnames to, e.g. "192.168.1.10" or "127.0.0.1". -/// Falls back to 127.0.0.1 if the file is missing or unreadable. +//! Loads `openfut.cfg` (next to this DLL) into a shared [`ServerConfig`]. +//! +//! There is intentionally **no loopback fallback**: if the file is missing, +//! empty, or invalid, this returns a [`ConfigError`] and the caller logs it and +//! declines to redirect. Missing configuration is an error, never `127.0.0.1`. +use openfut_common::{ConfigError, ServerConfig}; use windows_sys::Win32::System::LibraryLoader::GetModuleFileNameA; -pub fn read_redirect_ip(module: windows_sys::Win32::Foundation::HMODULE) -> String { - if let Some(cfg_path) = config_path(module) { - if let Ok(content) = std::fs::read_to_string(&cfg_path) { - let ip = content.trim().to_string(); - if !ip.is_empty() { - return ip; - } - } - } - "127.0.0.1".to_string() +/// Read and parse `openfut.cfg` from the same directory as this DLL. +pub fn load_config( + module: windows_sys::Win32::Foundation::HMODULE, +) -> Result { + let path = config_path(module).ok_or(ConfigError::ConfigMissing)?; + let contents = std::fs::read_to_string(&path).map_err(|_| ConfigError::ConfigMissing)?; + ServerConfig::parse(&contents) } fn config_path(module: windows_sys::Win32::Foundation::HMODULE) -> Option { diff --git a/openfut-hook/src/connect_hook.rs b/openfut-hook/src/connect_hook.rs index 35086c7..4d81f36 100644 --- a/openfut-hook/src/connect_hook.rs +++ b/openfut-hook/src/connect_hook.rs @@ -1,22 +1,23 @@ /// 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. +/// +/// The redirect destination (IP + port) comes entirely from the shared +/// [`crate::server`] state, which is populated once from `openfut.cfg`. This +/// hook does NOT choose an address itself — no hardcoded loopback, no per-hook +/// redirect IP. EA *source* ports are recognised via `openfut-common`'s port +/// map; the matching OpenFUT *destination* port + configured IP are substituted. 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], + sin_port: u16, + sin_addr: u32, + sin_zero: [u8; 8], } // Address of ws2_32!connect (set at hook installation) @@ -27,18 +28,26 @@ 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; + s: usize, + name: *const u8, + namelen: i32, + caller: *const (), + callee: *const (), + sqos: *const (), + gqos: *const (), +) -> i32; static REAL_WSA: OnceLock = OnceLock::new(); -pub fn set_real_wsa_connect(f: WsaConnectFn) { let _ = REAL_WSA.set(f); } +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.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); @@ -53,31 +62,46 @@ unsafe fn restore_original(target: *mut u8) { } unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 16], i32)> { - if namelen < 8 { return None; } + if namelen < 8 { + return None; + } let sa = &*(name as *const SockaddrIn); - if sa.sin_family != AF_INET { return None; } + 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, - }; + // Map the EA source port to an OpenFUT destination port from shared config. + // Returns None if this port isn't intercepted or no server is configured — + // in which case we leave the connection untouched (no loopback fallback). + let new_port_nbo = crate::server::dest_port_nbo_from_source_nbo(sa.sin_port)?; + // The destination IP is the configured/resolved OpenFUT server — never a + // hardcoded address. If unset, dest_port_nbo_from_source_nbo already + // returned None above, so this is guaranteed Some here. + let new_addr = crate::server::sin_addr()?; + let ni = new_addr.to_le_bytes(); crate::write_log(&format!( - "connect_hook: {}.{}.{}.{}:{} → 127.0.0.1:{}\n", - orig[3], orig[2], orig[1], orig[0], orig_port, + "connect_hook: {}.{}.{}.{}:{} → {}.{}.{}.{}:{} (configured OpenFUT server)\n", + orig[3], + orig[2], + orig[1], + orig[0], + orig_port, + ni[0], + ni[1], + ni[2], + ni[3], 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; + out.sin_port = new_port_nbo; + out.sin_addr = new_addr; Some((buf, 16)) } @@ -95,7 +119,13 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen: 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); + getsockopt( + s, + SOL_SOCKET as i32, + SO_TYPE, + &mut ty as *mut i32 as *mut u8, + &mut len, + ); ty }; crate::write_log(&format!( @@ -111,11 +141,25 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen: 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); + let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 = + core::mem::transmute(addr); f(s, buf.as_ptr(), len) }; + // connect() communicates nonblocking progress through WSAGetLastError. + // Reinstalling the detour calls VirtualProtect, which may overwrite that + // thread-local value before FIFA reads it. Preserve the real call's value + // across all hook maintenance and logging. + let wsa_error = if r != 0 { + use windows_sys::Win32::Networking::WinSock::WSAGetLastError; + WSAGetLastError() + } else { + 0 + }; write_hook(addr, hooked_connect as u64); + if r != 0 { + use windows_sys::Win32::Networking::WinSock::WSASetLastError; + WSASetLastError(wsa_error); + } return r; } else { (name, namelen) @@ -123,28 +167,37 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen: restore_original(addr); let r = { - let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 - = core::mem::transmute(addr); + let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 = core::mem::transmute(addr); f(s, call_name, call_len) }; + let wsa_error = if r != 0 { + use windows_sys::Win32::Networking::WinSock::WSAGetLastError; + WSAGetLastError() + } else { + 0 + }; 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")); + crate::write_log(&format!("connect_hook: result={r} wsa_err={wsa_error}\n")); } } + if r != 0 { + use windows_sys::Win32::Networking::WinSock::WSASetLastError; + WSASetLastError(wsa_error); + } r } pub unsafe extern "system" fn hooked_wsa_connect( - s: usize, name: *const u8, namelen: i32, - caller: *const (), callee: *const (), - sqos: *const (), gqos: *const (), + 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) { @@ -159,7 +212,9 @@ 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; } + 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, diff --git a/openfut-hook/src/connectex_hook.rs b/openfut-hook/src/connectex_hook.rs index a50b6b7..06d26ed 100644 --- a/openfut-hook/src/connectex_hook.rs +++ b/openfut-hook/src/connectex_hook.rs @@ -1,34 +1,27 @@ +use core::ffi::c_void; /// 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, + 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], + sin_port: u16, + sin_addr: u32, + sin_zero: [u8; 8], } // The real ConnectEx pointer, saved after WSAIoctl returns it @@ -65,7 +58,8 @@ 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.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); @@ -79,7 +73,7 @@ unsafe fn restore_wsaioctl(target: *mut u8) { VirtualProtect(target as _, 14, old, &mut old); } -/// Our ConnectEx wrapper: redirects EA ports to 127.0.0.1 +/// Our ConnectEx wrapper: redirects intercepted EA endpoints to the configured OpenFUT server unsafe extern "system" fn hooked_connectex( s: usize, name: *const u8, @@ -96,28 +90,53 @@ unsafe extern "system" fn hooked_connectex( 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 { + // Destination port + IP come from the shared configured server — + // never a hardcoded loopback. None => not intercepted / unconfigured, + // so the original connection is passed through untouched. + if let (Some(new_port_nbo), Some(new_addr)) = ( + crate::server::dest_port_nbo_from_source_nbo(sa.sin_port), + crate::server::sin_addr(), + ) { + let ni = new_addr.to_le_bytes(); crate::write_log(&format!( - "connectex_hook: {}.{}.{}.{}:{} → 127.0.0.1:{}\n", - o[3], o[2], o[1], o[0], orig_port, + "connectex_hook: {}.{}.{}.{}:{} → {}.{}.{}.{}:{} (configured OpenFUT server)\n", + o[3], + o[2], + o[1], + o[0], + orig_port, + ni[0], + ni[1], + ni[2], + ni[3], 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); + out.sin_port = new_port_nbo; + out.sin_addr = new_addr; + 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) + 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 @@ -138,25 +157,25 @@ pub unsafe extern "system" fn hooked_wsaioctl( 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) + 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() - { + 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() - { + 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")); + 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; @@ -168,7 +187,9 @@ pub unsafe extern "system" fn hooked_wsaioctl( 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; } + 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, diff --git a/openfut-hook/src/hooks.rs b/openfut-hook/src/hooks.rs index 6246c4c..26953c5 100644 --- a/openfut-hook/src/hooks.rs +++ b/openfut-hook/src/hooks.rs @@ -1,22 +1,23 @@ use std::{ ffi::CStr, sync::{ - OnceLock, atomic::{AtomicBool, Ordering}, + OnceLock, }, }; -use windows_sys::Win32::Networking::WinSock::{ADDRINFOA, getaddrinfo as sys_getaddrinfo}; +use windows_sys::Win32::Networking::WinSock::{getaddrinfo as sys_getaddrinfo, ADDRINFOA}; -type GetaddrinfoFn = unsafe extern "system" fn( - *const u8, - *const u8, - *const ADDRINFOA, - *mut *mut ADDRINFOA, -) -> i32; +type GetaddrinfoFn = + unsafe extern "system" fn(*const u8, *const u8, *const ADDRINFOA, *mut *mut ADDRINFOA) -> i32; static REAL: OnceLock = OnceLock::new(); -static REDIRECT_IP: OnceLock> = OnceLock::new(); +// NUL-terminated dotted-quad of the resolved OpenFUT server, built once at init +// from the SAME shared config the socket hooks use. getaddrinfo redirects EA +// hostnames here so DNS resolves to the configured server. If configuration was +// missing/invalid this stays empty and EA hostnames are NOT redirected (no +// loopback fallback). +static REDIRECT_HOST: OnceLock> = 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 @@ -27,10 +28,12 @@ pub fn set_real(f: GetaddrinfoFn) { let _ = REAL.set(f); } -pub fn set_redirect_ip(ip: String) { - let mut bytes = ip.into_bytes(); +/// Install the resolved redirect IPv4 (dotted-quad) getaddrinfo will hand back +/// for EA hostnames. Called once at init from the shared resolved server. +pub fn set_redirect_ip(ip: std::net::Ipv4Addr) { + let mut bytes = ip.to_string().into_bytes(); bytes.push(0); - let _ = REDIRECT_IP.set(bytes); + let _ = REDIRECT_HOST.set(bytes); } /// Returns true if `host` is an EA / EA-Sports domain that should be redirected @@ -60,18 +63,30 @@ pub unsafe extern "system" fn hooked_getaddrinfo( 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"); + 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"); + 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); + // Only redirect when a server was configured & resolved. If not, + // fall through to the real resolver — we never invent a loopback + // destination here. + match REDIRECT_HOST.get() { + Some(redirect) => { + let real = REAL.get().copied().unwrap_or(sys_getaddrinfo); + return real(redirect.as_ptr(), service_name, hints, result); + } + None => { + crate::write_log( + "openfut_hook: EA host seen but no OpenFUT server configured — NOT redirecting\n", + ); + } + } } } } diff --git a/openfut-hook/src/iat.rs b/openfut-hook/src/iat.rs index 746f3e6..78f82e3 100644 --- a/openfut-hook/src/iat.rs +++ b/openfut-hook/src/iat.rs @@ -72,7 +72,11 @@ pub unsafe fn patch_iat(original_fn: *const (), hook_fn: *const ()) -> usize { } /// 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 { +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; @@ -80,11 +84,7 @@ pub unsafe fn patch_iat_in(module_name: &[u8], original_fn: *const (), hook_fn: patch_module(module, original_fn, hook_fn) } -unsafe fn patch_module( - module: HMODULE, - original_fn: *const (), - hook_fn: *const (), -) -> usize { +unsafe fn patch_module(module: HMODULE, original_fn: *const (), hook_fn: *const ()) -> usize { if module.is_null() { return 0; } @@ -117,7 +117,12 @@ unsafe fn patch_module( if val == original_fn as usize { let target = iat_slot.add(i) as *const std::ffi::c_void; let mut old: u32 = 0; - VirtualProtect(target, std::mem::size_of::(), PAGE_EXECUTE_READWRITE, &mut old); + VirtualProtect( + target, + std::mem::size_of::(), + PAGE_EXECUTE_READWRITE, + &mut old, + ); *iat_slot.add(i) = hook_fn as usize; VirtualProtect(target, std::mem::size_of::(), old, &mut old); count += 1; diff --git a/openfut-hook/src/lib.rs b/openfut-hook/src/lib.rs index 54fe334..4f16e22 100644 --- a/openfut-hook/src/lib.rs +++ b/openfut-hook/src/lib.rs @@ -4,62 +4,115 @@ mod connectex_hook; mod hooks; mod iat; mod origin_spy; +mod server; mod ssl_patch; mod tls_bypass; use windows_sys::Win32::{ Foundation::{BOOL, HMODULE, TRUE}, - System::SystemServices::DLL_PROCESS_ATTACH, Networking::WinSock::ADDRINFOA, + System::SystemServices::DLL_PROCESS_ATTACH, }; pub(crate) fn write_log(msg: &str) { use std::io::Write; if let Ok(mut f) = std::fs::OpenOptions::new() - .create(true).append(true) + .create(true) + .append(true) .open(r"C:\openfut_hook.log") - { let _ = f.write_all(msg.as_bytes()); } + { + let _ = f.write_all(msg.as_bytes()); + } } #[no_mangle] pub unsafe extern "system" fn DllMain(module: HMODULE, reason: u32, _: *mut ()) -> BOOL { - if reason == DLL_PROCESS_ATTACH { install_hooks(module); } + if reason == DLL_PROCESS_ATTACH { + install_hooks(module); + } TRUE } unsafe fn install_hooks(module: HMODULE) { write_log("openfut_hook: DllMain fired\n"); - let ip = config::read_redirect_ip(module); - hooks::set_redirect_ip(ip); + + // Load the single source of truth for the OpenFUT destination. If it's + // missing/invalid we log and install NO redirection — traffic is left alone + // rather than silently sent to loopback. + match config::load_config(module).and_then(|c| c.resolve()) { + Ok(resolved) => { + server::set(resolved); + hooks::set_redirect_ip(resolved.redirect_ip); + let o = resolved.redirect_ip.octets(); + write_log(&format!( + "openfut_hook: OpenFUT server = {}.{}.{}.{} (https={} blaze_redir={} blaze_main={})\n", + o[0], o[1], o[2], o[3], + resolved.ports.https, resolved.ports.blaze_redirector, resolved.ports.blaze_main + )); + } + Err(e) => { + write_log(&format!( + "openfut_hook: NO OpenFUT server configured ({e}); redirection DISABLED. \ + Configure a server in the launcher and relaunch.\n" + )); + } + } 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); + 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 ()); + 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")); } - 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"); } + 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"); + } - if connect_hook::install_inline_connect_hook() { write_log("connect: inline-hooked\n"); } - else { write_log("connect: hook FAILED\n"); } + 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); + 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"); } + 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. @@ -72,31 +125,63 @@ unsafe fn install_hooks(module: HMODULE) { origin_spy::$setter(f); iat::patch_iat(ptr, $handler as *const ()); "ok" - } else { "miss" } + } else { + "miss" + } }}; } - let ra = hook_iat!(b"advapi32.dll\0", b"RegQueryValueExA\0", set_real_reg_a, + 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, + 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, + 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, + 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")); + 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); + 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 ()); + 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 (), + ); } } diff --git a/openfut-hook/src/origin_spy.rs b/openfut-hook/src/origin_spy.rs index fc0040c..8974945 100644 --- a/openfut-hook/src/origin_spy.rs +++ b/openfut-hook/src/origin_spy.rs @@ -27,28 +27,49 @@ static REAL_REG_W: OnceLock = OnceLock::new(); static REAL_MUTEX_A: OnceLock = OnceLock::new(); static REAL_MUTEX_W: OnceLock = 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); } +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(); } + 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(); } + if p.is_null() { + return "(null)".into(); + } let mut len = 0usize; - unsafe { while *p.add(len) != 0 { len += 1; } } + 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") + 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( @@ -93,8 +114,10 @@ pub unsafe extern "system" fn hooked_open_mutex_a( 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" })); + crate::write_log(&format!( + "origin_spy: OpenMutexA({name}) → {}\n", + if handle == 0 { "NOT_FOUND" } else { "FOUND" } + )); handle } @@ -106,7 +129,9 @@ pub unsafe extern "system" fn hooked_open_mutex_w( 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" })); + crate::write_log(&format!( + "origin_spy: OpenMutexW({name}) → {}\n", + if handle == 0 { "NOT_FOUND" } else { "FOUND" } + )); handle } diff --git a/openfut-hook/src/server.rs b/openfut-hook/src/server.rs new file mode 100644 index 0000000..2401684 --- /dev/null +++ b/openfut-hook/src/server.rs @@ -0,0 +1,45 @@ +//! The single, process-wide resolved OpenFUT destination. +//! +//! All three interception layers — `getaddrinfo`, `connect`, and `ConnectEx` — +//! read the destination from HERE. There is no per-hook redirect state. The +//! value is set exactly once during DLL init (after `openfut.cfg` is parsed and +//! the host resolved) and is never mutated afterward. +//! +//! If configuration was missing/invalid, this is never populated, and every +//! hook leaves traffic untouched (no loopback fallback). +use std::net::Ipv4Addr; +use std::sync::OnceLock; + +use openfut_common::{sin_addr_from_ipv4, sin_port_nbo, ResolvedServer}; + +static SERVER: OnceLock = OnceLock::new(); + +/// Install the resolved destination. Called once from DLL init. Ignores +/// subsequent calls (OnceLock semantics). +pub fn set(resolved: ResolvedServer) { + let _ = SERVER.set(resolved); +} + +/// The resolved destination, if configuration succeeded. +pub fn get() -> Option { + SERVER.get().copied() +} + +/// The resolved redirect IPv4, if configured. +pub fn redirect_ip() -> Option { + SERVER.get().map(|s| s.redirect_ip) +} + +/// `sockaddr_in.sin_addr` value (native-endian u32) for the configured server. +pub fn sin_addr() -> Option { + SERVER.get().map(|s| sin_addr_from_ipv4(s.redirect_ip)) +} + +/// Given an EA *source* port (network byte order, as seen in `sockaddr_in`), +/// return the OpenFUT *destination* port in network byte order — or `None` if +/// this port isn't intercepted or no server is configured. +pub fn dest_port_nbo_from_source_nbo(source_port_nbo: u16) -> Option { + let s = SERVER.get()?; + let source_host = u16::from_be(source_port_nbo); + s.ports.map_source_port(source_host).map(sin_port_nbo) +} diff --git a/openfut-hook/src/ssl_patch.rs b/openfut-hook/src/ssl_patch.rs index 4923076..59c3e02 100644 --- a/openfut-hook/src/ssl_patch.rs +++ b/openfut-hook/src/ssl_patch.rs @@ -9,11 +9,9 @@ // 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}, - }, +use windows_sys::Win32::System::{ + LibraryLoader::GetModuleHandleA, + Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE}, }; // Unique 22-byte prologue of ProtoSSL's cert-verify function. @@ -21,24 +19,26 @@ use windows_sys::Win32::{ 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 + 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 + 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; } + 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) { @@ -48,9 +48,19 @@ fn patch_module(module: isize, scan_bytes: usize) -> bool { 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); + 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); + VirtualProtect( + target as *const core::ffi::c_void, + PATCH.len(), + old_prot, + &mut old_prot, + ); } true } diff --git a/openfut-hook/src/tls_bypass.rs b/openfut-hook/src/tls_bypass.rs index fdca730..8e53f78 100644 --- a/openfut-hook/src/tls_bypass.rs +++ b/openfut-hook/src/tls_bypass.rs @@ -4,10 +4,10 @@ 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) + *const u8, // pszPolicyOID + *const (), // pChainContext + *const (), // pPolicyPara + *mut u32, // &mut pPolicyStatus.dwError (first field) ) -> BOOL; static REAL: OnceLock = OnceLock::new(); @@ -25,7 +25,12 @@ pub unsafe extern "system" fn hooked_cert_verify_chain_policy( 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); + 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() {