Skip to main content

One post tagged with "root检测"

View All Tags

· 10 min read
老陈

前言

当手机安装magisk后, 全局的挂载空间会受到变更, magisk给我们挂载上了一个su二进制, 这就是我们能够访问到su命令的原因 无论是Magisk hide还是Denylist, 我们都可以将它们的工作分成两个部分, 第一个部分是如何监控安卓进程的启动, 第二部分是在安卓进程启动(fork)之后, 尽快移除已经“污染”的挂载空间

ROOT隐藏关乎到magisk的核心原理mount 我们可以从linux内核获取到mount的相关信息

全局挂载空间

/proc/mounts /proc/mountinfo /proc/mountstats

进程的挂载空间

/proc/pid/mounts

如果我们成功刷入magisk, 以下命令将会有大量的挂载信息输出

cat /proc/mounts |grep magisk

在linux系统中, mount有好多种 我们注意关注的是“mount --bind” mount --bind命令来将两个目录连接起来, mount --bind命令是将前一个目录挂载到后一个目录上, 所有对后一个目录的访问其实都是对前一个目录的访问 就是说, 我们可以给文件目录戴上一个“面具”, 应用程序访问文件, 首先访问到的是“面具层”, 而不是真正的文件。

程序由数据和代码组成, 但是, 我们也可以说, 数据和代码都是基于“文件”, 因此对文件的改动, 就能改变程序的运行~ 所以我们可以面具实现许许多多的黑科技

在root下的程序可以切换到任意进程的命名空间, 进行mount(umount)操作

如果我们使用momo检测, 我们可以发现magisk这套检测并非运行得很完美

  1. magisk hide 的ptrace容易被检测到
  2. magisk hide 无法对isolated_zygote进程处理, 因为isolated_zygote与zygote进程共享挂载空间, 如果对isolated_zygote进程进行处理, 那么后面所有打开的app无法访问到su, 即无法获取到root权限。因此, 出现一个名为riru_unshare的插件可以使用unshare函数指定独立进程不与zygote共享命名空间。
  3. 还有一些可以绕过早期magisk hide(18)的方法, 就是自定义安卓的进程名后面加两个.., 如微信的“:hotpot..”进程会被magisk_hide忽略为无效的隐藏进程而被magisk hide忽视。

最常见检测安卓设备是否已ROOT的方法

一种最常见检测设备是否已root的方法
public static boolean isDeviceRooted() {
String[] paths = {
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin/su"
};

for (String path : paths) {
if (new File(path).exists()) {
return true;
}
}

return false;
}

上述方法在magisk时代下, 几乎毫无作用的, 因为我们可以使用面具随机地让某个指定程序看到哪些文件, 看不到哪些文件。

magisk hide/denylist 的源码分析

在magisk 23版本之前, 使用magisk hide进行隐藏, magisk hide使用ptrace监控进程启动, 如果是zygote进程且非isolated_zygote进程, 则暂停进程, 进行umount操作, 再继续运行进程

