mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-05-20 09:03:10 +00:00
Four small touches across zygisk and lsposed/native, all on adversarial-
or kernel-edge-case paths. Happy paths and the per-app filter behaviour
are unchanged.
zygisk hooks: recvmsg multi-iov no longer bypasses the filter
Old code bailed out as soon as `msg_iovlen != 1`. A caller could
hand recvmsg an iov array with one extra zero-length iov to skip
filtering entirely. Now we filter the portion of `ret` that landed
in iov[0] (recvmsg fills iovs in order) and propagate any shrink
back into the returned byte count. Bytes that fell into iov[1..]
pass through — bionic only ever uses iov[0], so legitimate dumps
are unaffected; an attacker who deliberately splits a netlink
message across iovs at least loses everything that landed in
iov[0], which is strictly better than the previous bypass.
zygisk install_hooks: roll back partial installs on error
install_hooks() walks five libc symbols and `?`-propagates the
first failure. Previously, if hook 1-4 succeeded and hook 5 failed,
the process kept hooks 1-4 in place — the app would see filtered
ioctl/getifaddrs/openat/recvmsg but unfiltered recv, a torn plan
that's worse than no install at all. Now we collect each
shadowhook stub as it installs and, on the first failure, walk
back through them in LIFO order calling shadowhook_unhook before
surfacing the original error. Added a thin `shadowhook::unhook`
wrapper so the FFI call site stays out of lib.rs.
zygisk hooks: document the TOCTOU window in is_dirfd_proc_net
match_rel_proc_net resolves dirfd via readlink before the open,
so a caller who races dup2/fchdir between the two can pass the
classifier and have the open land elsewhere. This is accepted
exposure (caller-controlled self-DoS, real detectors don't go
through dirfd anyway) — added a comment so the next reader
doesn't think it's an oversight.
lsposed/native for_each_rtattr: pass bounds-checked payload slice
Old callbacks computed `b.as_ptr().add(rta_off + 4)` and read
payload from there. If a kernel response set rta_len == 4
(header only, zero payload), the read would pass the rtattr
loop check (`if rta.rta_len < 4 { break }`) and then access
bytes belonging to the next attr or past the message end. Now
for_each_rtattr verifies `off + rta_len <= end` and yields a
`&[u8]` payload slice; callbacks check its length before
reading (e.g. `payload.len() >= 4` for an i32 ifindex).
Replaces the `*const i32` read with a safe `i32::from_ne_bytes`.
Verified on Pixel 8 Pro (husky, android14-6.1, Android 16):
Enforcing : 26/26 PASS, COLD start ~1.9 s.
Permissive : 22/26 PASS — same four by-design FAILs as before
(proc_dev / sys_class_net / proc_ipv6_route /
proc_if_inet6). No regression in netlink_getlink,
netlink_getroute, getifaddrs, ioctl_*, proc_route,
proc_fib_trie. zygisk and APK-native md5 match local
build artifacts.
|
||
|---|---|---|
| .. | ||
| src | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||