From ce8a3b32ec084c3a30ec511a46d30dfec7c9c298 Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 29 Jun 2026 17:56:23 -0700 Subject: [PATCH] feat(hook): capture LSX/Winsock traffic and harden logging - Hook send/recv and WSASend/WSARecv, filtered to LSX/XML content, to read the Ebisu-SDK <-> anadius conversation (challenge handshake, etc.). - Hook getaddrinfo/GetAddrInfoW to log DNS lookups. - Log connect() return value + WSA error; add accept/close diagnostics on the local listeners. - Serialize log writes behind a mutex so concurrent threads don't corrupt each other's lines. Used to establish that the online/offline decision is made via in-process detoured calls, not socket traffic (no GetAuthCode/GoOnline on the wire). Co-Authored-By: Claude Opus 4.8 (1M context) --- openfut-hook/src/lib.rs | 140 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/openfut-hook/src/lib.rs b/openfut-hook/src/lib.rs index 98a76f2..6e3559e 100644 --- a/openfut-hook/src/lib.rs +++ b/openfut-hook/src/lib.rs @@ -188,6 +188,7 @@ unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 { start_listener(LSX_PORT, "LSX"); start_listener(LOCAL_PORT, "BLZ"); hook_dns(); + hook_winsock_data(); hook_connect(); // Then the plaintext-capture detour on _ProtoSSLSendPacket. @@ -405,6 +406,145 @@ unsafe fn hook_dns() { 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())) {