移动应用中的 Root Detection — 本质、检测方法及工作原理

作者: IT Sectr 发布日期: 2026-04-03 阅读时间: 9 分钟

Root Detection — 一种保护 Android 应用不在具有超级用户权限的设备上运行的机制。银行、支付和企业应用会阻止或限制在已 root 设备上的功能,因为 root 访问会解除 Android 沙箱的限制,从而可以拦截流量、读取进程内存和篡改数据。根据 OWASP Mobile Top 10(2024年),缺少 Root Detection 属于 M8 类别(Security Decisions via Untrusted Inputs)。Root Detection 建立在文件系统静态检查和运行时行为动态分析的组合之上。

要点

  • Root Detection — 检查 Android 设备是否存在 root 访问,以保护应用不在受危害的环境中运行
  • 静态方法检查文件系统中是否存在 su 二进制文件、Superuser 应用以及系统分区的更改
  • 动态方法分析运行时:检查 Build.TAGS,尝试通过特权进程打开 /proc/self/maps
  • 原生实现通过 JNI 使用 C/C++ 确保对通过 Xposed 和 Frida 在 Java 层面绕过具有抵抗力
  • 安全性Root Detection 需要持续混淆和服务器端验证以防止检查结果被篡改

什么是 Root Detection?

Root Detection — 一种确定 Android 设备上是否存在 root 访问的软件机制。root 访问提供对操作系统的完全控制,允许应用和脚本以 UID 0 执行命令。在已 root 的设备上,应用隔离(Android Sandbox)丢失,这使得可以拦截键盘输入、读取其他应用的 SQLite 数据库、向进程中注入代码以及篡改受信任存储中的 SSL 证书。

对于金融和企业应用,在已 root 的设备上运行存在不可接受的风险:攻击者可以访问令牌、会话密钥和个人数据。包括 PCI Security Standards Council 在内的监管机构要求支付应用检测并对 root 访问做出反应。作为回应,Android 开发人员将 Root Detection 作为主动保护策略的一部分嵌入。

检测有两种方法:静态方法(分析文件系统和已安装的包)和动态方法(在运行时进行检查)。组合方法被认为是最可靠的,因为它覆盖了不同的绕过向量。根据 NowSecure(2025年) 的研究,Google Play 前 100 名中 76% 的银行应用包含某种形式的 Root Detection。

root 检测的静态方法

静态方法在应用启动时执行,检查 root 工具在文件系统中留下的 root 访问迹象。这些方法不需要执行特权命令,并且在普通应用的上下文中工作。

检查 su 二进制文件是否存在

root 访问的主要标志 — 在标准路径中存在 su 可执行文件:/system/bin/su、/system/xbin/su、/sbin/su、/su/bin/su。应用通过 File.exists() 或来自 libc 的 access() 原生实现检查文件是否存在。此外,可以尝试执行 su --version 或 su -c id 并检查退出代码。

搜索 root 管理器

管理 root 访问的典型应用:Superuser、SuperSU、Magisk Manager、KingRoot。通过 PackageManager.getPackageInfo() 或读取 /data/app/ 目录检查它们的存在。要检查的包:com.topjohnwu.magisk、eu.chainfire.supersu、com.noshufou.android.su、com.thirdparty.superuser、com.koushikdutta.superuser、com.zacharee1.systemuituner。

检查系统属性

Android 将系统状态信息存储在可通过 System.getProperty 和 Build.TAGS 访问的系统属性中。如果 Build.TAGS 的值包含 test-keys 而不是 release-keys — 这表示自定义固件,通常带有 root 访问权限。此外,通过读取 /system/build.prop 检查 ro.build.tags、ro.debuggable 和 ro.secure。

java
public class RootDetectionChecker {
    private static final String[] SU_PATHS = {
        "/system/bin/su",
        "/system/xbin/su",
        "/sbin/su",
        "/su/bin/su",
        "/system/sd/xbin/su"
    };

    public boolean checkRootByFiles() {
        for (String path : SU_PATHS) {
            if (new File(path).exists()) {
                return true;
            }
        }
        return false;
    }