Part1 监控安卓进程的启动
Magisk/native/jni/magiskhide/proc_monitor.cpp
void proc_monitor() {
monitor_thread = pthread_self();

// *******
setup_inotify();

// First try find existing zygotes
check_zygote();
if (!is_zygote_done()) {
// Periodic scan every 250ms
timeval val { .tv_sec = 0, .tv_usec = 250000 };
itimerval interval { .it_interval = val, .it_value = val };
setitimer(ITIMER_REAL, &interval, nullptr);
}

for (int status;;) {
pthread_sigmask(SIG_UNBLOCK, &unblock_set, nullptr);

const int pid = waitpid(-1, &status, __WALL | __WNOTHREAD);
if (pid < 0) {
if (errno == ECHILD) {
// Nothing to wait yet, sleep and wait till signal interruption
LOGD("proc_monitor: nothing to monitor, wait for signal\n");
struct timespec ts = {
.tv_sec = INT_MAX,
.tv_nsec = 0
};
nanosleep(&ts, nullptr);
}
continue;
}

pthread_sigmask(SIG_SETMASK, &orig_mask, nullptr);

if (!WIFSTOPPED(status) /* Ignore if not ptrace-stop */)
DETACH_AND_CONT;

int event = WEVENT(status);
int signal = WSTOPSIG(status);

if (signal == SIGTRAP && event) {
unsigned long msg;
xptrace(PTRACE_GETEVENTMSG, pid, nullptr, &msg);
if (zygote_map.count(pid)) {
// Zygote event
switch (event) {
case PTRACE_EVENT_FORK:
case PTRACE_EVENT_VFORK:
PTRACE_LOG("zygote forked: [%lu]\n", msg);
attaches[msg] = true;
break;
case PTRACE_EVENT_EXIT:
PTRACE_LOG("zygote exited with status: [%lu]\n", msg);
[[fallthrough]];
default:
zygote_map.erase(pid);
DETACH_AND_CONT;
}
} else {
switch (event) {
case PTRACE_EVENT_CLONE:
PTRACE_LOG("create new threads: [%lu]\n", msg);
if (attaches[pid] && check_pid(pid))
continue;
break;
case PTRACE_EVENT_EXEC:
case PTRACE_EVENT_EXIT:
PTRACE_LOG("exit or execve\n");
[[fallthrough]];
default:
DETACH_AND_CONT;
}
}
xptrace(PTRACE_CONT, pid);
} else if (signal == SIGSTOP) {
if (!attaches[pid]) {
// Double check if this is actually a process
attaches[pid] = is_process(pid);
}
if (attaches[pid]) {
// This is a process, continue monitoring
PTRACE_LOG("SIGSTOP from child\n");
xptrace(PTRACE_SETOPTIONS, pid, nullptr,
PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXEC | PTRACE_O_TRACEEXIT);
xptrace(PTRACE_CONT, pid);
} else {
// This is a thread, do NOT monitor
PTRACE_LOG("SIGSTOP from thread\n");
DETACH_AND_CONT;
}
} else {
// Not caused by us, resend signal
xptrace(PTRACE_CONT, pid, nullptr, signal);
PTRACE_LOG("signal [%d]\n", signal);
}
}
Part2 移除已经污染对挂载空间
Magisk/native/jni/magiskhide/proc_monitor.cpp
static bool check_pid(int pid) {
char path[128];
char cmdline[1024];
struct stat st;

sprintf(path, "/proc/%d", pid);
if (stat(path, &st)) {
// Process died unexpectedly, ignore
detach_pid(pid);
return true;
}

int uid = st.st_uid;

// UID hasn't changed
if (uid == 0)
return false;

sprintf(path, "/proc/%d/cmdline", pid);
if (auto f = open_file(path, "re")) {
fgets(cmdline, sizeof(cmdline), f.get());
} else {
// Process died unexpectedly, ignore
detach_pid(pid);
return true;
}

if (cmdline == "zygote"sv || cmdline == "zygote32"sv || cmdline == "zygote64"sv ||
cmdline == "usap32"sv || cmdline == "usap64"sv)
return false;

if (!is_hide_target(uid, cmdline, 95))
goto not_target;

// Ensure ns is separated
read_ns(pid, &st);
for (auto &zit : zygote_map) {
if (zit.second.st_ino == st.st_ino &&
zit.second.st_dev == st.st_dev) {
// ns not separated, abort
LOGW("proc_monitor: skip [%s] PID=[%d] UID=[%d]\n", cmdline, pid, uid);
goto not_target;
}
}

// Detach but the process should still remain stopped
// The hide daemon will resume the process after hiding it
LOGI("proc_monitor: [%s] PID=[%d] UID=[%d]\n", cmdline, pid, uid);
detach_pid(pid, SIGSTOP);
hide_daemon(pid); // 对指定pid进程进行隐藏
return true;

not_target:
PTRACE_LOG("[%s] is not our target\n", cmdline);
detach_pid(pid);
return true;
}

在magisk 25版本之后, 使用denylist, 如果你要使用denylist, 那需要先需要开启zygote, 因为denylist通过hook zygote的fork函数获知进程的启动。 denylist与magisk hide也是一样, 都是对污染的命名空间进行卸载, 但有一点不同, denylist是在自身的进程执行卸载代码, 而magisk_hide在magiskd进程进行操作, 需要先使用switch_to_ns切换到目标进程的命名空间。

/Users/caz/AndroidProject/Magisk25.2/Magisk/native/src/zygisk/hook.cpp
// Unmount stuffs in the process's private mount namespace
DCL_HOOK_FUNC(int, unshare, int flags) {
int res = old_unshare(flags);
if (g_ctx && (flags & CLONE_NEWNS) != 0 && res == 0 &&
// For some unknown reason, unmounting app_process in SysUI can break.
// This is reproducible on the official AVD running API 26 and 27.
// Simply avoid doing any unmounts for SysUI to avoid potential issues.
g_ctx->process && g_ctx->process != "com.android.systemui"sv) {
if (g_ctx->flags[DO_REVERT_UNMOUNT]) {
revert_unmount();
} else {
umount2("/system/bin/app_process64", MNT_DETACH);
umount2("/system/bin/app_process32", MNT_DETACH);
}
// Restore errno back to 0
errno = 0;
}
return res;
}

Yyds.Msu 的设计

Yyds.Msu与magisk hide反其道而行之。大道至简, 尽可能让方案更加简单才能更稳定。

  1. Yyds.Msu 是先卸载, 默认应用是没有污染命名空间的, 也就是说, 直接对zygote以及zygote64进程进行命名空间卸载。
  2. Yyds.Msu 在不使用zygote hook与ptrace的情况下, 是无法监听其它app进程的启动的, 但可以想办法监听到yyds.msu管理apk的启动, 那就是inotify, 我们只需要让yyds.msu管理apk在Application时候创建一个文件, 即可让我们魔改过的magiskd知晓到并进行挂载。
  3. 缺点是我们无法让其它app打开即进行挂载获得到root权限, 在不进行任何hook的情况下除非我们死循环地地扫描/proc目录。
  4. 在Yyds.Msu中我们手动使用超级运行对指定进程直接挂载root权限以及相关magisk bin. 对于多进程的app, 我们不能只给其中一条进程进行挂载, 为了兼容性, 我们直接对zygote进程挂载root权限并在app启动成功后恢复zygote的正常挂载, 这个就是超级运行的兼容模式。