package com.scottyab.rootbeer;
import android.content.Context;
import android.content.pm.PackageManager;
import com.scottyab.rootbeer.util.QLog;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Scanner;
/**
* A simple root checker that gives an *indication* if the device is rooted or not.
* Disclaimer: **root==god**, so there's no 100% way to check for root.
*/
public class RootBeer {
private final Context mContext;
private boolean loggingEnabled = true;
public RootBeer(Context context) {
mContext = context;
}
/**
* Run all the checks.
* To run the same check but without looking for the busybox binary to avoid a false positive for certain devices please
* see {@link #isRootedWithoutBusyBoxCheck() isRootedWithoutBusyBoxCheck}
*
* @return true, we think there's a good *indication* of root | false good *indication* of no root (could still be cloaked)
*/
public boolean isRooted() {
return detectRootManagementApps() || detectPotentiallyDangerousApps() || checkForBinary("su")
|| checkForBinary("busybox") || checkForDangerousProps() || checkForRWPaths()
|| detectTestKeys() || checkSuExists() || checkForRootNative();
}
/**
* Run all the checks apart from checking for the busybox binary. This is because it can sometimes be a false positive
* as some manufacturers leave the binary in production builds.
* @return true, we think there's a good *indication* of root | false good *indication* of no root (could still be cloaked)
*/
public boolean isRootedWithoutBusyBoxCheck() {
return detectRootManagementApps() || detectPotentiallyDangerousApps() || checkForBinary("su")
|| checkForDangerousProps() || checkForRWPaths()
|| detectTestKeys() || checkSuExists() || checkForRootNative();
}
/**
* Release-Keys and Test-Keys has to do with how the kernel is signed when it is compiled.
* Test-Keys means it was signed with a custom key generated by a third-party developer.
* @return true if signed with Test-keys
*/
public boolean detectTestKeys() {
String buildTags = android.os.Build.TAGS;
return buildTags != null && buildTags.contains("test-keys");
}
/**
* Using the PackageManager, check for a list of well known root apps. @link {Const.knownRootAppsPackages}
* @return true if one of the apps it's installed
*/
public boolean detectRootManagementApps() {
return detectRootManagementApps(null);
}
/**
* Using the PackageManager, check for a list of well known root apps. @link {Const.knownRootAppsPackages}
* @param additionalRootManagementApps - array of additional packagenames to search for
* @return true if one of the apps it's installed
*/
public boolean detectRootManagementApps(String[] additionalRootManagementApps) {
// Create a list of package names to iterate over from constants any others provided
ArrayList<String> packages = new ArrayList<>();
packages.addAll(Arrays.asList(Const.knownRootAppsPackages));
if (additionalRootManagementApps!=null && additionalRootManagementApps.length>0){
packages.addAll(Arrays.asList(additionalRootManagementApps));
}
return isAnyPackageFromListInstalled(packages);
}
/**
* Using the PackageManager, check for a list of well known apps that require root. @link {Const.knownRootAppsPackages}
* @return true if one of the apps it's installed
*/
public boolean detectPotentiallyDangerousApps() {
return detectPotentiallyDangerousApps(null);
}
/**
* Using the PackageManager, check for a list of well known apps that require root. @link {Const.knownRootAppsPackages}
* @param additionalDangerousApps - array of additional packagenames to search for
* @return true if one of the apps it's installed
*/
public boolean detectPotentiallyDangerousApps(String[] additionalDangerousApps) {
// Create a list of package names to iterate over from constants any others provided
ArrayList<String> packages = new ArrayList<>();
packages.addAll(Arrays.asList(Const.knownDangerousAppsPackages));
if (additionalDangerousApps!=null && additionalDangerousApps.length>0){
packages.addAll(Arrays.asList(additionalDangerousApps));
}
return isAnyPackageFromListInstalled(packages);
}
/**
* Using the PackageManager, check for a list of well known root cloak apps. @link {Const.knownRootAppsPackages}
* and checks for native library read access
* @return true if one of the apps it's installed
*/
public boolean detectRootCloakingApps() {
return detectRootCloakingApps(null) || canLoadNativeLibrary() && !checkForNativeLibraryReadAccess();
}
/**
* Using the PackageManager, check for a list of well known root cloak apps. @link {Const.knownRootAppsPackages}
* @param additionalRootCloakingApps - array of additional packagenames to search for
* @return true if one of the apps it's installed
*/
public boolean detectRootCloakingApps(String[] additionalRootCloakingApps) {
// Create a list of package names to iterate over from constants any others provided
ArrayList<String> packages = new ArrayList<>();
packages.addAll(Arrays.asList(Const.knownRootCloakingPackages));
if (additionalRootCloakingApps!=null && additionalRootCloakingApps.length>0){
packages.addAll(Arrays.asList(additionalRootCloakingApps));
}
return isAnyPackageFromListInstalled(packages);
}
/**
* Checks various (Const.suPaths) common locations for the SU binary
* @return true if found
*/
public boolean checkForSuBinary(){
return checkForBinary("su");
}
/**
* Checks various (Const.suPaths) common locations for the busybox binary (a well know root level program)
* @return true if found
*/
public boolean checkForBusyBoxBinary(){
return checkForBinary("busybox");
}
/**
*
* @param filename - check for this existence of this file
* @return true if found
*/
public boolean checkForBinary(String filename) {
String[] pathsArray = Const.suPaths;
boolean result = false;
for (String path : pathsArray) {
String completePath = path + filename;
File f = new File(completePath);
boolean fileExists = f.exists();
if (fileExists) {
QLog.v(completePath + " binary detected!");
result = true;
}
}
return result;
}
/**
*
* @param logging - set to true for logging
*/
public void setLogging(boolean logging) {
loggingEnabled = logging;
QLog.LOGGING_LEVEL = logging ? QLog.ALL : QLog.NONE;
}
private String[] propsReader() {
InputStream inputstream = null;
try {
inputstream = Runtime.getRuntime().exec("getprop").getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
String propval = "";
try {
propval = new Scanner(inputstream).useDelimiter("\\A").next();
} catch (NoSuchElementException e) {
QLog.e("Error getprop, NoSuchElementException: " +e.getMessage(), e);
}
return propval.split("\n");
}
private String[] mountReader() {
InputStream inputstream = null;
try {
inputstream = Runtime.getRuntime().exec("mount").getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
// If input steam is null, we can't read the file, so return null
if (inputstream == null) return null;
String propval = "";
try {
propval = new Scanner(inputstream).useDelimiter("\\A").next();
} catch (NoSuchElementException e) {
e.printStackTrace();
}
return propval.split("\n");
}
/**
* Check if any package in the list is installed
* @param packages - list of packages to search for
* @return true if any of the packages are installed
*/
private boolean isAnyPackageFromListInstalled(List<String> packages){
boolean result = false;
PackageManager pm = mContext.getPackageManager();
for (String packageName : packages) {
try {
// Root app detected
pm.getPackageInfo(packageName, 0);
QLog.e(packageName + " ROOT management app detected!");
result = true;
} catch (PackageManager.NameNotFoundException e) {
// Exception thrown, package is not installed into the system
}
}
return result;
}
/**
* Checks for several system properties for
* @return - true if dangerous props are found
*/
public boolean checkForDangerousProps() {
final Map<String, String> dangerousProps = new HashMap<>();
dangerousProps.put("ro.debuggable", "1");
dangerousProps.put("ro.secure", "0");
boolean result = false;
String[] lines = propsReader();
for (String line : lines) {
for (String key : dangerousProps.keySet()) {
if (line.contains(key)) {
String badValue = dangerousProps.get(key);
badValue = "[" + badValue + "]";
if (line.contains(badValue)) {
QLog.v(key + " = " + badValue + " detected!");
result = true;
}
}
}
}
return result;
}
/**
* When you're root you can change the permissions on common system directories, this method checks if any of these patha Const.pathsThatShouldNotBeWrtiable are writable.
* @return true if one of the dir is writable
*/
public boolean checkForRWPaths() {
boolean result = false;
String[] lines = mountReader();
for (String line : lines) {
// Split lines into parts
String[] args = line.split(" ");
if (args.length < 4){
// If we don't have enough options per line, skip this and log an error
QLog.e("Error formatting mount line: "+line);
continue;
}
String mountPoint = args[1];
String mountOptions = args[3];
for(String pathToCheck: Const.pathsThatShouldNotBeWrtiable) {
if (mountPoint.equalsIgnoreCase(pathToCheck)) {
// Split options out and compare against "rw" to avoid false positives
for (String option : mountOptions.split(",")){
if (option.equalsIgnoreCase("rw")){
QLog.v(pathToCheck+" path is mounted with rw permissions! "+line);
result = true;
break;
}
}
}
}
}
return result;
}
/**
* A variation on the checking for SU, this attempts a 'which su'
* @return true if su found
*/
public boolean checkSuExists() {
Process process = null;
try {
process = Runtime.getRuntime().exec(new String[] { "which", "su" });
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
return in.readLine() != null;
} catch (Throwable t) {
return false;
} finally {
if (process != null) process.destroy();
}
}
/**
* Checks if device has ReadAccess to the Native Library
* Precondition: canLoadNativeLibrary() ran before this and returned true
* @returm true if device has Read Access | false if UnsatisfiedLinkError Occurs
*
* Description: RootCloak automatically blocks read access to the Native Libraries, however
* allows for them to be loaded into memory. This check is an indication that RootCloak is
* installed onto the device.
*
* */
public boolean checkForNativeLibraryReadAccess() {
RootBeerNative rootBeerNative = new RootBeerNative();
try {
rootBeerNative.setLogDebugMessages(loggingEnabled);
return true;
} catch (UnsatisfiedLinkError e) {
return false;
}
}
/**
* Checks if it is possible to load our native library
* @return true if we can | false if not
*/
public boolean canLoadNativeLibrary(){
return new RootBeerNative().wasNativeLibraryLoaded();
}
/**
* Native checks are often harder to cloak/trick so here we call through to our native root checker
* @return true if we found su | false if not, or the native library could not be loaded / accessed
*/
public boolean checkForRootNative() {
if (!canLoadNativeLibrary()){
QLog.e("We could not load the native library to test for root");
return false;
}
String binaryName = "su";
String[] paths = new String[Const.suPaths.length];
for (int i = 0; i < paths.length; i++) {
paths[i] = Const.suPaths[i]+binaryName;
}
RootBeerNative rootBeerNative = new RootBeerNative();
try {
rootBeerNative.setLogDebugMessages(loggingEnabled);
return rootBeerNative.checkForRoot(paths) > 0;
} catch (UnsatisfiedLinkError e) {
return false;
}
}
}