Compare commits
2 Commits
bc6612697a
...
15a6f877ee
| Author | SHA1 | Date | |
|---|---|---|---|
| 15a6f877ee | |||
| ce8a3b32ec |
@@ -188,6 +188,7 @@ unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 {
|
|||||||
start_listener(LSX_PORT, "LSX");
|
start_listener(LSX_PORT, "LSX");
|
||||||
start_listener(LOCAL_PORT, "BLZ");
|
start_listener(LOCAL_PORT, "BLZ");
|
||||||
hook_dns();
|
hook_dns();
|
||||||
|
hook_winsock_data();
|
||||||
hook_connect();
|
hook_connect();
|
||||||
|
|
||||||
// Then the plaintext-capture detour on _ProtoSSLSendPacket.
|
// 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");
|
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`.
|
/// Resolve and detour ws2_32 `connect`.
|
||||||
unsafe fn hook_connect() {
|
unsafe fn hook_connect() {
|
||||||
let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) {
|
let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) {
|
||||||
|
|||||||
@@ -124,17 +124,25 @@ fn main() {
|
|||||||
// loads and call targets. Use this on a code anchor (e.g. the lea that
|
// loads and call targets. Use this on a code anchor (e.g. the lea that
|
||||||
// loads the "_ProtoSSLSendPacket" string) to read the real code.
|
// loads the "_ProtoSSLSendPacket" string) to read the real code.
|
||||||
Some("disasm") => {
|
Some("disasm") => {
|
||||||
let target = match args.get(1).and_then(|s| parse_hex(s)) {
|
let arg = match args.get(1) {
|
||||||
Some(t) => t,
|
Some(a) => a.clone(),
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Usage: protossl-scan disasm <hex-addr> [pid|name]");
|
eprintln!("Usage: protossl-scan disasm <hex-addr | module+0xoffset> [pid|name]");
|
||||||
eprintln!("Example: protossl-scan disasm 0x140EFA631");
|
eprintln!("Examples: protossl-scan disasm 0x140EFA631");
|
||||||
|
eprintln!(" protossl-scan disasm anadius64.dll+0x2BB90");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
||||||
let process = open_for_read(pid);
|
let process = open_for_read(pid);
|
||||||
let modules = enumerate_modules(pid);
|
let modules = enumerate_modules(pid);
|
||||||
|
let target = match resolve_target(&arg, &modules) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
eprintln!("Could not resolve '{arg}' (unknown module or bad address)");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
run_disasm(process, &modules, target);
|
run_disasm(process, &modules, target);
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = CloseHandle(process);
|
let _ = CloseHandle(process);
|
||||||
@@ -142,12 +150,11 @@ fn main() {
|
|||||||
}
|
}
|
||||||
// xref <hex-addr> [pid|name] (power-user form, exact absolute address)
|
// xref <hex-addr> [pid|name] (power-user form, exact absolute address)
|
||||||
Some("xref") => {
|
Some("xref") => {
|
||||||
let target = match args.get(1).and_then(|s| parse_hex(s)) {
|
let arg = match args.get(1) {
|
||||||
Some(t) => t,
|
Some(a) => a.clone(),
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Usage: protossl-scan xref <hex-addr> [pid|name]");
|
eprintln!("Usage: protossl-scan xref <hex-addr | module+0xoffset> [pid|name]");
|
||||||
eprintln!("Example: protossl-scan xref 0x147D198B9");
|
eprintln!("Example: protossl-scan xref 0x147D198B9");
|
||||||
eprintln!("Tip: pass the FULL absolute address, not the +offset.");
|
|
||||||
eprintln!("Or just use: protossl-scan xref-str ProtoSSLSend");
|
eprintln!("Or just use: protossl-scan xref-str ProtoSSLSend");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
@@ -155,6 +162,13 @@ fn main() {
|
|||||||
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
||||||
let process = open_for_read(pid);
|
let process = open_for_read(pid);
|
||||||
let modules = enumerate_modules(pid);
|
let modules = enumerate_modules(pid);
|
||||||
|
let target = match resolve_target(&arg, &modules) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
eprintln!("Could not resolve '{arg}' (unknown module or bad address)");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
run_xref(process, &modules, target);
|
run_xref(process, &modules, target);
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = CloseHandle(process);
|
let _ = CloseHandle(process);
|
||||||
@@ -525,6 +539,26 @@ fn parse_hex(s: &str) -> Option<usize> {
|
|||||||
usize::from_str_radix(trimmed, 16).ok()
|
usize::from_str_radix(trimmed, 16).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a target that is either a raw hex address or a `module+0xoffset`
|
||||||
|
/// form (e.g. `anadius64.dll+0x2BB90`). The module form is ASLR-robust: it adds
|
||||||
|
/// the offset to the module's CURRENT base in the running process.
|
||||||
|
fn resolve_target(s: &str, modules: &[ModuleInfo]) -> Option<usize> {
|
||||||
|
if let Some(idx) = s.find('+') {
|
||||||
|
let name = s[..idx].trim().to_ascii_lowercase();
|
||||||
|
let want = name.strip_suffix(".dll").unwrap_or(&name);
|
||||||
|
let off = parse_hex(s[idx + 1..].trim())?;
|
||||||
|
for m in modules {
|
||||||
|
let mn = m.name.to_ascii_lowercase();
|
||||||
|
let mn = mn.strip_suffix(".dll").unwrap_or(&mn);
|
||||||
|
if mn == want {
|
||||||
|
return Some(m.base + off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
parse_hex(s)
|
||||||
|
}
|
||||||
|
|
||||||
/// Read a single u64 from the target process at `addr`. Returns None if the
|
/// Read a single u64 from the target process at `addr`. Returns None if the
|
||||||
/// memory can't be read (e.g. unmapped).
|
/// memory can't be read (e.g. unmapped).
|
||||||
fn read_u64(process: HANDLE, addr: usize) -> Option<u64> {
|
fn read_u64(process: HANDLE, addr: usize) -> Option<u64> {
|
||||||
|
|||||||
Reference in New Issue
Block a user