/// 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 { use windows_sys::Win32::System::Memory::{ VirtualAlloc, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE, }; // Read enough prologue to walk instruction boundaries. let probe: [u8; 24] = core::array::from_fn(|i| *orig.add(i)); let hex: String = probe[..14].iter().map(|b| format!("{b:02x} ")).collect(); crate::write_log(&format!("recv_hook: {name} prologue {hex}\n")); // Copy WHOLE instructions until we've covered >= 14 bytes (the size of the JMP // patch), so the trampoline never splits an instruction. Copying a fixed 14 // bytes lands mid-instruction on these prologues and crashes on execution. let mut copy_len = 0usize; while copy_len < 14 { let (len, branch) = decode_instr_len(&probe[copy_len..]); if len == 0 || branch { crate::write_log(&format!( "recv_hook: {name} unrelocatable prologue (len={len} branch={branch}), skipping\n" )); return None; } copy_len += len; } let mem = VirtualAlloc( core::ptr::null_mut(), 64, 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, copy_len); // JMP [RIP+0] → orig+copy_len (resume at the next whole instruction) let cont = (orig as u64) + copy_len as u64; t.add(copy_len).write(0xFF); t.add(copy_len + 1).write(0x25); (t.add(copy_len + 2) as *mut u32).write(0); (t.add(copy_len + 6) as *mut u64).write(cont); crate::write_log(&format!( "recv_hook: {name} trampoline copy_len={copy_len}\n" )); 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); /// True if socket `s` is connected to the EA App LSX port (127.0.0.1:3216). /// Used in capture mode to tap only the LSX conversation. unsafe fn peer_is_lsx(s: usize) -> bool { use windows_sys::Win32::Networking::WinSock::getpeername; let mut sa = [0u8; 16]; let mut sl: i32 = 16; if getpeername(s, sa.as_mut_ptr() as *mut _, &mut sl) != 0 { return false; } // sockaddr_in: sa_family (2 bytes) then sin_port (2 bytes, network order). u16::from_be_bytes([sa[2], sa[3]]) == 3216 } // IAT-hook approach (no inline trampoline — FIFA's `recv`/`send` prologues have // instructions that straddle the 14-byte patch boundary, so an inline trampoline // corrupts them and crashes. IAT hooking only swaps import-table pointers and // never touches the function body). The real fns are resolved in lib.rs and set // here; our hooks call them directly. static REAL_RECV: AtomicUsize = AtomicUsize::new(0); static REAL_SEND: AtomicUsize = AtomicUsize::new(0); pub fn set_real_recv(f: unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32) { REAL_RECV.store(f as usize, Ordering::Relaxed); } pub fn set_real_send(f: unsafe extern "system" fn(usize, *const u8, i32, i32) -> i32) { REAL_SEND.store(f as usize, Ordering::Relaxed); } /// Inline-hook ws2_32!recv: build a boundary-safe trampoline (the "real" fn our /// hook calls) and overwrite the entry with a JMP to `hooked_recv`. Inline hooks /// catch calls from every module and dynamically-resolved calls, unlike IAT. 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) => REAL_RECV.store(t, Ordering::Relaxed), None => return false, } write_jmp(ptr, hooked_recv as u64); true } 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) => REAL_SEND.store(t, Ordering::Relaxed), None => return false, } write_jmp(ptr, hooked_send as u64); true } pub unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flags: i32) -> i32 { let t = REAL_RECV.load(Ordering::Relaxed); if t == 0 { return -1; } let f: unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32 = core::mem::transmute(t); // Pass through to anadius's real socket, then log what it sent back // (anadius's LSX response — the ground truth we want to diff against). let n = f(s, buf, len, flags); if n > 0 && peer_is_lsx(s) { let data = core::slice::from_raw_parts(buf, n as usize); let text = core::str::from_utf8(data).unwrap_or("(binary)"); crate::write_log(&format!( "CAP recv<-anadius s={s} n={n}: {}\n", &text[..text.len().min(2400)] )); } n } pub unsafe extern "system" fn hooked_send(s: usize, buf: *const u8, len: i32, flags: i32) -> i32 { if len > 0 && peer_is_lsx(s) { 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!( "CAP send->anadius s={s} len={len}: {}\n", &text[..text.len().min(2400)] )); } let t = REAL_SEND.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) }