From e6c539eac1324a4634db913e8fc693149c6fc638 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] 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/native-lib.cpp | 33 +++ app/src/main/cpp/syscall/syscall_wrapper.h | 230 ++++++++++++++++++ .../xff/launch/detector/NativeDetector.java | 18 ++ .../launch/detector/SideChannelDetector.java | 92 +++++++ 4 files changed, 373 insertions(+) diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 7e227b2..4e57e41 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -1393,6 +1393,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 922c12c..5560044 100644 --- a/app/src/main/java/com/xff/launch/detector/NativeDetector.java +++ b/app/src/main/java/com/xff/launch/detector/NativeDetector.java @@ -305,6 +305,24 @@ public class NativeDetector { */ public native boolean detectTimingAnomaly(long syscallTime, long libcTime, float threshold); + // ===================== 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 03d091a..97200e4 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; + } } +