    public boolean checkRootByPackages(Context ctx) {
        String[] packages = {
            "com.topjohnwu.magisk",
            "eu.chainfire.supersu",
            "com.noshufou.android.su",
            "com.koushikdutta.superuser"
        };
        for (String pkg : packages) {
            try {
                ctx.getPackageManager().getPackageInfo(pkg, 0);
                return true;
            } catch (PackageManager.NameNotFoundException e) {
                // 未找到包
            }
        }
        return false;
    }
}

动态检查方法

动态方法在应用运行期间执行并分析执行环境。与静态方法不同,它们可以检测通过 Magisk Hide 或 Zygisk 隐藏的 root,因为它们检查系统行为,而不仅仅是文件结构。

检查挂载点

在 root 访问时,一些系统分区以 rw(读写)标志挂载,而不是 ro(只读)。应用读取 /proc/mounts 并检查 /system 是否以 ro 方式挂载。如果 /system 以 rw 方式挂载 — 这是系统被修改的标志。此外,检查通过 Magisk 挂载 /su 的存在。

安全模式测试

Android 安全模式会禁用第三方应用,包括 root 管理器。正确的 Root Detection 实现可以检查设备是否在安全模式下运行。如果应用检测到 root 管理器不可见,但 su 二进制文件存在 — 这是 Magisk Hide 的标志

检查命令执行

尝试通过 ProcessBuilder 或 Runtime.exec 执行 su -c id — root 访问的直接测试。然而,Magisk 可以拦截此调用。更可靠的选项 — 通过原生代码检查:打开 /proc/1/limits 或 /proc/self/maps 并分析正在运行的进程的 UID。如果应用可以获取 UID 0 或读取仅 root 可访问的文件 — 设备已被入侵。

java
public boolean checkRootDynamically() {
    // 编译标志检查
    String buildTags = Build.TAGS;
    if (buildTags != null && buildTags.contains("test-keys")) {
        return true;
    }

    // /system 挂载检查
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(new FileInputStream("/proc/mounts"))
        );
        String line;
        while ((line = reader.readLine()) != null) {
            if (line.contains("/system")
                && line.contains("rw")) {
                reader.close();
                return true;
            }
        }
        reader.close();
    } catch (IOException e) {
        // 读取挂载时出错
    }

    return false;
}

C++ 中 Root Detection 的原生实现

用 Java 实现的 Root Detection 很容易通过 Xposed 模块或 Frida 绕过,它们拦截 Java 方法并篡改返回值。通过 JNI 用 C++ 实现的原生实现要强大得多:在 Java 层面工作的动态分析工具看不到 libc 的原生调用,如 stat、access、popen 和 dlopen。

cpp
#include <unistd.h>
#include <sys/stat.h>
#include <cstring>
#include <vector>

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_checker_RootCheck_nativeCheck(
    JNIEnv* env, jobject instance) {

    std::vector<const char*> paths = {
        "/system/bin/su",
        "/system/xbin/su",
        "/sbin/su",
        "/data/local/su"
    };

    struct stat st;
    for (const char* path : paths) {
        if (stat(path, &st) == 0) {
            return JNI_TRUE;
        }
    }
    return JNI_FALSE;
}

原生检查不使用 Java API,这使得它对在 Dalvik/ART 层面工作的绕过工具不可见。为了额外保护,建议将常量(路径列表)不存储在只读部分,而是通过简单的可逆函数计算它们。来自 libc 的 stat 调用直接调用 Linux 内核,绕过 Java 包装器,无法通过 Xposed 拦截。

绕过 Root Detection 的方法

安全开发人员需要了解现有的绕过方法,以构建强大的检测系统。每种绕过方法都需要在相应层面采取对策。

通过 Magisk Hide 和 Zygisk 绕过

Magisk — Android 9–14 上最流行的 root 工具。Magisk Hide 隐藏 su 在 /proc 中的存在并篡改路径检查结果。Magisk 在内核层面工作,在应用看到之前拦截 stat() 和 access()。对策:通过 /sbin/.magisk 的存在检查 Magisk 本身,或通过读取自己的 maps 检查 — Magisk 将其库嵌入到每个进程中。

通过 Frida 绕过

Frida — 一种动态插桩工具,可以通过 Ptrace 或 Dobby 拦截原生函数。Frida 替换每次检查的返回值,将 stat 的结果篡改为 ENOENT。对策:通过计算内存中指令的校验和来检查原生函数的完整性,并通过分析 /proc/self/maps 检测 Frida 是否存在 frida-agent.so 或 frida-helper。

