确定是否在已获取 Root 权限的设备上运行

364
我的应用程序有一项功能,只能在root可用的设备上运行。我希望在使用该功能之前,先进行根权限检查,如果没有根权限,则在首次使用时隐藏相关选项,而不是在使用时使该功能失败(然后向用户显示适当的错误消息)。是否有这样的方法可以实现呢?

19
目前没有可靠的方法来检查一个设备是否已经取得了 root 权限;下面的答案列出了常见特征,但某些设备可能没有按照通用方式进行 root。如果检测 root 变得普遍,那么 root 解决方案很可能会开始努力隐藏自己。由于它们可以修改操作系统行为,所以有很多选项可供选择。 - Chris Stratton
最好指出该功能不可用是由于缺乏根权限,向用户提供更多信息,而不是隐藏您的应用程序的功能,从而增加整体体验的歧义。 - nick fox
以下的答案适用于系统无根吗? - Piyush Kukadiya
1
它可以工作,但如果启用了像MagiskHide这样的root伪装/隐藏工具,那么你就完蛋了,因为这终究是个死胡同,没有办法克服它。 - FEBRYAN ASA PERDANA
29个回答

295

这里有一个类,可以通过三种方式之一来检查Root。

/** @author Kevin Kowalewski */
public class RootUtil {
    public static boolean isDeviceRooted() {
        return checkRootMethod1() || checkRootMethod2() || checkRootMethod3();
    }

    private static boolean checkRootMethod1() {
        String buildTags = android.os.Build.TAGS;
        return buildTags != null && buildTags.contains("test-keys");
    }

    private static boolean checkRootMethod2() {
        String[] paths = { "/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
                "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su"};
        for (String path : paths) {
            if (new File(path).exists()) return true;
        }
        return false;
    }

    private static boolean checkRootMethod3() {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(new String[] { "/system/xbin/which", "su" });
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            if (in.readLine() != null) return true;
            return false;
        } catch (Throwable t) {
            return false;
        } finally {
            if (process != null) process.destroy();
        }
    }
}

9
如果两个问题需要相同的答案,那么它们99%的情况下是重复的,所以请标记为重复而不是在两个问题上都发布相同的答案。谢谢。 - Kev
16
这种方法不可行,因为一些手机虽未经过Root,但包含了 su 二进制文件。 - neevek
14
想让您知道,Fox Digital Copy(Beta)应用程序几乎完全使用了您的代码,包括Root和ExecShell类以及checkRootMethod1/2/3方法。我发现这非常有趣。 - Matt Joseph
9
我可以像福克斯那样起诉他们吗? - Kevin Parker
4
这并不是作者的错,而是AIB公司的工程师和质量保证团队没有在各种设备和环境上测试脚本的行为。再次强调,没有可靠的方法来确定设备是否已经root。这种方法和其他任何方法一样容易出错。 - Idolon
显示剩余20条评论

109

如果您已经在使用 Fabric/Firebase Crashlytics,您可以调用:

CommonUtils.isRooted(context)

这是该方法的当前实现:

public static boolean isRooted(Context context) {
    boolean isEmulator = isEmulator(context);
    String buildTags = Build.TAGS;
    if (!isEmulator && buildTags != null && buildTags.contains("test-keys")) {
        return true;
    } else {
        File file = new File("/system/app/Superuser.apk");
        if (file.exists()) {
            return true;
        } else {
            file = new File("/system/xbin/su");
            return !isEmulator && file.exists();
        }
    }
}

public static boolean isEmulator(Context context) {
    String androidId = Secure.getString(context.getContentResolver(), "android_id");
    return "sdk".equals(Build.PRODUCT) || "google_sdk".equals(Build.PRODUCT) || androidId == null;
}

有史以来最好的答案。请使用此方法,而不是任何库,因为在中国设备上运行时会出现很多误报。 - Pedro Paulo Amorim
我在Nexus 5上进行了测试,使用https://download.chainfire.eu/363/CF-Root/CF-Auto-Root/CF-Auto-Root-hammerhead-hammerhead-nexus5.zip,但这个不准确。 - Jeffrey Liu
这段代码失败了。一组测试人员能够使用Magisk(默认配置)对多个设备进行root,但是这段代码没有检测到root。 - Emanuel
/system/xbin/su重命名为/system/xbin/ru,这种方法会失败。 - Jorge F. Sanchez
1
@sheko 我想这取决于你的服务有多安全。老实说,我从来没有添加过这种检查来防范那种欺诈行为。我使用它是为了保护用户在需要共享信用卡信息等时。 - kingston
显示剩余3条评论

