KernelSU Umount 检测

背景

KernelSU 安装任意模块后,会报两个检测点:umount 和 loopdev。其中 loopdev 检测叔叔已在 #2225 中修复,因此本文主要关注 umount 检测:

检测

检测工具报告 /debug_ramdisk 发生了泄漏。尝试两个分析方向:

  • /proc 下文件泄露,初步排查并非此处泄露
  • 内存中泄漏,编写了一个内存扫描工具:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
fn main() -> Result<()> {
let iter = process::all_processes()?;

for process in iter {
let process = attempt!(process);
let stat = process.stat()?;

if stat.comm != "com.termux" {
continue
}

let maps = process.maps()?;
let finder = Finder::new(b"debug_ramdisk");


maps.into_iter().for_each(|mmap| {
if (mmap.perms & MMPermissions::READ) == MMPermissions::empty() {
return
}

let address = mmap.address;
let mut buffer = vec![0u8; (address.1 - address.0) as usize];

let iov_local = IoSliceMut::new(&mut buffer);
let iov_remote = RemoteIoVec { base: address.0 as usize, len: (address.1 - address.0) as usize };

if let Err(err) = uio::process_vm_readv(Pid::from_raw(process.pid), &mut [iov_local], &[iov_remote]) {
println!("failed to read {:0>12x}-{:0>12x} {:?}: {:?}", address.0, address.1, mmap.pathname, err);
return
}

if finder.find(&buffer).is_some() {
println!("found in {mmap:?}");
fs::write(format!("/sdcard/Home/memchr/{:0>12x}-{:0>12x}", address.0, address.1), &buffer).expect("failed to write");
}
})
}

Ok(())
}

扫描后发现确实存在泄漏,将这块内存 hexdump 出来查看:

1
found in MemoryMap { address: (512878698496, 512878714880), perms: MMPermissions(READ | WRITE | PRIVATE), offset: 0, dev: (0, 0), inode: 0, pathname: Other("anon:stack_and_tls:main"), extension: MMapExtension { map: {}, vm_flags: VmFlags(0x0) } }

从结构判断像是 /proc/<pid>/mounts 的内容,考虑到这个检测点大概率是从 zygote 进程泄漏的,于是挂上 eBPF 程序,在 uid < 10000 的进程访问 /proc/mounts 时直接喂一个 signal 35 给它,从而获取到调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
backtrace:
#00 pc 00000000000b877a /apex/com.android.runtime/lib64/bionic/libc.so (__openat+10) (BuildId: fa337969c798946280caa45e2d71a2e7)
#01 pc 000000000006ce91 /apex/com.android.runtime/lib64/bionic/libc.so (open64+257) (BuildId: fa337969c798946280caa45e2d71a2e7)
#02 pc 00000000000c4c4f /apex/com.android.runtime/lib64/bionic/libc.so (fopen64+79) (BuildId: fa337969c798946280caa45e2d71a2e7)
#03 pc 00000000001ed2ca /system/lib64/libandroid_runtime.so (android::com_android_internal_os_Zygote_nativeInitNativeState(_JNIEnv*, _jclass*, unsigned char)+378) (BuildId: 58f47cb2665b829aa74415f458f1922a)
#04 pc 00000000001f416b /system/framework/x86_64/boot-framework.oat (art_jni_trampoline+139) (BuildId: e4c2202f7e80276bcdbfa046249d896342205aa9)
#05 pc 00000000007f0749 /system/framework/x86_64/boot-framework.oat (com.android.internal.os.ZygoteInit.main+2617) (BuildId: e4c2202f7e80276bcdbfa046249d896342205aa9)
#06 pc 0000000000378826 /apex/com.android.art/lib64/libart.so (art_quick_invoke_static_stub+806) (BuildId: b6dc79e02101ea00827a35a55ab6597a)
#07 pc 00000000003c538f /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+255) (BuildId: b6dc79e02101ea00827a35a55ab6597a)
#08 pc 00000000007f129f /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeWithVarArgs<art::ArtMethod*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, art::ArtMethod*, __va_list_tag*)+399) (BuildId: b6dc79e02101ea00827a35a55ab6597a)
#09 pc 00000000006a7edc /apex/com.android.art/lib64/libart.so (art::JNI<true>::CallStaticVoidMethodV(_JNIEnv*, _jclass*, _jmethodID*, __va_list_tag*)+668) (BuildId: b6dc79e02101ea00827a35a55ab6597a)
#10 pc 00000000000df088 /system/lib64/libandroid_runtime.so (_JNIEnv::CallStaticVoidMethod(_jclass*, _jmethodID*, ...)+136) (BuildId: 58f47cb2665b829aa74415f458f1922a)
#11 pc 00000000000ebb30 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::start(char const*, android::Vector<android::String8> const&, bool)+896) (BuildId: 58f47cb2665b829aa74415f458f1922a)
#12 pc 0000000000002fa6 /system/bin/app_process64 (main+1622) (BuildId: f11cda2b6bb6bff1e502077a2f3e6cf7)
#13 pc 00000000000529ef /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+95) (BuildId: fa337969c798946280caa45e2d71a2e7)

