/// 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, }; // 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 }