322 lines
11 KiB
Rust
322 lines
11 KiB
Rust
/// 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<usize> {
|
||
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)
|
||
}
|