/* 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 #include #include "magiskhide.h" using namespace std; extern char *system_block, *vendor_block, *data_block; static int inotify_fd = -1; // Workaround for the lack of pthread_cancel static void term_thread(int) { LOGD("proc_monitor: running cleanup\n"); hide_list.clear(); hide_uid.clear(); hide_enabled = false; pthread_mutex_destroy(&list_lock); close(inotify_fd); inotify_fd = -1; LOGD("proc_monitor: terminating\n"); pthread_exit(nullptr); } 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); } /* APK monitoring doesn't seem to require checking namespace * separation from PPID. Preserve this function just in case */ #if 0 static inline int parse_ppid(const 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; } #endif static bool is_pid_safetynet_process(const int pid) { char path[32]; char buf[64]; int fd; ssize_t len; sprintf(path, "/proc/%d/cmdline", pid); fd = open(path, O_RDONLY); if (fd == -1) return false; len = read(fd, buf, sizeof(buf)); close(fd); if (len == -1) return false; return !strcmp(buf, SAFETYNET_PROCESS); } static void hide_daemon(int pid) { LOGD("hide_daemon: handling PID=[%d]\n", pid); char buffer[4096]; vector mounts; manage_selinux(); clean_magisk_props(); if (switch_mnt_ns(pid)) goto exit; snprintf(buffer, sizeof(buffer), "/proc/%d", pid); chdir(buffer); mounts = file_to_vector("mounts"); // Unmount dummy skeletons and /sbin links for (auto &s : mounts) { if (str_contains(s, "tmpfs /system/") || str_contains(s, "tmpfs /vendor/") || str_contains(s, "tmpfs /sbin")) { sscanf(s.c_str(), "%*s %4096s", buffer); lazy_unmount(buffer); } } // Re-read mount infos mounts = file_to_vector("mounts"); // Unmount everything under /system, /vendor, and data mounts for (auto &s : mounts) { if ((str_contains(s, " /system/") || str_contains(s, " /vendor/")) && (str_contains(s, system_block) || str_contains(s, vendor_block) || str_contains(s, data_block))) { sscanf(s.c_str(), "%*s %4096s", buffer); lazy_unmount(buffer); } } exit: // Send resume signal kill(pid, SIGCONT); _exit(0); } // A mapping from pid to namespace inode to avoid time-consuming GC static map pid_ns_map; static bool process_pid(int pid) { // We're only interested in PIDs > 1000 if (pid <= 1000) return true; struct stat ns; int uid = get_uid(pid); if (hide_uid.count(uid)) { // Make sure we can read mount namespace if (read_ns(pid, &ns)) return true; // Check if it's a process we haven't already hijacked auto pos = pid_ns_map.find(pid); if (pos != pid_ns_map.end() && pos->second == ns.st_ino) return true; if (uid == gms_uid) { // Check /proc/uid/cmdline to see if it's SAFETYNET_PROCESS if (!is_pid_safetynet_process(pid)) return true; LOGD("proc_monitor: " SAFETYNET_PROCESS "\n"); } // Send pause signal ASAP if (kill(pid, SIGSTOP) == -1) return true; pid_ns_map[pid] = ns.st_ino; LOGI("proc_monitor: UID=[%d] PID=[%d] ns=[%llu]\n", uid, pid, ns.st_ino); /* * The setns system call do not support multithread processes * We have to fork a new process, setns, then do the unmounts */ if (fork_dont_care() == 0) hide_daemon(pid); } return true; } static int xinotify_add_watch(int fd, const char* path, uint32_t mask) { int ret = inotify_add_watch(fd, path, mask); if (ret >= 0) { LOGD("proc_monitor: Monitoring %s\n", path); } else { PLOGE("proc_monitor: Monitor %s", path); } return ret; } static char *append_path(char *eof, const char *name) { *(eof++) = '/'; char c; while ((c = *(name++))) *(eof++) = c; *eof = '\0'; return eof; } #define DATA_APP "/data/app" static int new_inotify; static int data_app_wd; static vector app_in_data; static void find_apks(char *path, char *eof) { DIR *dir = opendir(path); if (dir == nullptr) return; struct dirent *entry; char *dash; for (; (entry = xreaddir(dir)); *eof = '\0') { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; if (entry->d_type == DT_DIR) { find_apks(path, append_path(eof, entry->d_name)); } else if (strend(entry->d_name, ".apk") == 0) { append_path(eof, entry->d_name); /* Supported path will be in either format: * /data/app/[pkg]-[hash or 1 or 2]/base.apk * /data/app/[pkg]-[1 or 2].apk */ if ((dash = strchr(path, '-')) == nullptr) continue; *dash = '\0'; for (int i = 0; i < hide_list.size(); ++i) { if (hide_list[i] == path + sizeof(DATA_APP)) { *dash = '-'; append_path(eof, entry->d_name); xinotify_add_watch(new_inotify, path, IN_OPEN | IN_DELETE); app_in_data[i] = true; break; } } *dash = '-'; break; } } closedir(dir); } // Iterate through /data/app and search all .apk files void update_inotify_mask(bool refresh) { char buf[4096]; new_inotify = inotify_init(); if (new_inotify < 0) { LOGE("proc_monitor: Cannot initialize inotify: %s\n", strerror(errno)); term_thread(TERM_THREAD); } LOGD("proc_monitor: Updating inotify list\n"); strcpy(buf, DATA_APP); app_in_data.clear(); bool reinstall = false; { MutexGuard lock(list_lock); app_in_data.resize(hide_list.size(), false); find_apks(buf, buf + sizeof(DATA_APP) - 1); // Stop monitoring /data/app if (inotify_fd >= 0) inotify_rm_watch(inotify_fd, data_app_wd); // All apps on the hide list should be installed in data auto it = hide_list.begin(); for (bool in_data : app_in_data) { if (!in_data) { if (reinstall_apk(it->c_str()) != 0) { // Reinstallation failed, remove from hide list hide_list.erase(it); refresh = true; continue; } reinstall = true; } it++; } if (refresh && !reinstall) refresh_uid(); } if (reinstall) { // Rerun detection close(new_inotify); update_inotify_mask(refresh); return; } // Add /data/app itself to the watch list to detect app (un)installations/updates data_app_wd = xinotify_add_watch(new_inotify, DATA_APP, IN_CLOSE_WRITE | IN_MOVED_TO | IN_DELETE); int tmp = inotify_fd; inotify_fd = new_inotify; if (tmp >= 0) close(tmp); } void proc_monitor() { // Unblock user signals sigset_t block_set; sigemptyset(&block_set); sigaddset(&block_set, TERM_THREAD); pthread_sigmask(SIG_UNBLOCK, &block_set, nullptr); // Register the cancel signal struct sigaction act{}; act.sa_handler = term_thread; sigaction(TERM_THREAD, &act, nullptr); // Read inotify events ssize_t len; char buf[4096]; auto event = reinterpret_cast(buf); while ((len = read(inotify_fd, buf, sizeof(buf))) >= 0) { if (len < sizeof(*event)) continue; if (event->mask & IN_OPEN) { // Since we're just watching files, // extracting file name is not possible from querying event MutexGuard lock(list_lock); crawl_procfs(process_pid); } else if (!(event->mask & IN_IGNORED)) { LOGD("proc_monitor: inotify: /data/app change detected\n"); update_inotify_mask(true); } } PLOGE("proc_monitor: read inotify"); }