60

RootTools库提供了简单的方法来检查root:

RootTools.isRootAvailable()

参考资料


13
isRootAvailable()函数只是检查路径中和一些硬编码目录中是否存在su文件,以判断设备是否已被root。但是我听说有些卸载root工具会保留su文件,这会导致误判。 - Bob Whiteman
13
RootTools.isAccessGiven() 不仅会检查是否具有 root 权限,还会请求获取 root 权限;因此,未经 root 的设备将始终使用此方法返回 false。 - aggregate1166877
2
@aggregate1166877,你说得没错,但这还不够好。如果我在询问时不需要root权限怎么办?我只是想知道它是否已经rooted,但目前我并不需要root权限。 - neevek
4
即使设备已经被root,当用户拒绝权限时,isAccessGiven()会返回false。 - subair_a
这是我唯一觉得值得点赞的答案。如果你想要类似于复制粘贴的东西,或者需要更多细节,请查看下面我的答案。 - rsimp
谢谢..找到设备是否已经root的最佳解决方案... :) - KAMAL VERMA

60
在我的应用程序中,我通过执行"su"命令来检查设备是否已经root。但是今天我已经删除了这部分代码。为什么呢?
因为我的应用程序变成了一个内存杀手。怎么回事呢?让我给你讲讲我的故事。
有一些投诉说我的应用程序会减慢设备的速度(当然我认为这不可能是真的)。我试图找出原因。所以我使用MAT来获取堆转储并进行分析,一切看起来都很完美。但是在多次重新启动我的应用程序后,我意识到设备真的变得越来越慢,而停止我的应用程序并不能让它变快(除非我重新启动设备)。我在设备非常慢的情况下再次分析了转储文件。但是对于转储文件来说,一切仍然都是完美的。 然后我做了首要的事情。我列出了进程。
$ adb shell ps

惊喜;我的应用程序有许多进程(在清单中有我的应用程序的进程标签)。其中一些是僵尸进程,而另一些则不是。
通过一个只执行“su”命令的简单应用程序样本,我意识到每次启动应用程序时都会创建一个僵尸进程。起初,这些僵尸进程分配了0KB,但之后发生了一些事情,僵尸进程占用的内存与我的应用程序的主进程几乎相同,并且它们变成了标准进程。
在bugs.sun.com上有一个关于相同问题的错误报告:https://bugs.java.com/bugdatabase/view_bug?bug_id=6474073。这解释了如果找不到命令,将使用exec()方法创建僵尸进程。但我仍然不明白为什么它们会变成标准进程并占用大量内存。(这并不是每次都发生)
如果你愿意,你可以尝试下面的代码示例;
String commandToExecute = "su";
executeShellCommand(commandToExecute);

简单的命令执行方法;
private boolean executeShellCommand(String command){
    Process process = null;            
    try{
        process = Runtime.getRuntime().exec(command);
        return true;
    } catch (Exception e) {
        return false;
    } finally{
        if(process != null){
            try{
                process.destroy();
            }catch (Exception e) {
            }
        }
    }
}

总结一下,我没有办法告诉你如何确定设备是否已经取得了root权限。但是如果我是你的话,我不会使用Runtime.getRuntime().exec()这个方法。
顺便说一下,RootTools.isRootAvailable()也会引起同样的问题。

5
这非常令人担忧。我有一个检测已Root设备的类,也出现了同样的情况——在阅读了aegean上面详细描述后,我得到了确认。偶尔会留下僵尸进程,设备变慢等问题...... - AWT
1
我确认在GT-S5830i Android 2.3.6上使用RootTools 3.4存在问题。大多数僵尸进程都分配了内存,这个问题是系统性的。我需要在进行3-4次测试后重新启动设备。我建议将测试结果保存到共享首选项中。 - Christ
2
谷歌现在推荐使用ProcessBuilder()和start()命令。 - EntangledLoops
1
@NickS 很有趣,但你启动了什么命令?我在不同API级别的多个Android手机上发出命令时没有遇到相同的问题,范围从9到23。 - EntangledLoops
1
@EntangledLoops。谢谢。我启动了自己的二进制文件,并通过stdin/stdout与其交互。我再次检查如何停止它,并发现在某些情况下我错过了Process.destroy()。所以,没有僵尸进程。 - Nick S
显示剩余6条评论

