mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-08-18 04:53:47 +00:00
refactor(zygisk): clarify imports and hook flow
This commit is contained in:
parent
61f64bce74
commit
652ed790d7
5 changed files with 149 additions and 153 deletions
|
|
@ -11,8 +11,8 @@
|
|||
//!
|
||||
//! See the fork's commit message for the full rationale.
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
|
@ -24,9 +24,10 @@ fn main() {
|
|||
// the Android cdylib; skip the whole native build step.
|
||||
return;
|
||||
}
|
||||
if !target.starts_with("aarch64") {
|
||||
panic!("vpnhide-zygisk currently only supports aarch64-linux-android (target={target})");
|
||||
}
|
||||
assert!(
|
||||
target.starts_with("aarch64"),
|
||||
"vpnhide-zygisk currently only supports aarch64-linux-android (target={target})"
|
||||
);
|
||||
|
||||
// cargo-ndk sets ANDROID_NDK_HOME before invoking us.
|
||||
let ndk = env::var("ANDROID_NDK_HOME")
|
||||
|
|
@ -96,9 +97,9 @@ fn main() {
|
|||
/// Typical layout: `<ndk>/toolchains/llvm/prebuilt/<host>/lib/clang/<ver>/lib/linux/…`.
|
||||
fn find_ndk_builtins(ndk: &str) -> Option<PathBuf> {
|
||||
let base = PathBuf::from(ndk).join("toolchains/llvm/prebuilt");
|
||||
for host in std::fs::read_dir(&base).ok()?.flatten() {
|
||||
for host in fs::read_dir(&base).ok()?.flatten() {
|
||||
let clang_dir = host.path().join("lib/clang");
|
||||
let Ok(versions) = std::fs::read_dir(&clang_dir) else {
|
||||
let Ok(versions) = fs::read_dir(&clang_dir) else {
|
||||
continue;
|
||||
};
|
||||
for v in versions.flatten() {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,7 @@ fn compact_lines(data: &mut [u8], mut hide: impl FnMut(&[u8]) -> bool) -> usize
|
|||
let line_end = data[read_pos..]
|
||||
.iter()
|
||||
.position(|&b| b == b'\n')
|
||||
.map(|p| read_pos + p + 1)
|
||||
.unwrap_or(len);
|
||||
.map_or(len, |position| read_pos + position + 1);
|
||||
|
||||
if !hide(&data[read_pos..line_end]) {
|
||||
let line_len = line_end - read_pos;
|
||||
|
|
@ -96,12 +95,11 @@ pub fn filter_ipv6_route_buf(data: &mut [u8]) -> usize {
|
|||
/// drop lines whose interface name (trimmed, before the first ':') is a VPN
|
||||
/// interface. The two header lines have no `name:` token and are kept.
|
||||
pub fn filter_dev_buf(data: &mut [u8]) -> usize {
|
||||
compact_lines(data, |line| match line.iter().position(|&b| b == b':') {
|
||||
Some(colon) => {
|
||||
compact_lines(data, |line| {
|
||||
line.iter().position(|&b| b == b':').is_some_and(|colon| {
|
||||
let name = trim_ascii_ws(&line[..colon]);
|
||||
!name.is_empty() && is_vpn_iface_bytes(name)
|
||||
}
|
||||
None => false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +213,7 @@ fn parse_hex_u32(hex: &[u8]) -> Option<u32> {
|
|||
b'a'..=b'f' => b - b'a' + 10,
|
||||
_ => return None,
|
||||
};
|
||||
val = val.checked_shl(4)? | digit as u32;
|
||||
val = val.checked_shl(4)? | u32::from(digit);
|
||||
}
|
||||
Some(val)
|
||||
}
|
||||
|
|
@ -341,10 +339,8 @@ pub fn filter_netlink_dump(data: &mut [u8], vpn_indices: &[u32]) -> usize {
|
|||
vpn_indices.contains(&if_index)
|
||||
} else if nlmsg_type == RTM_NEWROUTE && nlmsg_len >= NLMSG_HDRLEN + RTMSG_HDRLEN {
|
||||
// Output interface is an RTA_OIF rtattr after struct rtmsg.
|
||||
match route_oif(&data[read_pos..read_pos + nlmsg_len]) {
|
||||
Some(oif) => vpn_indices.contains(&oif),
|
||||
None => false,
|
||||
}
|
||||
route_oif(&data[read_pos..read_pos + nlmsg_len])
|
||||
.is_some_and(|oif| vpn_indices.contains(&oif))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
|
@ -374,6 +370,7 @@ pub fn filter_netlink_dump(data: &mut [u8], vpn_indices: &[u32]) -> usize {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::str;
|
||||
|
||||
#[test]
|
||||
fn detects_tun0() {
|
||||
|
|
@ -390,7 +387,7 @@ face |bytes packets errs drop\n lo: 100 1 0 0\n wlan0: 200
|
|||
tun0: 300 3 0 0\n"
|
||||
.to_vec();
|
||||
let n = filter_dev_buf(&mut buf);
|
||||
let out = core::str::from_utf8(&buf[..n]).unwrap();
|
||||
let out = str::from_utf8(&buf[..n]).unwrap();
|
||||
assert!(out.contains("Inter-|"), "header kept");
|
||||
assert!(out.contains("face |"), "second header kept");
|
||||
assert!(out.contains("lo:"), "lo kept");
|
||||
|
|
@ -457,7 +454,7 @@ tun0: 300 3 0 0\n"
|
|||
rmnet0\tFEFFFFFF\t00000000\n";
|
||||
let mut buf = input.to_vec();
|
||||
let new_len = filter_route_buf(&mut buf);
|
||||
let result = core::str::from_utf8(&buf[..new_len]).unwrap();
|
||||
let result = str::from_utf8(&buf[..new_len]).unwrap();
|
||||
assert!(result.contains("Iface\t"));
|
||||
assert!(result.contains("wlan0\t"));
|
||||
assert!(result.contains("rmnet0\t"));
|
||||
|
|
@ -479,7 +476,7 @@ tun0: 300 3 0 0\n"
|
|||
let input = b"Iface\tDest\nwg0\t00000000\nwlan0\t00000000\n";
|
||||
let mut buf = input.to_vec();
|
||||
let new_len = filter_route_buf(&mut buf);
|
||||
let result = core::str::from_utf8(&buf[..new_len]).unwrap();
|
||||
let result = str::from_utf8(&buf[..new_len]).unwrap();
|
||||
assert!(!result.contains("wg0"));
|
||||
assert!(result.contains("wlan0"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
//! The actual libc hook functions.
|
||||
//!
|
||||
//! These are called INSTEAD OF the real libc symbols from any PLT we've
|
||||
//! patched. Each hook calls the saved original function via a function
|
||||
//! pointer stored in `statics` after `pltHookRegister` ran. We can't rely
|
||||
//! on linkage-time names like `libc::ioctl` because those would resolve
|
||||
//! back through our own PLT entry — which would either recurse infinitely
|
||||
//! or, in a different library's process, not be hooked at all.
|
||||
//! These replace the real libc symbols through shadowhook's inline trampolines.
|
||||
//! Each hook calls the original function through the trampoline captured when
|
||||
//! the inline hook was installed. We can't call linkage-time names such as
|
||||
//! `libc::ioctl`, because they would resolve back through the hooked entry point
|
||||
//! and recurse.
|
||||
//!
|
||||
//! ## Important safety notes
|
||||
//!
|
||||
//! * The `*const ()` function pointers used by Zygisk's PLT hook API are
|
||||
//! variadic in C (`ioctl` is `int ioctl(int fd, unsigned long req, ...)`)
|
||||
//! * The `*const ()` function pointers returned by shadowhook represent
|
||||
//! functions that may be variadic in C (`ioctl` is
|
||||
//! `int ioctl(int fd, unsigned long req, ...)`)
|
||||
//! but Rust doesn't have first-class variadic functions. We work around
|
||||
//! this by declaring the relevant `ioctl` signatures we care about
|
||||
//! (`SIOCGIFNAME`, `SIOCGIFFLAGS`) as 3-argument forms and trusting the
|
||||
|
|
@ -23,12 +23,17 @@
|
|||
//! original. Panicking would abort the entire process.
|
||||
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::ffi::{c_int, c_void};
|
||||
use core::ffi::{CStr, c_int, c_void};
|
||||
use core::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
|
||||
use core::{mem, ptr, slice};
|
||||
|
||||
use libc::{SIOCGIFCONF, SIOCGIFNAME, ifreq};
|
||||
|
||||
use crate::filter::is_vpn_iface_bytes;
|
||||
use crate::filter::{
|
||||
MAX_VPN_ADDRS, RTM_NEWADDR, RTM_NEWLINK, RTM_NEWROUTE, filter_dev_buf, filter_if_inet6_buf,
|
||||
filter_ipv6_route_buf, filter_netlink_dump, filter_route_buf, filter_tcp4_buf, filter_tcp6_buf,
|
||||
is_vpn_iface_bytes, is_vpn_iface_cstr,
|
||||
};
|
||||
|
||||
/// `struct ifconf` from `<net/if.h>`. Not exported by the `libc` crate.
|
||||
#[repr(C)]
|
||||
|
|
@ -46,7 +51,6 @@ struct ifconf {
|
|||
// causing errors like `ioctl(SIOCGIFFLAGS) for "tun0" failed in ifaddrs`
|
||||
// and corrupting NFC/HCE payment flows).
|
||||
thread_local! {
|
||||
#[allow(clippy::missing_const_for_thread_local)]
|
||||
static IN_GETIFADDRS: Cell<bool> = const { Cell::new(false) };
|
||||
|
||||
/// Reusable contiguous view for scatter/gather netlink recvmsg payloads.
|
||||
|
|
@ -57,6 +61,17 @@ thread_local! {
|
|||
static NETLINK_IOV_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(65536));
|
||||
}
|
||||
|
||||
/// Run one libc operation without letting the ioctl hook filter nested calls.
|
||||
/// Preserve the previous value so nested guarded operations remain correct.
|
||||
fn with_ioctl_passthrough<T>(operation: impl FnOnce() -> T) -> T {
|
||||
IN_GETIFADDRS.with(|guard| {
|
||||
let previous = guard.replace(true);
|
||||
let result = operation();
|
||||
guard.set(previous);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if `fd` is an `AF_NETLINK` socket.
|
||||
///
|
||||
/// Used by recv/recvmsg hooks to skip TCP/UDP/Unix sockets entirely —
|
||||
|
|
@ -69,7 +84,7 @@ thread_local! {
|
|||
/// chance accumulates to near-certain breakage.
|
||||
fn is_netlink_fd(fd: c_int) -> bool {
|
||||
let mut domain: c_int = 0;
|
||||
let mut optlen = core::mem::size_of::<c_int>() as libc::socklen_t;
|
||||
let mut optlen = mem::size_of::<c_int>() as libc::socklen_t;
|
||||
let rc = unsafe {
|
||||
libc::getsockopt(
|
||||
fd,
|
||||
|
|
@ -113,7 +128,7 @@ fn set_errno(val: c_int) {
|
|||
// Saved originals
|
||||
// ============================================================================
|
||||
|
||||
/// Declare a saved-original slot for one PLT-hooked libc function.
|
||||
/// Declare a saved-original slot for one inline-hooked libc function.
|
||||
///
|
||||
/// Each hook needs the same four items to call through to the function it
|
||||
/// replaced, so we generate them from one declaration instead of hand-copying
|
||||
|
|
@ -121,8 +136,7 @@ fn set_errno(val: c_int) {
|
|||
/// - `static $slot: AtomicPtr<c_void>` — the captured original pointer,
|
||||
/// - `type $ty` — its function-pointer shape (doc comments carry through),
|
||||
/// - `fn $getter() -> Option<$ty>` — None before install or if somehow null,
|
||||
/// - `pub fn $setter(p: *const ())` — what `pltHookRegister` feeds the
|
||||
/// original into.
|
||||
/// - `pub fn $setter(p: *const ())` — stores the original trampoline.
|
||||
///
|
||||
/// The slot is read/written with relaxed atomics: install happens-before any
|
||||
/// hook fires, so no stronger ordering is needed and no locks are taken.
|
||||
|
|
@ -134,7 +148,7 @@ macro_rules! saved_original {
|
|||
fn $getter:ident;
|
||||
pub fn $setter:ident;
|
||||
) => {
|
||||
static $slot: AtomicPtr<c_void> = AtomicPtr::new(core::ptr::null_mut());
|
||||
static $slot: AtomicPtr<c_void> = AtomicPtr::new(ptr::null_mut());
|
||||
|
||||
$(#[$tymeta])*
|
||||
type $ty = $sig;
|
||||
|
|
@ -147,11 +161,11 @@ macro_rules! saved_original {
|
|||
} else {
|
||||
// SAFETY: the slot only ever holds a valid pointer of this
|
||||
// exact shape, stored once at install via the setter below.
|
||||
Some(unsafe { core::mem::transmute::<*mut c_void, $ty>(raw) })
|
||||
Some(unsafe { mem::transmute::<*mut c_void, $ty>(raw) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Stash the original pointer returned by `pltHookRegister`.
|
||||
/// Stash the original trampoline returned by shadowhook.
|
||||
pub fn $setter(p: *const ()) {
|
||||
$slot.store(p as *mut c_void, Ordering::Relaxed);
|
||||
}
|
||||
|
|
@ -230,14 +244,14 @@ fn kernel_needs_socket_bind_hiding() -> bool {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
let mut uts = core::mem::MaybeUninit::<libc::utsname>::zeroed();
|
||||
let mut uts = mem::MaybeUninit::<libc::utsname>::zeroed();
|
||||
let hook = unsafe {
|
||||
if libc::uname(uts.as_mut_ptr()) != 0 {
|
||||
false
|
||||
} else {
|
||||
let uts = uts.assume_init();
|
||||
let release =
|
||||
core::slice::from_raw_parts(uts.release.as_ptr().cast::<u8>(), uts.release.len());
|
||||
slice::from_raw_parts(uts.release.as_ptr().cast::<u8>(), uts.release.len());
|
||||
let end = release
|
||||
.iter()
|
||||
.position(|&byte| byte == 0)
|
||||
|
|
@ -278,16 +292,16 @@ fn copy_from_self(src: *const c_void, dst: &mut [u8]) -> bool {
|
|||
let remote = libc::iovec {
|
||||
// process_vm_readv's ABI uses mutable iovec fields even though the
|
||||
// remote side is read-only.
|
||||
iov_base: src as *mut c_void,
|
||||
iov_base: src.cast_mut(),
|
||||
iov_len: dst.len(),
|
||||
};
|
||||
let copied = unsafe {
|
||||
libc::syscall(
|
||||
libc::SYS_process_vm_readv,
|
||||
libc::getpid(),
|
||||
&local as *const libc::iovec,
|
||||
ptr::from_ref(&local),
|
||||
1usize,
|
||||
&remote as *const libc::iovec,
|
||||
ptr::from_ref(&remote),
|
||||
1usize,
|
||||
0usize,
|
||||
)
|
||||
|
|
@ -306,11 +320,8 @@ fn ifindex_is_vpn(ifindex: c_int) -> Option<bool> {
|
|||
}
|
||||
|
||||
let mut name = [0u8; libc::IFNAMSIZ];
|
||||
let result = IN_GETIFADDRS.with(|guard| {
|
||||
let previous = guard.replace(true);
|
||||
let result = unsafe { libc::if_indextoname(ifindex as u32, name.as_mut_ptr().cast()) };
|
||||
guard.set(previous);
|
||||
result
|
||||
let result = with_ioctl_passthrough(|| unsafe {
|
||||
libc::if_indextoname(ifindex as u32, name.as_mut_ptr().cast())
|
||||
});
|
||||
(!result.is_null()).then(|| is_vpn_iface_bytes(&name))
|
||||
}
|
||||
|
|
@ -328,7 +339,7 @@ fn deny_ifindex_bind(ifindex: c_int, resolved_is_vpn: Option<bool>) -> bool {
|
|||
/// its native `EBADF`/`ENOTSOCK` ordering before we inspect the interface name.
|
||||
fn is_socket_fd(fd: c_int) -> bool {
|
||||
let mut socket_type: c_int = 0;
|
||||
let mut len = core::mem::size_of::<c_int>() as libc::socklen_t;
|
||||
let mut len = mem::size_of::<c_int>() as libc::socklen_t;
|
||||
unsafe {
|
||||
libc::getsockopt(
|
||||
fd,
|
||||
|
|
@ -415,13 +426,13 @@ pub unsafe extern "C" fn hooked_setsockopt(
|
|||
}
|
||||
|
||||
if optname == SO_BINDTOIFINDEX {
|
||||
if optlen < core::mem::size_of::<c_int>() as libc::socklen_t
|
||||
if optlen < mem::size_of::<c_int>() as libc::socklen_t
|
||||
|| optlen > c_int::MAX as libc::socklen_t
|
||||
{
|
||||
return unsafe { real(fd, level, optname, optval, optlen) };
|
||||
}
|
||||
|
||||
let mut raw = [0u8; core::mem::size_of::<c_int>()];
|
||||
let mut raw = [0u8; mem::size_of::<c_int>()];
|
||||
if !copy_from_self(optval, &mut raw) {
|
||||
return unsafe { real(fd, level, optname, optval, optlen) };
|
||||
}
|
||||
|
|
@ -439,7 +450,7 @@ pub unsafe extern "C" fn hooked_setsockopt(
|
|||
level,
|
||||
optname,
|
||||
(&ifindex as *const c_int).cast(),
|
||||
core::mem::size_of::<c_int>() as libc::socklen_t,
|
||||
mem::size_of::<c_int>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
|
@ -501,7 +512,7 @@ pub unsafe extern "C" fn hooked_ioctl(
|
|||
if ret == 0 && !arg.is_null() {
|
||||
let req = unsafe { &*(arg as *const ifreq) };
|
||||
let name_bytes = unsafe {
|
||||
core::slice::from_raw_parts(req.ifr_name.as_ptr().cast::<u8>(), req.ifr_name.len())
|
||||
slice::from_raw_parts(req.ifr_name.as_ptr().cast::<u8>(), req.ifr_name.len())
|
||||
};
|
||||
if is_vpn_iface_bytes(name_bytes) {
|
||||
set_errno(libc::ENODEV);
|
||||
|
|
@ -516,7 +527,7 @@ pub unsafe extern "C" fn hooked_ioctl(
|
|||
if !arg.is_null() && is_siocgif(request) {
|
||||
let req = unsafe { &*(arg as *const ifreq) };
|
||||
let name_bytes = unsafe {
|
||||
core::slice::from_raw_parts(req.ifr_name.as_ptr().cast::<u8>(), req.ifr_name.len())
|
||||
slice::from_raw_parts(req.ifr_name.as_ptr().cast::<u8>(), req.ifr_name.len())
|
||||
};
|
||||
if is_vpn_iface_bytes(name_bytes) {
|
||||
set_errno(libc::ENODEV);
|
||||
|
|
@ -553,21 +564,21 @@ unsafe fn filter_ifconf(ifc: *mut ifconf) {
|
|||
return;
|
||||
}
|
||||
|
||||
let entry_size = core::mem::size_of::<ifreq>() as c_int;
|
||||
let entry_size = mem::size_of::<ifreq>() as c_int;
|
||||
let n = ifc.ifc_len / entry_size;
|
||||
let mut dst = 0i32;
|
||||
|
||||
for i in 0..n {
|
||||
let entry = unsafe { &*ifc.ifc_req.offset(i as isize) };
|
||||
let name_bytes = unsafe {
|
||||
core::slice::from_raw_parts(entry.ifr_name.as_ptr().cast::<u8>(), entry.ifr_name.len())
|
||||
slice::from_raw_parts(entry.ifr_name.as_ptr().cast::<u8>(), entry.ifr_name.len())
|
||||
};
|
||||
if is_vpn_iface_bytes(name_bytes) {
|
||||
continue;
|
||||
}
|
||||
if dst != i {
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(
|
||||
ptr::copy_nonoverlapping(
|
||||
ifc.ifc_req.offset(i as isize),
|
||||
ifc.ifc_req.offset(dst as isize),
|
||||
1,
|
||||
|
|
@ -581,7 +592,7 @@ unsafe fn filter_ifconf(ifc: *mut ifconf) {
|
|||
// A caller can inspect its buffer past the shortened ifc_len. Erase
|
||||
// only slots returned by the kernel and removed by compaction.
|
||||
unsafe {
|
||||
core::ptr::write_bytes(ifc.ifc_req.offset(dst as isize), 0, (n - dst) as usize);
|
||||
ptr::write_bytes(ifc.ifc_req.offset(dst as isize), 0, (n - dst) as usize);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -688,9 +699,7 @@ pub unsafe extern "C" fn hooked_getifaddrs(ifap: *mut *mut libc::ifaddrs) -> c_i
|
|||
// Set the thread-local guard so hooked_ioctl passes through while
|
||||
// libc's real getifaddrs runs (it internally calls ioctl for each
|
||||
// interface to get flags — we must not filter those).
|
||||
IN_GETIFADDRS.with(|f| f.set(true));
|
||||
let rc = unsafe { real(ifap) };
|
||||
IN_GETIFADDRS.with(|f| f.set(false));
|
||||
let rc = with_ioctl_passthrough(|| unsafe { real(ifap) });
|
||||
|
||||
if rc != 0 || ifap.is_null() {
|
||||
return rc;
|
||||
|
|
@ -708,8 +717,8 @@ pub unsafe extern "C" fn hooked_getifaddrs(ifap: *mut *mut libc::ifaddrs) -> c_i
|
|||
let is_vpn = if name_ptr.is_null() {
|
||||
false
|
||||
} else {
|
||||
let name = core::ffi::CStr::from_ptr(name_ptr);
|
||||
crate::filter::is_vpn_iface_cstr(name)
|
||||
let name = CStr::from_ptr(name_ptr);
|
||||
is_vpn_iface_cstr(name)
|
||||
};
|
||||
if is_vpn {
|
||||
*slot = (*entry).ifa_next;
|
||||
|
|
@ -914,7 +923,7 @@ pub unsafe extern "C" fn hooked_openat(
|
|||
};
|
||||
|
||||
if !pathname.is_null() {
|
||||
let path = unsafe { core::ffi::CStr::from_ptr(pathname) };
|
||||
let path = unsafe { CStr::from_ptr(pathname) };
|
||||
let path_bytes = path.to_bytes();
|
||||
|
||||
let matched = if path_bytes.first() == Some(&b'/') {
|
||||
|
|
@ -1033,8 +1042,6 @@ unsafe fn open_filtered_proc_net(
|
|||
|
||||
/// Dispatch to the right filter function based on the file type.
|
||||
fn apply_filter(data: &mut [u8], kind: ProcNetFile) -> usize {
|
||||
use crate::filter::*;
|
||||
|
||||
match kind {
|
||||
ProcNetFile::Route => filter_route_buf(data),
|
||||
ProcNetFile::Ipv6Route => filter_ipv6_route_buf(data),
|
||||
|
|
@ -1066,16 +1073,13 @@ fn apply_filter(data: &mut [u8], kind: ProcNetFile) -> usize {
|
|||
/// getifaddrs calls ioctl(SIOCGIFFLAGS) internally. Returns `false` if
|
||||
/// the real getifaddrs() symbol couldn't be resolved or the call failed;
|
||||
/// the closure is then never invoked.
|
||||
unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&core::ffi::CStr, &libc::ifaddrs)) -> bool {
|
||||
unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&CStr, &libc::ifaddrs)) -> bool {
|
||||
let Some(real) = real_getifaddrs() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut ifap: *mut libc::ifaddrs = core::ptr::null_mut();
|
||||
|
||||
IN_GETIFADDRS.with(|flag| flag.set(true));
|
||||
let rc = unsafe { real(&mut ifap) };
|
||||
IN_GETIFADDRS.with(|flag| flag.set(false));
|
||||
let mut ifap: *mut libc::ifaddrs = ptr::null_mut();
|
||||
let rc = with_ioctl_passthrough(|| unsafe { real(&mut ifap) });
|
||||
|
||||
if rc != 0 || ifap.is_null() {
|
||||
return false;
|
||||
|
|
@ -1089,8 +1093,8 @@ unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&core::ffi::CStr, &libc::ifaddrs
|
|||
if entry.ifa_name.is_null() {
|
||||
continue;
|
||||
}
|
||||
let name = unsafe { core::ffi::CStr::from_ptr(entry.ifa_name) };
|
||||
if !crate::filter::is_vpn_iface_cstr(name) {
|
||||
let name = unsafe { CStr::from_ptr(entry.ifa_name) };
|
||||
if !is_vpn_iface_cstr(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1105,13 +1109,11 @@ unsafe fn walk_getifaddrs_vpn(mut f: impl FnMut(&core::ffi::CStr, &libc::ifaddrs
|
|||
/// real (unhooked) `getifaddrs`. Sets `IN_GETIFADDRS` guard so our
|
||||
/// ioctl hook doesn't interfere with libc's internal SIOCGIFFLAGS calls.
|
||||
fn collect_vpn_addrs() -> (
|
||||
[u32; crate::filter::MAX_VPN_ADDRS],
|
||||
[u32; MAX_VPN_ADDRS],
|
||||
usize,
|
||||
[[u32; 4]; crate::filter::MAX_VPN_ADDRS],
|
||||
[[u32; 4]; MAX_VPN_ADDRS],
|
||||
usize,
|
||||
) {
|
||||
use crate::filter::MAX_VPN_ADDRS;
|
||||
|
||||
let mut addrs4 = [0u32; MAX_VPN_ADDRS];
|
||||
let mut addrs6 = [[0u32; 4]; MAX_VPN_ADDRS];
|
||||
let mut n4 = 0usize;
|
||||
|
|
@ -1221,7 +1223,7 @@ pub unsafe extern "C" fn hooked_recvmsg(fd: c_int, msg: *mut libc::msghdr, flags
|
|||
hdr.msg_iovlen,
|
||||
in_iovecs,
|
||||
&mut scratch,
|
||||
|data| crate::filter::filter_netlink_dump(data, &indices[..n]),
|
||||
|data| filter_netlink_dump(data, &indices[..n]),
|
||||
)
|
||||
}) else {
|
||||
return ret;
|
||||
|
|
@ -1286,7 +1288,7 @@ unsafe fn rewrite_iovec_payload(
|
|||
if entry.iov_base.is_null() {
|
||||
return None;
|
||||
}
|
||||
let bytes = unsafe { core::slice::from_raw_parts(entry.iov_base.cast::<u8>(), take) };
|
||||
let bytes = unsafe { slice::from_raw_parts(entry.iov_base.cast::<u8>(), take) };
|
||||
scratch.extend_from_slice(bytes);
|
||||
remaining -= take;
|
||||
}
|
||||
|
|
@ -1306,7 +1308,7 @@ unsafe fn rewrite_iovec_payload(
|
|||
continue;
|
||||
}
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(
|
||||
ptr::copy_nonoverlapping(
|
||||
scratch.as_ptr().add(copied),
|
||||
entry.iov_base.cast::<u8>(),
|
||||
take,
|
||||
|
|
@ -1349,16 +1351,13 @@ unsafe fn maybe_filter_netlink_buf(fd: c_int, buf: *mut u8, ret: isize) -> isize
|
|||
return ret;
|
||||
}
|
||||
|
||||
let data = unsafe { core::slice::from_raw_parts_mut(buf, ret as usize) };
|
||||
let data = unsafe { slice::from_raw_parts_mut(buf, ret as usize) };
|
||||
|
||||
// Quick check: first message type must be one we filter. Route dumps
|
||||
// (RTM_GETROUTE) come back as RTM_NEWROUTE and must not be skipped —
|
||||
// that gap was the issue #86 `if<N>` leak.
|
||||
let nlmsg_type = u16::from_ne_bytes([data[4], data[5]]);
|
||||
if nlmsg_type != crate::filter::RTM_NEWADDR
|
||||
&& nlmsg_type != crate::filter::RTM_NEWLINK
|
||||
&& nlmsg_type != crate::filter::RTM_NEWROUTE
|
||||
{
|
||||
if nlmsg_type != RTM_NEWADDR && nlmsg_type != RTM_NEWLINK && nlmsg_type != RTM_NEWROUTE {
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -1367,7 +1366,7 @@ unsafe fn maybe_filter_netlink_buf(fd: c_int, buf: *mut u8, ret: isize) -> isize
|
|||
return ret;
|
||||
}
|
||||
|
||||
crate::filter::filter_netlink_dump(data, &indices[..n]) as isize
|
||||
filter_netlink_dump(data, &indices[..n]) as isize
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -1505,9 +1504,7 @@ pub unsafe extern "C" fn hooked_recvfrom_chk(
|
|||
/// Collect interface indices of VPN interfaces. Uses real_getifaddrs
|
||||
/// (with IN_GETIFADDRS guard) and `if_nametoindex` (which calls
|
||||
/// ioctl(SIOCGIFINDEX) — passed through by our ioctl hook).
|
||||
fn collect_vpn_iface_indices() -> ([u32; crate::filter::MAX_VPN_ADDRS], usize) {
|
||||
use crate::filter::MAX_VPN_ADDRS;
|
||||
|
||||
fn collect_vpn_iface_indices() -> ([u32; MAX_VPN_ADDRS], usize) {
|
||||
let mut indices = [0u32; MAX_VPN_ADDRS];
|
||||
let mut n = 0usize;
|
||||
|
||||
|
|
@ -1520,13 +1517,7 @@ fn collect_vpn_iface_indices() -> ([u32; crate::filter::MAX_VPN_ADDRS], usize) {
|
|||
// now blocks for VPN names. Run it under the IN_GETIFADDRS guard so
|
||||
// it passes through to the real index — otherwise this filter loses
|
||||
// the VPN indices and the netlink route dump (#86) stops filtering.
|
||||
let idx = IN_GETIFADDRS.with(|f| {
|
||||
let prev = f.get();
|
||||
f.set(true);
|
||||
let i = libc::if_nametoindex(entry.ifa_name);
|
||||
f.set(prev);
|
||||
i
|
||||
});
|
||||
let idx = with_ioctl_passthrough(|| libc::if_nametoindex(entry.ifa_name));
|
||||
if idx == 0 || indices[..n].contains(&idx) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,17 +78,25 @@ mod generated;
|
|||
mod hooks;
|
||||
mod shadowhook;
|
||||
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||
use core::ffi::{CStr, c_void};
|
||||
use core::ptr;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{Once, OnceLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use log::{debug, error, info};
|
||||
use android_logger::Config;
|
||||
use log::{LevelFilter, debug, error, info};
|
||||
use vpnhide_protocol as protocol;
|
||||
use vpnhide_protocol::hook_ids::{Hook, ZYGISK_HOOK_MASK};
|
||||
use zygisk_api::ZygiskModule;
|
||||
use zygisk_api::api::ZygiskApi;
|
||||
use zygisk_api::api::v2::{AppSpecializeArgs, V2, ZygiskOption};
|
||||
use zygisk_api::jni::JNIEnv;
|
||||
use zygisk_api::jni::{JNIEnv, objects::JString};
|
||||
use zygisk_api::{ZygiskModule, register_companion, register_module};
|
||||
|
||||
use crate::hooks::{
|
||||
hooked_getifaddrs, hooked_ioctl, hooked_openat, hooked_recv, hooked_recvfrom,
|
||||
|
|
@ -118,9 +126,9 @@ fn init_logger() {
|
|||
static INIT: Once = Once::new();
|
||||
INIT.call_once(|| {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default()
|
||||
Config::default()
|
||||
.with_tag(LOG_TAG)
|
||||
.with_max_level(log::LevelFilter::Trace),
|
||||
.with_max_level(LevelFilter::Trace),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -134,9 +142,9 @@ fn init_logger() {
|
|||
/// Same principle as HookLog.e on the Kotlin side.
|
||||
fn set_log_level(enabled: bool) {
|
||||
let level = if enabled {
|
||||
log::LevelFilter::Trace
|
||||
LevelFilter::Trace
|
||||
} else {
|
||||
log::LevelFilter::Error
|
||||
LevelFilter::Error
|
||||
};
|
||||
log::set_max_level(level);
|
||||
}
|
||||
|
|
@ -180,23 +188,26 @@ static CACHED_CONFIG: OnceLock<ZygiskConfig> = OnceLock::new();
|
|||
/// privileges, bypassing the SELinux restrictions that block direct file
|
||||
/// access on Magisk. A missing/unreadable/invalid file fails closed: no
|
||||
/// targets, logging off.
|
||||
fn load_config_from_dir_fd(dir_fd: std::os::fd::RawFd) -> ZygiskConfig {
|
||||
use std::io::Read;
|
||||
use std::os::fd::FromRawFd;
|
||||
fn load_config_from_dir_fd(dir_fd: RawFd) -> ZygiskConfig {
|
||||
let empty = ZygiskConfig {
|
||||
targets: Vec::new(),
|
||||
debug: false,
|
||||
};
|
||||
let filename = std::ffi::CString::new(TARGETS_FILENAME).unwrap();
|
||||
let fd = unsafe { libc::openat(dir_fd, filename.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
|
||||
let fd = unsafe {
|
||||
libc::openat(
|
||||
dir_fd,
|
||||
c"targets.txt".as_ptr(),
|
||||
libc::O_RDONLY | libc::O_CLOEXEC,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
log::warn!(
|
||||
"can't open {TARGETS_FILENAME} via module dir fd: {}",
|
||||
std::io::Error::last_os_error()
|
||||
io::Error::last_os_error()
|
||||
);
|
||||
return empty;
|
||||
}
|
||||
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
let mut file = unsafe { fs::File::from_raw_fd(fd) };
|
||||
let mut content = Vec::new();
|
||||
if let Err(e) = file.read_to_end(&mut content) {
|
||||
log::warn!("can't read {TARGETS_FILENAME}: {e}");
|
||||
|
|
@ -282,7 +293,12 @@ impl ZygiskModule for VpnHide {
|
|||
// UID is the targeting key (protocol §4.3). args.uid already holds the
|
||||
// UID this child will become, and one UID covers every process of a
|
||||
// multi-process app — so no package/sub-process name matching needed.
|
||||
let uid = *args.uid as u32;
|
||||
let Ok(uid) = u32::try_from(*args.uid) else {
|
||||
self.target_hookmask.store(0, Ordering::Relaxed);
|
||||
self.report_status.store(false, Ordering::Relaxed);
|
||||
mark_cleanup(&mut api);
|
||||
return;
|
||||
};
|
||||
// nice_name is still read, but only to recognise the VPN Hide app
|
||||
// itself for the status heartbeat — an identity check, not targeting.
|
||||
let package = read_jstring(&env, args.nice_name);
|
||||
|
|
@ -329,14 +345,14 @@ impl ZygiskModule for VpnHide {
|
|||
}
|
||||
|
||||
fn write_status_file() {
|
||||
let boot_id = match std::fs::read_to_string("/proc/sys/kernel/random/boot_id") {
|
||||
let boot_id = match fs::read_to_string("/proc/sys/kernel/random/boot_id") {
|
||||
Ok(v) => v.trim().to_string(),
|
||||
Err(err) => {
|
||||
error!("failed to read boot_id: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let timestamp = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||
let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(v) => v.as_secs(),
|
||||
Err(err) => {
|
||||
error!("failed to get timestamp: {err}");
|
||||
|
|
@ -347,16 +363,16 @@ fn write_status_file() {
|
|||
"version={}\nboot_id={}\npid={}\ntimestamp={}\n",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
boot_id,
|
||||
std::process::id(),
|
||||
process::id(),
|
||||
timestamp,
|
||||
);
|
||||
if let Some(parent) = std::path::Path::new(APP_STATUS_FILE).parent() {
|
||||
if let Err(err) = std::fs::create_dir_all(parent) {
|
||||
if let Some(parent) = Path::new(APP_STATUS_FILE).parent() {
|
||||
if let Err(err) = fs::create_dir_all(parent) {
|
||||
error!("failed to create status dir {}: {err}", parent.display());
|
||||
return;
|
||||
}
|
||||
}
|
||||
match std::fs::write(APP_STATUS_FILE, content) {
|
||||
match fs::write(APP_STATUS_FILE, content) {
|
||||
Ok(()) => info!("wrote zygisk status heartbeat to {APP_STATUS_FILE}"),
|
||||
Err(err) => error!("failed to write status heartbeat: {err}"),
|
||||
}
|
||||
|
|
@ -368,6 +384,13 @@ fn mark_cleanup(api: &mut ZygiskApi<'_, V2>) {
|
|||
api.set_option(ZygiskOption::DlCloseModuleLibrary);
|
||||
}
|
||||
|
||||
struct HookDesc {
|
||||
sym: &'static CStr,
|
||||
hook_id: Hook,
|
||||
replacement: *mut c_void,
|
||||
store_orig: fn(*const ()),
|
||||
}
|
||||
|
||||
/// Install selected inline hooks on `libc.so` via ByteDance shadowhook.
|
||||
///
|
||||
/// * `ioctl` — catches `SIOCGIFNAME` / `SIOCGIFFLAGS` interface
|
||||
|
|
@ -404,12 +427,6 @@ fn install_hooks(hookmask: u32) -> Result<(), String> {
|
|||
// itself is 12 bytes (3 instructions), safe for island-mode hooking.
|
||||
// recvfrom + __recvfrom_chk catch FORTIFY'd / direct callers that never
|
||||
// touch the `recv` symbol (issue #86 native route dump).
|
||||
struct HookDesc {
|
||||
sym: &'static core::ffi::CStr,
|
||||
hook_id: Hook,
|
||||
replacement: *mut core::ffi::c_void,
|
||||
store_orig: fn(*const ()),
|
||||
}
|
||||
let table = [
|
||||
HookDesc {
|
||||
sym: c"ioctl",
|
||||
|
|
@ -461,7 +478,7 @@ fn install_hooks(hookmask: u32) -> Result<(), String> {
|
|||
},
|
||||
];
|
||||
|
||||
let mut installed: Vec<*mut core::ffi::c_void> = Vec::with_capacity(table.len());
|
||||
let mut installed: Vec<*mut c_void> = Vec::with_capacity(table.len());
|
||||
for desc in table {
|
||||
if !zygisk_hook_enabled(hookmask, desc.hook_id) {
|
||||
continue;
|
||||
|
|
@ -495,11 +512,11 @@ fn install_hooks(hookmask: u32) -> Result<(), String> {
|
|||
/// pointer on success — caller keeps it for the partial-install
|
||||
/// rollback in `install_hooks`.
|
||||
fn hook_libc_sym(
|
||||
sym: &core::ffi::CStr,
|
||||
new_fn: *mut core::ffi::c_void,
|
||||
sym: &CStr,
|
||||
new_fn: *mut c_void,
|
||||
store_orig: fn(*const ()),
|
||||
) -> Result<*mut core::ffi::c_void, String> {
|
||||
let mut orig: *mut core::ffi::c_void = core::ptr::null_mut();
|
||||
) -> Result<*mut c_void, String> {
|
||||
let mut orig: *mut c_void = ptr::null_mut();
|
||||
// SAFETY: `new_fn` has an ABI-compatible signature with the target
|
||||
// libc symbol; `&mut orig` is a valid writable pointer.
|
||||
let stub = unsafe { shadowhook::hook_sym(c"libc.so", sym, new_fn, &mut orig) };
|
||||
|
|
@ -549,8 +566,6 @@ fn scrub_shadowhook_maps() {
|
|||
// not in the filter list today, but a future addition would silently
|
||||
// break this function.
|
||||
let maps = {
|
||||
use std::io::Read;
|
||||
use std::os::fd::FromRawFd;
|
||||
let fd = unsafe {
|
||||
libc::open(
|
||||
c"/proc/self/maps".as_ptr(),
|
||||
|
|
@ -561,7 +576,7 @@ fn scrub_shadowhook_maps() {
|
|||
log::warn!("scrub_shadowhook_maps: can't open /proc/self/maps");
|
||||
return;
|
||||
}
|
||||
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
let mut file = unsafe { fs::File::from_raw_fd(fd) };
|
||||
let mut buf = String::new();
|
||||
if file.read_to_string(&mut buf).is_err() {
|
||||
log::warn!("scrub_shadowhook_maps: can't read /proc/self/maps");
|
||||
|
|
@ -616,7 +631,7 @@ fn scrub_shadowhook_maps() {
|
|||
} else {
|
||||
log::warn!(
|
||||
"prctl(PR_SET_VMA_ANON_NAME) failed for {start_hex}-{end_hex}: errno={}",
|
||||
std::io::Error::last_os_error()
|
||||
io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -631,15 +646,10 @@ fn scrub_shadowhook_maps() {
|
|||
/// sub-process name handling (the old package-name path did).
|
||||
#[inline(never)]
|
||||
fn target_hookmask(uid: u32) -> u32 {
|
||||
match CACHED_CONFIG.get() {
|
||||
Some(cfg) => cfg
|
||||
.targets
|
||||
.iter()
|
||||
.find(|target| target.uid == uid)
|
||||
.map(|target| target.hookmask)
|
||||
.unwrap_or(0),
|
||||
None => 0,
|
||||
}
|
||||
CACHED_CONFIG
|
||||
.get()
|
||||
.and_then(|cfg| cfg.targets.iter().find(|target| target.uid == uid))
|
||||
.map_or(0, |target| target.hookmask)
|
||||
}
|
||||
|
||||
fn zygisk_hook_enabled(hookmask: u32, hook: Hook) -> bool {
|
||||
|
|
@ -648,10 +658,7 @@ fn zygisk_hook_enabled(hookmask: u32, hook: Hook) -> bool {
|
|||
|
||||
/// Decode a `JString` (as stored in `AppSpecializeArgs::nice_name`) into
|
||||
/// an owned Rust `String`. Returns None on any failure.
|
||||
fn read_jstring<'a>(
|
||||
env: &JNIEnv<'a>,
|
||||
jstr: &zygisk_api::jni::objects::JString<'a>,
|
||||
) -> Option<String> {
|
||||
fn read_jstring<'a>(env: &JNIEnv<'a>, jstr: &JString<'a>) -> Option<String> {
|
||||
if jstr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -659,11 +666,11 @@ fn read_jstring<'a>(
|
|||
env_clone
|
||||
.get_string(jstr)
|
||||
.ok()
|
||||
.and_then(|s| s.to_str().ok().map(|s| s.to_string()))
|
||||
.and_then(|s| s.to_str().ok().map(str::to_owned))
|
||||
}
|
||||
|
||||
zygisk_api::register_module!(VpnHide);
|
||||
register_module!(VpnHide);
|
||||
|
||||
// Empty companion — declared so we don't have to recompile the .so if we
|
||||
// ever want to add a root-privileged helper later.
|
||||
zygisk_api::register_companion!(|_| ());
|
||||
register_companion!(|_| ());
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//! is linked as a static archive (`libshadowhook.a`) built for
|
||||
//! `aarch64-linux-android`; see `build.rs`.
|
||||
|
||||
use core::ffi::{c_char, c_int, c_void};
|
||||
use core::ffi::{CStr, c_char, c_int, c_void};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// `SHADOWHOOK_MODE_UNIQUE` from shadowhook.h. We never use shared mode, so a
|
||||
|
|
@ -54,8 +54,8 @@ pub fn init_once() -> Result<(), c_int> {
|
|||
/// `new_fn` must be a valid function pointer with a signature ABI-compatible
|
||||
/// with the real target symbol. `out_orig` must be a valid writable pointer.
|
||||
pub unsafe fn hook_sym(
|
||||
lib: &core::ffi::CStr,
|
||||
sym: &core::ffi::CStr,
|
||||
lib: &CStr,
|
||||
sym: &CStr,
|
||||
new_fn: *mut c_void,
|
||||
out_orig: *mut *mut c_void,
|
||||
) -> *mut c_void {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue