在Android中如何判断手机是否Root以及应用是否获取了Root权限,下面我们将对开源项目RootTools的源码进行分析。

RootTools的源码地址:https://github.com/Stericson/RootTools

一、RootTools.isRootAvailable()判断手机是否已经Root
下面RootTools这个类中,RootTools.isRootAvailable()可以判断手机是否已经Root,下面我们来看看它的源码。

public static boolean isRootAvailable()
{
    return RootShell.isRootAvailable();
}

它实质调用的是RootShell.isRootAvailable()函数,我们接着来看看这个函数的源码。

public static boolean isRootAvailable() {
    return (findBinary("su")).size() > 0;
}

从上面的代码中我们可以看到它实际是在查找su这个执行文件是否存在,我们知道,我们切换到root用户下,使用的就是su命令,如果这个文件存在,就基本可以知道手机已经Root,因为我们可以运行su命令切换到root用户下。

下面我们来看看RootShell.findBinary(“su”)这个函数。

public static List<String> findBinary(final String binaryName) {
    return findBinary(binaryName, null);
}

public static List<String> findBinary(final String binaryName, List<String> searchPaths) {

    final List<String> foundPaths = new ArrayList<String>();

    boolean found = false;

    // 1、如果搜索路径为空,得到环境变量PATH路径
    if(searchPaths == null)
    {
        searchPaths = RootShell.getPath();
    }

    RootShell.log("Checking for " + binaryName);

    // 2、使用了两种方法来检查
    // 2.1、遍历所有路径,尝试使用stat命令来查看su文件信息
    //Try to use stat first
    try {
        for (String path : searchPaths) {

            if(!path.endsWith("/"))
            {
                path += "/";
            }

            final String currentPath = path;

            Command cc = new Command(0, false, "stat " + path + binaryName) {
                // 对执行stat命令后的输出信息进行判断
                // 如果包含了"File: "和"su"就表示这个路径存在su命令
                @Override
                public void commandOutput(int id, String line) {
                    if (line.contains("File: ") && line.contains(binaryName)) {
                        foundPaths.add(currentPath);

                        RootShell.log(binaryName + " was found here: " + currentPath);
                    }

                    RootShell.log(line);

                    super.commandOutput(id, line);
                }
            };

            RootShell.getShell(false).add(cc);
            commandWait(RootShell.getShell(false), cc);

        }

        found = !foundPaths.isEmpty();
    } catch (Exception e) {
        RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on.");
    }

    // 2.2、如果第一种方法没有找到,就使用下面这种方法
    // 遍历所有路径,使用ls命令查看su文件是否存在
    if (!found) {
        RootShell.log("Trying second method");

        for (String path : searchPaths) {

            if(!path.endsWith("/"))
            {
                path += "/";
            }

            if (RootShell.exists(path + binaryName)) {
                RootShell.log(binaryName + " was found here: " + path);
                foundPaths.add(path);
            } else {
                RootShell.log(binaryName + " was NOT found here: " + path);
            }
        }
    }

    Collections.reverse(foundPaths);

    return foundPaths;
}

1、如果搜索路径为空,得到环境变量PATH路径
这一步的关键就是RootShell.getPath()这个函数。我们来重点看看这个函数。

public static List<String> getPath() {
    return Arrays.asList(System.getenv("PATH").split(":"));
}

我们可以很容易的看出,它首先获取环境变量PATH的值,然后得到PATH里面配置的所有路径。

2、针对上面得到的路径,这些路径可能就是su文件所在的路径,进行遍历,主要提供了两种方法。

2.1、遍历所有路径,尝试使用stat命令来查看su文件信息
(1)使用for循环依次得到每个路径,然后将su文件拼接在后面。例如我们得到一个/system/bin/路径,然后将su拼接在后面就是/system/bin/su。
(2)创建一个”stat /system/bin/su”的Command命令。并且重写了Command的commandOutput方法,这个方法可以得到执行”stat /system/bin/su”的输出信息,然后对输出信息进行比较判断,如果输出信息包含了”File: “和”su”,那么就说明这个路径下面包含了su命令,就将它添加到foundPaths列表中,最终如果foundPaths为空,表示没有找到,如果不为空就表示找到了,如果为空,就使用第二种方法接着查找。

