/* proc_monitor.cpp - Monitor am_proc_start events and unmount * * We monitor the listed APK files from /data/app until they get opened * via inotify to detect a new app launch. * * If it's a target we pause it ASAP, and fork a new process to join * its mount namespace and do all the unmounting/mocking. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "magiskhide.h" using namespace std; extern char *system_block, *vendor_block, *data_block; static int inotify_fd = -1; static void term_thread(int sig = SIGTERMTHRD); static void new_zygote(int pid); /************************ * All the maps and sets ************************/ set> hide_set; /* set of pair */ static map zygote_map; /* zygote pid -> mnt ns */ static map> uid_proc_map; /* uid -> list of process */ pthread_mutex_t monitor_lock; static set attaches; /* A set of pid that should be attached */ static set detaches; /* A set of tid that should be detached */ static set unknown; /* A set of pid/tid that is unknown */ /******** * Utils ********/ static inline int read_ns(const int pid, struct stat *st) { char path[32]; sprintf(path, "/proc/%d/ns/mnt", pid); return stat(path, st); } static inline void lazy_unmount(const char* mountpoint) { if (umount2(mountpoint, MNT_DETACH) != -1) LOGD("hide_daemon: Unmounted (%s)\n", mountpoint); } static int parse_ppid(int pid) { char path[32]; int ppid; sprintf(path, "/proc/%d/stat", pid); FILE *stat = fopen(path, "re"); if (stat == nullptr) return -1; /* PID COMM STATE PPID ..... */ fscanf(stat, "%*d %*s %*c %d", &ppid); fclose(stat); return ppid; } static long xptrace(bool log, int request, pid_t pid, void *addr, void *data) { long ret = ptrace(request, pid, addr, data); if (log && ret == -1) PLOGE("ptrace %d", pid); return ret; } static long xptrace(int request, pid_t pid, void *addr, void *data) { return xptrace(true, request, pid, addr, data); } static long xptrace(int request, pid_t pid, void *addr = nullptr, intptr_t data = 0) { return xptrace(true, request, pid, addr, reinterpret_cast(data)); } static bool parse_packages_xml(string_view s) { if (!str_starts(s, " */ char *start = (char *) s.data(); start[s.length() - 1] = '\0'; /* Remove trailing '>' */ start += 9; /* Skip ' bool { char buf[512]; snprintf(buf, sizeof(buf), "/proc/%d/cmdline", pid); FILE *f = fopen(buf, "re"); if (f) { fgets(buf, sizeof(buf), f); if (strncmp(buf, "zygote", 6) == 0 && parse_ppid(pid) == 1) new_zygote(pid); fclose(f); } return true; }); } } void *update_uid_map(void*) { MutexGuard lock(monitor_lock); uid_proc_map.clear(); file_readline("/data/system/packages.xml", parse_packages_xml, true); return nullptr; } /************************* * The actual hide daemon *************************/ static void hide_daemon(int pid) { RunFinally fin([=]() -> void { // Send resume signal kill(pid, SIGCONT); _exit(0); }); if (switch_mnt_ns(pid)) return; LOGD("hide_daemon: handling PID=[%d]\n", pid); manage_selinux(); clean_magisk_props(); vector targets; // Unmount dummy skeletons and /sbin links file_readline("/proc/self/mounts", [&](string_view s) -> bool { if (str_contains(s, "tmpfs /system/") || str_contains(s, "tmpfs /vendor/") || str_contains(s, "tmpfs /sbin")) { char *path = (char *) s.data(); // Skip first token strtok_r(nullptr, " ", &path); targets.emplace_back(strtok_r(nullptr, " ", &path)); } return true; }); for (auto &s : targets) lazy_unmount(s.data()); targets.clear(); // Unmount everything under /system, /vendor, and data mounts file_readline("/proc/self/mounts", [&](string_view s) -> bool { if ((str_contains(s, " /system/") || str_contains(s, " /vendor/")) && (str_contains(s, system_block) || str_contains(s, vendor_block) || str_contains(s, data_block))) { char *path = (char *) s.data(); // Skip first token strtok_r(nullptr, " ", &path); targets.emplace_back(strtok_r(nullptr, " ", &path)); } return true; }); for (auto &s : targets) lazy_unmount(s.data()); } /************************ * Async signal handlers ************************/ static void inotify_event(int) { /* Make sure we can actually read stuffs * or else the whole thread will be blocked.*/ struct pollfd pfd = { .fd = inotify_fd, .events = POLLIN, .revents = 0 }; if (poll(&pfd, 1, 0) <= 0) return; // Nothing to read char buf[512]; auto event = reinterpret_cast(buf); read(inotify_fd, buf, sizeof(buf)); if ((event->mask & IN_CLOSE_WRITE) && strcmp(event->name, "packages.xml") == 0) { LOGD("proc_monitor: /data/system/packages.xml updated\n"); new_daemon_thread(update_uid_map); } } // Workaround for the lack of pthread_cancel static void term_thread(int) { LOGD("proc_monitor: cleaning up\n"); // Clear maps uid_proc_map.clear(); zygote_map.clear(); // Clear sets hide_set.clear(); attaches.clear(); detaches.clear(); unknown.clear(); // Misc hide_enabled = false; pthread_mutex_destroy(&monitor_lock); close(inotify_fd); inotify_fd = -1; LOGD("proc_monitor: terminate\n"); pthread_exit(nullptr); } /****************** * Ptrace Madness ******************/ /* Ptrace is super tricky, preserve all excessive debug in code * but disable when actually building for usage (you won't want * your logcat spammed with new thread events from all apps) */ //#define PTRACE_LOG(fmt, args...) LOGD("PID=[%d] " fmt, pid, ##args) #define PTRACE_LOG(...) static bool check_pid(int pid) { char path[128]; char cmdline[1024]; sprintf(path, "/proc/%d/cmdline", pid); FILE *f = fopen(path, "re"); // Process killed unexpectedly, ignore if (!f) return true; fgets(cmdline, sizeof(cmdline), f); fclose(f); if (strncmp(cmdline, "zygote", 6) == 0) return false; /* This process is fully initialized, we will stop * tracing it no matter if it is a target or not. */ attaches.erase(pid); sprintf(path, "/proc/%d", pid); struct stat st; lstat(path, &st); int uid = st.st_uid % 100000; auto it = uid_proc_map.find(uid); if (it != uid_proc_map.end()) { for (auto &s : it->second) { if (s == cmdline) { // Double check whether ns is separated read_ns(pid, &st); bool mnt_ns = true; for (auto &zit : zygote_map) { if (zit.second.st_ino == st.st_ino && zit.second.st_dev == st.st_dev) { mnt_ns = false; break; } } // For some reason ns is not separated, abort if (!mnt_ns) break; /* Finally this is our target! * Detach from ptrace but should still remain stopped. * The hide daemon will resume the process. */ xptrace(PTRACE_DETACH, pid, nullptr, SIGSTOP); LOGI("proc_monitor: [%s] PID=[%d] UID=[%d]\n", cmdline, pid, uid); PTRACE_LOG("target found\n"); if (fork_dont_care() == 0) hide_daemon(pid); return true; } } } PTRACE_LOG("not our target\n"); xptrace(PTRACE_DETACH, pid); return true; } static void handle_unknown(int tid, int pid = -1) { if (unknown.count(tid)) { unknown.erase(tid); tgkill(pid < 0 ? tid : pid, tid, SIGSTOP); } } static void new_zygote(int pid) { if (zygote_map.count(pid)) return; LOGD("proc_monitor: ptrace zygote PID=[%d]\n", pid); struct stat st; if (read_ns(pid, &st)) return; zygote_map[pid] = st; xptrace(PTRACE_ATTACH, pid); waitpid(pid, nullptr, __WALL | __WNOTHREAD); xptrace(PTRACE_SETOPTIONS, pid, nullptr, PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | PTRACE_O_TRACEEXIT); xptrace(PTRACE_CONT, pid); } void proc_monitor() { inotify_fd = xinotify_init1(IN_CLOEXEC); if (inotify_fd < 0) term_thread(); // Unblock some signals sigset_t block_set; sigemptyset(&block_set); sigaddset(&block_set, SIGTERMTHRD); sigaddset(&block_set, SIGIO); pthread_sigmask(SIG_UNBLOCK, &block_set, nullptr); struct sigaction act{}; act.sa_handler = term_thread; sigaction(SIGTERMTHRD, &act, nullptr); act.sa_handler = inotify_event; sigaction(SIGIO, &act, nullptr); // Setup inotify asynchronous I/O fcntl(inotify_fd, F_SETFL, O_ASYNC); struct f_owner_ex ex = { .type = F_OWNER_TID, .pid = gettid() }; fcntl(inotify_fd, F_SETOWN_EX, &ex); // Start monitoring packages.xml inotify_add_watch(inotify_fd, "/data/system", IN_CLOSE_WRITE); // First find existing zygotes check_zygote(); int status; for (;;) { int pid = waitpid(-1, &status, __WALL | __WNOTHREAD); if (pid < 0) continue; if (WIFSTOPPED(status)) { if (detaches.count(pid)) { PTRACE_LOG("detach\n"); detaches.erase(pid); xptrace(PTRACE_DETACH, pid); continue; } if (WSTOPSIG(status) == SIGTRAP && WEVENT(status)) { unsigned long msg; xptrace(PTRACE_GETEVENTMSG, pid, nullptr, &msg); if (zygote_map.count(pid)) { // Zygote event switch (WEVENT(status)) { case PTRACE_EVENT_FORK: case PTRACE_EVENT_VFORK: PTRACE_LOG("zygote forked: [%d]\n", msg); attaches.insert(msg); handle_unknown(msg); break; case PTRACE_EVENT_EXIT: PTRACE_LOG("zygote exited with status: [%d]\n", msg); zygote_map.erase(pid); break; default: PTRACE_LOG("unknown event: %d\n", WEVENT(status)); break; } xptrace(PTRACE_CONT, pid); } else { switch (WEVENT(status)) { case PTRACE_EVENT_CLONE: PTRACE_LOG("create new threads: [%d]\n", msg); detaches.insert(msg); handle_unknown(msg, pid); if (attaches.count(pid) && check_pid(pid)) continue; break; case PTRACE_EVENT_EXIT: PTRACE_LOG("exited with status: [%d]\n", msg); attaches.erase(pid); unknown.erase(pid); break; default: PTRACE_LOG("unknown event: %d\n", WEVENT(status)); break; } xptrace(PTRACE_CONT, pid); } } else if (WSTOPSIG(status) == SIGSTOP) { if (attaches.count(pid)) { PTRACE_LOG("SIGSTOP from zygote child\n"); xptrace(PTRACE_SETOPTIONS, pid, nullptr, PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXIT); } else { PTRACE_LOG("SIGSTOP from unknown\n"); unknown.insert(pid); } xptrace(PTRACE_CONT, pid); } else { // Not caused by us, resend signal xptrace(PTRACE_CONT, pid, nullptr, WSTOPSIG(status)); PTRACE_LOG("signal [%d]\n", WSTOPSIG(status)); } } else { // Nothing to do with us ptrace(PTRACE_DETACH, pid); PTRACE_LOG("terminate\n"); } } }