查源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int UnmountTree(const char* path) {
ATRACE_CALL();

size_t path_len = strlen(path);

FILE* fp = setmntent("/proc/mounts", "r");
if (fp == nullptr) {
ALOGE("Error opening /proc/mounts: %s", strerror(errno));
return -errno;
}

// Some volumes can be stacked on each other, so force unmount in
// reverse order to give us the best chance of success.
std::list<std::string> to_unmount;
mntent* mentry;
while ((mentry = getmntent(fp)) != nullptr) {
if (strncmp(mentry->mnt_dir, path, path_len) == 0) {
to_unmount.push_front(std::string(mentry->mnt_dir));
}
}
endmntent(fp);

for (const auto& path : to_unmount) {
if (umount2(path.c_str(), MNT_DETACH)) {
ALOGW("Failed to unmount %s: %s", path.c_str(), strerror(errno));
}
}
return 0;
}

中间两层 static 函数可能被内联,因此调用栈上只看到 nativeInitNativeState,符合预期。接下来分析内存泄漏的具体位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
mntent* getmntent(FILE* fp) {
auto& tls = __get_bionic_tls();
return getmntent_r(fp, &tls.mntent_buf, tls.mntent_strings, sizeof(tls.mntent_strings));
}

mntent* getmntent_r(FILE* fp, struct mntent* e, char* buf, int buf_len) {
memset(e, 0, sizeof(*e));
while (fgets(buf, buf_len, fp) != nullptr) {
// Entries look like "proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0".
// That is: mnt_fsname mnt_dir mnt_type mnt_opts 0 0.
int fsname0, fsname1, dir0, dir1, type0, type1, opts0, opts1;
if (sscanf(buf, " %n%*s%n %n%*s%n %n%*s%n %n%*s%n %d %d",
&fsname0, &fsname1, &dir0, &dir1, &type0, &type1, &opts0, &opts1,
&e->mnt_freq, &e->mnt_passno) == 2) {
e->mnt_fsname = &buf[fsname0];
buf[fsname1] = '\0';

e->mnt_dir = &buf[dir0];
buf[dir1] = '\0';

e->mnt_type = &buf[type0];
buf[type1] = '\0';

e->mnt_opts = &buf[opts0];
buf[opts1] = '\0';

return e;
}
}
return nullptr;
}

最终定位到 getmntent_r,该函数会读取数据到 tls.mntent_strings,泄漏点就在此处。

隐藏

考虑在 post-fs-data 阶段先读取一份干净的 tls.mntent_strings,待 zygote 启动或 fork 后,再将这份干净的缓冲区拷贝进去

也可以自己维护一份干净的缓冲区,替换到目标进程里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
use crate::define_impl;
use crate::hide::Nao;
use crate::utils::ptrace::Tracee;
use anyhow::{bail, Context, Error, Result};
use common_utils::debug_on;
use common_utils::ext::LogIfError;
use log::debug;
use nix::libc::{c_char, c_int, iovec, uintptr_t, PTRACE_GETREGSET};
use once_cell::sync::OnceCell;
use std::arch::asm;
use std::ffi::c_void;
use std::mem::offset_of;
use std::{fs, result};

const TLS_SLOT_BIONIC_TLS: isize = -1;

// https://cs.android.com/android/platform/superproject/main/+/main:bionic/libc/private/bionic_tls.h;l=120;drc=61197364367c9e404c7da6900658f1b16c42d0da
#[repr(C)]
#[allow(non_camel_case_types)]
struct pthread_key_data_t {
_seq: uintptr_t,
_data: *const c_void,
}

#[repr(C)]
#[allow(non_camel_case_types)]
struct mntent {
_mnt_fstype: *const c_char,
_mnt_dir: *const c_char,
_mnt_types: *const c_char,
_mnt_opts: *const c_char,
_mnt_freq: c_int,
_mnt_passno: c_int,
}