43

2017更新

现在你可以使用Google Safetynet API。SafetyNet API提供了Attestation API,可以帮助您评估应用程序运行的Android环境的安全性和兼容性。

此认证可以帮助确定特定设备是否被篡改或修改过。

Attestation API返回像这样的JWS响应

{
  "nonce": "R2Rra24fVm5xa2Mg",
  "timestampMs": 9860437986543,
  "apkPackageName": "com.package.name.of.requesting.app",
  "apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
                                  certificate used to sign requesting app"],
  "apkDigestSha256": "base64 encoded, SHA-256 hash of the app's APK",
  "ctsProfileMatch": true,
  "basicIntegrity": true,
}

解析此响应可帮助您确定设备是否已经取得了 root 权限。

已取得 root 权限的设备似乎会导致 ctsProfileMatch=false。

您可以在客户端上进行,但建议在服务器端解析响应。 具有 SafetyNet API 的基本客户端服务器架构如下所示:

请查看图片描述


4
非常有用的信息,如果在另一个背景下,我相信这将是正确的答案。不幸的是,提问者的问题并不是关于保护他的应用程序免受不安全环境的攻击,而是检测root权限以启用他的应用程序中仅限root使用的功能。对于提问者的预期目的,这个过程似乎过于复杂。 - rsimp
请问您能展示一下使用它的代码吗? - android developer
https://github.com/googlesamples/android-play-safetynet/ - Hitesh Sahu
Safetynet每天有10K个请求的配额限制。https://support.google.com/googleplay/android-developer/contact/safetynetqr - DAC84

41

这里列出的许多答案存在固有问题:

  • 检查测试密钥与root访问权限相关,但不一定保证其存在。
  • “PATH”目录应从实际的“PATH”环境变量派生,而不是硬编码。
  • “su”可执行文件的存在并不一定意味着设备已被root化。
  • “which”可执行文件可能已经安装或未安装,如果可能的话,您应该让系统解析它的路径。
  • 仅仅因为SuperUser应用程序已安装在设备上并不意味着设备已经具备了root访问权限。

Stericson的RootTools库似乎更可靠地检查root。它还有很多额外的工具和实用程序,因此我强烈推荐它。然而,没有解释如何特别检查root,并且它可能比大多数应用程序更重。

我做了几个实用方法,它们基于RootTools库。如果您只想检查设备上是否有“su”可执行文件,则可以使用以下方法:

public static boolean isRootAvailable(){
    for(String pathDir : System.getenv("PATH").split(":")){
        if(new File(pathDir, "su").exists()) {
            return true;
        }
    }
    return false;
}

这种方法简单地遍历“PATH”环境变量中列出的目录,检查其中是否存在一个“su”文件。

要真正检查root访问权限,必须实际运行“su”命令。如果已安装类似SuperUser的应用程序,则此时可能会请求root访问权限,或者如果已经授予/拒绝了访问权限,则可能显示一个toast指示是否已授予/拒绝了访问权限。一个好的命令是“id”,以便您可以验证用户ID是否为0(即root)。

以下是一个示例方法,用于确定是否已授予root访问权限:

public static boolean isRootGiven(){
    if (isRootAvailable()) {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(new String[]{"su", "-c", "id"});
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String output = in.readLine();
            if (output != null && output.toLowerCase().contains("uid=0"))
                return true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (process != null)
                process.destroy();
        }
    }

    return false;
}

测试运行“su”命令非常重要,因为一些模拟器预装了“su”可执行文件,但只允许某些用户访问它,比如adb shell。

在尝试运行“su”之前检查“su”可执行文件的存在也很重要,因为Android已知无法正确处理试图运行缺失命令的进程。这些幽灵进程会随着时间的推移增加内存消耗。


