From 7affac4aa8ed4e74cc713cbdec6e128def746e40 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] 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;