通过 APK 补丁绕过

用 Java 实现的 Root Detection 在 2–3 分钟内被移除:APK 通过 apktool 解包,在 smali 代码中将方法的返回值改为 false,APK 重新组装并签名。对策:将关键逻辑转移到原生代码,并通过 Signature API 在运行时检查应用的数字签名,或将 APK 哈希与服务器上的参考值进行比较。

可靠保护建议

有效的 Root Detection 建立在多层架构之上。没有任何单一方法能提供足够的保护水平。静态和动态检查、原生代码和服务器验证的组合提供了最大的抵抗力。

服务器端验证

不要仅依赖客户端检查。将 Root Detection 结果连同一次性会话令牌发送到服务器。服务器做出阻止或限制功能的决定。这可以防止 API 层面的攻击,因为客户端应用可能被修改,但服务器仍是受信任方。

检查代码混淆

Root Detection 代码必须进行混淆。如果攻击者在 jadx 中看到清晰的 su 路径检查序列 — 绕过只需几分钟。使用 ProGuard 或 DexGuard 来混淆控制流和加密字符串。混淆将安全代码的分析时间从几分钟增加到几个小时。

定期更新签名

检查的路径、包和指标列表必须随每个应用版本更新。新的 root 和绕过工具每个月都会出现。一年未更改的静态列表无法检测现代方法。建议在应用启动时从服务器加载最新签名,然后执行检查。

常见问题

为什么应用需要 Root Detection?

Root Detection 保护应用不在 Android 沙箱被禁用的设备上运行。在已 root 的设备上,任何应用都可以读取其他应用的数据。根据 PCI DSS 要求和 OWASP Mobile Security 建议,银行和支付应用必须阻止在已 root 设备上的运行。

Magisk Hide 是如何工作的?

Magisk Hide 使用挂载命名空间(mount namespace)机制。对于例外列表中指定的每个进程,Magisk 创建一个隔离的命名空间,其中 su 二进制文件不可见。此命名空间中的系统调用 stat、access 和 open 看不到 Magisk 文件。可以通过检查 /proc/self/maps 是否存在并搜索 magisk dump 来检测 Magisk。

可以在没有 root 的情况下绕过 Root Detection 吗?

可以,如果应用不检查自身代码的完整性。通过 Frida 可以拦截 Java 检查方法并强制返回 false。对策 — 在 C++ 中原生实现关键逻辑并通过 DEX 文件的哈希检查完整性。如果没有混淆,任何用 Java 实现的 Root Detection 都可以在 5–10 分钟内绕过。

什么是 safety net 和 attestation?

SafetyNet(已弃用)和 Play Integrity API — 这是 Google 的服务器端检查,用于确认设备完整性。它们包括引导加载程序、系统签名和 root 状态的检查。Play Integrity API — SafetyNet 的推荐替代品,提供三个级别:BASIC、DEVICE 和 STRONG。客户端的 Root Detection 补充了服务器端认证。

如何在您自己的应用中测试 Root Detection?

将应用安装到真正的已 root 设备上(例如,带 Magisk 的 Pixel)。检查阻止是否生效。然后尝试通过 Magisk Hide 为您的应用隐藏 root 并重复测试。要深入检查,使用 Frida 拦截目标方法,并确保原生保护无法绕过。

总结

  • Root Detection — 银行、支付和企业 Android 应用的强制性安全组件,基于静态和动态检查的组合工作
  • 静态方法包括在标准路径中搜索 su 二进制文件、检查已安装的 root 管理器以及分析 Build.TAGS 系统属性
  • 动态方法分析 /system 以 rw 模式挂载、检查 /proc/mounts 的完整性并执行特权命令运行测试
  • 原生实现通过 JNI 用 C++ 实现使检查对在 Java 层面工作的 Xposed 和 Frida 不可见,并需要通过 Dobby 或 Ptrace 绕过
  • Magisk Hide — 通过挂载命名空间绕过 Root Detection 的主要工具,通过分析 /proc/self/maps 是否存在 magisk 库来检测
  • 推荐架构:原生检查 + 代码混淆 + 结果的服务器验证 + 定期更新签名
  • Google 的 Play Integrity API 通过设备的服务器端认证补充客户端的 Root Detection,提供全面保护

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读