2.2、遍历所有路径,使用ls命令查看su文件是否存在

它的核心就是RootShell.exists(path + binaryName)这个检查函数。

public static boolean exists(final String file) {
    return exists(file, false);
}

public static boolean exists(final String file, boolean isDir) {
    final List<String> result = new ArrayList<String>();

    String cmdToExecute = "ls " + (isDir ? "-d " : " ");

    Command command = new Command(0, false, cmdToExecute + file) {
        @Override
        public void commandOutput(int id, String line) {
            RootShell.log(line);
            result.add(line);

            super.commandOutput(id, line);
        }
    };

    try {
        //Try without root...
        RootShell.getShell(false).add(command);
        commandWait(RootShell.getShell(false), command);

    } catch (Exception e) {
        return false;
    }

    for (String line : result) {
        if (line.trim().equals(file)) {
            return true;
        }
    }

    result.clear();

    try {
        RootShell.getShell(true).add(command);
        commandWait(RootShell.getShell(true), command);

    } catch (Exception e) {
        return false;
    }

    //Avoid concurrent modification...
    List<String> final_result = new ArrayList<String>();
    final_result.addAll(result);

    for (String line : final_result) {
        if (line.trim().equals(file)) {
            return true;
        }
    }

    return false;

}

可以看到,这个逻辑跟上面2.1的基本一样,就是执行的Command命令不一样,2.1执行的是stat命令,这里执行的是ls命令。

既然这两种方法的思想是一样的,那么下面,我们来看看这个Command命令是如何执行的。

1、创建Command命令

Command cc = new Command(0, false, "stat " + path + binaryName) {
    @Override
    public void commandOutput(int id, String line) {
        if (line.contains("File: ") && line.contains(binaryName)) {
            foundPaths.add(currentPath);

            RootShell.log(binaryName + " was found here: " + currentPath);
        }

        RootShell.log(line);

        super.commandOutput(id, line);
    }
};

2、执行RootShell.getShell(false)

public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException {
    return RootShell.getShell(root, 0);
}

public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException {
    return getShell(root, timeout, Shell.defaultContext, 3);
}

public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException {
    if (root) {
        return Shell.startRootShell(timeout, shellContext, retry);
    } else {
        return Shell.startShell(timeout);
    }
}

我们可以看到,最终执行的是Shell.startShell(timeout)函数。

public static Shell startShell(int timeout) throws IOException, TimeoutException {

    try {
        if (Shell.shell == null) {
            RootShell.log("Starting Shell!");
            Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout);
        } else {
            RootShell.log("Using Existing Shell!");
        }
        return Shell.shell;
    } catch (RootDeniedException e) {
        //Root Denied should never be thrown.
        throw new IOException();
    }
}

如果shell为空,就创建一个shell,我们来看看这个创建过程new Shell(“/system/bin/sh”, ShellType.NORMAL, ShellContext.NORMAL, timeout)。

