//! openfut-hook — FIFA 23 `version.dll` proxy + ProtoSSL plaintext capture. //! //! This DLL is built as `version.dll` and dropped next to `FIFA23.exe`. The //! game loads it (DLL search-order / sideload), at which point we: //! //! 1. (Task 2) Forward every real `version.dll` export to the genuine system //! DLL, so the game keeps working. We prove load with a log line. //! 2. (Task 3) Pattern-scan FIFA23.exe for `_ProtoSSLSendPacket` (the DirtySDK //! TLS record assembler we located at FIFA23.exe+0xEFA530) and install an //! inline detour. At its entry the record payload is still PLAINTEXT, so we //! log the content type + the source buffers, then call the original. //! //! Everything is written to `C:\openfut\hook.log` with timestamps, in stages, //! so the log alone tells us how far initialization got. //! //! Clean-room: this is generic proxy/loader/hook scaffolding plus a byte //! pattern derived from observing the binary the user owns. No EA source. use core::ffi::c_void; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Mutex; use retour::RawDetour; use windows::core::{PCSTR, PCWSTR}; use windows::Win32::Foundation::{BOOL, HMODULE}; use windows::Win32::Networking::WinSock::{WSAGetLastError, AF_INET, SOCKADDR, SOCKADDR_IN}; use windows::Win32::System::LibraryLoader::{ DisableThreadLibraryCalls, GetModuleHandleW, GetProcAddress, LoadLibraryW, }; use windows::Win32::System::ProcessStatus::{GetModuleInformation, MODULEINFO}; use windows::Win32::System::SystemInformation::GetLocalTime; use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH; use windows::Win32::System::Threading::{ CreateThread, GetCurrentProcess, GetCurrentThreadStackLimits, Sleep, THREAD_CREATION_FLAGS, }; // --------------------------------------------------------------------------- // Task 2: version.dll export forwarding // --------------------------------------------------------------------------- /// Resolved addresses of the real system `version.dll` exports. Each proxy stub /// below tail-jumps to its slot. AtomicUsize (not `static mut`) keeps this sound /// and lets the naked stubs read the raw pointer via a simple memory load. static REAL: [AtomicUsize; 17] = [const { AtomicUsize::new(0) }; 17]; /// The 17 exports a real `version.dll` provides, in the SAME order as the proxy /// stubs' indices below. const EXPORTS: [&str; 17] = [ "GetFileVersionInfoA", "GetFileVersionInfoByHandle", "GetFileVersionInfoExA", "GetFileVersionInfoExW", "GetFileVersionInfoSizeA", "GetFileVersionInfoSizeExA", "GetFileVersionInfoSizeExW", "GetFileVersionInfoSizeW", "GetFileVersionInfoW", "VerFindFileA", "VerFindFileW", "VerInstallFileA", "VerInstallFileW", "VerLanguageNameA", "VerLanguageNameW", "VerQueryValueA", "VerQueryValueW", ]; /// Generate an exported, naked proxy stub that tail-jumps to `REAL[idx]`. /// A tail `jmp` leaves every register/stack arg untouched, so it forwards any /// signature correctly regardless of how many arguments the real function takes. macro_rules! proxy_stub { ($idx:literal, $name:ident) => { #[no_mangle] #[unsafe(naked)] pub unsafe extern "system" fn $name() { core::arch::naked_asm!( "jmp qword ptr [rip + {base} + {off}]", base = sym REAL, off = const $idx * 8, ); } }; } proxy_stub!(0, GetFileVersionInfoA); proxy_stub!(1, GetFileVersionInfoByHandle); proxy_stub!(2, GetFileVersionInfoExA); proxy_stub!(3, GetFileVersionInfoExW); proxy_stub!(4, GetFileVersionInfoSizeA); proxy_stub!(5, GetFileVersionInfoSizeExA); proxy_stub!(6, GetFileVersionInfoSizeExW); proxy_stub!(7, GetFileVersionInfoSizeW); proxy_stub!(8, GetFileVersionInfoW); proxy_stub!(9, VerFindFileA); proxy_stub!(10, VerFindFileW); proxy_stub!(11, VerInstallFileA); proxy_stub!(12, VerInstallFileW); proxy_stub!(13, VerLanguageNameA); proxy_stub!(14, VerLanguageNameW); proxy_stub!(15, VerQueryValueA); proxy_stub!(16, VerQueryValueW); /// Load the genuine `version.dll` by full path (so we don't re-load ourselves) /// and fill `REAL[]` with each export's address. Done synchronously in DllMain /// so the slots are ready before the game calls any version function. unsafe fn resolve_exports() { let path = wide("C:\\Windows\\System32\\version.dll"); let module = match LoadLibraryW(PCWSTR(path.as_ptr())) { Ok(m) => m, Err(e) => { log(&format!("FATAL: could not load real version.dll: {e:?}")); return; } }; let mut missing = 0; for (i, name) in EXPORTS.iter().enumerate() { let cname = std::ffi::CString::new(*name).unwrap(); let addr = GetProcAddress(module, PCSTR(cname.as_ptr() as *const u8)) .map(|f| f as usize) .unwrap_or(0); REAL[i].store(addr, Ordering::SeqCst); if addr == 0 { missing += 1; log(&format!("warn: real version.dll missing export {name}")); } } log(&format!("forwarded {} version.dll exports", 17 - missing)); } // --------------------------------------------------------------------------- // Task 3: the _ProtoSSLSendPacket detour // --------------------------------------------------------------------------- /// Trampoline to the original `_ProtoSSLSendPacket`, stored after we hook. static ORIG: AtomicUsize = AtomicUsize::new(0); /// Signature derived from the disassembly (Windows x64 ABI): /// `_ProtoSSLSendPacket(pState, contentType, bufA, lenA, bufB, lenB) -> i32`. type SendPacket = unsafe extern "system" fn(*mut c_void, u32, *const u8, i32, *const u8, i32) -> i32; /// Byte pattern for the `_ProtoSSLSendPacket` prologue. The four `0x00` bytes at /// the masked positions are the stack-cookie `mov rax,[rip+disp32]` displacement /// (position-dependent), so they're wildcarded via `MASK`. const PATTERN: [u8; 36] = [ 0x40, 0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x57, 0x48, 0x81, 0xEC, 0xB0, 0x00, 0x00, 0x00, 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00, 0x48, 0x33, 0xC4, 0x48, 0x89, 0x84, 0x24, 0xA0, 0x00, 0x00, 0x00, ]; /// `false` = wildcard byte (don't compare). Indices 21..=24 are the disp32. const MASK: [bool; 36] = { let mut m = [true; 36]; m[21] = false; m[22] = false; m[23] = false; m[24] = false; m }; /// Our detour. At entry the record payload is still plaintext, so we log the /// content type and source buffers, then forward to the original unchanged. unsafe extern "system" fn hooked( p_state: *mut c_void, content_type: u32, buf_a: *const u8, len_a: i32, buf_b: *const u8, len_b: i32, ) -> i32 { log_frame(content_type as u8, buf_a, len_a, buf_b, len_b); let orig = ORIG.load(Ordering::SeqCst); if orig != 0 { let orig: SendPacket = core::mem::transmute(orig); orig(p_state, content_type, buf_a, len_a, buf_b, len_b) } else { // Should never happen (we store ORIG before the game can call us), but // never crash the game if it does. 0 } } /// Background init: pattern-scan FIFA23.exe for the function, then detour it. /// Retries for a while in case the image isn't fully paged in at load. unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 { // Connection capture first. Listen on the LSX port (gate 1, so the launcher // bootstrap succeeds) and on the redirect port (for external TLS/Blaze). store_exe_range(); start_listener(LSX_PORT, "LSX"); start_listener(LOCAL_PORT, "BLZ"); hook_dns(); hook_winsock_data(); hook_connect(); // Then the plaintext-capture detour on _ProtoSSLSendPacket, plus the // read-only anadius GoOnline probe (M1). Retry until both are installed. let mut send_done = false; let mut anadius_done = false; for _ in 0..60 { if !send_done { if let Some(addr) = find_send_packet() { install_hook(addr); send_done = true; } } if !anadius_done && hook_anadius_probes() { anadius_done = true; } if send_done && anadius_done { return 0; } Sleep(1000); } if !send_done { log("ERROR: _ProtoSSLSendPacket pattern not found after 60s"); } if !anadius_done { log("ERROR: anadius64.dll not loaded after 60s; GoOnline probe not installed"); } 0 } /// Scan the FIFA23.exe image for the `_ProtoSSLSendPacket` prologue pattern. unsafe fn find_send_packet() -> Option { let module = GetModuleHandleW(PCWSTR::null()).ok()?; // null => the EXE itself let base = module.0 as usize; let mut info = MODULEINFO::default(); GetModuleInformation( GetCurrentProcess(), module, &mut info, core::mem::size_of::() as u32, ) .ok()?; let size = info.SizeOfImage as usize; let hay = core::slice::from_raw_parts(base as *const u8, size); let plen = PATTERN.len(); if hay.len() < plen { return None; } for i in 0..=hay.len() - plen { // Cheap first-byte gate before the full compare. if hay[i] != PATTERN[0] { continue; } let mut ok = true; for j in 1..plen { if MASK[j] && hay[i + j] != PATTERN[j] { ok = false; break; } } if ok { return Some(base + i); } } None } /// Install the inline detour on the resolved function address. unsafe fn install_hook(addr: usize) { let detour = match RawDetour::new(addr as *const (), hooked as *const ()) { Ok(d) => d, Err(e) => { log(&format!("ERROR: could not create detour: {e:?}")); return; } }; if let Err(e) = detour.enable() { log(&format!("ERROR: could not enable detour: {e:?}")); return; } // Leak the detour so it lives forever (dropping it would un-hook), and // publish its trampoline so `hooked` can call the original. let detour: &'static RawDetour = Box::leak(Box::new(detour)); ORIG.store(detour.trampoline() as *const () as usize, Ordering::SeqCst); log(&format!( "hook installed: _ProtoSSLSendPacket @ 0x{addr:X} (FIFA23.exe+0x{:X})", addr - module_base(), )); } // --------------------------------------------------------------------------- // Connection capture: log every connect() and redirect external attempts to a // local listener, so the client's TCP succeeds and it starts sending TLS. // --------------------------------------------------------------------------- /// Local port our in-DLL listener binds; redirected connections land here. const LOCAL_PORT: u16 = 7777; /// The EA launcher LSX port. The game connects here for launcher↔game bootstrap /// (gate 1). We listen so the connect succeeds and we can see the LSX protocol. const LSX_PORT: u16 = 3216; /// Trampoline to the original ws2_32 `connect`. static ORIG_CONNECT: AtomicUsize = AtomicUsize::new(0); type ConnectFn = unsafe extern "system" fn(usize, *const SOCKADDR, i32) -> i32; /// Our `connect` detour: log the real destination, and for any non-loopback /// IPv4 target, rewrite it to 127.0.0.1:LOCAL_PORT before calling the original. unsafe extern "system" fn hooked_connect(s: usize, name: *const SOCKADDR, namelen: i32) -> i32 { let orig: ConnectFn = core::mem::transmute(ORIG_CONNECT.load(Ordering::SeqCst)); if !name.is_null() && (*name).sa_family == AF_INET { let sin = name as *const SOCKADDR_IN; let port = u16::from_be((*sin).sin_port); let octets = (*sin).sin_addr.S_un.S_addr.to_ne_bytes(); let is_loopback = octets[0] == 127; let dst = format!("{}.{}.{}.{}:{}", octets[0], octets[1], octets[2], octets[3], port); if !is_loopback { // Build a fresh 127.0.0.1:LOCAL_PORT address and connect there. let mut local: SOCKADDR_IN = core::mem::zeroed(); local.sin_family = AF_INET; local.sin_port = LOCAL_PORT.to_be(); local.sin_addr.S_un.S_addr = u32::from_ne_bytes([127, 0, 0, 1]); let ret = orig( s, &local as *const SOCKADDR_IN as *const SOCKADDR, core::mem::size_of::() as i32, ); let err = if ret != 0 { WSAGetLastError().0 } else { 0 }; log(&format!("CONNECT -> {dst} [redirected->7777] ret={ret} err={err}")); return ret; } else { let ret = orig(s, name, namelen); let err = if ret != 0 { WSAGetLastError().0 } else { 0 }; log(&format!("CONNECT -> {dst} ret={ret} err={err}")); return ret; } } orig(s, name, namelen) } // --- DNS logging: what hostnames does the client try to resolve? ---------- static ORIG_GAIW: AtomicUsize = AtomicUsize::new(0); static ORIG_GAI: AtomicUsize = AtomicUsize::new(0); type GaiWFn = unsafe extern "system" fn(PCWSTR, PCWSTR, *const c_void, *mut *mut c_void) -> i32; type GaiFn = unsafe extern "system" fn(PCSTR, PCSTR, *const c_void, *mut *mut c_void) -> i32; unsafe extern "system" fn hooked_gaiw( node: PCWSTR, svc: PCWSTR, hints: *const c_void, res: *mut *mut c_void, ) -> i32 { let name = if node.is_null() { "".to_string() } else { node.to_string().unwrap_or_else(|_| "".into()) }; let orig: GaiWFn = core::mem::transmute(ORIG_GAIW.load(Ordering::SeqCst)); let ret = orig(node, svc, hints, res); log(&format!("DNS GetAddrInfoW(\"{name}\") ret={ret}")); ret } unsafe extern "system" fn hooked_gai( node: PCSTR, svc: PCSTR, hints: *const c_void, res: *mut *mut c_void, ) -> i32 { let name = if node.is_null() { "".to_string() } else { node.to_string().unwrap_or_else(|_| "".into()) }; let orig: GaiFn = core::mem::transmute(ORIG_GAI.load(Ordering::SeqCst)); let ret = orig(node, svc, hints, res); log(&format!("DNS getaddrinfo(\"{name}\") ret={ret}")); ret } /// Generic: resolve `name` in `module`, install a detour to `detour`, store the /// trampoline in `slot`. unsafe fn install_detour( module: HMODULE, name: &[u8], detour: *const (), slot: &AtomicUsize, label: &str, ) { let addr = match GetProcAddress(module, PCSTR(name.as_ptr())) { Some(f) => f as usize, None => { log(&format!("ERROR: {label} not found")); return; } }; install_detour_at(addr, detour, slot, label); } /// Install an inline detour at a raw address (for non-exported targets such as /// internal anadius handlers located by module+offset). unsafe fn install_detour_at(addr: usize, detour: *const (), slot: &AtomicUsize, label: &str) { let d = match RawDetour::new(addr as *const (), detour) { Ok(d) => d, Err(e) => { log(&format!("ERROR: {label} detour create: {e:?}")); return; } }; if d.enable().is_err() { log(&format!("ERROR: {label} detour enable failed")); return; } let d: &'static RawDetour = Box::leak(Box::new(d)); slot.store(d.trampoline() as *const () as usize, Ordering::SeqCst); log(&format!("{label} hook installed")); } // --- M1 read-only probe: anadius GoOnline handler ------------------------- static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0); static EXE_BASE: AtomicUsize = AtomicUsize::new(0); static EXE_SIZE: AtomicUsize = AtomicUsize::new(0); static STACK_LOGGED: AtomicUsize = AtomicUsize::new(0); /// Record FIFA23.exe's base + size so we can recognise its frames in a backtrace. unsafe fn store_exe_range() { if let Ok(h) = GetModuleHandleW(PCWSTR::null()) { let mut mi = MODULEINFO::default(); if GetModuleInformation( GetCurrentProcess(), h, &mut mi, core::mem::size_of::() as u32, ) .is_ok() { EXE_BASE.store(h.0 as usize, Ordering::SeqCst); EXE_SIZE.store(mi.SizeOfImage as usize, Ordering::SeqCst); } } } /// Scan the raw stack for values that land in FIFA23.exe (the game-side /// online-flow return addresses that called into GoOnline). Unwind-free, so it /// survives the detour trampolines that break RtlCaptureStackBackTrace. /// One-shot to avoid log spam. unsafe fn log_fifa_callstack(tag: &str) { if STACK_LOGGED.swap(1, Ordering::SeqCst) != 0 { return; } let base = EXE_BASE.load(Ordering::SeqCst); let size = EXE_SIZE.load(Ordering::SeqCst); if base == 0 || size == 0 { log(&format!("{tag} stack scan skipped (exe range unknown)")); return; } // Address of a local ~= current rsp; the stack grows down, so callers' // return addresses sit at HIGHER addresses. Scan upward, but NEVER past the // committed stack top (reading beyond it faults — that crashed the game). let mut low: usize = 0; let mut high: usize = 0; GetCurrentThreadStackLimits(&mut low, &mut high); let probe: usize = 0; let sp = &probe as *const usize as usize; let end = high; // scan the whole rest of the stack (committed, safe) let mut line = format!( "{tag} stack[low=0x{low:X} high=0x{high:X} sp=0x{sp:X}] FIFA23.exe refs:" ); let mut count = 0; let mut p = sp; while p + 8 <= end { let val = *(p as *const usize); if val >= base && val < base + size { line.push_str(&format!(" +0x{:X}", val - base)); count += 1; if count >= 40 { break; } } p += 8; } log(&line); } /// M2 flip on anadius's GoOnline handler (anadius64.dll+0x2BB90). The original /// handler is `mov rcx,rdx; lea r8,[+0xADD73]; lea rdx,[+0xADE64 = "0"]; call /// +0x25BE0; mov al,1` — i.e. it builds its ErrorSuccess response with the value /// "0" (offline). We replicate it but pass "1" (+0xAF530 = the connected value), /// so GoOnline reports online, then return success (al=1). unsafe extern "system" fn hooked_goonline(_a: usize, b: usize, _c: usize, _d: usize) -> usize { log_fifa_callstack("GoOnline"); let base = ANADIUS_BASE.load(Ordering::SeqCst); if base != 0 { log("FLIP GoOnline -> reporting online (\"1\")"); let builder: unsafe extern "system" fn(usize, usize, usize) -> usize = core::mem::transmute(base + 0x25BE0); // 0x25BE0(rcx = handler's rdx, rdx = "1", r8 = +0xADD73) builder(b, base + 0xAF530, base + 0xADD73); return 1; } let orig = ORIG_GOONLINE.load(Ordering::SeqCst); if orig != 0 { let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = core::mem::transmute(orig); f(_a, b, _c, _d) } else { 0 } } // --- M2 flip: force GetInternetConnectedState to report "connected" -------- static ORIG_ICS: AtomicUsize = AtomicUsize::new(0); static ANADIUS_BASE: AtomicUsize = AtomicUsize::new(0); /// anadius's GetInternetConnectedState handler (anadius64.dll+0x27790) builds an /// LSX response whose `connected` value is: /// (byte[+0xCAB1B] || byte[+0xCAB1A]) ? connected : offline /// Both default to 0 → offline → the game aborts at "connecting". We force both /// flags to 1 before the original runs, so it builds the "connected" response. unsafe extern "system" fn hooked_ics(a: usize, b: usize, c: usize, d: usize) -> usize { let base = ANADIUS_BASE.load(Ordering::SeqCst); if base != 0 { core::ptr::write_volatile((base + 0xCAB1A) as *mut u8, 1u8); core::ptr::write_volatile((base + 0xCAB1B) as *mut u8, 1u8); } log("FLIP GetInternetConnectedState -> forcing connected (flags set)"); let orig = ORIG_ICS.load(Ordering::SeqCst); if orig != 0 { let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = core::mem::transmute(orig); f(a, b, c, d) } else { 0 } } /// Resolve anadius64.dll's runtime base, detour the GoOnline probe, and install /// the M2 GetInternetConnectedState flip. Returns false if anadius isn't loaded. unsafe fn hook_anadius_probes() -> bool { let base = match GetModuleHandleW(PCWSTR(wide("anadius64.dll").as_ptr())) { Ok(m) => m.0 as usize, Err(_) => return false, // not loaded yet }; ANADIUS_BASE.store(base, Ordering::SeqCst); log(&format!("anadius64.dll base = 0x{base:X}")); install_detour_at( base + 0x2BB90, hooked_goonline as *const (), &ORIG_GOONLINE, "PROBE anadius GoOnline @ +0x2BB90", ); install_detour_at( base + 0x27790, hooked_ics as *const (), &ORIG_ICS, "FLIP anadius GetInternetConnectedState @ +0x27790", ); true } /// Detour the DNS resolvers so we see every hostname lookup. unsafe fn hook_dns() { let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) { Ok(m) => m, Err(e) => { log(&format!("ERROR: load ws2_32 for DNS: {e:?}")); return; } }; install_detour(ws2, b"GetAddrInfoW\0", hooked_gaiw as *const (), &ORIG_GAIW, "GetAddrInfoW"); install_detour(ws2, b"getaddrinfo\0", hooked_gai as *const (), &ORIG_GAI, "getaddrinfo"); } // --- LSX capture: read the Ebisu-SDK <-> anadius XML conversation ---------- static ORIG_SEND: AtomicUsize = AtomicUsize::new(0); static ORIG_RECV: AtomicUsize = AtomicUsize::new(0); type SendFn = unsafe extern "system" fn(usize, *const u8, i32, i32) -> i32; type RecvFn = unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32; /// Cheap test: does this buffer look like LSX/Ebisu XML (not TLS/binary)? fn looks_like_lsx(buf: &[u8]) -> bool { let n = buf.len().min(64); let head = &buf[..n]; let has_lt = head.iter().any(|&b| b == b'<'); let has_gt = head.iter().any(|&b| b == b'>'); head.windows(3).any(|w| w == b"LSX") || head.windows(5).any(|w| w == b"Ebisu") || (has_lt && has_gt) } unsafe extern "system" fn hooked_send(s: usize, buf: *const u8, len: i32, flags: i32) -> i32 { if len > 0 && !buf.is_null() { let head = core::slice::from_raw_parts(buf, (len as usize).min(64)); if looks_like_lsx(head) { let show = core::slice::from_raw_parts(buf, (len as usize).min(800)); log(&format!("LSX send sock={s} {len}B: {}", ascii_render(show))); } } let orig: SendFn = core::mem::transmute(ORIG_SEND.load(Ordering::SeqCst)); orig(s, buf, len, flags) } unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flags: i32) -> i32 { let orig: RecvFn = core::mem::transmute(ORIG_RECV.load(Ordering::SeqCst)); let ret = orig(s, buf, len, flags); if ret > 0 && !buf.is_null() { let head = core::slice::from_raw_parts(buf, (ret as usize).min(64)); if looks_like_lsx(head) { let show = core::slice::from_raw_parts(buf, (ret as usize).min(800)); log(&format!("LSX recv sock={s} {ret}B: {}", ascii_render(show))); } } ret } // Async (overlapped/IOCP) variants. A WSABUF is { len, buf }. #[repr(C)] struct WsaBuf { len: u32, buf: *mut u8, } static ORIG_WSASEND: AtomicUsize = AtomicUsize::new(0); static ORIG_WSARECV: AtomicUsize = AtomicUsize::new(0); type WsaSendFn = unsafe extern "system" fn( usize, *const WsaBuf, u32, *mut u32, u32, *mut c_void, *mut c_void, ) -> i32; type WsaRecvFn = unsafe extern "system" fn( usize, *const WsaBuf, u32, *mut u32, *mut u32, *mut c_void, *mut c_void, ) -> i32; unsafe extern "system" fn hooked_wsasend( s: usize, bufs: *const WsaBuf, count: u32, sent: *mut u32, flags: u32, ovl: *mut c_void, cr: *mut c_void, ) -> i32 { // Outgoing data is readable before the call — capture the first buffer. if !bufs.is_null() && count > 0 { let b0 = &*bufs; if b0.len > 0 && !b0.buf.is_null() { let head = core::slice::from_raw_parts(b0.buf, (b0.len as usize).min(64)); if looks_like_lsx(head) { let show = core::slice::from_raw_parts(b0.buf, (b0.len as usize).min(800)); log(&format!("LSX WSASend sock={s} {}B: {}", b0.len, ascii_render(show))); } } } let orig: WsaSendFn = core::mem::transmute(ORIG_WSASEND.load(Ordering::SeqCst)); orig(s, bufs, count, sent, flags, ovl, cr) } unsafe extern "system" fn hooked_wsarecv( s: usize, bufs: *const WsaBuf, count: u32, recvd: *mut u32, flags: *mut u32, ovl: *mut c_void, cr: *mut c_void, ) -> i32 { let orig: WsaRecvFn = core::mem::transmute(ORIG_WSARECV.load(Ordering::SeqCst)); let ret = orig(s, bufs, count, recvd, flags, ovl, cr); // Only the synchronous case (no overlapped) has data ready on return. if ret == 0 && ovl.is_null() && !recvd.is_null() && !bufs.is_null() && count > 0 { let n = *recvd as usize; let b0 = &*bufs; if n > 0 && !b0.buf.is_null() { let cap = n.min(b0.len as usize); let head = core::slice::from_raw_parts(b0.buf, cap.min(64)); if looks_like_lsx(head) { let show = core::slice::from_raw_parts(b0.buf, cap.min(800)); log(&format!("LSX WSARecv sock={s} {n}B: {}", ascii_render(show))); } } } ret } /// Detour ws2_32 send/recv (sync) and WSASend/WSARecv (async) to capture LSX XML. unsafe fn hook_winsock_data() { let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) { Ok(m) => m, Err(e) => { log(&format!("ERROR: load ws2_32 for send/recv: {e:?}")); return; } }; install_detour(ws2, b"send\0", hooked_send as *const (), &ORIG_SEND, "send"); install_detour(ws2, b"recv\0", hooked_recv as *const (), &ORIG_RECV, "recv"); install_detour(ws2, b"WSASend\0", hooked_wsasend as *const (), &ORIG_WSASEND, "WSASend"); install_detour(ws2, b"WSARecv\0", hooked_wsarecv as *const (), &ORIG_WSARECV, "WSARecv"); } /// Resolve and detour ws2_32 `connect`. unsafe fn hook_connect() { let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) { Ok(m) => m, Err(e) => { log(&format!("ERROR: could not load ws2_32.dll: {e:?}")); return; } }; let addr = match GetProcAddress(ws2, PCSTR(b"connect\0".as_ptr())) { Some(f) => f as usize, None => { log("ERROR: connect not found in ws2_32.dll"); return; } }; let detour = match RawDetour::new(addr as *const (), hooked_connect as *const ()) { Ok(d) => d, Err(e) => { log(&format!("ERROR: could not create connect detour: {e:?}")); return; } }; if let Err(e) = detour.enable() { log(&format!("ERROR: could not enable connect detour: {e:?}")); return; } let detour: &'static RawDetour = Box::leak(Box::new(detour)); ORIG_CONNECT.store(detour.trampoline() as *const () as usize, Ordering::SeqCst); log("connect hook installed (external IPv4 -> 127.0.0.1:7777)"); } /// Spawn a TCP listener on `127.0.0.1:port`, tagging logged traffic with `tag`. /// Accepts connections and logs what the client sends — as readable text (for /// the text-based LSX protocol) plus a hex prefix (for binary TLS/Blaze). fn start_listener(port: u16, tag: &'static str) { std::thread::spawn(move || { let listener = match std::net::TcpListener::bind(("127.0.0.1", port)) { Ok(l) => l, Err(e) => { log(&format!("ERROR: listener[{tag}] bind {port} failed: {e}")); return; } }; let bound = listener .local_addr() .map(|a| a.to_string()) .unwrap_or_default(); log(&format!("listener[{tag}] up, bound {bound}, accepting")); // Explicit accept loop so accept errors are visible (not swallowed). loop { match listener.accept() { Ok((stream, peer)) => { log(&format!("ACCEPT[{tag}] from {peer}")); std::thread::spawn(move || handle_conn(stream, tag, peer.to_string())); } Err(e) => { log(&format!("ACCEPT[{tag}] error: {e}")); std::thread::sleep(std::time::Duration::from_millis(250)); } } } }); } fn handle_conn(mut stream: std::net::TcpStream, tag: &'static str, peer: String) { use std::io::Read; let mut buf = [0u8; 2048]; let mut total = 0usize; loop { match stream.read(&mut buf) { Ok(0) | Err(_) => break, Ok(n) => { total += n; let ascii = ascii_render(&buf[..n.min(400)]); let hexp = hex(&buf[..n.min(24)]); log(&format!("RECV[{tag}] {n}B | hex: {hexp} | text: {ascii}")); } } } log(&format!("CLOSE[{tag}] from {peer} after {total}B total")); } /// Render bytes as printable ASCII (non-printable -> '.'), for text protocols. fn ascii_render(bytes: &[u8]) -> String { bytes .iter() .map(|&b| { if (0x20..=0x7e).contains(&b) || b == b'\n' || b == b'\r' || b == b'\t' { b as char } else { '.' } }) .collect() } // --------------------------------------------------------------------------- // Logging // --------------------------------------------------------------------------- fn log_frame(content_type: u8, buf_a: *const u8, len_a: i32, buf_b: *const u8, len_b: i32) { let label = match content_type { 0x14 => "ccs", 0x15 => "alert", 0x16 => "handshake", 0x17 => "appdata", _ => "?", }; let mut line = format!("SEND type=0x{content_type:02X}({label}) lenA={len_a} lenB={len_b}"); if !buf_a.is_null() && len_a > 0 { let n = (len_a as usize).min(48); let bytes = unsafe { core::slice::from_raw_parts(buf_a, n) }; line.push_str(&format!(" | A: {}", hex(bytes))); } if !buf_b.is_null() && len_b > 0 { let n = (len_b as usize).min(16); let bytes = unsafe { core::slice::from_raw_parts(buf_b, n) }; line.push_str(&format!(" | B: {}", hex(bytes))); } log(&line); } fn hex(bytes: &[u8]) -> String { bytes .iter() .map(|b| format!("{b:02X}")) .collect::>() .join(" ") } /// Serializes log writes so concurrent threads don't corrupt each other's lines. static LOG_LOCK: Mutex<()> = Mutex::new(()); /// Append a timestamped line to `C:\openfut\hook.log`. fn log(msg: &str) { use std::io::Write; let _guard = LOG_LOCK.lock(); let _ = std::fs::create_dir_all("C:\\openfut"); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) .append(true) .open("C:\\openfut\\hook.log") { let _ = writeln!(f, "[{}] {}", now(), msg); } } fn now() -> String { let st = unsafe { GetLocalTime() }; format!( "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond ) } // --------------------------------------------------------------------------- // Helpers + entry point // --------------------------------------------------------------------------- fn wide(s: &str) -> Vec { s.encode_utf16().chain(std::iter::once(0)).collect() } /// FIFA23.exe base address (the main module), for pretty logging. fn module_base() -> usize { unsafe { GetModuleHandleW(PCWSTR::null()) .map(|h| h.0 as usize) .unwrap_or(0) } } #[no_mangle] pub extern "system" fn DllMain(module: HMODULE, reason: u32, _reserved: *mut c_void) -> BOOL { if reason == DLL_PROCESS_ATTACH { unsafe { let _ = DisableThreadLibraryCalls(module); let host = std::env::current_exe() .map(|p| p.display().to_string()) .unwrap_or_default(); log(&format!( "openfut-hook loaded | pid {} | host {host}", std::process::id() )); // Forward exports synchronously (must be ready before any call)... resolve_exports(); // ...then do the pattern scan + hook on a background thread (never // do real work directly in DllMain — loader lock). let _ = CreateThread( None, 0, Some(init_thread), None, THREAD_CREATION_FLAGS(0), None, ); } } BOOL(1) }