1
我认为这是想确保根不可用和想确保它可用之间的区别。如果您想确保设备没有被root,建议的检查是很好的。虽然会出现误报,但当在非受损设备上运行代码不受影响是您的主要关注时,这是可以接受的。 - Jeffrey Blattman
1
@DAC84,我不确定我是否理解你的问题。如果你在你的root应用程序上运行isRootGiven和deny,那么它应该返回false。这不是发生的情况吗?如果你想避免弹出警报,你可以使用isRootAvailable,它也可以被命名为doesSUExist。你也可以尝试配置你的root应用程序自由地分配root而不是管理它。 - rsimp
1
@BeeingJk 不是很确定,尽管这是你在不运行 su 的情况下可以检查的最多的内容。但在尝试执行之前,您需要检查 PATH 中是否有 su。然而,实际执行 su 通常会导致一个 toast 消息或与 root 管理应用程序的交互,这可能不是您想要的。对于您自己的逻辑,您可能认为 su 的存在就足够了。这仍然可能会在某些模拟器中产生误报,这些模拟器可能包含 su 可执行文件,但锁定对其的访问。 - rsimp
1
@BeeingJk 的 isRootAvailable 可能就是你所需要的,但我想要表达的重点是,像那样的名称,甚至是 doesSUExist,提供了比 isDeviceRooted 更好的语义。如果你真的需要在继续之前验证完整的 root 访问权限,你需要尝试使用 su 运行一个命令,就像在 isRootGiven 中编码的那样。 - rsimp
1
@yras8 当我写这个答案时,我主要注意到某些模拟器限制对adb的访问。但本质上,任何情况下,如果文件存在但用户没有可执行权限,或者可执行文件无法给您root权限,都会出现此问题。这是一个更加技术正确的解决方案。即使您不想预先检查运行su,我仍然建议将该函数命名为isRootAvailable或hasSuCommand,以获得更好的语义。无论哪种方式最适合您。 - rsimp
显示剩余7条评论

31

在Java级别进行根检查不是一个安全的解决方案。如果您的应用程序有安全问题在Rooted设备上运行,则请使用此解决方案。

Kevin的答案有效,除非手机还安装了像RootCloak这样的应用程序。这些应用程序在手机root后具有Java API的处理功能,并将这些API模拟为返回未root的手机。

我基于Kevin的回答编写了本地级别的代码,它甚至可以与RootCloak一起使用!而且它不会导致任何内存泄漏问题。

#include <string.h>
#include <jni.h>
#include <time.h>
#include <sys/stat.h>
#include <stdio.h>
#include "android_log.h"
#include <errno.h>
#include <unistd.h>
#include <sys/system_properties.h>

JNIEXPORT int JNICALL Java_com_test_RootUtils_checkRootAccessMethod1(
        JNIEnv* env, jobject thiz) {


    //Access function checks whether a particular file can be accessed
    int result = access("/system/app/Superuser.apk",F_OK);

    ANDROID_LOGV( "File Access Result %d\n", result);

    int len;
    char build_tags[PROP_VALUE_MAX]; // PROP_VALUE_MAX from <sys/system_properties.h>.
    len = __system_property_get(ANDROID_OS_BUILD_TAGS, build_tags); // On return, len will equal (int)strlen(model_id).
    if(strcmp(build_tags,"test-keys") == 0){
        ANDROID_LOGV( "Device has test keys\n", build_tags);
        result = 0;
    }
    ANDROID_LOGV( "File Access Result %s\n", build_tags);
    return result;

}

JNIEXPORT int JNICALL Java_com_test_RootUtils_checkRootAccessMethod2(
        JNIEnv* env, jobject thiz) {
    //which command is enabled only after Busy box is installed on a rooted device
    //Outpput of which command is the path to su file. On a non rooted device , we will get a null/ empty path
    //char* cmd = const_cast<char *>"which su";
    FILE* pipe = popen("which su", "r");
    if (!pipe) return -1;
    char buffer[128];
    std::string resultCmd = "";
    while(!feof(pipe)) {
        if(fgets(buffer, 128, pipe) != NULL)
            resultCmd += buffer;
    }
    pclose(pipe);

    const char *cstr = resultCmd.c_str();
    int result = -1;
    if(cstr == NULL || (strlen(cstr) == 0)){
        ANDROID_LOGV( "Result of Which command is Null");
    }else{
        result = 0;
        ANDROID_LOGV( "Result of Which command %s\n", cstr);
        }
    return result;

}