private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException {

    RootShell.log("Starting shell: " + cmd);
    RootShell.log("Context: " + shellContext.getValue());
    RootShell.log("Timeout: " + shellTimeout);

    this.shellType = shellType;
    this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout;
    this.shellContext = shellContext;

    // 1、这里会执行cmd命令,就是上面传过来的"/system/bin/sh"
    if (this.shellContext == ShellContext.NORMAL) {
        this.proc = Runtime.getRuntime().exec(cmd);
    } else {
        String display = getSuVersion(false);
        String internal = getSuVersion(true);

        //only done for root shell...
        //Right now only SUPERSU supports the --context switch
        if (isSELinuxEnforcing() &&
                (display != null) &&
                (internal != null) &&
                (display.endsWith("SUPERSU")) &&
                (Integer.valueOf(internal) >= 190)) {
            cmd += " --context " + this.shellContext.getValue();
        } else {
            RootShell.log("Su binary --context switch not supported!");
            RootShell.log("Su binary display version: " + display);
            RootShell.log("Su binary internal version: " + internal);
            RootShell.log("SELinuxEnforcing: " + isSELinuxEnforcing());
        }

        this.proc = Runtime.getRuntime().exec(cmd);

    }

    // 2、得到执行cmd命令之后的输入流、输出流和错误流
    this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), "UTF-8"));
    this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), "UTF-8"));
    this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), "UTF-8");

    //3、启动一个Worker线程去不断的读取输入流
    Worker worker = new Worker(this);
    worker.start();

    try {
        worker.join(this.shellTimeout);

        if (worker.exit == -911) {

            try {
                this.proc.destroy();
            } catch (Exception e) {
            }

            closeQuietly(this.inputStream);
            closeQuietly(this.errorStream);
            closeQuietly(this.outputStream);

            throw new TimeoutException(this.error);
        }
        /**
         * Root access denied?
         */
        else if (worker.exit == -42) {

            try {
                this.proc.destroy();
            } catch (Exception e) {
            }

            closeQuietly(this.inputStream);
            closeQuietly(this.errorStream);
            closeQuietly(this.outputStream);

            throw new RootDeniedException("Root Access Denied");
        }
        /**
         * Normal exit
         */
        else {
            // 4、启动一个线程,向输入流中输入命令
            Thread si = new Thread(this.input, "Shell Input");
            si.setPriority(Thread.NORM_PRIORITY);
            si.start();

            // 5、启动一个线程,得到执行命令之后输出流的信息
            Thread so = new Thread(this.output, "Shell Output");
            so.setPriority(Thread.NORM_PRIORITY);
            so.start();
        }
    } catch (InterruptedException ex) {
        worker.interrupt();
        Thread.currentThread().interrupt();
        throw new TimeoutException();
    }
}

1、执行”/system/bin/sh”命令

this.proc = Runtime.getRuntime().exec(cmd);

2、得到执行命令之后的输入流、输出流、错误流

this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), "UTF-8"));
this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), "UTF-8"));
this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), "UTF-8");

3、启动一个Worker线程去不断的读取输入流

protected static class Worker extends Thread {

    public int exit = -911;

    public Shell shell;

    private Worker(Shell shell) {
        this.shell = shell;
    }

    public void run() {
        try {
            shell.outputStream.write("echo Started\n");
            shell.outputStream.flush();

            // 这里有一个死循环,不断的读取输入流
            while (true) {
                String line = shell.inputStream.readLine();

                if (line == null) {
                    throw new EOFException();
                } else if ("".equals(line)) {
                    continue;
                } else if ("Started".equals(line)) {
                    this.exit = 1;
                    setShellOom();
                    break;
                }

                shell.error = "unknown error occurred.";
            }
        } catch (IOException e) {
            exit = -42;
            if (e.getMessage() != null) {
                shell.error = e.getMessage();
            } else {
                shell.error = "RootAccess denied?.";
            }
        }

    }
}

4、启动一个线程,向输入流中输入命令

我们来看看input这个传入的Runnable参数

private Runnable input = new Runnable() {
    public void run() {

        try {
            while (true) {

                synchronized (commands) {
                    while (!close && write >= commands.size()) {
                        isExecuting = false;
                        commands.wait();
                    }
                }

                if (write >= maxCommands) {
                    while (read != write) {
                        RootShell.log("Waiting for read and write to catch up before cleanup.");
                    }
                    /**
                     * Clean up the commands, stay neat.
                     */
                    cleanCommands();
                }

                // 向outputStream中写入一条命令,也就是sh执行了一条命令
                if (write < commands.size()) {
                    isExecuting = true;
                    Command cmd = commands.get(write);
                    cmd.startExecution();
                    RootShell.log("Executing: " + cmd.getCommand() + " with context: " + shellContext);

                    outputStream.write(cmd.getCommand());
                    String line = "\necho " + token + " " + totalExecuted + " $?\n";
                    outputStream.write(line);
                    outputStream.flush();
                    write++;
                    totalExecuted++;
                } else if (close) {
                    isExecuting = false;
                    outputStream.write("\nexit 0\n");
                    outputStream.flush();
                    RootShell.log("Closing shell");
                    return;
                }
            }
        } catch (IOException e) {
            RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
        } catch (InterruptedException e) {
            RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
        } finally {
            write = 0;
            closeQuietly(outputStream);
        }
    }
};

5、启动一个线程,得到执行命令之后输出流的信息

我们来看看output这个传入的Runnable参数

