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 访问的软件机制。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 可执行文件:/system/bin/su、/system/xbin/su、/sbin/su、/su/bin/su。应用通过 File.exists() 或来自 libc 的 access() 原生实现检查文件是否存在。此外,可以尝试执行 su --version 或 su -c id 并检查退出代码。
管理 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。
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 可访问的文件 — 设备已被入侵。
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;
}
用 Java 实现的 Root Detection 很容易通过 Xposed 模块或 Frida 绕过,它们拦截 Java 方法并篡改返回值。通过 JNI 用 C++ 实现的原生实现要强大得多:在 Java 层面工作的动态分析工具看不到 libc 的原生调用,如 stat、access、popen 和 dlopen。
#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 拦截。
安全开发人员需要了解现有的绕过方法,以构建强大的检测系统。每种绕过方法都需要在相应层面采取对策。
Magisk — Android 9–14 上最流行的 root 工具。Magisk Hide 隐藏 su 在 /proc 中的存在并篡改路径检查结果。Magisk 在内核层面工作,在应用看到之前拦截 stat() 和 access()。对策:通过 /sbin/.magisk 的存在检查 Magisk 本身,或通过读取自己的 maps 检查 — Magisk 将其库嵌入到每个进程中。
Frida — 一种动态插桩工具,可以通过 Ptrace 或 Dobby 拦截原生函数。Frida 替换每次检查的返回值,将 stat 的结果篡改为 ENOENT。对策:通过计算内存中指令的校验和来检查原生函数的完整性,并通过分析 /proc/self/maps 检测 Frida 是否存在 frida-agent.so 或 frida-helper。
用 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 保护应用不在 Android 沙箱被禁用的设备上运行。在已 root 的设备上,任何应用都可以读取其他应用的数据。根据 PCI DSS 要求和 OWASP Mobile Security 建议,银行和支付应用必须阻止在已 root 设备上的运行。
Magisk Hide 使用挂载命名空间(mount namespace)机制。对于例外列表中指定的每个进程,Magisk 创建一个隔离的命名空间,其中 su 二进制文件不可见。此命名空间中的系统调用 stat、access 和 open 看不到 Magisk 文件。可以通过检查 /proc/self/maps 是否存在并搜索 magisk dump 来检测 Magisk。
可以,如果应用不检查自身代码的完整性。通过 Frida 可以拦截 Java 检查方法并强制返回 false。对策 — 在 C++ 中原生实现关键逻辑并通过 DEX 文件的哈希检查完整性。如果没有混淆,任何用 Java 实现的 Root Detection 都可以在 5–10 分钟内绕过。
SafetyNet(已弃用)和 Play Integrity API — 这是 Google 的服务器端检查,用于确认设备完整性。它们包括引导加载程序、系统签名和 root 状态的检查。Play Integrity API — SafetyNet 的推荐替代品,提供三个级别:BASIC、DEVICE 和 STRONG。客户端的 Root Detection 补充了服务器端认证。
将应用安装到真正的已 root 设备上(例如,带 Magisk 的 Pixel)。检查阻止是否生效。然后尝试通过 Magisk Hide 为您的应用隐藏 root 并重复测试。要深入检查,使用 Frida 拦截目标方法,并确保原生保护无法绕过。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。