#[repr(C)]
#[allow(non_camel_case_types)]
struct bionic_tls {
_key_data: [pthread_key_data_t; 130],
_locale: *const c_void,
_basename_buf: [u8; 4096],
_dirname_buf: [u8; 4096],
_mntent_buf: mntent,
mntent_strings: [u8; 1024],
}

#[derive(Default)]
struct CleanBuffer {
clean_buffer: OnceCell<Vec<u8>>,
}

extern "C" {
fn setmntent(filename: *const c_char, ty: *const c_char) -> *const c_void;
fn getmntent(stream: *const c_void) -> *mut mntent;
fn endmntent(stream: *const c_void) -> i32;
}

impl CleanBuffer {
fn dirty_buffer(&self) -> Result<Vec<u8>> {
unsafe {
let fp = setmntent(c"/proc/mounts".as_ptr(), c"r".as_ptr());
while !getmntent(fp).is_null() {}
endmntent(fp);
}

let tls: *const *const c_void;

unsafe {
asm!("mrs {0}, tpidr_el0", out(reg) tls);
}

let bionic_tls = unsafe { *tls.offset(TLS_SLOT_BIONIC_TLS) as *const bionic_tls };
let bionic_tls = unsafe { &*bionic_tls };

Ok(bionic_tls.mntent_strings.to_vec())
}

fn clean_buffer(&self) -> Result<Vec<u8>> {
let mut clean_buffer = [0u8; 1024];

let mounts = fs::read_to_string("/proc/mounts")?;

mounts.lines().for_each(|line| {
let parts: Vec<_> = line.split(' ').collect();

if parts[0] == "KSU" {
debug!("skip ksu mount: {line}");
return;
}

if parts[1].starts_with("/data/adb") {
debug!("skip module mount: {line}");
return;
}

clean_buffer[..line.len()].copy_from_slice(line.as_bytes());
clean_buffer[line.len()] = 0;

let mut index = 0;

#[allow(clippy::needless_range_loop)]
for i in 0..4 {
index += parts[i].len();
clean_buffer[index] = 0;
index += 1;
}
});

Ok(clean_buffer.to_vec())
}

fn buffer_to_string(&self, buffer: &[u8]) -> String {
let filtered: Vec<_> = buffer.iter().map(|ch| if ch.is_ascii_graphic() { *ch } else { b' ' }).collect();

String::from_utf8_lossy(&filtered).trim_ascii_end().into()
}

fn replace_remote_buffer(&self, tracee: &Tracee) -> Result<()> {
let mut tpidr_el0: usize = 0;
let iov = iovec {
iov_base: &mut tpidr_el0 as *mut _ as _,
iov_len: size_of_val(&tpidr_el0),
};

tracee.ptrace_raw(PTRACE_GETREGSET, 0x401 /* NT_ARM_TLS */, &iov as *const _ as _)?;

let bionic_tls_ptr = tpidr_el0 as isize + TLS_SLOT_BIONIC_TLS * size_of::<usize>() as isize;
let bionic_tls = tracee.peek(bionic_tls_ptr as usize)?;

let Some(buffer) = self.clean_buffer.get() else {
bail!("clean buffer is not ready");
};

if debug_on!("hide.mntent") {
debug!("{tracee} remote tls = 0x{tpidr_el0:0>12x}, bionic_tls = 0x{bionic_tls:0>12x}");
}

tracee.poke_data(bionic_tls + offset_of!(bionic_tls, mntent_strings), buffer)?;

Ok(())
}
}

impl Nao for CleanBuffer {
fn on_post_fs_data(&self) {
let result: result::Result<_, Error> = self.clean_buffer.get_or_try_init(|| {
let clean_buffer = self.clean_buffer()?;

if debug_on!("hide.mntent") {
debug!("mntent strings (dirty): {}", self.buffer_to_string(&self.dirty_buffer()?));
debug!("mntent strings (clean): {}", self.buffer_to_string(&clean_buffer));
}

Ok(clean_buffer)
});

result.context("failed to handle mntent buffer").log_if_error()
}

fn on_embryo_start(&self, embryo: &Tracee) {
self.replace_remote_buffer(embryo)
.context("failed to replace remote buffer")
.log_if_error();
}
}

define_impl!(CleanBuffer);

KernelSU Umount 检测
https://neo.mufanc.xyz/posts/13133/
作者
Mufanc
发布于
2025年6月21日
许可协议