private Runnable output = new Runnable() {
    public void run() {
        try {
            Command command = null;

            while (!close || inputStream.ready() || read < commands.size()) {
                isReading = false;
                // 从输入流中读出一行
                String outputLine = inputStream.readLine();
                isReading = true;

                if (outputLine == null) {
                    break;
                }

                if (command == null) {
                    if (read >= commands.size()) {
                        if (close) {
                            break;
                        }

                        continue;
                    }

                    command = commands.get(read);
                }

                int pos = -1;

                pos = outputLine.indexOf(token);
                // 最终调用我们Command重新的那个函数,用来处理输出信息
                if (pos == -1) {
                    command.output(command.id, outputLine);
                } else if (pos > 0) {
                    command.output(command.id, outputLine.substring(0, pos));
                }

                if (pos >= 0) {
                    outputLine = outputLine.substring(pos);
                    String fields[] = outputLine.split(" ");

                    if (fields.length >= 2 && fields[1] != null) {
                        int id = 0;

                        try {
                            id = Integer.parseInt(fields[1]);
                        } catch (NumberFormatException e) {
                        }

                        int exitCode = -1;

                        try {
                            exitCode = Integer.parseInt(fields[2]);
                        } catch (NumberFormatException e) {
                        }

                        if (id == totalRead) {
                            processErrors(command);

                            int iterations = 0;
                            while (command.totalOutput > command.totalOutputProcessed) {

                                if(iterations == 0)
                                {
                                    iterations++;
                                    RootShell.log("Waiting for output to be processed. " + command.totalOutputProcessed + " Of " + command.totalOutput);
                                }

                                try {

                                    synchronized (this)
                                    {
                                        this.wait(2000);
                                    }
                                } catch (Exception e) {
                                    RootShell.log(e.getMessage());
                                }
                            }

                            RootShell.log("Read all output");

                            command.setExitCode(exitCode);
                            command.commandFinished();
                            command = null;

                            read++;
                            totalRead++;
                            continue;
                        }
                    }
                }
            }

            try {
                proc.waitFor();
                proc.destroy();
            } catch (Exception e) {
            }

            while (read < commands.size()) {
                if (command == null) {
                    command = commands.get(read);
                }

                if(command.totalOutput < command.totalOutputProcessed)
                {
                    command.terminated("All output not processed!");
                    command.terminated("Did you forget the super.commandOutput call or are you waiting on the command object?");
                }
                else
                {
                    command.terminated("Unexpected Termination.");
                }

                command = null;
                read++;
            }

            read = 0;

        } catch (IOException e) {
            RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
        } finally {
            closeQuietly(outputStream);
            closeQuietly(errorStream);
            closeQuietly(inputStream);

            RootShell.log("Shell destroyed");
            isClosed = true;
            isReading = false;
        }
    }
};

二、RootTools.isAccessGiven()判断app是否被授予root权限

public static boolean isAccessGiven()
{
    return RootShell.isAccessGiven();
}

可以看到它实际调用了RootShell.isAccessGiven()方法,所以我们来看看它的源码。

public static boolean isAccessGiven() {
    final Set<String> ID = new HashSet<String>();
    final int IAG = 158;

    try {
        RootShell.log("Checking for Root access");

        Command command = new Command(IAG, false, "id") {
            @Override
            public void commandOutput(int id, String line) {
                if (id == IAG) {
                    ID.addAll(Arrays.asList(line.split(" ")));
                }

                super.commandOutput(id, line);
            }
        };

        Shell.startRootShell().add(command);
        commandWait(Shell.startRootShell(), command);

        //parse the userid
        for (String userid : ID) {
            RootShell.log(userid);

            if (userid.toLowerCase().contains("uid=0")) {
                RootShell.log("Access Given");
                return true;
            }
        }

        return false;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

可以看到这里的思想跟上面2.1和2.2的也基本相同,都是执行Command命令,这里执行的是id这个命令。它的原理就是如果使用id得到所有的id信息,然后对这些id信息进行判断,如果里面包含”uid=0”就表示应用获取到了Root权限。

参考文章:

检查Android是否已经获取root权限

Android中判断手机是否已经Root

http://bbs.csdn.net/topics/390885158

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