From f8d36b6294ea97d71d27c9c608e5fec2a1529d80 Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:54:49 +0800 Subject: [PATCH 1/9] fix(ui): correct inverted boolean logic in setLayerResult calls Fix detection layer display showing inverted results across multiple detectors. The boolean parameter in setLayerResult(layer, detected) should be true when a risk is detected, and false when safe. - ReadlinkDetector: invert all setLayerResult boolean values - ZygoteDetector: invert all setLayerResult boolean values - SideChannelDetector: fix SYSCALL layer from !isHooked to isHooked in timing-based detection methods - DebugDetector: fix detectPtrace (TracerPid==0 was true, now TracerPid!=0 is true) and detectPtraceSelfProtection (safe condition was true, now risk condition is true) --- .../xff/launch/detector/DebugDetector.java | 16 ++-- .../xff/launch/detector/ReadlinkDetector.java | 60 ++++++------- .../launch/detector/SideChannelDetector.java | 6 +- .../xff/launch/detector/ZygoteDetector.java | 90 +++++++++---------- 4 files changed, 86 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/com/xff/launch/detector/DebugDetector.java b/app/src/main/java/com/xff/launch/detector/DebugDetector.java index 0d11dd8..5314783 100644 --- a/app/src/main/java/com/xff/launch/detector/DebugDetector.java +++ b/app/src/main/java/com/xff/launch/detector/DebugDetector.java @@ -131,10 +131,10 @@ public DetectionItem detectPtrace() { // Native layer - read TracerPid via native int nativeTracerPid = nativeDetector.getTracerPid(); - // Set layer results (INVERTED: true = safe, false = detected) - item.setLayerResult(DetectionLayer.JAVA, javaTracerPid == 0); - item.setLayerResult(DetectionLayer.NATIVE, nativeTracerPid == 0); - item.setLayerResult(DetectionLayer.SYSCALL, syscallTracerPid == 0); + // Set layer results: true = detected (TracerPid > 0), false = safe + item.setLayerResult(DetectionLayer.JAVA, javaTracerPid != 0); + item.setLayerResult(DetectionLayer.NATIVE, nativeTracerPid != 0); + item.setLayerResult(DetectionLayer.SYSCALL, syscallTracerPid != 0); // Check all threads String allThreadsStatus = checkAllThreadsTracerPid(); @@ -232,10 +232,10 @@ public DetectionItem detectPtraceSelfProtection() { detail = "TracerPid:0 (正常 - 无反调试保护)"; } - // Set layer results (true = safe/normal, false = risk) - item.setLayerResult(DetectionLayer.JAVA, tracerPid == 0 || isProtected); - item.setLayerResult(DetectionLayer.NATIVE, tracerPid == 0 || isProtected); - item.setLayerResult(DetectionLayer.SYSCALL, tracerPid == 0 || isProtected); + // Set layer results: true = detected risk (external debugger), false = safe/normal + item.setLayerResult(DetectionLayer.JAVA, tracerPid != 0 && !isProtected); + item.setLayerResult(DetectionLayer.NATIVE, tracerPid != 0 && !isProtected); + item.setLayerResult(DetectionLayer.SYSCALL, tracerPid != 0 && !isProtected); item.setStatus(status); item.setDetail(detail); diff --git a/app/src/main/java/com/xff/launch/detector/ReadlinkDetector.java b/app/src/main/java/com/xff/launch/detector/ReadlinkDetector.java index 256b064..5cc2752 100644 --- a/app/src/main/java/com/xff/launch/detector/ReadlinkDetector.java +++ b/app/src/main/java/com/xff/launch/detector/ReadlinkDetector.java @@ -133,9 +133,9 @@ private DetectionItem checkProcSelfExe() { // Check consistency between layers boolean consistent = nativeExePath.equals(syscallExePath); - item.setLayerResult(DetectionLayer.JAVA, !javaRisk && javaOk); - item.setLayerResult(DetectionLayer.NATIVE, !nativeRisk && nativeOk); - item.setLayerResult(DetectionLayer.SYSCALL, !syscallRisk && syscallOk); + item.setLayerResult(DetectionLayer.JAVA, javaRisk || !javaOk); + item.setLayerResult(DetectionLayer.NATIVE, nativeRisk || !nativeOk); + item.setLayerResult(DetectionLayer.SYSCALL, syscallRisk || !syscallOk); if (javaRisk || nativeRisk || syscallRisk) { item.setStatus(DetectionStatus.RISK); @@ -174,9 +174,9 @@ private DetectionItem checkProcSelfMaps() { // Check for hidden mappings (indication of hook framework) boolean hasHiddenMaps = nativeDetector.checkHiddenMapsSyscall(); - item.setLayerResult(DetectionLayer.JAVA, hasContent); - item.setLayerResult(DetectionLayer.NATIVE, !hasSuspicious); - item.setLayerResult(DetectionLayer.SYSCALL, !hasHiddenMaps); + item.setLayerResult(DetectionLayer.JAVA, !hasContent); + item.setLayerResult(DetectionLayer.NATIVE, hasSuspicious); + item.setLayerResult(DetectionLayer.SYSCALL, hasHiddenMaps); if (hasHiddenMaps) { item.setStatus(DetectionStatus.RISK); @@ -216,9 +216,9 @@ private DetectionItem checkProcSelfMounts() { boolean hasBindMount = mountContent.contains("magisk") || mountContent.contains("ksu") || mountContent.contains("apatch"); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, !hasOverlay); - item.setLayerResult(DetectionLayer.SYSCALL, !hasBindMount); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, hasOverlay); + item.setLayerResult(DetectionLayer.SYSCALL, hasBindMount); if (hasBindMount) { item.setStatus(DetectionStatus.RISK); @@ -253,9 +253,9 @@ private DetectionItem checkProcSelfRoot() { boolean syscallOk = "/".equals(syscallRoot); boolean consistent = nativeRoot.equals(syscallRoot); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, nativeOk); - item.setLayerResult(DetectionLayer.SYSCALL, syscallOk); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, !nativeOk); + item.setLayerResult(DetectionLayer.SYSCALL, !syscallOk); if (!syscallOk) { item.setStatus(DetectionStatus.RISK); @@ -289,9 +289,9 @@ private DetectionItem checkProcSelfCwd() { boolean hasSuspicious = containsSuspicious(syscallCwd); boolean consistent = nativeCwd.equals(syscallCwd); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, !containsSuspicious(nativeCwd)); - item.setLayerResult(DetectionLayer.SYSCALL, !hasSuspicious); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, containsSuspicious(nativeCwd)); + item.setLayerResult(DetectionLayer.SYSCALL, hasSuspicious); if (hasSuspicious) { item.setStatus(DetectionStatus.RISK); @@ -325,9 +325,9 @@ private DetectionItem checkProcSelfFd() { // Also check via native int nativeFdCount = nativeDetector.checkSuspiciousFdsNative(); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, nativeFdCount == 0); - item.setLayerResult(DetectionLayer.SYSCALL, suspiciousFdCount == 0); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, nativeFdCount != 0); + item.setLayerResult(DetectionLayer.SYSCALL, suspiciousFdCount != 0); if (suspiciousFdCount > 0) { item.setStatus(DetectionStatus.RISK); @@ -381,9 +381,9 @@ private DetectionItem checkSuSymlinks() { } } - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, nativeFound == 0); - item.setLayerResult(DetectionLayer.SYSCALL, syscallFound == 0); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, nativeFound > 0); + item.setLayerResult(DetectionLayer.SYSCALL, syscallFound > 0); if (syscallFound > 0) { item.setStatus(DetectionStatus.RISK); @@ -453,9 +453,9 @@ private DetectionItem checkSystemBinaries() { } } - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, nativeSuspicious == 0); - item.setLayerResult(DetectionLayer.SYSCALL, syscallSuspicious == 0); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, nativeSuspicious > 0); + item.setLayerResult(DetectionLayer.SYSCALL, syscallSuspicious > 0); if (syscallSuspicious > 0) { item.setStatus(DetectionStatus.RISK); @@ -524,9 +524,9 @@ private DetectionItem checkAppPath() { boolean consistent = nativeReal.equals(syscallReal); boolean hasSuspicious = containsSuspicious(nativeReal) || containsSuspicious(syscallReal); - item.setLayerResult(DetectionLayer.JAVA, !hasSuspicious); - item.setLayerResult(DetectionLayer.NATIVE, !nativeIsLink || !hasSuspicious); - item.setLayerResult(DetectionLayer.SYSCALL, !syscallIsLink || !hasSuspicious); + item.setLayerResult(DetectionLayer.JAVA, hasSuspicious); + item.setLayerResult(DetectionLayer.NATIVE, nativeIsLink && hasSuspicious); + item.setLayerResult(DetectionLayer.SYSCALL, syscallIsLink && hasSuspicious); if (hasSuspicious) { item.setStatus(DetectionStatus.RISK); @@ -565,9 +565,9 @@ private DetectionItem checkMountNamespace() { boolean nativeCheck = nativeDetector.checkMountNamespaceNative(); boolean syscallCheck = nativeDetector.checkMountNamespaceSyscall(); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, nativeCheck); - item.setLayerResult(DetectionLayer.SYSCALL, syscallCheck); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, !nativeCheck); + item.setLayerResult(DetectionLayer.SYSCALL, !syscallCheck); if (!syscallCheck) { item.setStatus(DetectionStatus.RISK); diff --git a/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java b/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java index 03d091a..f6d73e8 100644 --- a/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java +++ b/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java @@ -121,7 +121,7 @@ private DetectionItem checkSyscallTimingOpenat() { // Set layer results item.setLayerResult(DetectionLayer.JAVA, false); // Java layer N/A item.setLayerResult(DetectionLayer.NATIVE, isHooked); - item.setLayerResult(DetectionLayer.SYSCALL, !isHooked); + item.setLayerResult(DetectionLayer.SYSCALL, isHooked); if (isHooked) { item.setStatus(DetectionStatus.RISK); @@ -157,7 +157,7 @@ private DetectionItem checkSyscallTimingAccess() { item.setLayerResult(DetectionLayer.JAVA, false); item.setLayerResult(DetectionLayer.NATIVE, isHooked); - item.setLayerResult(DetectionLayer.SYSCALL, !isHooked); + item.setLayerResult(DetectionLayer.SYSCALL, isHooked); if (isHooked) { item.setStatus(DetectionStatus.RISK); @@ -193,7 +193,7 @@ private DetectionItem checkSyscallTimingStat() { item.setLayerResult(DetectionLayer.JAVA, false); item.setLayerResult(DetectionLayer.NATIVE, isHooked); - item.setLayerResult(DetectionLayer.SYSCALL, !isHooked); + item.setLayerResult(DetectionLayer.SYSCALL, isHooked); if (isHooked) { item.setStatus(DetectionStatus.RISK); diff --git a/app/src/main/java/com/xff/launch/detector/ZygoteDetector.java b/app/src/main/java/com/xff/launch/detector/ZygoteDetector.java index 282b0ba..ab88d8e 100644 --- a/app/src/main/java/com/xff/launch/detector/ZygoteDetector.java +++ b/app/src/main/java/com/xff/launch/detector/ZygoteDetector.java @@ -156,9 +156,9 @@ private DetectionItem checkZygiskInjection() { boolean javaDetected = mapsDetected || propDetected; - item.setLayerResult(DetectionLayer.JAVA, !javaDetected); - item.setLayerResult(DetectionLayer.NATIVE, !nativeDetected); - item.setLayerResult(DetectionLayer.SYSCALL, !syscallDetected); + item.setLayerResult(DetectionLayer.JAVA, javaDetected); + item.setLayerResult(DetectionLayer.NATIVE, nativeDetected); + item.setLayerResult(DetectionLayer.SYSCALL, syscallDetected); if (syscallDetected || nativeDetected) { item.setStatus(DetectionStatus.RISK); @@ -237,9 +237,9 @@ private DetectionItem checkZygiskSUDaemon() { // Also check via native detector boolean nativeDetected = nativeDetector.checkZygiskSyscall(); - item.setLayerResult(DetectionLayer.JAVA, !detected); - item.setLayerResult(DetectionLayer.NATIVE, !nativeDetected); - item.setLayerResult(DetectionLayer.SYSCALL, !nativeDetected); + item.setLayerResult(DetectionLayer.JAVA, detected); + item.setLayerResult(DetectionLayer.NATIVE, nativeDetected); + item.setLayerResult(DetectionLayer.SYSCALL, nativeDetected); if (detected) { item.setStatus(DetectionStatus.RISK); @@ -282,9 +282,9 @@ private DetectionItem checkRiruInjection() { } } - item.setLayerResult(DetectionLayer.JAVA, !pathExists); - item.setLayerResult(DetectionLayer.NATIVE, !nativeDetected); - item.setLayerResult(DetectionLayer.SYSCALL, !syscallDetected); + item.setLayerResult(DetectionLayer.JAVA, pathExists); + item.setLayerResult(DetectionLayer.NATIVE, nativeDetected); + item.setLayerResult(DetectionLayer.SYSCALL, syscallDetected); if (syscallDetected || nativeDetected) { item.setStatus(DetectionStatus.RISK); @@ -341,9 +341,9 @@ private DetectionItem checkNativeBridge() { boolean nativeOk = nativeValue.isEmpty() || nativeValue.equals("0"); boolean syscallOk = syscallValue.isEmpty() || syscallValue.equals("0"); - item.setLayerResult(DetectionLayer.JAVA, javaOk && !isSuspicious); - item.setLayerResult(DetectionLayer.NATIVE, nativeOk && !isSuspicious); - item.setLayerResult(DetectionLayer.SYSCALL, syscallOk && !isSuspicious); + item.setLayerResult(DetectionLayer.JAVA, !javaOk || isSuspicious); + item.setLayerResult(DetectionLayer.NATIVE, !nativeOk || isSuspicious); + item.setLayerResult(DetectionLayer.SYSCALL, !syscallOk || isSuspicious); if (isSuspicious) { item.setStatus(DetectionStatus.RISK); @@ -391,9 +391,9 @@ private DetectionItem checkSELinuxContext() { boolean hasMagiskContext = currentContext.contains("magisk") || prevContext.contains("magisk"); - item.setLayerResult(DetectionLayer.JAVA, !hasMagiskContext); - item.setLayerResult(DetectionLayer.NATIVE, !hasZygoteAnomaly); - item.setLayerResult(DetectionLayer.SYSCALL, !hasMagiskContext && !hasZygoteAnomaly); + item.setLayerResult(DetectionLayer.JAVA, hasMagiskContext); + item.setLayerResult(DetectionLayer.NATIVE, hasZygoteAnomaly); + item.setLayerResult(DetectionLayer.SYSCALL, hasMagiskContext || hasZygoteAnomaly); if (hasMagiskContext) { item.setStatus(DetectionStatus.RISK); @@ -443,9 +443,9 @@ private DetectionItem checkMemoryMaps() { // Syscall check int syscallCount = nativeDetector.checkSuspiciousMapsSyscall(); - item.setLayerResult(DetectionLayer.JAVA, suspiciousCount == 0); - item.setLayerResult(DetectionLayer.NATIVE, nativeCount == 0); - item.setLayerResult(DetectionLayer.SYSCALL, syscallCount == 0); + item.setLayerResult(DetectionLayer.JAVA, suspiciousCount > 0); + item.setLayerResult(DetectionLayer.NATIVE, nativeCount > 0); + item.setLayerResult(DetectionLayer.SYSCALL, syscallCount > 0); if (syscallCount > 0 || suspiciousCount > 0) { item.setStatus(DetectionStatus.RISK); @@ -540,9 +540,9 @@ else if (!nativeDetector.checkFileIntegritySyscall(path)) { // Syscall check boolean syscallOk = nativeDetector.checkAppProcessSyscall(); - item.setLayerResult(DetectionLayer.JAVA, !hasAnomaly); - item.setLayerResult(DetectionLayer.NATIVE, nativeOk); - item.setLayerResult(DetectionLayer.SYSCALL, syscallOk); + item.setLayerResult(DetectionLayer.JAVA, hasAnomaly); + item.setLayerResult(DetectionLayer.NATIVE, !nativeOk); + item.setLayerResult(DetectionLayer.SYSCALL, !syscallOk); if (!syscallOk || hasAnomaly) { item.setStatus(DetectionStatus.RISK); @@ -606,9 +606,9 @@ private DetectionItem checkZygoteModules() { } } - item.setLayerResult(DetectionLayer.JAVA, foundCount == 0); - item.setLayerResult(DetectionLayer.NATIVE, foundCount == 0); - item.setLayerResult(DetectionLayer.SYSCALL, foundCount == 0); + item.setLayerResult(DetectionLayer.JAVA, foundCount > 0); + item.setLayerResult(DetectionLayer.NATIVE, foundCount > 0); + item.setLayerResult(DetectionLayer.SYSCALL, foundCount > 0); if (foundCount > 0) { item.setStatus(DetectionStatus.RISK); @@ -661,9 +661,9 @@ private DetectionItem checkMemoryIntegrity() { // Check for inline hooks boolean hasInlineHooks = nativeDetector.checkInlineHooksSyscall(); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, nativeIntact); - item.setLayerResult(DetectionLayer.SYSCALL, syscallIntact && !hasInlineHooks); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, !nativeIntact); + item.setLayerResult(DetectionLayer.SYSCALL, !syscallIntact || hasInlineHooks); if (hasInlineHooks) { item.setStatus(DetectionStatus.RISK); @@ -700,9 +700,9 @@ private DetectionItem checkAnonymousMemory() { String smaps = nativeDetector.readFileSyscall("/proc/self/smaps"); boolean hasPrivateDirty = checkPrivateDirtyPages(smaps); - item.setLayerResult(DetectionLayer.JAVA, !hasPrivateDirty); - item.setLayerResult(DetectionLayer.NATIVE, !hasSuspiciousAnon); - item.setLayerResult(DetectionLayer.SYSCALL, !hasSuspiciousAnon); + item.setLayerResult(DetectionLayer.JAVA, hasPrivateDirty); + item.setLayerResult(DetectionLayer.NATIVE, hasSuspiciousAnon); + item.setLayerResult(DetectionLayer.SYSCALL, hasSuspiciousAnon); if (hasSuspiciousAnon) { item.setStatus(DetectionStatus.RISK); @@ -737,9 +737,9 @@ private DetectionItem checkLibraryHooks() { // Native layer check boolean nativeOk = nativeDetector.checkLibraryHooksNative(); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, nativeOk); - item.setLayerResult(DetectionLayer.SYSCALL, !libcHooked && !artHooked); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, !nativeOk); + item.setLayerResult(DetectionLayer.SYSCALL, libcHooked || artHooked); if (libcHooked || artHooked) { item.setStatus(DetectionStatus.RISK); @@ -831,9 +831,9 @@ private DetectionItem checkZygoteParentProcess() { if (zygoteInfo.equals("not_found")) { item.setStatus(DetectionStatus.UNKNOWN); item.setDetail("未找到Zygote进程"); - item.setLayerResult(DetectionLayer.JAVA, true); - item.setLayerResult(DetectionLayer.NATIVE, true); - item.setLayerResult(DetectionLayer.SYSCALL, true); + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, false); + item.setLayerResult(DetectionLayer.SYSCALL, false); return item; } @@ -851,9 +851,9 @@ private DetectionItem checkZygoteParentProcess() { // Check via native boolean hasAbnormalParent = nativeDetector.checkZygoteParentNative(); - item.setLayerResult(DetectionLayer.JAVA, parentPid == 1); - item.setLayerResult(DetectionLayer.NATIVE, !hasAbnormalParent); - item.setLayerResult(DetectionLayer.SYSCALL, parentPid == 1); + item.setLayerResult(DetectionLayer.JAVA, parentPid != 1); + item.setLayerResult(DetectionLayer.NATIVE, hasAbnormalParent); + item.setLayerResult(DetectionLayer.SYSCALL, parentPid != 1); // Normal: parent should be init (PID 1) if (parentPid != 1 || hasAbnormalParent) { @@ -892,9 +892,9 @@ private DetectionItem checkAnonymousRwxMemory() { // Get details String details = nativeDetector.getAnonymousRwxDetails(); - item.setLayerResult(DetectionLayer.JAVA, count == 0); - item.setLayerResult(DetectionLayer.NATIVE, count < 5); // Some JIT is normal - item.setLayerResult(DetectionLayer.SYSCALL, count < 10); + item.setLayerResult(DetectionLayer.JAVA, count > 0); + item.setLayerResult(DetectionLayer.NATIVE, count >= 5); // Some JIT is normal + item.setLayerResult(DetectionLayer.SYSCALL, count >= 10); if (count > 100) { // Very high count -> likely hook/injection @@ -956,9 +956,9 @@ private DetectionItem checkInMemoryDexLoader() { depth++; } - item.setLayerResult(DetectionLayer.JAVA, inMemoryDexCount == 0); - item.setLayerResult(DetectionLayer.NATIVE, inMemoryDexCount == 0); - item.setLayerResult(DetectionLayer.SYSCALL, inMemoryDexCount == 0); + item.setLayerResult(DetectionLayer.JAVA, inMemoryDexCount > 0); + item.setLayerResult(DetectionLayer.NATIVE, inMemoryDexCount > 0); + item.setLayerResult(DetectionLayer.SYSCALL, inMemoryDexCount > 0); if (inMemoryDexCount > 0) { item.setStatus(DetectionStatus.RISK); From f3c8c8bf13a1bf44ed2641d7077dd7be759c67e0 Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:18:00 +0800 Subject: [PATCH 2/9] fix(native): fix false positives in contains_suspicious due to short keyword substring matching Short keywords like "su", "hide", "ksu" in SUSPICIOUS_KEYWORDS caused false positives when matched as substrings of normal paths. For example, the BLAST architecture (introduced in Android 11) produces fd paths like "/dmabuf:VRI[MainActivity]#1(BLAST Consumer)" where "Consumer" contains "su", triggering a false detection. Similar false positives occur with /dev/ashmem, result, resume, override, etc. Solution: - Split keywords into two categories: long/unique keywords (simple substring match) and short/ambiguous keywords (word boundary match) - Add find_with_boundary() that requires non-alphanumeric chars or string boundaries around the keyword - Move "su", "hide", "ksu" to BOUNDARY_KEYWORDS with boundary-aware matching - Apply the same fix to checkMountNamespaceNative/Syscall --- app/src/main/cpp/native-lib.cpp | 66 +++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 7e227b2..af8dc0e 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -315,13 +315,44 @@ Java_com_xff_launch_detector_NativeDetector_readFileSyscall(JNIEnv *env, jobject // ===================== Readlink Detection (Syscall-based) ===================== // Suspicious keywords to check in paths +// Long/unique keywords - safe for simple substring matching static const char* SUSPICIOUS_KEYWORDS[] = { - "magisk", "su", "supersu", "superuser", "busybox", - "ksu", "kernelsu", "apatch", "lsposed", "edxposed", - "xposed", "riru", "zygisk", "shamiko", "hide", + "magisk", "supersu", "superuser", "busybox", + "kernelsu", "apatch", "lsposed", "edxposed", + "xposed", "riru", "zygisk", "shamiko", "frida", "substrate", "cydia" }; -static const int SUSPICIOUS_KEYWORDS_COUNT = 18; +static const int SUSPICIOUS_KEYWORDS_COUNT = 15; + +// Short/ambiguous keywords - require word boundary matching to avoid false positives +// e.g. "su" in "Consumer", "ashmem", "result"; "hide" in "override", "hidden" +// "ksu" is also short enough to warrant boundary check +static const char* BOUNDARY_KEYWORDS[] = { + "su", "hide", "ksu" +}; +static const int BOUNDARY_KEYWORDS_COUNT = 3; + +// Check if the character is a word boundary (not alphanumeric or underscore) +static bool is_word_boundary(char c) { + return !isalnum(static_cast(c)) && c != '_'; +} + +// Check if keyword exists as a whole word/path segment in the string +static bool find_with_boundary(const std::string& text, const char* keyword) { + size_t keyLen = strlen(keyword); + size_t pos = 0; + while ((pos = text.find(keyword, pos)) != std::string::npos) { + // Check left boundary: start of string or non-alphanumeric char + bool leftOk = (pos == 0) || is_word_boundary(text[pos - 1]); + // Check right boundary: end of string or non-alphanumeric char + bool rightOk = (pos + keyLen >= text.size()) || is_word_boundary(text[pos + keyLen]); + if (leftOk && rightOk) { + return true; + } + pos += 1; + } + return false; +} static bool contains_suspicious(const std::string& path) { if (path.empty()) return false; @@ -329,11 +360,21 @@ static bool contains_suspicious(const std::string& path) { for (char& c : lower) { c = tolower(c); } + + // Check long/unique keywords with simple substring matching for (int i = 0; i < SUSPICIOUS_KEYWORDS_COUNT; i++) { if (lower.find(SUSPICIOUS_KEYWORDS[i]) != std::string::npos) { return true; } } + + // Check short/ambiguous keywords with word boundary matching + for (int i = 0; i < BOUNDARY_KEYWORDS_COUNT; i++) { + if (find_with_boundary(lower, BOUNDARY_KEYWORDS[i])) { + return true; + } + } + return false; } @@ -611,8 +652,13 @@ Java_com_xff_launch_detector_NativeDetector_checkMountNamespaceNative(JNIEnv *en if (fp) { char line[512]; while (fgets(line, sizeof(line), fp)) { - if (strstr(line, "magisk") || strstr(line, "ksu") || - strstr(line, "apatch") || strstr(line, "overlay")) { + std::string lineStr(line); + std::string lineLower = lineStr; + for (char& c : lineLower) c = tolower(c); + if (lineLower.find("magisk") != std::string::npos || + find_with_boundary(lineLower, "ksu") || + lineLower.find("apatch") != std::string::npos || + lineLower.find("overlay") != std::string::npos) { fclose(fp); return false; // Suspicious mount found } @@ -641,10 +687,14 @@ Java_com_xff_launch_detector_NativeDetector_checkMountNamespaceSyscall(JNIEnv *e // Check for suspicious mounts bool suspicious = false; suspicious |= (mounts.find("magisk") != std::string::npos); - suspicious |= (mounts.find("/su") != std::string::npos); suspicious |= (mounts.find("supersu") != std::string::npos); - suspicious |= (mounts.find("ksu") != std::string::npos); suspicious |= (mounts.find("apatch") != std::string::npos); + // Use boundary matching for short keywords to avoid false positives + // e.g. "/su" could match "/surface", "/suspend" etc. + std::string mountsLower = mounts; + for (char& c : mountsLower) c = tolower(c); + suspicious |= find_with_boundary(mountsLower, "su"); + suspicious |= find_with_boundary(mountsLower, "ksu"); // Check for overlay on system partitions (common root hiding technique) bool overlayOnSystem = false; From 24193b5bd80b8390c879a08df964d5d02d32bdbc Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:07:37 +0800 Subject: [PATCH 3/9] feat(root): add /proc/mounts Magisk signature and zygote context detection - Add legacy Magisk paths (/sbin/.magisk/mirror, /sbin/.magisk/block, /sbin/.core/*) to getMagiskPaths() - Add /sbin/.magisk/ and /sbin/.core/ to suspicious mount patterns - Add checkMountsForMagisk{Native,Syscall} for /proc/self/mounts scanning - Add checkZygoteContext{Native,Syscall} for /proc/self/attr/prev verification - Register both new detection items in RootDetector.getAllDetections() --- app/src/main/cpp/detector/root_detector.cpp | 84 +++++++++++++++++++ app/src/main/cpp/detector/root_detector.h | 8 ++ app/src/main/cpp/native-lib.cpp | 22 +++++ .../xff/launch/detector/NativeDetector.java | 8 ++ .../com/xff/launch/detector/RootDetector.java | 79 +++++++++++++++++ 5 files changed, 201 insertions(+) diff --git a/app/src/main/cpp/detector/root_detector.cpp b/app/src/main/cpp/detector/root_detector.cpp index 4de4f0c..5553f41 100644 --- a/app/src/main/cpp/detector/root_detector.cpp +++ b/app/src/main/cpp/detector/root_detector.cpp @@ -30,6 +30,11 @@ const std::vector& RootDetector::getSuPaths() { const std::vector& RootDetector::getMagiskPaths() { static std::vector paths = { "/sbin/.magisk", + "/sbin/.magisk/mirror", + "/sbin/.magisk/block", + "/sbin/.core", // Legacy Magisk (v15-v18) core directory + "/sbin/.core/mirror", // Legacy Magisk mirror mount + "/sbin/.core/img", // Legacy Magisk module image "/data/adb/magisk", "/data/adb/magisk.img", "/data/adb/magisk.db", @@ -190,6 +195,8 @@ bool RootDetector::checkSuspiciousMountsNative() { // Note: overlay and tmpfs are normal system mounts, don't check them const std::vector suspiciousPatterns = { "magisk", + "/sbin/.magisk/", // Magisk working directory + "/sbin/.core/", // Legacy Magisk (v15-v18) core directory "zygisk", "zygisksu", "kernelsu", @@ -321,6 +328,8 @@ bool RootDetector::checkSuspiciousMountsSyscall() { // Note: overlay and tmpfs are normal system mounts, don't check them const std::vector suspiciousPatterns = { "magisk", + "/sbin/.magisk/", // Magisk working directory + "/sbin/.core/", // Legacy Magisk (v15-v18) core directory "zygisk", "zygisksu", "zygisk_su", @@ -388,6 +397,81 @@ bool RootDetector::checkBuildTags() { return content.find("ro.build.tags=test-keys") != std::string::npos; } +// ===================== /proc/mounts Magisk Detection ===================== +// Read /proc//mounts via direct syscall, search for Magisk signatures + +bool RootDetector::checkMountsForMagiskNative() { + std::ifstream file("/proc/self/mounts"); + if (!file.is_open()) return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find("/sbin/.magisk/") != std::string::npos || + line.find("magisk") != std::string::npos || + line.find("/sbin/.core/") != std::string::npos) { + LOGD("Magisk mount signature found (native): %s", line.c_str()); + return true; + } + } + return false; +} + +bool RootDetector::checkMountsForMagiskSyscall() { + std::string content = syscall_read_file("/proc/self/mounts", 32768); + if (content.empty()) return false; + + if (content.find("/sbin/.magisk/") != std::string::npos || + content.find("magisk") != std::string::npos || + content.find("/sbin/.core/") != std::string::npos) { + LOGD("Magisk mount signature found (syscall)"); + return true; + } + return false; +} + +// ===================== Zygote Context Detection ===================== +// Read /proc//attr/prev, search for "zygote" in SELinux context +// Normal Android app processes are forked from zygote, so attr/prev +// should contain the zygote's SELinux context (e.g. "u:r:zygote:s0") + +bool RootDetector::checkZygoteContextNative() { + std::ifstream file("/proc/self/attr/prev"); + if (!file.is_open()) return false; + + std::string content; + std::getline(file, content); + + // Normal: should contain "zygote" (e.g. "u:r:zygote:s0") + if (content.find("zygote") != std::string::npos) { + return false; // Normal - process was forked from zygote + } + + // Abnormal: attr/prev does not contain zygote context + // This may indicate the process was spawned abnormally + LOGD("Abnormal zygote context (native): %s", content.c_str()); + return true; +} + +bool RootDetector::checkZygoteContextSyscall() { + std::string content = syscall_read_file("/proc/self/attr/prev", 256); + + // If we can't read attr/prev, try attr/current as fallback reference + if (content.empty()) { + content = syscall_read_file("/proc/self/attr/current", 256); + } + + if (content.empty()) return false; + + // Normal: should contain "zygote" (e.g. "u:r:zygote:s0") + if (content.find("zygote") != std::string::npos) { + return false; // Normal - process was forked from zygote + } + + // Abnormal: context does not contain zygote + LOGD("Abnormal zygote context (syscall): %s", content.c_str()); + return true; +} + bool RootDetector::checkSelinuxStatus() { std::string content = syscall_read_file("/sys/fs/selinux/enforce"); return !content.empty() && content[0] == '0'; diff --git a/app/src/main/cpp/detector/root_detector.h b/app/src/main/cpp/detector/root_detector.h index d0206e0..785bbc4 100644 --- a/app/src/main/cpp/detector/root_detector.h +++ b/app/src/main/cpp/detector/root_detector.h @@ -76,6 +76,14 @@ class RootDetector { static bool checkRootHidingSyscall(); static bool checkSuspiciousMountsSyscall(); + // /proc/mounts Magisk signature detection + static bool checkMountsForMagiskNative(); + static bool checkMountsForMagiskSyscall(); + + // Zygote SELinux context detection via /proc/self/attr/prev + static bool checkZygoteContextNative(); + static bool checkZygoteContextSyscall(); + // System property checks static bool checkBuildTags(); static bool checkSelinuxStatus(); diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index af8dc0e..970f0b7 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -143,6 +143,28 @@ Java_com_xff_launch_detector_NativeDetector_checkMountInfoSyscall(JNIEnv *env, j return RootDetector::checkMountInfoSyscall(); } +// /proc/mounts Magisk signature detection +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkMountsForMagiskNative(JNIEnv *env, jobject thiz) { + return RootDetector::checkMountsForMagiskNative(); +} + +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkMountsForMagiskSyscall(JNIEnv *env, jobject thiz) { + return RootDetector::checkMountsForMagiskSyscall(); +} + +// Zygote SELinux context detection via /proc/self/attr/prev +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkZygoteContextNative(JNIEnv *env, jobject thiz) { + return RootDetector::checkZygoteContextNative(); +} + +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkZygoteContextSyscall(JNIEnv *env, jobject thiz) { + return RootDetector::checkZygoteContextSyscall(); +} + // ===================== Hook Detection ===================== JNIEXPORT jboolean 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..b292941 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -36,6 +36,14 @@ public class NativeDetector { public native boolean checkMountInfoNative(); public native boolean checkMountInfoSyscall(); + /** Check /proc/self/mounts for Magisk mount signatures via direct syscall */ + public native boolean checkMountsForMagiskNative(); + public native boolean checkMountsForMagiskSyscall(); + + /** Check /proc/self/attr/prev for zygote SELinux context anomaly */ + public native boolean checkZygoteContextNative(); + public native boolean checkZygoteContextSyscall(); + // ===================== Hook Detection ===================== public native boolean checkXposedNative(); 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..5b50f2c 100644 --- a/app/src/main/java/com/xff/launch/detector/RootDetector.java +++ b/app/src/main/java/com/xff/launch/detector/RootDetector.java @@ -334,6 +334,83 @@ public DetectionItem detectSuspiciousMounts() { return item; } + /** + * Detect Magisk mount signatures in /proc/self/mounts + * Uses direct syscall to bypass libc hooks + */ + public DetectionItem detectMountsForMagisk() { + DetectionItem item = new DetectionItem("Mounts Magisk 签名", + "通过 /proc/mounts 搜索 Magisk 挂载特征"); + + // Native layer + boolean nativeResult = nativeDetector.checkMountsForMagiskNative(); + item.setLayerResult(DetectionLayer.NATIVE, nativeResult); + + // Syscall layer + boolean syscallResult = nativeDetector.checkMountsForMagiskSyscall(); + item.setLayerResult(DetectionLayer.SYSCALL, syscallResult); + + if (item.getMostTrustworthyResult()) { + item.setStatus(DetectionStatus.RISK); + item.setDetail("检测到 Magisk 挂载签名"); + item.addDetectionDetail("💾 /proc/mounts", "Magisk 挂载特征", + "搜索词: /sbin/.magisk/, magisk, /sbin/.core/", + DetectionLayer.SYSCALL, "🔍"); + } else { + item.setStatus(DetectionStatus.SAFE); + item.setDetail("未检测到"); + } + + if (item.hasInconsistentResults()) { + item.setDetail(item.getDetail() + " (检测层不一致)"); + } + + return item; + } + + /** + * Detect zygote SELinux context anomaly via /proc/self/attr/prev + * Uses direct syscall to bypass libc hooks + * Normal app processes should have "zygote" in their prev SELinux context + */ + public DetectionItem detectZygoteContext() { + DetectionItem item = new DetectionItem("Zygote 上下文检测", + "通过 /proc/self/attr/prev 检测 Zygote SELinux 上下文"); + + // Native layer + boolean nativeResult = nativeDetector.checkZygoteContextNative(); + item.setLayerResult(DetectionLayer.NATIVE, nativeResult); + + // Syscall layer + boolean syscallResult = nativeDetector.checkZygoteContextSyscall(); + item.setLayerResult(DetectionLayer.SYSCALL, syscallResult); + + if (item.getMostTrustworthyResult()) { + item.setStatus(DetectionStatus.WARNING); + item.setDetail("Zygote 上下文异常"); + item.addDetectionDetail("🔑 SELinux 上下文", "/proc/self/attr/prev", + "正常应包含 \"zygote\" (如 u:r:zygote:s0)\n" + + "上下文中未找到 zygote 标识", + DetectionLayer.SYSCALL, "⚠️"); + } else { + item.setStatus(DetectionStatus.SAFE); + item.setDetail("Zygote 上下文正常"); + + // 显示当前 SELinux context 作为参考 + String context = nativeDetector.getSELinuxContextNative(); + if (context != null && !context.isEmpty()) { + item.addDetectionDetail("🔑 SELinux 上下文", "attr/prev", + "当前上下文: " + context, DetectionLayer.NATIVE, "✅"); + } + } + + if (item.hasInconsistentResults()) { + item.setDetail(item.getDetail() + " (检测层不一致)"); + } + + return item; + } + /** * Get all root detection items */ @@ -347,6 +424,8 @@ public List getAllDetections() { items.add(detectRootManagers()); items.add(detectRootHiding()); items.add(detectSuspiciousMounts()); + items.add(detectMountsForMagisk()); + items.add(detectZygoteContext()); return items; } From 55578e5ae68752bfa31e79d97e049ca5848b3cc4 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 4/9] 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 | 101 +++++++ 3 files changed, 371 insertions(+) diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 970f0b7..6b90115 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -1682,6 +1682,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 b292941..ce11f9a 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -313,6 +313,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 5b50f2c..4350af5 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 */ @@ -424,11 +427,63 @@ public List getAllDetections() { items.add(detectRootManagers()); items.add(detectRootHiding()); items.add(detectSuspiciousMounts()); +<<<<<<< HEAD items.add(detectMountsForMagisk()); items.add(detectZygoteContext()); +======= + items.add(detectSameUidProcesses()); +>>>>>>> fba3cf9 (feat(root): add same-UID process scanning detection) 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() { @@ -967,4 +1022,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); + } + } } From 701966810a8320d80d293ce6e90811233dd33be1 Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:52:21 +0800 Subject: [PATCH 5/9] feat(native): add anti-timing attack and DumpArtMethod hook detection - Add DebugDetector::checkInitTimingAttack() to detect debugger breakpoints by measuring initialization elapsed time (>= 2s threshold) - Add HookDetector::checkDumpArtMethodHookNative/Syscall() to scan /proc/self/maps for dumpArtMethod symbol and related ART dumping tools (FDex2, DexDump, DexHunter, etc.) - Add JNI bindings: captureInitStartTime(), checkInitTimingAttack(), checkDumpArtMethodHookNative(), checkDumpArtMethodHookSyscall() - Add Java native declarations in NativeDetector.java - Sanitize all comments to remove reverse-engineering source references --- app/src/main/cpp/detector/debug_detector.cpp | 22 +++++ app/src/main/cpp/detector/debug_detector.h | 7 ++ app/src/main/cpp/detector/hook_detector.cpp | 89 +++++++++++++++++++ app/src/main/cpp/detector/hook_detector.h | 6 ++ app/src/main/cpp/native-lib.cpp | 48 ++++++++++ .../xff/launch/detector/NativeDetector.java | 35 ++++++++ .../com/xff/launch/detector/RootDetector.java | 3 - 7 files changed, 207 insertions(+), 3 deletions(-) diff --git a/app/src/main/cpp/detector/debug_detector.cpp b/app/src/main/cpp/detector/debug_detector.cpp index c53f245..b62857e 100644 --- a/app/src/main/cpp/detector/debug_detector.cpp +++ b/app/src/main/cpp/detector/debug_detector.cpp @@ -98,3 +98,25 @@ MultiLayerResult DebugDetector::detectPtrace() { result.syscallResult = false; // Can't check ptrace via syscall reliably return result; } + +// Anti-timing attack detection +// Detects debugger breakpoints by measuring initialization elapsed time. +// If a debugger inserts breakpoints during security initialization, +// execution will take abnormally long (>= 2 seconds). +// The caller should capture time_t at the start of initialization, then +// call this method to check if too much time has elapsed. +bool DebugDetector::checkInitTimingAttack(time_t initStartTime) { + time_t currentTime; + time(¤tTime); + + time_t elapsed = currentTime - initStartTime; + + if (elapsed >= 2) { + LOGD("🚨 Anti-timing attack: init took %ld seconds (>= 2s threshold), " + "debugger breakpoints suspected!", (long)elapsed); + return true; // Timing attack detected + } + + LOGD("Anti-timing: init completed in %ld seconds (normal)", (long)elapsed); + return false; // Normal execution time +} diff --git a/app/src/main/cpp/detector/debug_detector.h b/app/src/main/cpp/detector/debug_detector.h index 0fcae8c..dab15de 100644 --- a/app/src/main/cpp/detector/debug_detector.h +++ b/app/src/main/cpp/detector/debug_detector.h @@ -3,6 +3,7 @@ #include #include +#include struct MultiLayerResult; @@ -22,6 +23,12 @@ class DebugDetector { // Get TracerPid value static int getTracerPid(); + + // Anti-timing attack detection + // Measures initialization elapsed time, if >= 2s, debugger breakpoints detected + // @param initStartTime The time_t value captured at init start + // @return true if timing attack detected (init took >= 2 seconds) + static bool checkInitTimingAttack(time_t initStartTime); }; #endif // LAUNCH_DEBUG_DETECTOR_H diff --git a/app/src/main/cpp/detector/hook_detector.cpp b/app/src/main/cpp/detector/hook_detector.cpp index 69777d0..c0ab4c7 100644 --- a/app/src/main/cpp/detector/hook_detector.cpp +++ b/app/src/main/cpp/detector/hook_detector.cpp @@ -1282,3 +1282,92 @@ MultiLayerResult HookDetector::detectMemoryHooks() { result.syscallResult = checkMapsForHooksSyscall() || checkFridaMemorySyscall(); return result; } + +// ===================== DumpArtMethod Hook Detection ===================== +// +// The dumpArtMethod symbol is used by ART method dumping/hooking tools: +// - FDex2: Dumps DEX files from memory +// - DexDump: ART method dumping +// - Various ART hooking frameworks that patch ArtMethod structures +// +// Detection: Read /proc/self/maps and search for "dumpArtMethod" symbol +// If found, it means a hooking tool has injected a library that exports +// (or references) the dumpArtMethod function. + +bool HookDetector::checkDumpArtMethodHookNative() { + LOGD("=== checkDumpArtMethodHookNative() START ==="); + + // Read /proc/self/maps using libc + std::ifstream maps("/proc/self/maps"); + if (!maps.is_open()) { + LOGD("Failed to open /proc/self/maps (native)"); + return false; + } + + std::string line; + while (std::getline(maps, line)) { + // Search for dumpArtMethod symbol in memory maps + if (line.find("dumpArtMethod") != std::string::npos) { + LOGD("🚨 [DETECTED] dumpArtMethod found in maps (native): %s", line.c_str()); + maps.close(); + return true; + } + + // Also check for related dumping tool signatures + if (line.find("libdexdump") != std::string::npos || + line.find("libFDex2") != std::string::npos || + line.find("fdex2") != std::string::npos || + line.find("dexdump") != std::string::npos || + line.find("libdexhunter") != std::string::npos || + line.find("dexhunter") != std::string::npos || + line.find("libunpacker") != std::string::npos) { + LOGD("🚨 [DETECTED] ART dumping tool found in maps (native): %s", line.c_str()); + maps.close(); + return true; + } + } + + maps.close(); + LOGD("=== checkDumpArtMethodHookNative() END - Clean ==="); + return false; +} + +bool HookDetector::checkDumpArtMethodHookSyscall() { + LOGD("=== checkDumpArtMethodHookSyscall() START ==="); + + // Read /proc/self/maps using direct syscall (bypass libc hooks) + std::string maps = syscall_read_file("/proc/self/maps", 131072); // 128KB + if (maps.empty()) { + LOGD("Failed to read /proc/self/maps (syscall)"); + return false; + } + + // Search for dumpArtMethod symbol + if (maps.find("dumpArtMethod") != std::string::npos) { + LOGD("🚨 [DETECTED] dumpArtMethod found in maps (syscall)"); + return true; + } + + // Also check for related dumping tool signatures + if (maps.find("libdexdump") != std::string::npos || + maps.find("libFDex2") != std::string::npos || + maps.find("fdex2") != std::string::npos || + maps.find("dexdump") != std::string::npos || + maps.find("libdexhunter") != std::string::npos || + maps.find("dexhunter") != std::string::npos || + maps.find("libunpacker") != std::string::npos) { + LOGD("🚨 [DETECTED] ART dumping tool found in maps (syscall)"); + return true; + } + + LOGD("=== checkDumpArtMethodHookSyscall() END - Clean ==="); + return false; +} + +MultiLayerResult HookDetector::detectDumpArtMethodHook() { + MultiLayerResult result; + result.javaResult = false; + result.nativeResult = checkDumpArtMethodHookNative(); + result.syscallResult = checkDumpArtMethodHookSyscall(); + return result; +} diff --git a/app/src/main/cpp/detector/hook_detector.h b/app/src/main/cpp/detector/hook_detector.h index ae6b5fe..1567b6c 100644 --- a/app/src/main/cpp/detector/hook_detector.h +++ b/app/src/main/cpp/detector/hook_detector.h @@ -66,6 +66,12 @@ class HookDetector { static MultiLayerResult detectZygisk(); // 改为通用 Zygisk 检测 static MultiLayerResult detectSmapsHook(); // SMAPS 内存取证检测 static MultiLayerResult detectMemoryHooks(); + static MultiLayerResult detectDumpArtMethodHook(); // DumpArtMethod Hook 检测 + + // DumpArtMethod Hook Detection + // Scans /proc/self/maps for dumpArtMethod symbol - indicates ART method dumping tools + static bool checkDumpArtMethodHookNative(); + static bool checkDumpArtMethodHookSyscall(); }; #endif // LAUNCH_HOOK_DETECTOR_H diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 6b90115..c6fd052 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -1938,4 +1938,52 @@ Java_com_xff_launch_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) { return env->NewStringUTF("Launch - 设备环境检测"); } +// ===================== Anti-Timing Attack Detection ===================== + +/** + * Capture current time for anti-timing attack measurement + * Call this at the START of security initialization + * @return Current time as seconds since epoch (to pass to checkInitTimingAttack) + */ +JNIEXPORT jlong JNICALL +Java_com_xff_launch_detector_NativeDetector_captureInitStartTime(JNIEnv *env, jobject thiz) { + time_t timer; + time(&timer); + return (jlong)timer; +} + +/** + * Check if security initialization took too long (>= 2 seconds) + * This indicates debugger breakpoints were inserted during initialization + * @param initStartTime The value returned by captureInitStartTime() + * @return true if timing attack detected (init took >= 2 seconds) + */ +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkInitTimingAttack(JNIEnv *env, jobject thiz, + jlong initStartTime) { + return DebugDetector::checkInitTimingAttack((time_t)initStartTime); +} + +// ===================== DumpArtMethod Hook Detection ===================== + +/** + * Check for dumpArtMethod hook via native (libc) + * Scans /proc/self/maps for dumpArtMethod symbol and related dumping tools + * @return true if dumpArtMethod hook detected + */ +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkDumpArtMethodHookNative(JNIEnv *env, jobject thiz) { + return HookDetector::checkDumpArtMethodHookNative(); +} + +/** + * Check for dumpArtMethod hook via direct syscall + * Same check but using direct syscall to bypass libc hooks + * @return true if dumpArtMethod hook detected + */ +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkDumpArtMethodHookSyscall(JNIEnv *env, jobject thiz) { + return HookDetector::checkDumpArtMethodHookSyscall(); +} + } // extern "C" 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 ce11f9a..b8d4008 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -333,6 +333,41 @@ public class NativeDetector { * @return JSON string with process details (pid, name, data_dir existence) */ public native String getSameUidProcessDetails(); + + // ===================== Anti-Timing Attack Detection ===================== + + /** + * Capture current time for anti-timing attack measurement. + * Call this at the START of security initialization. + * @return Current time as seconds since epoch + */ + public native long captureInitStartTime(); + + /** + * Check if security initialization took too long (>= 2 seconds). + * If initialization is suspiciously slow, it indicates a debugger has + * inserted breakpoints during the process. + * @param initStartTime The value returned by captureInitStartTime() + * @return true if timing attack detected (init took >= 2 seconds) + */ + public native boolean checkInitTimingAttack(long initStartTime); + + // ===================== DumpArtMethod Hook Detection ===================== + + /** + * Check for dumpArtMethod hook via native (libc). + * Scans /proc/self/maps for dumpArtMethod symbol and related + * ART method dumping tools (FDex2, DexDump, DexHunter, etc.) + * @return true if dumpArtMethod hook detected + */ + public native boolean checkDumpArtMethodHookNative(); + + /** + * Check for dumpArtMethod hook via direct syscall. + * Same check but using direct syscall to bypass libc hooks. + * @return true if dumpArtMethod hook detected + */ + public native boolean checkDumpArtMethodHookSyscall(); // 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 4350af5..f9aebac 100644 --- a/app/src/main/java/com/xff/launch/detector/RootDetector.java +++ b/app/src/main/java/com/xff/launch/detector/RootDetector.java @@ -427,12 +427,9 @@ public List getAllDetections() { items.add(detectRootManagers()); items.add(detectRootHiding()); items.add(detectSuspiciousMounts()); -<<<<<<< HEAD items.add(detectMountsForMagisk()); items.add(detectZygoteContext()); -======= items.add(detectSameUidProcesses()); ->>>>>>> fba3cf9 (feat(root): add same-UID process scanning detection) return items; } From a496abbb300079f28962809e35c9fcfa9f4e61ca Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:49:53 +0800 Subject: [PATCH 6/9] feat(hook): add missing anti-debug and anti-Frida detection methods - Add /proc/net/tcp hex port scanning for IDA (0x5D8A=23946) and Frida (0x69A2=27042) - Add linjector thread name detection in checkFridaThreads() via both comm and syscall status - Add frida-agent-32/frida-agent-64 signatures to memory maps scanning - Add /proc/self/fd linjector pipe scanning via syscall(readlinkat) - Add linjector to suspicious FD keywords in native-lib.cpp - Integrate new detections into detectFrida() combined result and JNI layer - Add checkIdaPortTcp/checkFridaPortTcp/checkFridaFdLinjector native methods - Update Java-side collectFridaDetails() with TCP port and FD linjector details --- app/src/main/cpp/detector/hook_detector.cpp | 181 +++++++++++++++++- app/src/main/cpp/detector/hook_detector.h | 11 +- app/src/main/cpp/native-lib.cpp | 37 +++- .../xff/launch/detector/DebugDetector.java | 12 ++ .../com/xff/launch/detector/HookDetector.java | 44 ++++- .../xff/launch/detector/NativeDetector.java | 14 ++ 6 files changed, 286 insertions(+), 13 deletions(-) diff --git a/app/src/main/cpp/detector/hook_detector.cpp b/app/src/main/cpp/detector/hook_detector.cpp index c0ab4c7..9315a10 100644 --- a/app/src/main/cpp/detector/hook_detector.cpp +++ b/app/src/main/cpp/detector/hook_detector.cpp @@ -6,12 +6,24 @@ #include #include #include +#include #include #include #include #include #include #include +#include +#include + +// linux_dirent64 structure definition (not directly exposed by NDK) +struct linux_dirent64 { + uint64_t d_ino; + int64_t d_off; + unsigned short d_reclen; + unsigned char d_type; + char d_name[0]; +}; #define LOG_TAG "HookDetector" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) @@ -116,7 +128,10 @@ bool HookDetector::checkFridaMemoryNative() { if (line.find("frida") != std::string::npos || line.find("gadget") != std::string::npos || line.find("gum-js-loop") != std::string::npos || - line.find("LIBFRIDA") != std::string::npos) { + line.find("LIBFRIDA") != std::string::npos || + line.find("frida-agent-32") != std::string::npos || + line.find("frida-agent-64") != std::string::npos || + line.find("frida-agent") != std::string::npos) { LOGD("Frida memory signature found (native): %s", line.c_str()); return true; } @@ -281,7 +296,10 @@ bool HookDetector::checkFridaMemorySyscall() { if (content.find("frida") != std::string::npos || content.find("gadget") != std::string::npos || content.find("gum-js-loop") != std::string::npos || - content.find("LIBFRIDA") != std::string::npos) { + content.find("LIBFRIDA") != std::string::npos || + content.find("frida-agent-32") != std::string::npos || + content.find("frida-agent-64") != std::string::npos || + content.find("frida-agent") != std::string::npos) { LOGD("Frida memory signature found (syscall)"); return true; } @@ -418,17 +436,166 @@ bool HookDetector::checkFridaThreads() { if (threadName.find("gmain") != std::string::npos || threadName.find("gdbus") != std::string::npos || threadName.find("gum-js-loop") != std::string::npos || - threadName.find("pool-frida") != std::string::npos) { + threadName.find("pool-frida") != std::string::npos || + threadName.find("linjector") != std::string::npos) { LOGD("Frida thread found: %s", threadName.c_str()); closedir(dir); return true; } } + + // Also read Name field from /proc/self/task//status via syscall + // This approach is harder to intercept by hooking than reading comm + std::string tidStatusPath = "/proc/self/task/" + std::string(entry->d_name) + "/status"; + std::string statusContent = syscall_read_file(tidStatusPath.c_str(), 1024); + if (!statusContent.empty()) { + // Find the "Name:" line + size_t namePos = statusContent.find("Name:"); + if (namePos != std::string::npos) { + size_t lineEnd = statusContent.find('\n', namePos); + std::string nameLine = statusContent.substr(namePos + 5, + lineEnd != std::string::npos ? lineEnd - namePos - 5 : std::string::npos); + // Trim leading/trailing whitespace + size_t start = nameLine.find_first_not_of(" \t"); + if (start != std::string::npos) { + nameLine = nameLine.substr(start); + } + if (nameLine.find("linjector") != std::string::npos || + nameLine.find("gmain") != std::string::npos) { + LOGD("Frida thread found via syscall status: %s (tid: %s)", + nameLine.c_str(), entry->d_name); + closedir(dir); + return true; + } + } + } } closedir(dir); return false; } +// ===================== /proc/net/tcp IDA Port Scanning ===================== + +/** + * Detect IDA Pro android_server port (23946 = 0x5D8A) by parsing /proc/net/tcp + * Uses libc file I/O + */ +bool HookDetector::checkIdaPortTcpNative() { + std::ifstream tcp("/proc/net/tcp"); + if (!tcp.is_open()) return false; + + std::string line; + while (std::getline(tcp, line)) { + // /proc/net/tcp format: sl local_address rem_address ... + // local_address format: IP:PORT (hex) + // IDA default port 23946 = 0x5D8A + if (line.find(":5D8A") != std::string::npos) { + LOGD("IDA port 23946 (0x5D8A) found in /proc/net/tcp (native): %s", line.c_str()); + return true; + } + } + return false; +} + +/** + * Detect IDA port by reading /proc/net/tcp via syscall + * Uses direct syscall to bypass libc hooks + */ +bool HookDetector::checkIdaPortTcpSyscall() { + std::string content = syscall_read_file("/proc/net/tcp", 65536); + if (content.empty()) return false; + + if (content.find(":5D8A") != std::string::npos) { + LOGD("IDA port 23946 (0x5D8A) found in /proc/net/tcp (syscall)"); + return true; + } + return false; +} + +/** + * Detect Frida default port (27042 = 0x69A2) by parsing /proc/net/tcp + * Uses libc file I/O + */ +bool HookDetector::checkFridaPortTcpNative() { + std::ifstream tcp("/proc/net/tcp"); + if (!tcp.is_open()) return false; + + std::string line; + while (std::getline(tcp, line)) { + // Frida default port 27042 = 0x69A2 + // Frida alternate port 27043 = 0x69A3 + if (line.find(":69A2") != std::string::npos || + line.find(":69A3") != std::string::npos) { + LOGD("Frida port found in /proc/net/tcp (native): %s", line.c_str()); + return true; + } + } + return false; +} + +/** + * Detect Frida port by reading /proc/net/tcp via syscall + */ +bool HookDetector::checkFridaPortTcpSyscall() { + std::string content = syscall_read_file("/proc/net/tcp", 65536); + if (content.empty()) return false; + + if (content.find(":69A2") != std::string::npos || + content.find(":69A3") != std::string::npos) { + LOGD("Frida port found in /proc/net/tcp (syscall)"); + return true; + } + return false; +} + +// ===================== FD linjector Scanning ===================== + +/** + * Scan /proc/self/fd via syscall(readlinkat) for linjector + */ +bool HookDetector::checkFridaFdLinjectorSyscall() { + // Open /proc/self/fd directory using syscall + int dirFd = syscall(__NR_openat, AT_FDCWD, "/proc/self/fd", O_RDONLY | O_DIRECTORY); + if (dirFd < 0) return false; + + char buffer[4096]; + char linkPath[64]; + char targetPath[PATH_MAX]; + bool found = false; + + while (true) { + int nread = syscall(__NR_getdents64, dirFd, buffer, sizeof(buffer)); + if (nread <= 0) break; + + int pos = 0; + while (pos < nread) { + struct linux_dirent64* d = (struct linux_dirent64*)(buffer + pos); + + if (d->d_name[0] != '.') { + snprintf(linkPath, sizeof(linkPath), "/proc/self/fd/%s", d->d_name); + + ssize_t len = syscall(__NR_readlinkat, AT_FDCWD, linkPath, + targetPath, sizeof(targetPath) - 1); + if (len > 0) { + targetPath[len] = '\0'; + // Search for linjector (Frida injector pipe/file) + if (strstr(targetPath, "linjector") != nullptr) { + LOGD("Frida linjector FD found: %s -> %s", linkPath, targetPath); + found = true; + break; + } + } + } + + pos += d->d_reclen; + } + if (found) break; + } + + syscall(__NR_close, dirFd); + return found; +} + // Get detailed LSPosed injection information std::string HookDetector::getLSPosedDetails() { std::string details; @@ -1158,7 +1325,7 @@ bool HookDetector::checkZygiskSyscall() { LOGD("[6/7] 检测 Zygote 注入 (分析父进程)..."); std::string statContent = syscall_read_file("/proc/self/stat", 1024); if (!statContent.empty()) { - LOGD(" ├─ 成功读取 /proc/self/stat: %lu 字节", statContent.size()); + LOGD(" ├─ 成功读取 /proc/self/stat: %zu 字节", statContent.size()); // Parse PPID std::istringstream iss(statContent); @@ -1240,8 +1407,10 @@ MultiLayerResult HookDetector::detectFrida() { MultiLayerResult result; result.javaResult = false; result.nativeResult = checkFridaNative() || checkFridaPortsNative() || - checkFridaMemoryNative() || checkFridaThreads(); - result.syscallResult = checkFridaSyscall() || checkFridaMemorySyscall(); + checkFridaMemoryNative() || checkFridaThreads() || + checkFridaPortTcpNative(); + result.syscallResult = checkFridaSyscall() || checkFridaMemorySyscall() || + checkFridaPortTcpSyscall() || checkFridaFdLinjectorSyscall(); return result; } diff --git a/app/src/main/cpp/detector/hook_detector.h b/app/src/main/cpp/detector/hook_detector.h index 1567b6c..bbf3894 100644 --- a/app/src/main/cpp/detector/hook_detector.h +++ b/app/src/main/cpp/detector/hook_detector.h @@ -39,9 +39,18 @@ class HookDetector { static bool checkRiruZygiskSyscall(); static bool checkMapsForHooksSyscall(); - // Thread name detection for Frida + // Thread name detection for Frida (including linjector via syscall) static bool checkFridaThreads(); + // /proc/net/tcp port scanning (IDA & Frida) + static bool checkIdaPortTcpNative(); // IDA port 23946 (0x5D8A) + static bool checkIdaPortTcpSyscall(); + static bool checkFridaPortTcpNative(); // Frida port 27042 (0x69A2) + static bool checkFridaPortTcpSyscall(); + + // FD linjector scanning (Frida injector pipe detection) + static bool checkFridaFdLinjectorSyscall(); + // Get detailed info about LSPosed injection static std::string getLSPosedDetails(); diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index c6fd052..b0c2717 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -180,12 +180,14 @@ Java_com_xff_launch_detector_NativeDetector_checkXposedSyscall(JNIEnv *env, jobj JNIEXPORT jboolean JNICALL Java_com_xff_launch_detector_NativeDetector_checkFridaNative(JNIEnv *env, jobject thiz) { return HookDetector::checkFridaNative() || HookDetector::checkFridaPortsNative() || - HookDetector::checkFridaMemoryNative() || HookDetector::checkFridaThreads(); + HookDetector::checkFridaMemoryNative() || HookDetector::checkFridaThreads() || + HookDetector::checkFridaPortTcpNative(); } JNIEXPORT jboolean JNICALL Java_com_xff_launch_detector_NativeDetector_checkFridaSyscall(JNIEnv *env, jobject thiz) { - return HookDetector::checkFridaSyscall() || HookDetector::checkFridaMemorySyscall(); + return HookDetector::checkFridaSyscall() || HookDetector::checkFridaMemorySyscall() || + HookDetector::checkFridaPortTcpSyscall() || HookDetector::checkFridaFdLinjectorSyscall(); } JNIEXPORT jboolean JNICALL @@ -262,6 +264,33 @@ Java_com_xff_launch_detector_NativeDetector_checkZygiskSyscall(JNIEnv *env, jobj return HookDetector::checkZygiskSyscall(); } +// /proc/net/tcp port scanning (IDA & Frida) +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkIdaPortTcpNative(JNIEnv *env, jobject thiz) { + return HookDetector::checkIdaPortTcpNative(); +} + +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkIdaPortTcpSyscall(JNIEnv *env, jobject thiz) { + return HookDetector::checkIdaPortTcpSyscall(); +} + +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkFridaPortTcpNative(JNIEnv *env, jobject thiz) { + return HookDetector::checkFridaPortTcpNative(); +} + +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkFridaPortTcpSyscall(JNIEnv *env, jobject thiz) { + return HookDetector::checkFridaPortTcpSyscall(); +} + +// Frida FD linjector detection +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkFridaFdLinjectorSyscall(JNIEnv *env, jobject thiz) { + return HookDetector::checkFridaFdLinjectorSyscall(); +} + // ===================== Emulator Detection ===================== JNIEXPORT jboolean JNICALL @@ -342,9 +371,9 @@ static const char* SUSPICIOUS_KEYWORDS[] = { "magisk", "supersu", "superuser", "busybox", "kernelsu", "apatch", "lsposed", "edxposed", "xposed", "riru", "zygisk", "shamiko", - "frida", "substrate", "cydia" + "frida", "substrate", "cydia", "linjector" }; -static const int SUSPICIOUS_KEYWORDS_COUNT = 15; +static const int SUSPICIOUS_KEYWORDS_COUNT = 16; // Short/ambiguous keywords - require word boundary matching to avoid false positives // e.g. "su" in "Consumer", "ashmem", "result"; "hide" in "override", "hidden" diff --git a/app/src/main/java/com/xff/launch/detector/DebugDetector.java b/app/src/main/java/com/xff/launch/detector/DebugDetector.java index 5314783..13e5b94 100644 --- a/app/src/main/java/com/xff/launch/detector/DebugDetector.java +++ b/app/src/main/java/com/xff/launch/detector/DebugDetector.java @@ -547,6 +547,18 @@ private void collectJdwpDetails(DetectionItem item) { line.trim(), DetectionLayer.SYSCALL, "🔍"); } + // IDA Pro android_server 默认端口 23946 = 0x5D8A + if (line.contains(":5D8A")) { + item.addDetectionDetail("🌐 IDA 端口", "/proc/net/tcp", + "检测到 IDA Pro 端口 23946 (0x5D8A): " + line.trim(), + DetectionLayer.JAVA, "🔌"); + } + // Frida 默认端口 27042 = 0x69A2, 27043 = 0x69A3 + if (line.contains(":69A2") || line.contains(":69A3")) { + item.addDetectionDetail("🌐 Frida 端口", "/proc/net/tcp", + "检测到 Frida 端口: " + line.trim(), + DetectionLayer.JAVA, "🔌"); + } } reader.close(); } catch (Exception ignored) { diff --git a/app/src/main/java/com/xff/launch/detector/HookDetector.java b/app/src/main/java/com/xff/launch/detector/HookDetector.java index 59a2fff..774aede 100644 --- a/app/src/main/java/com/xff/launch/detector/HookDetector.java +++ b/app/src/main/java/com/xff/launch/detector/HookDetector.java @@ -1997,7 +1997,10 @@ private void collectFridaDetails(DetectionItem item) { String lowerLine = line.toLowerCase(); if (lowerLine.contains("frida") || lowerLine.contains("gum-js-loop") || - lowerLine.contains("frida-agent")) { + lowerLine.contains("frida-agent") || + lowerLine.contains("frida-agent-32") || + lowerLine.contains("frida-agent-64") || + lowerLine.contains("linjector")) { fridaMaps++; if (fridaMaps <= 3) { String[] parts = line.split("\\s+"); @@ -2035,7 +2038,8 @@ private void collectFridaDetails(DetectionItem item) { (threadName.contains("gmain") || threadName.contains("gdbus") || threadName.contains("gum-js-loop") || - threadName.contains("pool-frida"))) { + threadName.contains("pool-frida") || + threadName.contains("linjector"))) { item.addDetectionDetail("🧵 Frida 线程", threadName, "TID: " + task.getName() + "\n线程名: " + threadName, DetectionLayer.NATIVE, "🔗"); @@ -2046,5 +2050,41 @@ private void collectFridaDetails(DetectionItem item) { } } catch (Exception ignored) { } + + // 检测 /proc/net/tcp 中的 Frida/IDA 端口 (十六进制扫描) + try { + java.io.BufferedReader reader = new java.io.BufferedReader( + new java.io.FileReader("/proc/net/tcp")); + String line; + while ((line = reader.readLine()) != null) { + // Frida 默认端口 27042 = 0x69A2 + if (line.contains(":69A2") || line.contains(":69A3")) { + item.addDetectionDetail("🌐 Frida TCP 端口", "/proc/net/tcp", + "检测到 Frida 端口 (十六进制扫描): " + line.trim(), + DetectionLayer.JAVA, "🔌"); + } + } + reader.close(); + } catch (Exception ignored) { + } + + // 检测 FD 中的 linjector (Frida 注入器管道) + try { + java.io.File fdDir = new java.io.File("/proc/self/fd"); + String[] fds = fdDir.list(); + if (fds != null) { + for (String fd : fds) { + try { + String target = nativeDetector.readlinkSyscall("/proc/self/fd/" + fd); + if (target != null && target.contains("linjector")) { + item.addDetectionDetail("📁 Frida 注入器", "FD linjector", + "FD " + fd + " -> " + target, + DetectionLayer.SYSCALL, "💉"); + } + } catch (Exception ignored) {} + } + } + } catch (Exception ignored) { + } } } 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 b8d4008..65bcf08 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -74,6 +74,20 @@ public class NativeDetector { public native boolean checkZygiskNative(); public native boolean checkZygiskSyscall(); + // /proc/net/tcp port scanning (IDA & Frida) + /** Detect IDA port 23946 (0x5D8A) via /proc/net/tcp - native */ + public native boolean checkIdaPortTcpNative(); + /** Detect IDA port 23946 (0x5D8A) via /proc/net/tcp - syscall */ + public native boolean checkIdaPortTcpSyscall(); + /** Detect Frida port 27042 (0x69A2) via /proc/net/tcp - native */ + public native boolean checkFridaPortTcpNative(); + /** Detect Frida port 27042 (0x69A2) via /proc/net/tcp - syscall */ + public native boolean checkFridaPortTcpSyscall(); + + // Frida FD linjector detection + /** Scan /proc/self/fd via syscall(readlinkat) for linjector injector */ + public native boolean checkFridaFdLinjectorSyscall(); + // ===================== Emulator Detection ===================== public native boolean checkEmulatorNative(); From 72a8e559c3eeb4af5416e1047243c2682b947a16 Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:15:23 +0800 Subject: [PATCH 7/9] feat(native): add suspicious tool path detection Add multi-layer detection for 13 suspicious tool paths including IDA/GDB debuggers, Frida gadget, injection tools, and unpackers (FART/BlackDex/DEX Dump). - Add checkSuspiciousToolPathsNative/Syscall in DebugDetector (C++) - Add JNI bridge functions in native-lib.cpp - Add native method declarations in NativeDetector.java - Add detectSuspiciousToolPaths() with Java/Native/Syscall layers in DebugDetector.java - Add detail collector with per-path breakdown and detection layer info - Add string resource entry in strings.xml --- app/src/main/cpp/detector/debug_detector.cpp | 87 ++++++++++ app/src/main/cpp/detector/debug_detector.h | 9 + app/src/main/cpp/native-lib.cpp | 17 ++ .../xff/launch/detector/DebugDetector.java | 162 ++++++++++++++++++ .../xff/launch/detector/NativeDetector.java | 8 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 284 insertions(+) diff --git a/app/src/main/cpp/detector/debug_detector.cpp b/app/src/main/cpp/detector/debug_detector.cpp index b62857e..6e1309e 100644 --- a/app/src/main/cpp/detector/debug_detector.cpp +++ b/app/src/main/cpp/detector/debug_detector.cpp @@ -119,4 +119,91 @@ bool DebugDetector::checkInitTimingAttack(time_t initStartTime) { LOGD("Anti-timing: init completed in %ld seconds (normal)", (long)elapsed); return false; // Normal execution time + +// ===================== Suspicious Tool Path Detection ===================== +// Iterates over suspicious tool path array, using access() to check file existence + +// Suspicious tool path entries with descriptions +struct ToolPathEntry { + const char* path; + const char* description; +}; + +static const ToolPathEntry SUSPICIOUS_TOOL_PATHS[] = { + {"/data/local/tmp/android_server", "IDA debugger (32-bit)"}, + {"/data/local/tmp/android_server64", "IDA debugger (64-bit)"}, + {"/data/local/tmp/gdbserver", "GDB debugger"}, + {"/data/local/tmp/inject", "Injection tool"}, + {"/data/local/tmp/libhello.so", "Frida gadget"}, + {"/sdcard/xxxx/", "Unpacker (FART etc.)"}, + {"/sdcard/ooxx/", "Unpacker"}, + {"/sdcard/fart/", "FART unpacker"}, + {"/sdcard/Download/dexDump/", "DEX dump tool"}, + {"/sdcard/Download/top.niunaijun.blackdexa32_logcat.txt", "BlackDex log (32-bit)"}, + {"/sdcard/Download/top.niunaijun.blackdexa64_logcat.txt", "BlackDex log (64-bit)"}, + {"/data/data/top.niunaijun.blackdexa32", "BlackDex app (32-bit)"}, + {"/data/data/top.niunaijun.blackdexa64", "BlackDex app (64-bit)"}, +}; +static const int SUSPICIOUS_TOOL_PATHS_COUNT = sizeof(SUSPICIOUS_TOOL_PATHS) / sizeof(SUSPICIOUS_TOOL_PATHS[0]); + +bool DebugDetector::checkSuspiciousToolPathsNative() { + for (int i = 0; i < SUSPICIOUS_TOOL_PATHS_COUNT; i++) { + if (access(SUSPICIOUS_TOOL_PATHS[i].path, F_OK) == 0) { + LOGD("Suspicious tool path found (native): %s [%s]", + SUSPICIOUS_TOOL_PATHS[i].path, SUSPICIOUS_TOOL_PATHS[i].description); + return true; + } + } + return false; +} + +bool DebugDetector::checkSuspiciousToolPathsSyscall() { + for (int i = 0; i < SUSPICIOUS_TOOL_PATHS_COUNT; i++) { + if (syscall_file_exists(SUSPICIOUS_TOOL_PATHS[i].path)) { + LOGD("Suspicious tool path found (syscall): %s [%s]", + SUSPICIOUS_TOOL_PATHS[i].path, SUSPICIOUS_TOOL_PATHS[i].description); + return true; + } + } + return false; +} + +std::string DebugDetector::getDetectedSuspiciousToolPaths() { + std::string result = "["; + bool first = true; + + for (int i = 0; i < SUSPICIOUS_TOOL_PATHS_COUNT; i++) { + bool nativeExists = (access(SUSPICIOUS_TOOL_PATHS[i].path, F_OK) == 0); + bool syscallExists = syscall_file_exists(SUSPICIOUS_TOOL_PATHS[i].path); + + if (nativeExists || syscallExists) { + if (!first) result += ","; + first = false; + + result += "{\"path\":\""; + result += SUSPICIOUS_TOOL_PATHS[i].path; + result += "\",\"desc\":\""; + result += SUSPICIOUS_TOOL_PATHS[i].description; + result += "\",\"native\":"; + result += nativeExists ? "true" : "false"; + result += ",\"syscall\":"; + result += syscallExists ? "true" : "false"; + result += "}"; + + LOGD("Detected suspicious tool: %s [%s] (native:%d, syscall:%d)", + SUSPICIOUS_TOOL_PATHS[i].path, SUSPICIOUS_TOOL_PATHS[i].description, + nativeExists, syscallExists); + } + } + + result += "]"; + return result; +} + +MultiLayerResult DebugDetector::detectSuspiciousToolPaths() { + MultiLayerResult result; + result.javaResult = false; // Not checked at Java layer + result.nativeResult = checkSuspiciousToolPathsNative(); + result.syscallResult = checkSuspiciousToolPathsSyscall(); + return result; } diff --git a/app/src/main/cpp/detector/debug_detector.h b/app/src/main/cpp/detector/debug_detector.h index dab15de..1d46429 100644 --- a/app/src/main/cpp/detector/debug_detector.h +++ b/app/src/main/cpp/detector/debug_detector.h @@ -29,6 +29,15 @@ class DebugDetector { // @param initStartTime The time_t value captured at init start // @return true if timing attack detected (init took >= 2 seconds) static bool checkInitTimingAttack(time_t initStartTime); + + // Suspicious tool path detection + // Detects debuggers, injection tools, Frida gadgets, unpackers, etc. + static bool checkSuspiciousToolPathsNative(); + static bool checkSuspiciousToolPathsSyscall(); + static std::string getDetectedSuspiciousToolPaths(); + + // Combined detection for suspicious tool paths + static MultiLayerResult detectSuspiciousToolPaths(); }; #endif // LAUNCH_DEBUG_DETECTOR_H diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index b0c2717..3a0446f 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -337,6 +337,23 @@ Java_com_xff_launch_detector_NativeDetector_getTracerPid(JNIEnv *env, jobject th return DebugDetector::getTracerPid(); } +// Suspicious tool path detection +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkSuspiciousToolPathsNative(JNIEnv *env, jobject thiz) { + return DebugDetector::checkSuspiciousToolPathsNative(); +} + +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_checkSuspiciousToolPathsSyscall(JNIEnv *env, jobject thiz) { + return DebugDetector::checkSuspiciousToolPathsSyscall(); +} + +JNIEXPORT jstring JNICALL +Java_com_xff_launch_detector_NativeDetector_getDetectedSuspiciousToolPaths(JNIEnv *env, jobject thiz) { + std::string details = DebugDetector::getDetectedSuspiciousToolPaths(); + return env->NewStringUTF(details.c_str()); +} + // ===================== File Operations via Syscall ===================== JNIEXPORT jboolean JNICALL diff --git a/app/src/main/java/com/xff/launch/detector/DebugDetector.java b/app/src/main/java/com/xff/launch/detector/DebugDetector.java index 13e5b94..ef2dacb 100644 --- a/app/src/main/java/com/xff/launch/detector/DebugDetector.java +++ b/app/src/main/java/com/xff/launch/detector/DebugDetector.java @@ -243,6 +243,44 @@ public DetectionItem detectPtraceSelfProtection() { return item; } + /** + * Detect suspicious tool paths + * Checks for debuggers (IDA/GDB), injection tools, Frida gadgets, + * unpackers (FART/BlackDex/DEX Dump), etc. + */ + public DetectionItem detectSuspiciousToolPaths() { + DetectionItem item = new DetectionItem("可疑工具路径", "检测调试器/脱壳工具/注入工具等文件路径"); + + // Java layer - check via File.exists() + boolean javaResult = checkSuspiciousToolPathsJava(); + item.setLayerResult(DetectionLayer.JAVA, javaResult); + + // Native layer - check via libc access() + boolean nativeResult = nativeDetector.checkSuspiciousToolPathsNative(); + item.setLayerResult(DetectionLayer.NATIVE, nativeResult); + + // Syscall layer - check via direct syscall + boolean syscallResult = nativeDetector.checkSuspiciousToolPathsSyscall(); + item.setLayerResult(DetectionLayer.SYSCALL, syscallResult); + + if (item.getMostTrustworthyResult()) { + item.setStatus(DetectionStatus.RISK); + item.setDetail("检测到可疑工具"); + + // Collect detailed detection info + collectSuspiciousToolPathDetails(item); + } else { + item.setStatus(DetectionStatus.SAFE); + item.setDetail("未检测到"); + } + + if (item.hasInconsistentResults()) { + item.setDetail(item.getDetail() + " (检测层不一致)"); + } + + return item; + } + /** * Get all debug detection items */ @@ -253,6 +291,7 @@ public List getAllDetections() { items.add(detectJdwp()); items.add(detectPtrace()); items.add(detectPtraceSelfProtection()); + items.add(detectSuspiciousToolPaths()); return items; } @@ -381,6 +420,38 @@ private String checkAllThreadsTracerPid() { return result.toString(); } + /** + * Java layer check for suspicious tool paths using File.exists() + * Checks suspicious tool paths via Java File API + */ + private boolean checkSuspiciousToolPathsJava() { + String[] toolPaths = { + "/data/local/tmp/android_server", + "/data/local/tmp/android_server64", + "/data/local/tmp/gdbserver", + "/data/local/tmp/inject", + "/data/local/tmp/libhello.so", + "/sdcard/xxxx/", + "/sdcard/ooxx/", + "/sdcard/fart/", + "/sdcard/Download/dexDump/", + "/sdcard/Download/top.niunaijun.blackdexa32_logcat.txt", + "/sdcard/Download/top.niunaijun.blackdexa64_logcat.txt", + "/data/data/top.niunaijun.blackdexa32", + "/data/data/top.niunaijun.blackdexa64" + }; + + for (String path : toolPaths) { + try { + if (new java.io.File(path).exists()) { + return true; + } + } catch (Exception ignored) { + } + } + return false; + } + // ===================== Detail Collection Methods ===================== /** @@ -651,4 +722,95 @@ private void collectPtraceDetails(DetectionItem item, int javaTracerPid, "PID: " + myPid, DetectionLayer.JAVA, "🆔"); } + + /** + * Collect suspicious tool path detection details + */ + private void collectSuspiciousToolPathDetails(DetectionItem item) { + // Get detected path details from native layer (JSON format) + String detectedJson = null; + try { + detectedJson = nativeDetector.getDetectedSuspiciousToolPaths(); + } catch (Exception ignored) { + } + + // Define all paths with descriptions and categories (consistent with native layer) + String[][] toolPaths = { + // {path, description, icon, category} + {"/data/local/tmp/android_server", "IDA 调试器 (32位)", "🔴", "调试器"}, + {"/data/local/tmp/android_server64", "IDA 调试器 (64位)", "🔴", "调试器"}, + {"/data/local/tmp/gdbserver", "GDB 调试器", "🔴", "调试器"}, + {"/data/local/tmp/inject", "注入工具", "🟠", "注入/Frida"}, + {"/data/local/tmp/libhello.so", "Frida gadget", "🟠", "注入/Frida"}, + {"/sdcard/xxxx/", "脱壳工具", "🟡", "脱壳工具"}, + {"/sdcard/ooxx/", "脱壳工具", "🟡", "脱壳工具"}, + {"/sdcard/fart/", "FART 脱壳工具", "🟡", "脱壳工具"}, + {"/sdcard/Download/dexDump/", "DEX Dump 工具", "🟡", "脱壳工具"}, + {"/sdcard/Download/top.niunaijun.blackdexa32_logcat.txt", "BlackDex 日志 (32位)", "🟡", "脱壳工具"}, + {"/sdcard/Download/top.niunaijun.blackdexa64_logcat.txt", "BlackDex 日志 (64位)", "🟡", "脱壳工具"}, + {"/data/data/top.niunaijun.blackdexa32", "BlackDex 应用 (32位)", "🟡", "脱壳工具"}, + {"/data/data/top.niunaijun.blackdexa64", "BlackDex 应用 (64位)", "🟡", "脱壳工具"}, + }; + + int detectedCount = 0; + StringBuilder detectedNames = new StringBuilder(); + + for (String[] entry : toolPaths) { + String path = entry[0]; + String desc = entry[1]; + String icon = entry[2]; + String category = entry[3]; + + // Check via Java layer + boolean javaExists = false; + try { + javaExists = new java.io.File(path).exists(); + } catch (Exception ignored) { + } + + // Check via native layer (fileExistsNative) + boolean nativeExists = false; + try { + nativeExists = nativeDetector.fileExistsNative(path); + } catch (Exception ignored) { + } + + // Check via syscall layer + boolean syscallExists = false; + try { + syscallExists = nativeDetector.fileExistsSyscall(path); + } catch (Exception ignored) { + } + + if (javaExists || nativeExists || syscallExists) { + detectedCount++; + + StringBuilder detail = new StringBuilder(); + detail.append("路径: ").append(path); + detail.append("\n检测层: "); + if (javaExists) detail.append("Java ✓ "); + if (nativeExists) detail.append("Native ✓ "); + if (syscallExists) detail.append("Syscall ✓ "); + + // Determine which detection layer to report + DetectionLayer reportLayer = syscallExists ? DetectionLayer.SYSCALL : + nativeExists ? DetectionLayer.NATIVE : + DetectionLayer.JAVA; + + item.addDetectionDetail(icon + " " + category, desc, + detail.toString(), reportLayer, icon); + + if (detectedNames.length() > 0) detectedNames.append(", "); + detectedNames.append(desc); + } + } + + // Add detection summary + if (detectedCount > 0) { + item.addDetectionDetail("📊 检测统计", "发现可疑路径", + "数量: " + detectedCount + "/" + toolPaths.length + "\n" + + "发现: " + detectedNames.toString(), + DetectionLayer.SYSCALL, "📈"); + } + } } 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 65bcf08..190fa62 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -105,6 +105,14 @@ public class NativeDetector { public native int getTracerPid(); + // Suspicious tool path detection + /** Check suspicious tool paths - Native layer (access) */ + public native boolean checkSuspiciousToolPathsNative(); + /** Check suspicious tool paths - Syscall layer */ + public native boolean checkSuspiciousToolPathsSyscall(); + /** Get detected suspicious tool path details (JSON format) */ + public native String getDetectedSuspiciousToolPaths(); + // ===================== File Operations ===================== public native boolean fileExistsNative(String path); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9350c1..65087ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,6 +109,7 @@ 调试器连接 JDWP 检测 Ptrace 检测 + 可疑工具路径 设备指纹信息 From 5744b86e3cfc261b03653c72e1a8e94ae103053a Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:23:59 +0800 Subject: [PATCH 8/9] feat(emulator): expand emulator detection file paths coverage Add 21 new emulator-specific file paths across both Native and Java layers: - QEMU: /sys/devices/virtual/misc/qemu_pipe, /sys/class/misc/qemu_pipe, libc_malloc_debug_qemu.so-arm, libqemu_wl.txt, qemu_list.txt - Android Emulator: libEGL_emulation.so - VirtualBox: /sys/module/vboxsf, /ueventd.vbox86.rc - Nox: nox-vbox-sf, libnox.so, libnb.so - Droid4X: /system/droid4x, droid4x-vbox-sf - TiantianVM: libEGL_tiantianVM.so, ttVM-vbox-sf - BlueStacks: superuser.daemon, vboxsf.ko - AndroVM: androVM-vbox-sf - Yiwan: yiwan-prop, yiwan-sf --- .../main/cpp/detector/emulator_detector.cpp | 38 ++++++++++++++++--- .../xff/launch/detector/EmulatorDetector.java | 33 +++++++++++++++- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/app/src/main/cpp/detector/emulator_detector.cpp b/app/src/main/cpp/detector/emulator_detector.cpp index a678f38..df75f7f 100644 --- a/app/src/main/cpp/detector/emulator_detector.cpp +++ b/app/src/main/cpp/detector/emulator_detector.cpp @@ -14,29 +14,55 @@ const std::vector& EmulatorDetector::getEmulatorFiles() { "/dev/qemu_pipe", "/dev/qemu_trace", "/dev/goldfish_pipe", - "/system/lib/libc_malloc_debug_qemu.so", + "/sys/devices/virtual/misc/qemu_pipe", + "/sys/class/misc/qemu_pipe", "/sys/qemu_trace", + "/system/lib/libc_malloc_debug_qemu.so", + "/system/lib/libc_malloc_debug_qemu.so-arm", "/system/bin/qemu-props", + "/system/framework/libqemu_wl.txt", + "/data/downloads/qemu_list.txt", "/init.goldfish.rc", "/init.ranchu.rc", + // Android Emulator + "/system/lib/egl/libEGL_emulation.so", // Genymotion "/dev/socket/genyd", "/dev/socket/baseband_genyd", - // Nox + // VirtualBox + "/sys/module/vboxsf", + "/ueventd.vbox86.rc", + "/fstab.vbox86", + // Nox (夜神模拟器) "/data/data/com.bignox.app.store.hd", "/system/bin/nox", "/system/bin/nox-prop", + "/system/bin/nox-vbox-sf", + "/system/lib/libnox.so", + "/system/lib/libnb.so", "/fstab.nox", - // LDPlayer + // LDPlayer (雷电模拟器) "/system/bin/ldinit", "/system/bin/ldmountsf", "/fstab.andy", - // MEmu + // MEmu (逍遥模拟器) "/data/data/com.microvirt.guide", "/system/bin/microvirt", - "/fstab.vbox86", + // Droid4X (海马玩模拟器) + "/system/droid4x", + "/system/bin/droid4x-vbox-sf", + // TiantianVM (天天模拟器) + "/system/lib/egl/libEGL_tiantianVM.so", + "/system/bin/ttVM-vbox-sf", // BlueStacks - "/data/data/com.bluestacks.settings" + "/data/data/com.bluestacks.settings", + "/dev/com.bluestacks.superuser.daemon", + "/boot/bstmods/vboxsf.ko", + // AndroVM + "/system/bin/androVM-vbox-sf", + // Yiwan (逸玩模拟器) + "/system/bin/yiwan-prop", + "/system/bin/yiwan-sf" }; return files; } diff --git a/app/src/main/java/com/xff/launch/detector/EmulatorDetector.java b/app/src/main/java/com/xff/launch/detector/EmulatorDetector.java index a554692..03858e1 100644 --- a/app/src/main/java/com/xff/launch/detector/EmulatorDetector.java +++ b/app/src/main/java/com/xff/launch/detector/EmulatorDetector.java @@ -424,14 +424,43 @@ private void collectBatteryDetails(DetectionItem item) { */ private void collectEmulatorFilesDetails(DetectionItem item) { String[] emulatorFiles = { + // QEMU "/dev/qemu_pipe", "/dev/qemu_trace", "/dev/goldfish_pipe", - "/system/lib/libc_malloc_debug_qemu.so", + "/sys/devices/virtual/misc/qemu_pipe", + "/sys/class/misc/qemu_pipe", "/sys/qemu_trace", + "/system/lib/libc_malloc_debug_qemu.so", + "/system/lib/libc_malloc_debug_qemu.so-arm", "/system/bin/qemu-props", + "/system/framework/libqemu_wl.txt", + "/data/downloads/qemu_list.txt", "/system/etc/init.goldfish.rc", - "/system/etc/init.ranchu.rc" + "/system/etc/init.ranchu.rc", + // Android Emulator + "/system/lib/egl/libEGL_emulation.so", + // VirtualBox + "/sys/module/vboxsf", + "/ueventd.vbox86.rc", + // Nox (夜神模拟器) + "/system/bin/nox-vbox-sf", + "/system/lib/libnox.so", + "/system/lib/libnb.so", + // Droid4X (海马玩模拟器) + "/system/droid4x", + "/system/bin/droid4x-vbox-sf", + // TiantianVM (天天模拟器) + "/system/lib/egl/libEGL_tiantianVM.so", + "/system/bin/ttVM-vbox-sf", + // BlueStacks + "/dev/com.bluestacks.superuser.daemon", + "/boot/bstmods/vboxsf.ko", + // AndroVM + "/system/bin/androVM-vbox-sf", + // Yiwan (逸玩模拟器) + "/system/bin/yiwan-prop", + "/system/bin/yiwan-sf" }; for (String path : emulatorFiles) { From 6f243e58168eecff57dda3e8bd02d24271cc6c06 Mon Sep 17 00:00:00 2001 From: ZnDong <81907400+ZnDong@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:19:14 +0800 Subject: [PATCH 9/9] feat(root): add KernelSU timing side-channel detection Implement kernel-level syscall hook detection for KernelSU by comparing faccessat (hooked by KSU) vs fchownat (not hooked) execution timing. Detection algorithm: - Bind thread to big core for stable measurements - Collect 10000 timing samples for both faccessat and fchownat - Use hardware counter (CNTVCT_EL0) on ARM64 for nanosecond precision - Sort both arrays to reduce noise and outliers - Compare element-by-element: count cases where faccessat > fchownat + 1 - If anomaly rate exceeds 70% (7000/10000), KernelSU hook detected Key design decisions: - Both syscalls use dirfd=-1 (invalid fd) to ensure symmetric kernel failure paths; using AT_FDCWD would cause faccessat to enter deeper kernel code (path resolution + page fault) leading to false positives - faccessat chosen because it is in KSU hook list, is simple, fast, and failure has no side effects Files changed: - syscall_wrapper.h: core detection functions (counter, CPU binding, sample collection, comparison, threshold check) - native-lib.cpp: JNI bindings for ksuSideChannelCheck/Detected - NativeDetector.java: native method declarations - SideChannelDetector.java: Java detection item integration --- app/src/main/cpp/detector/debug_detector.cpp | 3 +- app/src/main/cpp/native-lib.cpp | 33 +++ app/src/main/cpp/syscall/syscall_wrapper.h | 230 ++++++++++++++++++ .../xff/launch/detector/NativeDetector.java | 17 ++ .../launch/detector/SideChannelDetector.java | 92 +++++++ 5 files changed, 374 insertions(+), 1 deletion(-) diff --git a/app/src/main/cpp/detector/debug_detector.cpp b/app/src/main/cpp/detector/debug_detector.cpp index 6e1309e..efd1a50 100644 --- a/app/src/main/cpp/detector/debug_detector.cpp +++ b/app/src/main/cpp/detector/debug_detector.cpp @@ -119,7 +119,8 @@ bool DebugDetector::checkInitTimingAttack(time_t initStartTime) { LOGD("Anti-timing: init completed in %ld seconds (normal)", (long)elapsed); return false; // Normal execution time - +} + // ===================== Suspicious Tool Path Detection ===================== // Iterates over suspicious tool path array, using access() to check file existence diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 3a0446f..b361f04 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -1511,6 +1511,39 @@ Java_com_xff_launch_detector_NativeDetector_detectTimingAnomaly(JNIEnv *env, job return (jboolean)detect_timing_anomaly(syscallTime, libcTime, threshold); } +// ===================== KernelSU Side-Channel Detection ===================== + +/** + * Perform KernelSU side-channel check and return anomaly ratio (0-100) + * + * Detection principle: + * - faccessat is hooked by KernelSU (in its syscall hook list) + * - fchownat is NOT hooked by KernelSU + * - Normal: faccessat is faster than fchownat + * - With KSU: faccessat is slower due to hook overhead + * - Collect 10000 samples, sort, compare: if >70% anomalous, KSU detected + */ +JNIEXPORT jint JNICALL +Java_com_xff_launch_detector_NativeDetector_ksuSideChannelCheck(JNIEnv *env, jobject thiz) { + int anomaly_count = 0; + int total_samples = 0; + + ksu_side_channel_check(&anomaly_count, &total_samples); + + if (total_samples <= 0) return -1; + + // Return percentage (0-100) + return (jint)((anomaly_count * 100) / total_samples); +} + +/** + * Quick boolean check: is KernelSU detected via side-channel? + */ +JNIEXPORT jboolean JNICALL +Java_com_xff_launch_detector_NativeDetector_ksuSideChannelDetected(JNIEnv *env, jobject thiz) { + return (jboolean)ksu_side_channel_check(nullptr, nullptr); +} + // ===================== System Property ===================== JNIEXPORT jstring JNICALL diff --git a/app/src/main/cpp/syscall/syscall_wrapper.h b/app/src/main/cpp/syscall/syscall_wrapper.h index 7f200fd..60aae73 100644 --- a/app/src/main/cpp/syscall/syscall_wrapper.h +++ b/app/src/main/cpp/syscall/syscall_wrapper.h @@ -399,4 +399,234 @@ static inline bool detect_timing_anomaly(long long syscall_time, long long libc_ return ratio > threshold_multiplier; } +// ===================== KernelSU Side-Channel Detection ===================== +// Timing side-channel detection for KernelSU kernel-level syscall hooks. +// +// Key insight: KernelSU hooks __NR_faccessat (syscall 48 on ARM64) to intercept +// file access checks. __NR_fchownat is NOT hooked by KernelSU. +// Normally, faccessat is faster than fchownat. If faccessat is hooked by KernelSU, +// it will consistently be slower than fchownat due to the hook overhead. +// +// Detection flow: +// 1. Collect N timing samples for both faccessat and fchownat +// 2. Sort both arrays to reduce noise and extreme outliers +// 3. Compare element-by-element: count how many times faccessat > fchownat + 1 +// 4. If anomaly count exceeds threshold (70%), KernelSU hook is likely present + +#include +#include +#include + +// Number of timing samples to collect +#define KSU_NUM_SAMPLES 10000 + +// Anomaly threshold: 7000 out of 10000 (70%) +// Threshold 0x1B58 = 7000 +#define KSU_ANOMALY_THRESHOLD 7000 + +/** + * Read hardware counter for precise timing (ARM64 only) + * Uses ISB + CNTVCT_EL0 + ISB pattern for nanosecond-level accuracy. + * + * On non-ARM64, falls back to CLOCK_MONOTONIC_RAW + */ +static inline uint64_t ksu_read_counter() { +#if defined(__aarch64__) + uint64_t val; + // ISB ensures all previous instructions complete before reading counter + __asm__ volatile("isb" ::: "memory"); + // Read virtual counter register (CNTVCT_EL0) for nanosecond precision + __asm__ volatile("mrs %0, cntvct_el0" : "=r"(val)); + __asm__ volatile("isb" ::: "memory"); + return val; +#else + // Fallback for non-ARM64 architectures + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC_RAW, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +#endif +} + +/** + * Comparator for qsort - ascending order for int64_t timing values + */ +static int ksu_compare_int64(const void* a, const void* b) { + int64_t va = *(const int64_t*)a; + int64_t vb = *(const int64_t*)b; + if (va < vb) return -1; + if (va > vb) return 1; + return 0; +} + +/** + * Try to bind current thread to a performance (big) core for stable measurements. + * + * @return true if successfully bound to a big core + */ +static inline bool ksu_bind_big_core() { + // Read CPU max frequencies to identify big cores + int max_freq = 0; + int big_core = -1; + int num_cpus = sysconf(_SC_NPROCESSORS_CONF); + + if (num_cpus < 2) return false; + + for (int i = 0; i < num_cpus && i < 16; i++) { + char path[128]; + snprintf(path, sizeof(path), + "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i); + + int fd = (int)syscall_raw(__NR_openat, AT_FDCWD, (long)path, O_RDONLY, 0); + if (fd < 0) continue; + + char buf[32] = {0}; + ssize_t n = (ssize_t)syscall_raw(__NR_read, fd, (long)buf, sizeof(buf) - 1); + syscall_raw(__NR_close, fd); + + if (n > 0) { + int freq = atoi(buf); + if (freq > max_freq) { + max_freq = freq; + big_core = i; + } + } + } + + if (big_core >= 0) { + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(big_core, &cpuset); + return sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) == 0; + } + + return false; +} + +/** + * Restore CPU affinity to allow scheduling on all cores + */ +static inline void ksu_restore_affinity() { + int num_cpus = sysconf(_SC_NPROCESSORS_CONF); + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + for (int i = 0; i < num_cpus && i < 16; i++) { + CPU_SET(i, &cpuset); + } + sched_setaffinity(0, sizeof(cpu_set_t), &cpuset); +} + +/** + * Collect timing samples for __NR_faccessat syscall. + * faccessat(dirfd=-1, pathname=NULL, mode=-1, flags=0) + * Intentionally invalid args for fast failure. + * + * @param samples Output array (must be at least KSU_NUM_SAMPLES * sizeof(int64_t)) + */ +static inline void ksu_collect_faccessat_timing(int64_t* samples) { + for (int i = 0; i < KSU_NUM_SAMPLES; i++) { + uint64_t start = ksu_read_counter(); + // Call faccessat with invalid args - will fail immediately but still enters kernel + // The hook overhead is incurred regardless of success/failure + // Args: dirfd=-1, pathname=NULL, mode=-1, flags=0 + syscall_raw(__NR_faccessat, (long)-1, 0, (long)-1, 0); + uint64_t end = ksu_read_counter(); + samples[i] = (int64_t)(end - start); + } +} + +/** + * Collect timing samples for __NR_fchownat syscall. + * fchownat(dirfd=-1, pathname=NULL, owner=0, group=0, flags=-1) + * Intentionally invalid args for fast failure. + * This syscall is NOT hooked by KernelSU, serves as baseline reference. + * + * @param samples Output array (must be at least KSU_NUM_SAMPLES * sizeof(int64_t)) + */ +static inline void ksu_collect_fchownat_timing(int64_t* samples) { + for (int i = 0; i < KSU_NUM_SAMPLES; i++) { + uint64_t start = ksu_read_counter(); + // Call fchownat with invalid args - NOT hooked by KernelSU + // Args: dirfd=-1, pathname=NULL, owner=0, group=0, flags=-1 + syscall_raw(__NR_fchownat, (long)-1, 0, 0, 0, (long)-1); + uint64_t end = ksu_read_counter(); + samples[i] = (int64_t)(end - start); + } +} + +/** + * Core KernelSU side-channel detection. + * + * 1. Bind to big core for stable measurements + * 2. Allocate sample arrays for faccessat and fchownat + * 3. Collect timing samples for both syscalls + * 4. Sort both arrays (qsort) to reduce noise + * 5. Compare sorted arrays element-by-element: + * Count anomalies where faccessat_time > fchownat_time + 1 + * 6. If anomaly count > threshold (7000/10000 = 70%), KSU detected + * + * @param out_anomaly_count Output: number of anomalies detected + * @param out_total_samples Output: total samples compared + * @return true if KernelSU hook detected (anomaly_count > threshold) + */ +static inline bool ksu_side_channel_check(int* out_anomaly_count, int* out_total_samples) { + // Phase 1: Bind to big core for stable measurement + bool bound = ksu_bind_big_core(); + + // Phase 2: Allocate and zero timing arrays + // Original uses malloc(80000) = 10000 * 8 bytes (int64_t) + int64_t* faccessat_times = (int64_t*)malloc(KSU_NUM_SAMPLES * sizeof(int64_t)); + int64_t* fchownat_times = (int64_t*)malloc(KSU_NUM_SAMPLES * sizeof(int64_t)); + + if (!faccessat_times || !fchownat_times) { + free(faccessat_times); + free(fchownat_times); + if (out_anomaly_count) *out_anomaly_count = 0; + if (out_total_samples) *out_total_samples = 0; + if (bound) ksu_restore_affinity(); + return false; + } + + memset(faccessat_times, 0, KSU_NUM_SAMPLES * sizeof(int64_t)); + memset(fchownat_times, 0, KSU_NUM_SAMPLES * sizeof(int64_t)); + + // Phase 3: Collect timing samples + ksu_collect_faccessat_timing(faccessat_times); + ksu_collect_fchownat_timing(fchownat_times); + + // Phase 4: Sort both arrays to reduce extreme value impact + qsort(faccessat_times, KSU_NUM_SAMPLES, sizeof(int64_t), ksu_compare_int64); + qsort(fchownat_times, KSU_NUM_SAMPLES, sizeof(int64_t), ksu_compare_int64); + + // Phase 5: Compare sorted arrays element-by-element + // Original uses NEON vectorized comparison for performance + // Simplified equivalent: + // if (faccessat[i] > fchownat[i] + 1) anomaly++ + // + // Rationale: Normally faccessat should be FASTER than fchownat + // because faccessat is a simpler operation (just check access). + // If faccessat is consistently SLOWER, it means there's a hook + // adding overhead (KernelSU hooks faccessat, not fchownat). + int anomaly_count = 0; + for (int i = 0; i < KSU_NUM_SAMPLES; i++) { + if (faccessat_times[i] > fchownat_times[i] + 1) { + anomaly_count++; + } + } + + // Phase 6: Cleanup + free(faccessat_times); + free(fchownat_times); + + // Restore CPU affinity + if (bound) ksu_restore_affinity(); + + // Output results + if (out_anomaly_count) *out_anomaly_count = anomaly_count; + if (out_total_samples) *out_total_samples = KSU_NUM_SAMPLES; + + // Phase 7: Threshold check + // Original: if (v24 > 0x1B58) => if (anomaly > 7000) + return anomaly_count > KSU_ANOMALY_THRESHOLD; +} + #endif // LAUNCH_SYSCALL_WRAPPER_H 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 190fa62..7422cde 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -390,6 +390,23 @@ public class NativeDetector { * @return true if dumpArtMethod hook detected */ public native boolean checkDumpArtMethodHookSyscall(); + // ===================== KernelSU Side-Channel Detection ===================== + + /** + * Perform KernelSU side-channel timing detection. + * Compares faccessat (hooked by KSU) vs fchownat (not hooked) timing. + * + * Returns anomaly ratio as percentage (0-100). + * If > 70%, KernelSU hook is likely present. + * Returns -1 on error. + */ + public native int ksuSideChannelCheck(); + + /** + * Quick KernelSU side-channel detection. + * @return true if KernelSU kernel-level hook detected + */ + public native boolean ksuSideChannelDetected(); // Singleton instance private static NativeDetector instance; diff --git a/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java b/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java index f6d73e8..7e05823 100644 --- a/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java +++ b/app/src/main/java/com/xff/launch/detector/SideChannelDetector.java @@ -14,6 +14,7 @@ * Detects: * - SELinux status (related to Root detection) * - Timing-based hook detection for Frida/Xposed + * - KernelSU kernel-level hook detection via timing side-channel */ public class SideChannelDetector { @@ -36,6 +37,9 @@ public List getAllDetections() { items.add(checkSyscallTimingAccess()); items.add(checkSyscallTimingStat()); + // KernelSU kernel-level hook detection via timing side-channel + items.add(checkKernelSUSideChannel()); + return items; } @@ -211,4 +215,92 @@ private DetectionItem checkSyscallTimingStat() { return item; } + + // ===================== KernelSU Side-Channel Detection ===================== + + /** + * KernelSU Timing Side-Channel Detection + * + * Detection principle: + * + * KernelSU hooks __NR_faccessat in the kernel to intercept file access checks. + * __NR_fchownat is NOT in KernelSU's hook list. + * + * Normal path (no KSU): + * syscall -> kernel -> fast return (~50-100ns) + * faccessat is naturally faster than fchownat + * + * With KernelSU: + * faccessat: syscall -> kernel -> KSU hook layer -> return (~200-500ns) + * fchownat: syscall -> kernel -> fast return (~50-100ns) + * faccessat becomes consistently SLOWER than fchownat + * + * Algorithm: + * 1. Bind to big core for stable measurements + * 2. Collect 10000 timing samples for each syscall + * 3. Sort both arrays (qsort) to reduce noise + * 4. Compare sorted arrays: count cases where faccessat > fchownat + 1 + * 5. If anomaly count > 7000 (70%), KernelSU detected + * + * Why faccessat? + * - It's in KSU's hook list + * - Simple, fast, failure has no side effects + * - Other hooked calls are more complex or have side effects + */ + private DetectionItem checkKernelSUSideChannel() { + DetectionItem item = new DetectionItem( + "KernelSU 侧信道检测", + "基于faccessat/fchownat时间差异的KernelSU内核Hook检测" + ); + + try { + // Perform full side-channel check + // Returns anomaly percentage (0-100), or -1 on error + int anomalyPercent = nativeDetector.ksuSideChannelCheck(); + + if (anomalyPercent < 0) { + // Error case + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, false); + item.setLayerResult(DetectionLayer.SYSCALL, false); + item.setStatus(DetectionStatus.UNKNOWN); + item.setDetail("侧信道检测执行失败(内存分配错误)"); + return item; + } + + // Threshold: 70% (0x1B58 = 7000/10000) + boolean ksuDetected = anomalyPercent > 70; + + // Set layer results + // This detection operates at the syscall/kernel level + item.setLayerResult(DetectionLayer.JAVA, false); // Java layer N/A + item.setLayerResult(DetectionLayer.NATIVE, false); // Native layer N/A + item.setLayerResult(DetectionLayer.SYSCALL, ksuDetected); + + if (ksuDetected) { + item.setStatus(DetectionStatus.RISK); + item.setDetail(String.format( + "检测到KernelSU内核级Hook - 异常率: %d%% (阈值: 70%%)\n" + + "faccessat执行耗时异常偏高,表明存在内核级syscall拦截", + anomalyPercent + )); + } else { + item.setStatus(DetectionStatus.SAFE); + item.setDetail(String.format( + "内核syscall时序正常 - 异常率: %d%% (阈值: 70%%)", + anomalyPercent + )); + } + + } catch (Exception e) { + item.setLayerResult(DetectionLayer.JAVA, false); + item.setLayerResult(DetectionLayer.NATIVE, false); + item.setLayerResult(DetectionLayer.SYSCALL, false); + item.setStatus(DetectionStatus.UNKNOWN); + item.setDetail("侧信道检测异常: " + e.getMessage()); + } + + return item; + } } +