应用安全 --- app加固 之 root检测
下面我将详细列举 Android 平台上的 Root 检测方法及其对抗方案。
Root 检测与对抗全景图
Root 检测的核心思路是寻找设备被 Root 后留下的 “痕迹”。对抗的核心思路则是 “隐藏痕迹” 或 “欺骗检测API”。
一、常规路径与文件检测
这是最基础、最常见的检测方法,检查通常只有 Root 后才会存在的特殊文件、路径和二进制工具。
检测方法 | 检测目标示例 | 对抗方法 |
---|---|---|
检查 Superuser APK | /system/app/Superuser.apk /system/app/SuperSU.apk |
重命名/隐藏APK:使用 Magisk Hide 或类似功能隐藏管理应用。 |
检查 SU 二进制文件 | /system/bin/su /system/xbin/su /sbin/su /vendor/bin/su |
移除默认SU路径:Magisk 将其 magisk 二进制文件放在随机化的路径下,并通过 su 符号链接调用。隐藏符号链接是关键。 |
检查其他Root工具 | busybox , sqlite3 等常用工具 |
避免安装:不要安装不必要的、可能暴露环境的工具。 |
检查测试密钥 | ro.build.tags=test-keys |
修改Build属性:使用 MagiskHide Props Config 模块,将指纹 (fingerprint ) 和标签 (tags ) 模拟为官方发布密钥 (release-keys )。 |
对抗代码示例 (Hook java.io.File.exists()
):
Java.perform(function() {
var suspiciousPaths = [
"/system/bin/su", "/system/xbin/su", "/sbin/su",
"/system/app/Superuser.apk", "/system/app/SuperSU"
];
var File = Java.use("java.io.File");
File.exists.implementation = function() {
var path = this.getAbsolutePath();
for (var i = 0; i < suspiciousPaths.length; i++) {
if (path.startsWith(suspiciousPaths[i])) {
console.log("[Bypass] Blocking access to: " + path);
return false; // 告诉调用者文件不存在
}
}
return this.exists(); // 正常调用原方法
};
});
二、环境属性检测
检查系统的各种属性,寻找与官方系统不一致的“非正常”值。
检测方法 | 检测目标示例 | 对抗方法 |
---|---|---|
检查 ro.debuggable |
getprop ro.debuggable != 0 |
动态修改属性值:使用 Magisk Hide 或通过 Hook 在应用读取时临时返回 0 。 |
检查 ro.secure |
getprop ro.secure != 1 |
同上,动态修改返回值。 |
检查 ro.build.type |
getprop ro.build.type != user |
修改为 user 。 |
检查特定Magisk属性 | ro.boot.vbmeta.device_state ro.boot.veritymode |
使用 Magisk 本身:Magisk 会主动隐藏这些痕迹。 |
对抗代码示例 (Hook android.os.SystemProperties.get()
):
Java.perform(function() {
var SystemProperties = Java.use("android.os.SystemProperties");
SystemProperties.get.overload('java.lang.String').implementation = function(key) {
if (key === "ro.debuggable") {
return "0";
} else if (key === "ro.secure") {
return "1";
} else if (key === "ro.build.type") {
return "user";
} else if (key === "ro.build.tags") {
return "release-keys";
}
return this.get(key); // 其他属性正常返回
};
// 另一个重载方法也需要Hook
SystemProperties.get.overload('java.lang.String', 'java.lang.String').implementation = function(key, def) {
var origResult = this.get(key, def);
if (key === "ro.debuggable") {
return "0";
}
// ... 其他属性同上
return origResult;
};
});
三、运行命令检测
尝试执行 which su
或 pm list packages
等命令,查看输出结果。
检测方法 | 检测目标示例 | 对抗方法 |
---|---|---|
which su |
检查命令返回值或输出 | 隐藏 su 命令:Magisk 通过修改 PATH 环境变量或挂钩子 (hook ) 系统调用来实现。 |
pm list packages |
检查包列表中是否存在 magisk 、supersu 等 |
隐藏管理包名:使用 Magisk Hide 功能隐藏特定的应用程序。 |
对抗代码示例 (Hook java.lang.Runtime.exec()
):
Java.perform(function() {
var Runtime = Java.use("java.lang.Runtime");
var execOverloads = ['java.lang.String', '[Ljava.lang.String;'];
for (var i = 0; i < execOverloads.length; i++) {
Runtime.exec.overload(execOverloads[i]).implementation = function() {
var cmd = arguments[0];
if (typeof cmd === 'string') {
if (cmd.includes("which su") || cmd.includes("/system/bin/which su")) {
console.log("[Bypass] Blocking command: " + cmd);
// 可以返回一个空的Process对象或抛异常,但更安全的是返回一个“未找到”的模拟输出
// 这里需要更复杂的实现来模拟一个空的InputStream
return null; // 简单示例,实际处理更复杂
}
}
return this.exec.apply(this, arguments);
};
}
});
四、Native层检测
在 C/C++ 层进行更底层、更隐蔽的检测。
检测方法 | 检测目标示例 | 对抗方法 |
---|---|---|
直接使用 access() 系统调用 |
access("/system/bin/su", F_OK) |
Hook Native 函数:使用 Frida 或 Xposed (Whale) 来 Hook libc 中的 access 、fopen 、stat 等函数。 |
检查进程列表 | 读取 /proc/self/mounts 查找 magisk 路径 |
隐藏Magisk路径:Magisk 会使用 mount 命名空间来隔离其修改,使其对目标应用不可见。 |
检测加载的库 | 检查 maps 中是否加载了 libsu.so 等 |
隐藏库:避免使用会注入so库的Root管理App,或Hook读取 /proc/self/maps 的函数。 |
对抗代码示例 (Hook Native access()
):
// Frida JS 代码
var libc = Module.findBaseName('libc.so');
var accessAddr = Module.findExportByName(libc, 'access');
Interceptor.attach(accessAddr, {
onEnter: function(args) {
this.path = args[0].readCString();
if (this.path && this.path.includes("su")) {
console.log("[Bypass] Blocking access to: " + this.path);
// 让 access 调用返回 -1 (表示文件不存在),并设置 errno 为 ENOENT
this.errno = 0x20000002; // 根据系统定义,通常为 2
}
},
onLeave: function(retval) {
if (this.errno) {
retval.replace(-1); // 返回 -1
// 在某些架构上,还需要通过 __errno location 来设置 errno
}
}
});
五、综合与高阶检测
检测方法 | 检测目标示例 | 对抗方法 |
---|---|---|
SafetyNet API | Google 官方的完整性检验 API。 | Magisk 的核心功能:Magisk 的 Universal SafetyNet Fix 模块通过模拟硬件 attestation 来通过验证。 |
执行结果差异性 | 对比 stat("/system/bin/su") 和 stat("/system/bin/ls") 的权限、用户组等。 |
完善的隐藏方案:需要完整的Root解决方案(如Magisk)在系统层面进行全局隐藏,而不是局部Hook。 |
内核特征检测 | 检测 syscall 表是否被修改(挂钩子)。 |
KernelSU:另一种Root方案,修改内核但力求隐藏得更好。对抗难度极大。 |
总结与最佳实践
对于普通用户/测试人员:
-
首选 Magisk:它是目前隐藏能力最强、更新最活跃的Root方案。
-
开启 Magisk Hide:在设置中启用,并勾选需要隐藏Root的目标应用(如银行App、游戏等)。
-
安装模块:安装
Universal SafetyNet Fix
和MagiskHide Props Config
等模块来通过更严格的检测。 -
隐藏 Magisk 应用:在 Magisk 设置中,有一个选项可以将其自身应用包名随机化。
对于安全研究人员(需要对抗更强检测):
-
组合使用工具:
Magisk
+Frida
。 -
定制化Hook:分析目标App的检测逻辑,使用上述 Frida 脚本示例,编写针对性的Hook代码。
-
环境隔离:使用基于内核的隔离方案(如 KernelSU),或者直接在定制ROM中实现隐藏。
-
动态分析:如果Hook失败,可能是因为检测在Native层或使用了反调试。需要逆向App的so库,找到检测函数并下断点分析,然后制定更底层的Hook方案。
记住:这是一场持续的攻防战。新的检测方法不断出现,而隐藏技术也在不断进化。没有一劳永逸的方案,关键在于理解原理,灵活运用各种工具。
过检工具
firda脚本绕过
https://github.com/AshenOneYe/FridaAntiRootDetection
Shamiko模块绕过
https://github.com/LSPosed/LSPosed.github.io/releases/tag/shamiko-344
狐狸面具绕过(推荐这种方法,一劳永逸)
对抗
const-string ([vp]\d+), “*.su”
替换
const-string $1, "/no_su"
最终效果
修改前:
const-string v0, "/system/xbin/su" # 加载一个真实存在的Root路径
# file.exists() -> true (检测到Root)
修改后:
const-string v0, "/no su" # 加载一个绝对不存在的假路径
# file.exists() -> false (未检测到Root)
通过将检测Root的关键字符串(su
的路径)替换为一个无意义的、不存在的字符串,应用程序的Root检查逻辑就会被绕过,认为设备没有Root权限,从而允许应用继续正常运行。