在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