JNIEXPORT int JNICALL Java_com_test_RootUtils_checkRootAccessMethod3(
        JNIEnv* env, jobject thiz) {


    int len;
    char build_tags[PROP_VALUE_MAX]; // PROP_VALUE_MAX from <sys/system_properties.h>.
    int result = -1;
    len = __system_property_get(ANDROID_OS_BUILD_TAGS, build_tags); // On return, len will equal (int)strlen(model_id).
    if(len >0 && strstr(build_tags,"test-keys") != NULL){
        ANDROID_LOGV( "Device has test keys\n", build_tags);
        result = 0;
    }

    return result;

}

在您的Java代码中,您需要创建包装器类RootUtils来进行本地调用。

    public boolean checkRooted() {

       if( rootUtils.checkRootAccessMethod3()  == 0 || rootUtils.checkRootAccessMethod1()  == 0 || rootUtils.checkRootAccessMethod2()  == 0 )
           return true;
      return false;
     }

1
我认为root检测可以分为两类,一种是启用依赖root的功能,另一种是基于安全性的措施,以尝试减轻已经root的手机所带来的安全问题。对于依赖root的功能,我认为Kevin的答案相当糟糕。在这个答案的背景下,这些方法更有意义。虽然我会重写第二种方法,不使用"which",而是迭代PATH环境变量来搜索"su"。因为"which"不能保证在手机上存在。 - rsimp
1
请问您能否提供一个如何在Java中使用这段C代码的示例? - mrid
@mrid 请查看如何在Android上从Java进行JNI调用。 - Alok Kulkarni
这种方法使用RootCloak应用程序来防止root检测绕过。是否有任何已知的root绕过技术会失败这些方法? - Nidhin
1
Termux附带了一个su二进制文件,这会导致根检测器误报。非常感谢您在发现此问题方面的帮助! - user1158559

23

http://code.google.com/p/roottools/

如果您不想使用jar文件,请使用以下代码:

public static boolean findBinary(String binaryName) {
        boolean found = false;
        if (!found) {
            String[] places = { "/sbin/", "/system/bin/", "/system/xbin/",
                    "/data/local/xbin/", "/data/local/bin/",
                    "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/" };
            for (String where : places) {
                if (new File(where + binaryName).exists()) {
                    found = true;

                    break;
                }
            }
        }
        return found;
    }

程序将尝试查找su文件夹:

private static boolean isRooted() {
        return findBinary("su");
    }

例子:

if (isRooted()) {
   textView.setText("Device Rooted");

} else {
   textView.setText("Device Unrooted");
}

3
永远不要在布尔值中添加== true,这无意义且显得不好看。 - minipif
这会减慢您的应用程序用户的手机。 - smoothBlue
2
@smoothBlue 为什么会呢?它并没有像 DevrimTuncer 的解决方案那样生成任何进程。 - FD_
1
一个更好的想法是迭代 PATH,而不是硬编码典型的 PATH 目录。 - rsimp
1
使用 if (isRooted()) 检查而不是明确地写 true。最好遵循代码编写模式。 - blueware
显示剩余5条评论

14

RootBeer是由Scott和Matthew开发的一个Android设备root检查库。它使用各种检查来判断设备是否已被root。

Java检查

  • CheckRootManagementApps

  • CheckPotentiallyDangerousApps

  • CheckRootCloakingApps

  • CheckTestKeys

  • checkForDangerousProps

  • checkForBusyBoxBinary

  • checkForSuBinary

  • checkSuExists

  • checkForRWSystem

本地检查

我们通过调用本地root检查器来运行一些自己的检查。本地检查通常更难以伪装,因此一些root伪装应用程序会阻止包含特定关键字的本地库的加载。

  • checkForSuBinary

13

你可以使用isAccessGiven()代替isRootAvailable()。直接来自RootToolswiki

if (RootTools.isAccessGiven()) {
    // your app has been granted root access
}

RootTools.isAccessGiven() 不仅检查设备是否已经 root,还会为您的应用程序调用 su,请求权限,并在您的应用程序成功获得 root 权限时返回 true。这可以作为您的应用程序中的第一个检查,以确保在需要访问时将被授予权限。

参考资料


但是用户必须授予根访问权限,如果我的目的是防止我的应用在设备已经取得 root 权限时运行,那么我的选择非常有限。 - Nasz Njoka Sr.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接