From fba3cf971dd6eefc5644d16bb5fb941f2810483d Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:23:34 +0800 Subject: [PATCH] feat(root): add same-UID process scanning detection Implement process-level scanning for Same UID detection (Category 8): - Add native libc and syscall-based /proc enumeration to find processes sharing the same UID - Check /data/data/ directory existence for each same-UID process - Add JSON-based detail reporting for detected same-UID processes - Register new detection item in RootDetector.getAllDetections() Files modified: - native-lib.cpp: scan_same_uid_processes_impl(), get_same_uid_process_details(), JNI exports - NativeDetector.java: scanSameUidProcessesNative/Syscall(), getSameUidProcessDetails() - RootDetector.java: detectSameUidProcesses(), collectSameUidProcessDetails() --- app/src/main/cpp/native-lib.cpp | 249 ++++++++++++++++++ .../xff/launch/detector/NativeDetector.java | 21 ++ .../com/xff/launch/detector/RootDetector.java | 98 +++++++ 3 files changed, 368 insertions(+) diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 7e227b2..f356ed1 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -1610,6 +1610,255 @@ Java_com_xff_launch_detector_NativeDetector_checkFunctionHook(JNIEnv *env, jobje return result; } +// ===================== Same UID Process Scanning ===================== + +/** + * Scan processes running under the same UID + * Enumerate all processes with the same UID (via /proc/[pid]/status), + * then check if their /data/data/ directory exists. + * Abnormal same-UID processes may indicate shared UID attacks. + * + * @return Number of suspicious same-UID processes found + */ +static int scan_same_uid_processes_impl(bool use_syscall) { + pid_t my_pid = getpid(); + uid_t my_uid = getuid(); + int count = 0; + + // Open /proc directory + DIR* proc_dir = nullptr; + int proc_fd = -1; + + if (use_syscall) { + proc_fd = syscall(__NR_openat, AT_FDCWD, "/proc", O_RDONLY | O_DIRECTORY); + if (proc_fd < 0) return 0; + } else { + proc_dir = opendir("/proc"); + if (!proc_dir) return 0; + } + + if (use_syscall) { + // Syscall-based directory enumeration + char dir_buf[4096]; + while (true) { + int nread = syscall(__NR_getdents64, proc_fd, dir_buf, sizeof(dir_buf)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64* d = (struct linux_dirent64*)(dir_buf + pos); + + // Only process numeric directories (PIDs) + if (d->d_type == DT_DIR && isdigit(d->d_name[0])) { + int pid = atoi(d->d_name); + if (pid > 0 && pid != my_pid) { + // Read /proc/[pid]/status to get UID + char status_path[256]; + snprintf(status_path, sizeof(status_path), "/proc/%d/status", pid); + std::string status_content = syscall_read_file(status_path, 4096); + + if (!status_content.empty()) { + // Parse "Uid:\t\t\t..." + size_t uid_pos = status_content.find("Uid:"); + if (uid_pos != std::string::npos) { + uid_t proc_uid = 0; + sscanf(status_content.c_str() + uid_pos + 4, "%u", &proc_uid); + + if (proc_uid == my_uid) { + // Same UID process found, read cmdline + char cmdline_path[256]; + snprintf(cmdline_path, sizeof(cmdline_path), "/proc/%d/cmdline", pid); + std::string cmdline = syscall_read_file(cmdline_path, 256); + + if (!cmdline.empty()) { + // Extract process name (first null-terminated string) + size_t null_pos = cmdline.find('\0'); + if (null_pos != std::string::npos) { + cmdline = cmdline.substr(0, null_pos); + } + + // Check if /data/data/ exists + // Skip self package name and common system processes + if (!cmdline.empty() && cmdline[0] != '/' && + cmdline.find(':') == std::string::npos) { + char data_path[512]; + snprintf(data_path, sizeof(data_path), "/data/data/%s", cmdline.c_str()); + + // Check directory existence via syscall + if (syscall_file_exists(data_path)) { + LOGD("[SameUID] Suspicious process: PID=%d, name=%s, data_dir=%s", + pid, cmdline.c_str(), data_path); + count++; + } + } + } + } + } + } + } + } + pos += d->d_reclen; + } + } + syscall(__NR_close, proc_fd); + } else { + // libc-based directory enumeration + struct dirent* entry; + while ((entry = readdir(proc_dir)) != nullptr) { + if (entry->d_type != DT_DIR || !isdigit(entry->d_name[0])) continue; + + int pid = atoi(entry->d_name); + if (pid <= 0 || pid == my_pid) continue; + + // Read /proc/[pid]/status to get UID + char status_path[256]; + snprintf(status_path, sizeof(status_path), "/proc/%d/status", pid); + FILE* status_fp = fopen(status_path, "r"); + if (!status_fp) continue; + + uid_t proc_uid = (uid_t)-1; + char line[256]; + while (fgets(line, sizeof(line), status_fp)) { + if (strncmp(line, "Uid:", 4) == 0) { + sscanf(line + 4, "%u", &proc_uid); + break; + } + } + fclose(status_fp); + + if (proc_uid != my_uid) continue; + + // Same UID process found, read cmdline + char cmdline_path[256]; + snprintf(cmdline_path, sizeof(cmdline_path), "/proc/%d/cmdline", pid); + FILE* cmdline_fp = fopen(cmdline_path, "r"); + if (!cmdline_fp) continue; + + char cmdline[256] = {0}; + size_t len = fread(cmdline, 1, sizeof(cmdline) - 1, cmdline_fp); + fclose(cmdline_fp); + + if (len <= 0) continue; + + // cmdline is null-terminated, extract first segment + std::string process_name(cmdline); + + // Check if /data/data/ exists + // Skip paths (starting with /), sub-processes (containing :) + if (!process_name.empty() && process_name[0] != '/' && + process_name.find(':') == std::string::npos) { + char data_path[512]; + snprintf(data_path, sizeof(data_path), "/data/data/%s", process_name.c_str()); + + if (access(data_path, F_OK) == 0) { + LOGD("[SameUID] Suspicious process: PID=%d, name=%s, data_dir=%s", + pid, process_name.c_str(), data_path); + count++; + } + } + } + closedir(proc_dir); + } + + return count; +} + +/** + * Get detailed info about same-UID processes + * Returns JSON with process details + */ +static std::string get_same_uid_process_details() { + pid_t my_pid = getpid(); + uid_t my_uid = getuid(); + + std::string result = "["; + int count = 0; + + DIR* proc_dir = opendir("/proc"); + if (!proc_dir) return "[]"; + + struct dirent* entry; + while ((entry = readdir(proc_dir)) != nullptr && count < 20) { + if (entry->d_type != DT_DIR || !isdigit(entry->d_name[0])) continue; + + int pid = atoi(entry->d_name); + if (pid <= 0 || pid == my_pid) continue; + + // Read UID from /proc/[pid]/status + char status_path[256]; + snprintf(status_path, sizeof(status_path), "/proc/%d/status", pid); + std::string status_content = syscall_read_file(status_path, 4096); + if (status_content.empty()) continue; + + size_t uid_pos = status_content.find("Uid:"); + if (uid_pos == std::string::npos) continue; + + uid_t proc_uid = 0; + sscanf(status_content.c_str() + uid_pos + 4, "%u", &proc_uid); + if (proc_uid != my_uid) continue; + + // Read cmdline + char cmdline_path[256]; + snprintf(cmdline_path, sizeof(cmdline_path), "/proc/%d/cmdline", pid); + std::string cmdline = syscall_read_file(cmdline_path, 256); + if (cmdline.empty()) continue; + + // Extract process name + size_t null_pos = cmdline.find('\0'); + if (null_pos != std::string::npos) { + cmdline = cmdline.substr(0, null_pos); + } + + // Skip sub-processes (containing :) and path-based names + if (cmdline.empty() || cmdline[0] == '/') continue; + + // Check data directory + bool has_data_dir = false; + std::string process_base = cmdline; + size_t colon_pos = process_base.find(':'); + if (colon_pos != std::string::npos) { + process_base = process_base.substr(0, colon_pos); + } + + char data_path[512]; + snprintf(data_path, sizeof(data_path), "/data/data/%s", process_base.c_str()); + has_data_dir = (access(data_path, F_OK) == 0); + + if (count > 0) result += ","; + result += "{\"pid\":" + std::to_string(pid) + + ",\"name\":\"" + cmdline + "\"" + + ",\"has_data_dir\":" + (has_data_dir ? "true" : "false") + + ",\"data_path\":\"" + data_path + "\"}"; + count++; + } + closedir(proc_dir); + + result += "]"; + return result; +} + +JNIEXPORT jint JNICALL +Java_com_xff_launch_detector_NativeDetector_scanSameUidProcessesNative(JNIEnv *env, jobject thiz) { + LOGD("[SameUID] Starting same UID process scan (native libc)"); + int count = scan_same_uid_processes_impl(false); + LOGD("[SameUID] Native scan result: %d suspicious processes", count); + return count; +} + +JNIEXPORT jint JNICALL +Java_com_xff_launch_detector_NativeDetector_scanSameUidProcessesSyscall(JNIEnv *env, jobject thiz) { + LOGD("[SameUID] Starting same UID process scan (syscall)"); + int count = scan_same_uid_processes_impl(true); + LOGD("[SameUID] Syscall scan result: %d suspicious processes", count); + return count; +} + +JNIEXPORT jstring JNICALL +Java_com_xff_launch_detector_NativeDetector_getSameUidProcessDetails(JNIEnv *env, jobject thiz) { + std::string details = get_same_uid_process_details(); + return env->NewStringUTF(details.c_str()); +} + // ===================== Compatibility Method ===================== JNIEXPORT jstring JNICALL diff --git a/app/src/main/java/com/xff/launch/detector/NativeDetector.java b/app/src/main/java/com/xff/launch/detector/NativeDetector.java index 922c12c..600e9aa 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -305,6 +305,27 @@ public class NativeDetector { */ public native boolean detectTimingAnomaly(long syscallTime, long libcTime, float threshold); + // ===================== Same UID Process Scanning ===================== + + /** + * Scan processes running under the same UID via native libc + * Checks if other same-UID processes have /data/data/ directories + * @return Number of suspicious same-UID processes found + */ + public native int scanSameUidProcessesNative(); + + /** + * Scan processes running under the same UID via direct syscall + * @return Number of suspicious same-UID processes found + */ + public native int scanSameUidProcessesSyscall(); + + /** + * Get detailed information about same-UID processes + * @return JSON string with process details (pid, name, data_dir existence) + */ + public native String getSameUidProcessDetails(); + // Singleton instance private static NativeDetector instance; diff --git a/app/src/main/java/com/xff/launch/detector/RootDetector.java b/app/src/main/java/com/xff/launch/detector/RootDetector.java index 62d70ed..e1561ab 100644 --- a/app/src/main/java/com/xff/launch/detector/RootDetector.java +++ b/app/src/main/java/com/xff/launch/detector/RootDetector.java @@ -13,6 +13,9 @@ import java.util.ArrayList; import java.util.List; +import org.json.JSONArray; +import org.json.JSONObject; + /** * Root detection implementation with multi-layer support */ @@ -347,9 +350,58 @@ public List getAllDetections() { items.add(detectRootManagers()); items.add(detectRootHiding()); items.add(detectSuspiciousMounts()); + items.add(detectSameUidProcesses()); return items; } + /** + * Detect suspicious same-UID processes + * Scans /proc for processes running under the same UID and checks if their + * /data/data/ directories exist, which could indicate shared-UID attacks + * or abnormal process injection. + */ + public DetectionItem detectSameUidProcesses() { + DetectionItem item = new DetectionItem("同 UID 进程扫描", "检测同 UID 下的异常进程"); + + // Native layer - uses libc opendir/readdir/fopen/access + int nativeCount = 0; + try { + nativeCount = nativeDetector.scanSameUidProcessesNative(); + } catch (Exception ignored) { + } + // Native detects other same-UID processes with data dirs + // Count > 1 means there are OTHER apps beyond self (self is excluded in native) + item.setLayerResult(DetectionLayer.NATIVE, nativeCount > 1); + + // Syscall layer - uses direct syscalls to bypass libc hooks + int syscallCount = 0; + try { + syscallCount = nativeDetector.scanSameUidProcessesSyscall(); + } catch (Exception ignored) { + } + item.setLayerResult(DetectionLayer.SYSCALL, syscallCount > 1); + + // Use the higher count for status determination + int maxCount = Math.max(nativeCount, syscallCount); + + if (maxCount > 1) { + item.setStatus(DetectionStatus.WARNING); + item.setDetail("检测到 " + maxCount + " 个同 UID 进程"); + + // Collect detailed process information + collectSameUidProcessDetails(item); + } else { + item.setStatus(DetectionStatus.SAFE); + item.setDetail("未检测到异常同 UID 进程"); + } + + if (item.hasInconsistentResults()) { + item.setDetail(item.getDetail() + " (检测层不一致)"); + } + + return item; + } + // ===================== Java Layer Methods ===================== private boolean checkSuFilesJava() { @@ -888,4 +940,50 @@ private void collectSuspiciousMountsDetails(DetectionItem item) { android.util.Log.e("RootDetector", "Error reading mountinfo", e); } } + + /** + * Collect detailed same-UID process information + */ + private void collectSameUidProcessDetails(DetectionItem item) { + try { + String detailsJson = nativeDetector.getSameUidProcessDetails(); + if (detailsJson == null || detailsJson.equals("[]")) return; + + JSONArray processes = new JSONArray(detailsJson); + int totalProcesses = processes.length(); + int withDataDir = 0; + + for (int i = 0; i < processes.length() && i < 10; i++) { + JSONObject proc = processes.getJSONObject(i); + int pid = proc.getInt("pid"); + String name = proc.getString("name"); + boolean hasDataDir = proc.getBoolean("has_data_dir"); + String dataPath = proc.getString("data_path"); + + if (hasDataDir) { + withDataDir++; + } + + String detail = "PID: " + pid + + "\n进程名: " + name + + "\n数据目录: " + dataPath + + "\n目录存在: " + (hasDataDir ? "✓ 是" : "✗ 否"); + + String icon = hasDataDir ? "⚠️" : "ℹ️"; + item.addDetectionDetail("🔍 同 UID 进程", name, + detail, DetectionLayer.NATIVE, icon); + } + + // Add summary + String summary = "同 UID 进程总数: " + totalProcesses + + "\n拥有数据目录: " + withDataDir + " 个" + + "\n当前 UID: " + android.os.Process.myUid() + + "\n当前 PID: " + android.os.Process.myPid(); + item.addDetectionDetail("📊 扫描统计", "同 UID 进程概览", + summary, DetectionLayer.SYSCALL, "📈"); + + } catch (Exception e) { + android.util.Log.e("RootDetector", "Error parsing same UID process details", e); + } + } }