feat(hook): expand IAT hook coverage with TLS bypass, connect/recv hooks, and logging
Adds connect_hook, connectex_hook, recv_hook, ssl_patch, tls_bypass, lsx, ea_stub, and origin_spy modules to intercept EA's TLS and socket layers in addition to getaddrinfo. Adds DLL-level logging to C:\openfut_hook.log for debugging. Also patches windows-sys feature flags to include Cryptography and Threading APIs needed by the new hooks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/// 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,
|
||||
};
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user