diff --git a/test-app/native/src/lib.rs b/test-app/native/src/lib.rs index d311410..1833551 100644 --- a/test-app/native/src/lib.rs +++ b/test-app/native/src/lib.rs @@ -1,6 +1,6 @@ +use jni::JNIEnv; use jni::objects::JClass; use jni::sys::jstring; -use jni::JNIEnv; use std::ffi::CStr; use std::io::ErrorKind; @@ -259,7 +259,9 @@ fn open_netlink() -> Result { let e = std::io::Error::last_os_error(); libc::close(fd); return Err(if is_selinux_denial(&e) { - format!("PASS: netlink bind denied by SELinux ({e}) — app cannot enumerate interfaces") + format!( + "PASS: netlink bind denied by SELinux ({e}) — app cannot enumerate interfaces" + ) } else { format!("FAIL: bind error: {e}") }); @@ -302,7 +304,12 @@ unsafe fn parse_netlink_msgs( /// /// # Safety /// `buf[start..end]` must contain valid rtattr entries. -unsafe fn for_each_rtattr(buf: &[u8], start: usize, end: usize, mut on_attr: impl FnMut(&Rtattr, usize)) { +unsafe fn for_each_rtattr( + buf: &[u8], + start: usize, + end: usize, + mut on_attr: impl FnMut(&Rtattr, usize), +) { let mut off = start; while off + 4 <= end { let rta = unsafe { &*(buf.as_ptr().add(off) as *const Rtattr) }; @@ -333,7 +340,13 @@ fn check_netlink_getlink() -> String { req.nlh.nlmsg_flags = (libc::NLM_F_REQUEST | libc::NLM_F_DUMP) as u16; req.nlh.nlmsg_seq = 1; - if libc::send(fd, std::ptr::from_ref(&req).cast(), req.nlh.nlmsg_len as usize, 0) < 0 { + if libc::send( + fd, + std::ptr::from_ref(&req).cast(), + req.nlh.nlmsg_len as usize, + 0, + ) < 0 + { let e = last_os_error(); libc::close(fd); return format!("FAIL: send error: {e}"); @@ -350,27 +363,37 @@ fn check_netlink_getlink() -> String { if len <= 0 { break; } - let cont = parse_netlink_msgs(&buf, len as usize, libc::RTM_NEWLINK, |b, offset, msg_len| { - let data_start = offset + hdr_plus_ifinfo; - let msg_end = offset + msg_len; - for_each_rtattr(b, data_start, msg_end, |rta, rta_off| { - if rta.rta_type == IFLA_IFNAME { - let name = cstr_to_str(b.as_ptr().add(rta_off + 4) as *const libc::c_char); - logi(&format!(" netlink RTM_NEWLINK: interface '{name}'")); - if is_vpn_iface(&name) { - vpn.push(name.clone()); + let cont = parse_netlink_msgs( + &buf, + len as usize, + libc::RTM_NEWLINK, + |b, offset, msg_len| { + let data_start = offset + hdr_plus_ifinfo; + let msg_end = offset + msg_len; + for_each_rtattr(b, data_start, msg_end, |rta, rta_off| { + if rta.rta_type == IFLA_IFNAME { + let name = + cstr_to_str(b.as_ptr().add(rta_off + 4) as *const libc::c_char); + logi(&format!(" netlink RTM_NEWLINK: interface '{name}'")); + if is_vpn_iface(&name) { + vpn.push(name.clone()); + } + all.push(name); } - all.push(name); - } - }); - }); + }); + }, + ); if !cont { break; } } libc::close(fd); - format_iface_result(&all, &vpn, &format!("{} interfaces via netlink:", all.len())) + format_iface_result( + &all, + &vpn, + &format!("{} interfaces via netlink:", all.len()), + ) } } @@ -393,7 +416,13 @@ fn check_netlink_getroute() -> String { req.nlh.nlmsg_flags = (libc::NLM_F_REQUEST | libc::NLM_F_DUMP) as u16; req.nlh.nlmsg_seq = 1; - if libc::send(fd, std::ptr::from_ref(&req).cast(), req.nlh.nlmsg_len as usize, 0) < 0 { + if libc::send( + fd, + std::ptr::from_ref(&req).cast(), + req.nlh.nlmsg_len as usize, + 0, + ) < 0 + { let e = last_os_error(); libc::close(fd); return format!("FAIL: send error: {e}"); @@ -402,33 +431,40 @@ fn check_netlink_getroute() -> String { let mut buf = [0u8; 32768]; let mut vpn = Vec::new(); let mut total = 0u32; - let hdr_plus_rtmsg = - std::mem::size_of::() + std::mem::size_of::(); + let hdr_plus_rtmsg = std::mem::size_of::() + std::mem::size_of::(); loop { let len = libc::recv(fd, buf.as_mut_ptr().cast(), buf.len(), 0); if len <= 0 { break; } - let cont = parse_netlink_msgs(&buf, len as usize, libc::RTM_NEWROUTE, |b, offset, msg_len| { - total += 1; - let data_start = offset + hdr_plus_rtmsg; - let msg_end = offset + msg_len; - for_each_rtattr(b, data_start, msg_end, |rta, rta_off| { - if rta.rta_type == RTA_OIF { - let ifindex = *(b.as_ptr().add(rta_off + 4) as *const i32); - let mut ifname_buf = [0u8; libc::IF_NAMESIZE]; - let ptr = libc::if_indextoname(ifindex as u32, ifname_buf.as_mut_ptr().cast()); - if !ptr.is_null() { - let name = cstr_to_str(ptr); - if is_vpn_iface(&name) { - logi(&format!(" RTM_GETROUTE: VPN route via '{name}'")); - vpn.push(name); + let cont = parse_netlink_msgs( + &buf, + len as usize, + libc::RTM_NEWROUTE, + |b, offset, msg_len| { + total += 1; + let data_start = offset + hdr_plus_rtmsg; + let msg_end = offset + msg_len; + for_each_rtattr(b, data_start, msg_end, |rta, rta_off| { + if rta.rta_type == RTA_OIF { + let ifindex = *(b.as_ptr().add(rta_off + 4) as *const i32); + let mut ifname_buf = [0u8; libc::IF_NAMESIZE]; + let ptr = libc::if_indextoname( + ifindex as u32, + ifname_buf.as_mut_ptr().cast(), + ); + if !ptr.is_null() { + let name = cstr_to_str(ptr); + if is_vpn_iface(&name) { + logi(&format!(" RTM_GETROUTE: VPN route via '{name}'")); + vpn.push(name); + } } } - } - }); - }); + }); + }, + ); if !cont { break; } @@ -484,18 +520,63 @@ macro_rules! jni_fn { }; } -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkIoctlSiocgifflags, check_ioctl_siocgifflags()); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkIoctlSiocgifconf, check_ioctl_siocgifconf()); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkGetifaddrs, check_getifaddrs()); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetRoute, check_proc_file("/proc/net/route")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetIfInet6, check_proc_file("/proc/net/if_inet6")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink, check_netlink_getlink()); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetroute, check_netlink_getroute()); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetIpv6Route, check_proc_file("/proc/net/ipv6_route")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetTcp, check_proc_file("/proc/net/tcp")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetTcp6, check_proc_file("/proc/net/tcp6")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetUdp, check_proc_file("/proc/net/udp")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetUdp6, check_proc_file("/proc/net/udp6")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetDev, check_proc_file("/proc/net/dev")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetFibTrie, check_proc_file("/proc/net/fib_trie")); -jni_fn!(Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkSysClassNet, check_sys_class_net()); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkIoctlSiocgifflags, + check_ioctl_siocgifflags() +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkIoctlSiocgifconf, + check_ioctl_siocgifconf() +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkGetifaddrs, + check_getifaddrs() +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetRoute, + check_proc_file("/proc/net/route") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetIfInet6, + check_proc_file("/proc/net/if_inet6") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink, + check_netlink_getlink() +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetroute, + check_netlink_getroute() +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetIpv6Route, + check_proc_file("/proc/net/ipv6_route") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetTcp, + check_proc_file("/proc/net/tcp") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetTcp6, + check_proc_file("/proc/net/tcp6") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetUdp, + check_proc_file("/proc/net/udp") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetUdp6, + check_proc_file("/proc/net/udp6") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetDev, + check_proc_file("/proc/net/dev") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetFibTrie, + check_proc_file("/proc/net/fib_trie") +); +jni_fn!( + Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkSysClassNet, + check_sys_class_net() +); diff --git a/zygisk/build.rs b/zygisk/build.rs index eb7885b..bd13fe2 100644 --- a/zygisk/build.rs +++ b/zygisk/build.rs @@ -90,7 +90,9 @@ fn find_ndk_builtins(ndk: &str) -> Option { let base = PathBuf::from(ndk).join("toolchains/llvm/prebuilt"); for host in std::fs::read_dir(&base).ok()?.flatten() { let clang_dir = host.path().join("lib/clang"); - let Ok(versions) = std::fs::read_dir(&clang_dir) else { continue }; + let Ok(versions) = std::fs::read_dir(&clang_dir) else { + continue; + }; for v in versions.flatten() { let candidate = v .path() diff --git a/zygisk/src/filter.rs b/zygisk/src/filter.rs index d813f0c..14cbd4f 100644 --- a/zygisk/src/filter.rs +++ b/zygisk/src/filter.rs @@ -73,7 +73,7 @@ fn contains_ignore_ascii_case(haystack: &[u8], needle: &[u8]) -> bool { if window .iter() .zip(needle.iter()) - .all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase()) + .all(|(a, b)| a.eq_ignore_ascii_case(b)) { return true; } @@ -87,9 +87,9 @@ fn contains_ignore_ascii_case(haystack: &[u8], needle: &[u8]) -> bool { /// /// Format: /// ```text -/// Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT -/// wlan0 00000000 0101A8C0 0003 0 0 0 00000000 0 0 0 -/// tun0 00000000 010010AC 0003 0 0 0 00000000 0 0 0 +/// Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT +/// wlan0 00000000 0101A8C0 0003 0 0 0 00000000 0 0 0 +/// tun0 00000000 010010AC 0003 0 0 0 00000000 0 0 0 /// ``` /// The header line (starting with "Iface") is always kept. pub fn filter_route_buf(data: &mut [u8]) -> usize { @@ -522,10 +522,10 @@ mod tests { let total_len: u32 = 24; let mut msg = Vec::new(); msg.extend_from_slice(&total_len.to_ne_bytes()); // nlmsg_len - msg.extend_from_slice(&msg_type.to_ne_bytes()); // nlmsg_type - msg.extend_from_slice(&0u16.to_ne_bytes()); // nlmsg_flags - msg.extend_from_slice(&1u32.to_ne_bytes()); // nlmsg_seq - msg.extend_from_slice(&0u32.to_ne_bytes()); // nlmsg_pid + msg.extend_from_slice(&msg_type.to_ne_bytes()); // nlmsg_type + msg.extend_from_slice(&0u16.to_ne_bytes()); // nlmsg_flags + msg.extend_from_slice(&1u32.to_ne_bytes()); // nlmsg_seq + msg.extend_from_slice(&0u32.to_ne_bytes()); // nlmsg_pid // payload: 4 bytes (family etc) + 4 bytes (if_index) msg.extend_from_slice(&[0u8; 4]); msg.extend_from_slice(&if_index.to_ne_bytes()); @@ -593,9 +593,9 @@ mod tests { fn netlink_filter_preserves_non_newaddr_msgs() { let nlmsg_done_type: u16 = 3; // NLMSG_DONE let mut buf = Vec::new(); - buf.extend(make_nlmsg(RTM_NEWADDR, 7)); // VPN — remove - buf.extend(make_nlmsg(nlmsg_done_type, 0)); // DONE — keep - buf.extend(make_nlmsg(RTM_NEWADDR, 2)); // wlan — keep + buf.extend(make_nlmsg(RTM_NEWADDR, 7)); // VPN — remove + buf.extend(make_nlmsg(nlmsg_done_type, 0)); // DONE — keep + buf.extend(make_nlmsg(RTM_NEWADDR, 2)); // wlan — keep let new_len = filter_netlink_dump(&mut buf, &[7]); // Should keep DONE + wlan = 48 bytes diff --git a/zygisk/src/hooks.rs b/zygisk/src/hooks.rs index 427d671..629ba2f 100644 --- a/zygisk/src/hooks.rs +++ b/zygisk/src/hooks.rs @@ -1,3 +1,4 @@ +#![allow(clippy::missing_const_for_thread_local)] //! The actual libc hook functions. //! //! These are called INSTEAD OF the real libc symbols from any PLT we've @@ -139,9 +140,7 @@ pub unsafe extern "C" fn hooked_ioctl( if request == SIOCGIFFLAGS as libc::c_ulong { if !arg.is_null() { let req = unsafe { &*(arg as *const ifreq) }; - let name_bytes = unsafe { - &*(&req.ifr_name as *const [libc::c_char] as *const [u8]) - }; + let name_bytes = unsafe { &*(&req.ifr_name as *const [libc::c_char]) }; if is_vpn_iface_bytes(name_bytes) { set_errno(libc::ENODEV); return -1; @@ -156,9 +155,7 @@ pub unsafe extern "C" fn hooked_ioctl( let ret = unsafe { real(fd, request, arg) }; if ret == 0 && !arg.is_null() { let req = unsafe { &*(arg as *const ifreq) }; - let name_bytes = unsafe { - &*(&req.ifr_name as *const [libc::c_char] as *const [u8]) - }; + let name_bytes = unsafe { &*(&req.ifr_name as *const [libc::c_char]) }; if is_vpn_iface_bytes(name_bytes) { set_errno(libc::ENODEV); return -1; @@ -200,9 +197,7 @@ unsafe fn filter_ifconf(ifc: *mut ifconf) { for i in 0..n { let entry = unsafe { &*ifc.ifc_req.offset(i as isize) }; - let name_bytes = unsafe { - &*(&entry.ifr_name as *const [libc::c_char] as *const [u8]) - }; + let name_bytes = unsafe { &*(&entry.ifr_name as *const [libc::c_char]) }; if is_vpn_iface_bytes(name_bytes) { continue; } @@ -427,9 +422,7 @@ fn is_dirfd_proc_net(dirfd: c_int) -> bool { if let Some(slash) = rest.iter().position(|&b| b == b'/') { let pid = &rest[..slash]; let tail = &rest[slash..]; - return !pid.is_empty() - && pid.iter().all(|b| b.is_ascii_digit()) - && tail == b"/net"; + return !pid.is_empty() && pid.iter().all(|b| b.is_ascii_digit()) && tail == b"/net"; } } false @@ -518,9 +511,7 @@ unsafe fn open_filtered_proc_net( if remaining == 0 { break; } - let n = unsafe { - libc::read(fd, buf[total..].as_mut_ptr() as *mut c_void, remaining) - }; + let n = unsafe { libc::read(fd, buf[total..].as_mut_ptr() as *mut c_void, remaining) }; if n <= 0 { break; } @@ -531,9 +522,7 @@ unsafe fn open_filtered_proc_net( let filtered_len = apply_filter(&mut buf[..total], kind); let mfd_flags: libc::c_uint = if flags & libc::O_CLOEXEC != 0 { 1 } else { 0 }; - let memfd = unsafe { - libc::syscall(libc::SYS_memfd_create, c"".as_ptr(), mfd_flags) as c_int - }; + let memfd = unsafe { libc::syscall(libc::SYS_memfd_create, c"".as_ptr(), mfd_flags) as c_int }; if memfd < 0 { set_errno(libc::EIO); return -1; @@ -668,11 +657,7 @@ pub fn set_real_recvmsg_ptr(p: *const ()) { /// /// Only handles the common single-iov case. Multi-iov netlink responses /// pass through unfiltered (extremely rare in practice). -pub unsafe extern "C" fn hooked_recvmsg( - fd: c_int, - msg: *mut libc::msghdr, - flags: c_int, -) -> isize { +pub unsafe extern "C" fn hooked_recvmsg(fd: c_int, msg: *mut libc::msghdr, flags: c_int) -> isize { let Some(real) = real_recvmsg() else { set_errno(libc::EFAULT); return -1; @@ -695,9 +680,7 @@ pub unsafe extern "C" fn hooked_recvmsg( return ret; } - let buf = unsafe { - core::slice::from_raw_parts_mut(iov.iov_base as *mut u8, ret as usize) - }; + let buf = unsafe { core::slice::from_raw_parts_mut(iov.iov_base as *mut u8, ret as usize) }; // Quick check: first message type must be RTM_NEWADDR or RTM_NEWLINK. let nlmsg_type = u16::from_ne_bytes([buf[4], buf[5]]); diff --git a/zygisk/src/lib.rs b/zygisk/src/lib.rs index 94f254c..0fccffe 100644 --- a/zygisk/src/lib.rs +++ b/zygisk/src/lib.rs @@ -36,8 +36,8 @@ use std::sync::Once; use jni::JNIEnv; use log::{debug, error, info}; use zygisk_api::ZygiskModule; -use zygisk_api::api::v5::{AppSpecializeArgs, V5, ZygiskOption}; use zygisk_api::api::ZygiskApi; +use zygisk_api::api::v5::{AppSpecializeArgs, V5, ZygiskOption}; use crate::hooks::{ hooked_getifaddrs, hooked_ioctl, hooked_openat, hooked_recvmsg, set_real_getifaddrs_ptr, @@ -146,18 +146,18 @@ fn mark_cleanup(api: &mut ZygiskApi<'_, V5>) { /// Install inline hooks on `libc.so` via ByteDance shadowhook. We patch /// three entry points: /// -/// * `ioctl` — catches `SIOCGIFNAME` / `SIOCGIFFLAGS` interface -/// probes from native code. -/// * `getifaddrs` — catches the higher-level interface enumeration -/// API used by `NetworkInterface.getNetworkInterfaces()` -/// inside libcore, by the Dart VM's -/// `NetworkInterface.list()`, and by anything in C/C++ -/// that calls `getifaddrs()` directly. -/// * `openat` — intercepts opens of `/proc/net/{route,ipv6_route, -/// if_inet6,tcp,tcp6}`; returns a `memfd` with VPN -/// entries stripped out. -/// * `recvmsg` — filters netlink `RTM_NEWADDR` / `RTM_NEWLINK` -/// dump responses, removing VPN interface entries. +/// * `ioctl` — catches `SIOCGIFNAME` / `SIOCGIFFLAGS` interface +/// probes from native code. +/// * `getifaddrs` — catches the higher-level interface enumeration +/// API used by `NetworkInterface.getNetworkInterfaces()` +/// inside libcore, by the Dart VM's +/// `NetworkInterface.list()`, and by anything in C/C++ +/// that calls `getifaddrs()` directly. +/// * `openat` — intercepts opens of `/proc/net/{route,ipv6_route, +/// if_inet6,tcp,tcp6}`; returns a `memfd` with VPN +/// entries stripped out. +/// * `recvmsg` — filters netlink `RTM_NEWADDR` / `RTM_NEWLINK` +/// dump responses, removing VPN interface entries. /// /// This replaces the earlier PLT-hook approach. PLT hooks can only patch /// callers that are already mapped at `post_app_specialize` time — which @@ -168,7 +168,11 @@ fn install_hooks() -> Result<(), String> { shadowhook::init_once().map_err(|rc| format!("shadowhook_init: rc={rc}"))?; hook_libc_sym(c"ioctl", hooked_ioctl as *mut _, set_real_ioctl_ptr)?; - hook_libc_sym(c"getifaddrs", hooked_getifaddrs as *mut _, set_real_getifaddrs_ptr)?; + hook_libc_sym( + c"getifaddrs", + hooked_getifaddrs as *mut _, + set_real_getifaddrs_ptr, + )?; hook_libc_sym(c"openat", hooked_openat as *mut _, set_real_openat_ptr)?; hook_libc_sym(c"recvmsg", hooked_recvmsg as *mut _, set_real_recvmsg_ptr)?; @@ -248,10 +252,18 @@ fn scrub_shadowhook_maps() { } // Parse the start-end addresses from the first column. - let Some(range) = line.split_whitespace().next() else { continue }; - let Some((start_hex, end_hex)) = range.split_once('-') else { continue }; - let Ok(start) = usize::from_str_radix(start_hex, 16) else { continue }; - let Ok(end) = usize::from_str_radix(end_hex, 16) else { continue }; + let Some(range) = line.split_whitespace().next() else { + continue; + }; + let Some((start_hex, end_hex)) = range.split_once('-') else { + continue; + }; + let Ok(start) = usize::from_str_radix(start_hex, 16) else { + continue; + }; + let Ok(end) = usize::from_str_radix(end_hex, 16) else { + continue; + }; let len = end.saturating_sub(start); if len == 0 { continue; @@ -264,7 +276,7 @@ fn scrub_shadowhook_maps() { let rc = unsafe { libc::prctl( 0x53564d41_u32 as libc::c_int, // PR_SET_VMA - 0, // PR_SET_VMA_ANON_NAME + 0, // PR_SET_VMA_ANON_NAME start, len, c"".as_ptr(), diff --git a/zygisk/src/shadowhook.rs b/zygisk/src/shadowhook.rs index 4727578..022a950 100644 --- a/zygisk/src/shadowhook.rs +++ b/zygisk/src/shadowhook.rs @@ -13,7 +13,6 @@ use core::sync::atomic::{AtomicBool, Ordering}; #[repr(C)] #[derive(Copy, Clone)] #[allow(dead_code)] -#[repr(C)] pub enum ShadowhookMode { /// Multiple coexisting hooks per symbol (LIFO chain). shadowhook's /// default. Values must match `shadowhook_mode_t` in `shadowhook.h`.