Determine if running on a rooted device - android
My app has a certain piece of functionality that will only work on a device where root is available. Rather than having this feature fail when it is used (and then show an appropriate error message to the user), I'd prefer an ability to silently check if root is available first, and if not,hide the respective options in the first place.
Is there a way to do this?
Here is a class that will check for Root one of three ways.
/** #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();
}
}
}
If you are already using Fabric/Firebase Crashlytics you can call
CommonUtils.isRooted(context)
This is the current implementation of that method:
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;
}
The RootTools library offers simple methods to check for root:
RootTools.isRootAvailable()
Reference
In my application I was checking if device is rooted or not by executing "su" command. But today I've removed this part of my code. Why?
Because my application became a memory killer. How? Let me tell you my story.
There were some complaints that my application was slowing down devices(Of course I thought that can not be true). I tried to figure out why. So I used MAT to get heap dumps and analyze, and everything seemed perfect. But after relaunching my app many times I realized that device is really getting slower and stopping my application didn't make it faster (unless I restart device). I analyzed dump files again while device is very slow. But everything was still perfect for dump file.
Then I did what must be done at first. I listed processes.
$ adb shell ps
Surprize; there were many processes for my application (with my application's process tag at manifest). Some of them was zombie some of them not.
With a sample application which has a single Activity and executes just "su" command, I realized that a zombie process is being created on every launch of application. At first these zombies allocate 0KB but than something happens and zombie processes are holding nearly same KBs as my application's main process and they became standart processes.
There is a bug report for same issue on bugs.sun.com: http://bugs.sun.com/view_bug.do?bug_id=6474073 this explains if command is not found zombies are going to be created with exec() method. But I still don't understand why and how can they become standart processes and hold significant KBs. (This is not happening all the time)
You can try if you want with code sample below;
String commandToExecute = "su";
executeShellCommand(commandToExecute);
Simple command execution method;
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) {
}
}
}
}
To sum up; I have no advice for you to determine if device is rooted or not. But if I were you I would not use Runtime.getRuntime().exec().
By the way; RootTools.isRootAvailable() causes same problem.
Update 2017
You can do it now with Google Safetynet API. The SafetyNet API provides Attestation API which helps you assess the security and compatibility of the Android environments in which your apps run.
This attestation can helps to determine whether or not the particular device has been tampered with or otherwise modified.
The Attestation API returns a JWS response like this
{
"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,
}
Parsing this response can help you determine if device is rooted or not
Rooted devices seem to cause ctsProfileMatch=false.
You can do it on client side but parsing response on server side is recommend.
A basic client server archtecture with safety net API will look like this:-
Many of the answers listed here have inherent issues:
Checking for test-keys is correlated with root access but doesn't necessarily guarantee it
"PATH" directories should be derived from the actual "PATH" environment variable instead of being hard coded
The existence of the "su" executable doesn't necessarily mean the device has been rooted
The "which" executable may or may not be installed, and you should let the system resolve its path if possible
Just because the SuperUser app is installed on the device does not mean the device has root access yet
The RootTools library from Stericson seems to be checking for root more legitimately. It also has lots of extra tools and utilities so I highly recommend it. However, there's no explanation of how it specifically checks for root, and it may be a bit heavier than most apps really need.
I've made a couple of utility methods that are loosely based on the RootTools library. If you simply want to check if the "su" executable is on the device you can use the following method:
public static boolean isRootAvailable(){
for(String pathDir : System.getenv("PATH").split(":")){
if(new File(pathDir, "su").exists()) {
return true;
}
}
return false;
}
This method simply loops through the directories listed in the "PATH" environment variable and checks if a "su" file exists in one of them.
In order to truly check for root access the "su" command must actually be run. If an app like SuperUser is installed, then at this point it may ask for root access, or if its already been granted/denied a toast may be shown indicating whether access was granted/denied. A good command to run is "id" so that you can verify that the user id is in fact 0 (root).
Here's a sample method to determine whether root access has been granted:
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;
}
It's important to actually test running the "su" command because some emulators have the "su" executable pre-installed, but only allow certain users to access it like the adb shell.
It's also important to check for the existence of the "su" executable before trying to run it, because android has been known to not properly dispose of processes that try to run missing commands. These ghost processes can run up memory consumption over time.
Root check at Java level is not a safe solution. If your app has Security Concerns to run on a Rooted device , then please use this solution.
Kevin's answer works unless the phone also has an app like RootCloak . Such apps have a Handle over Java APIs once phone is rooted and they mock these APIs to return phone is not rooted.
I have written a native level code based on Kevin's answer , it works even with RootCloak ! Also it does not cause any memory leak issues.
#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;
}
In your Java code , you need to create wrapper class RootUtils to make the native calls
public boolean checkRooted() {
if( rootUtils.checkRootAccessMethod3() == 0 || rootUtils.checkRootAccessMethod1() == 0 || rootUtils.checkRootAccessMethod2() == 0 )
return true;
return false;
}
http://code.google.com/p/roottools/
If you do not want to use the jar file just use the code:
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;
}
Program will try to find su folder:
private static boolean isRooted() {
return findBinary("su");
}
Example:
if (isRooted()) {
textView.setText("Device Rooted");
} else {
textView.setText("Device Unrooted");
}
RootBeer is a root checking Android library by Scott and Matthew.
It uses various checks to indicate whether device is rooted or not.
Java checks
CheckRootManagementApps
CheckPotentiallyDangerousAppss
CheckRootCloakingApps
CheckTestKeys
checkForDangerousProps
checkForBusyBoxBinary
checkForSuBinary
checkSuExists
checkForRWSystem
Native checks
We call through to our native root checker to run some of it's own
checks. Native checks are typically harder to cloak, so some root
cloak apps just block the loading of native libraries that contain
certain key words.
checkForSuBinary
Instead of using isRootAvailable() you can use isAccessGiven(). Direct from RootTools wiki:
if (RootTools.isAccessGiven()) {
// your app has been granted root access
}
RootTools.isAccessGiven() not only checks that a device is rooted, it
also calls su for your app, requests permission, and returns true if
your app was successfully granted root permissions. This can be used
as the first check in your app to make sure that you will be granted
access when you need it.
Reference
Some modified builds used to set the system property ro.modversion for this purpose. Things seem to have moved on; my build from TheDude a few months ago has this:
cmb#apollo:~$ adb -d shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [htc_dream-eng 1.5 CUPCAKE eng.TheDudeAbides.20090427.235325 test-keys]
[ro.build.version.incremental]: [eng.TheDude.2009027.235325]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Mon Apr 20 01:42:32 CDT 2009]
[ro.build.date.utc]: [1240209752]
[ro.build.type]: [eng]
[ro.build.user]: [TheDude]
[ro.build.host]: [ender]
[ro.build.tags]: [test-keys]
[ro.build.product]: [dream]
[ro.build.description]: [kila-user 1.1 PLAT-RC33 126986 ota-rel-keys,release-keys]
[ro.build.fingerprint]: [tmobile/kila/dream/trout:1.1/PLAT-RC33/126986:user/ota-rel-keys,release-keys]
[ro.build.changelist]: [17615# end build properties]
The emulator from the 1.5 SDK on the other hand, running the 1.5 image, also has root, is probably similar to the Android Dev Phone 1 (which you presumably want to allow) and has this:
cmb#apollo:~$ adb -e shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.version.incremental]: [148875]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Thu May 14 18:09:10 PDT 2009]
[ro.build.date.utc]: [1242349750]
[ro.build.type]: [eng]
[ro.build.user]: [android-build]
[ro.build.host]: [undroid16.mtv.corp.google.com]
[ro.build.tags]: [test-keys]
[ro.build.product]: [generic]
[ro.build.description]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.fingerprint]: [generic/sdk/generic/:1.5/CUPCAKE/148875:eng/test-keys]
As for the retail builds, I don't have one to hand, but various searches under site:xda-developers.com are informative. Here is a G1 in the Netherlands, you can see that ro.build.tags does not have test-keys, and I think that's probably the most reliable property to use.
I suggest using native code for root detection.
Here is a full working example.
JAVA wrapper:
package com.kozhevin.rootchecks.util;
import android.support.annotation.NonNull;
import com.kozhevin.rootchecks.BuildConfig;
public class MeatGrinder {
private final static String LIB_NAME = "native-lib";
private static boolean isLoaded;
private static boolean isUnderTest = false;
private MeatGrinder() {
}
public boolean isLibraryLoaded() {
if (isLoaded) {
return true;
}
try {
if(isUnderTest) {
throw new UnsatisfiedLinkError("under test");
}
System.loadLibrary(LIB_NAME);
isLoaded = true;
} catch (UnsatisfiedLinkError e) {
if (BuildConfig.DEBUG) {
e.printStackTrace();
}
}
return isLoaded;
}
public native boolean isDetectedDevKeys();
public native boolean isDetectedTestKeys();
public native boolean isNotFoundReleaseKeys();
public native boolean isFoundDangerousProps();
public native boolean isPermissiveSelinux();
public native boolean isSuExists();
public native boolean isAccessedSuperuserApk();
public native boolean isFoundSuBinary();
public native boolean isFoundBusyboxBinary();
public native boolean isFoundXposed();
public native boolean isFoundResetprop();
public native boolean isFoundWrongPathPermission();
public native boolean isFoundHooks();
#NonNull
public static MeatGrinder getInstance() {
return InstanceHolder.INSTANCE;
}
private static class InstanceHolder {
private static final MeatGrinder INSTANCE = new MeatGrinder();
}
}
JNI wrapper(native-lib.c):
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedTestKeys(
JNIEnv *env,
jobject this ) {
return (jboolean) isDetectedTestKeys();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedDevKeys(
JNIEnv *env,
jobject this ) {
return (jboolean) isDetectedDevKeys();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isNotFoundReleaseKeys(
JNIEnv *env,
jobject this ) {
return (jboolean) isNotFoundReleaseKeys();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundDangerousProps(
JNIEnv *env,
jobject this ) {
return (jboolean) isFoundDangerousProps();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isPermissiveSelinux(
JNIEnv *env,
jobject this ) {
return (jboolean) isPermissiveSelinux();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isSuExists(
JNIEnv *env,
jobject this ) {
return (jboolean) isSuExists();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isAccessedSuperuserApk(
JNIEnv *env,
jobject this ) {
return (jboolean) isAccessedSuperuserApk();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundSuBinary(
JNIEnv *env,
jobject this ) {
return (jboolean) isFoundSuBinary();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundBusyboxBinary(
JNIEnv *env,
jobject this ) {
return (jboolean) isFoundBusyboxBinary();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundXposed(
JNIEnv *env,
jobject this ) {
return (jboolean) isFoundXposed();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundResetprop(
JNIEnv *env,
jobject this ) {
return (jboolean) isFoundResetprop();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundWrongPathPermission(
JNIEnv *env,
jobject this ) {
return (jboolean) isFoundWrongPathPermission();
}
JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundHooks(
JNIEnv *env,
jobject this ) {
return (jboolean) isFoundHooks();
}
constants:
// Comma-separated tags describing the build, like= "unsigned,debug".
const char *const ANDROID_OS_BUILD_TAGS = "ro.build.tags";
// A string that uniquely identifies this build. 'BRAND/PRODUCT/DEVICE:RELEASE/ID/VERSION.INCREMENTAL:TYPE/TAGS'.
const char *const ANDROID_OS_BUILD_FINGERPRINT = "ro.build.fingerprint";
const char *const ANDROID_OS_SECURE = "ro.secure";
const char *const ANDROID_OS_DEBUGGABLE = "ro.debuggable";
const char *const ANDROID_OS_SYS_INITD = "sys.initd";
const char *const ANDROID_OS_BUILD_SELINUX = "ro.build.selinux";
//see https://android.googlesource.com/platform/system/core/+/master/adb/services.cpp#86
const char *const SERVICE_ADB_ROOT = "service.adb.root";
const char * const MG_SU_PATH[] = {
"/data/local/",
"/data/local/bin/",
"/data/local/xbin/",
"/sbin/",
"/system/bin/",
"/system/bin/.ext/",
"/system/bin/failsafe/",
"/system/sd/xbin/",
"/su/xbin/",
"/su/bin/",
"/magisk/.core/bin/",
"/system/usr/we-need-root/",
"/system/xbin/",
0
};
const char * const MG_EXPOSED_FILES[] = {
"/system/lib/libxposed_art.so",
"/system/lib64/libxposed_art.so",
"/system/xposed.prop",
"/cache/recovery/xposed.zip",
"/system/framework/XposedBridge.jar",
"/system/bin/app_process64_xposed",
"/system/bin/app_process32_xposed",
"/magisk/xposed/system/lib/libsigchain.so",
"/magisk/xposed/system/lib/libart.so",
"/magisk/xposed/system/lib/libart-disassembler.so",
"/magisk/xposed/system/lib/libart-compiler.so",
"/system/bin/app_process32_orig",
"/system/bin/app_process64_orig",
0
};
const char * const MG_READ_ONLY_PATH[] = {
"/system",
"/system/bin",
"/system/sbin",
"/system/xbin",
"/vendor/bin",
"/sbin",
"/etc",
0
};
root detections from native code:
struct mntent *getMntent(FILE *fp, struct mntent *e, char *buf, int buf_len) {
while (fgets(buf, buf_len, fp) != NULL) {
// Entries look like "/dev/block/vda /system ext4 ro,seclabel,relatime,data=ordered 0 0".
// That is: mnt_fsname mnt_dir mnt_type mnt_opts mnt_freq mnt_passno.
int fsname0, fsname1, dir0, dir1, type0, type1, opts0, opts1;
if (sscanf(buf, " %n%*s%n %n%*s%n %n%*s%n %n%*s%n %d %d",
&fsname0, &fsname1, &dir0, &dir1, &type0, &type1, &opts0, &opts1,
&e->mnt_freq, &e->mnt_passno) == 2) {
e->mnt_fsname = &buf[fsname0];
buf[fsname1] = '\0';
e->mnt_dir = &buf[dir0];
buf[dir1] = '\0';
e->mnt_type = &buf[type0];
buf[type1] = '\0';
e->mnt_opts = &buf[opts0];
buf[opts1] = '\0';
return e;
}
}
return NULL;
}
bool isPresentMntOpt(const struct mntent *pMnt, const char *pOpt) {
char *token = pMnt->mnt_opts;
const char *end = pMnt->mnt_opts + strlen(pMnt->mnt_opts);
const size_t optLen = strlen(pOpt);
while (token != NULL) {
const char *tokenEnd = token + optLen;
if (tokenEnd > end) break;
if (memcmp(token, pOpt, optLen) == 0 &&
(*tokenEnd == '\0' || *tokenEnd == ',' || *tokenEnd == '=')) {
return true;
}
token = strchr(token, ',');
if (token != NULL) {
token++;
}
}
return false;
}
static char *concat2str(const char *pString1, const char *pString2) {
char *result;
size_t lengthBuffer = 0;
lengthBuffer = strlen(pString1) +
strlen(pString2) + 1;
result = malloc(lengthBuffer);
if (result == NULL) {
GR_LOGW("malloc failed\n");
return NULL;
}
memset(result, 0, lengthBuffer);
strcpy(result, pString1);
strcat(result, pString2);
return result;
}
static bool
isBadPropertyState(const char *key, const char *badValue, bool isObligatoryProperty, bool isExact) {
if (badValue == NULL) {
GR_LOGE("badValue may not be NULL");
return false;
}
if (key == NULL) {
GR_LOGE("key may not be NULL");
return false;
}
char value[PROP_VALUE_MAX + 1];
int length = __system_property_get(key, value);
bool result = false;
/* A length 0 value indicates that the property is not defined */
if (length > 0) {
GR_LOGI("property:[%s]==[%s]", key, value);
if (isExact) {
if (strcmp(value, badValue) == 0) {
GR_LOGW("bad value[%s] equals to [%s] in the property [%s]", value, badValue, key);
result = true;
}
} else {
if (strlen(value) >= strlen(badValue) && strstr(value, badValue) != NULL) {
GR_LOGW("bad value[%s] found in [%s] in the property [%s]", value, badValue, key);
result = true;
}
}
} else {
GR_LOGI("[%s] property not found", key);
if (isObligatoryProperty) {
result = true;
}
}
return result;
}
bool isDetectedTestKeys() {
const char *TEST_KEYS_VALUE = "test-keys";
return isBadPropertyState(ANDROID_OS_BUILD_TAGS, TEST_KEYS_VALUE, true, false);
}
bool isDetectedDevKeys() {
const char *DEV_KEYS_VALUE = "dev-keys";
return isBadPropertyState(ANDROID_OS_BUILD_TAGS, DEV_KEYS_VALUE, true, false);
}
bool isNotFoundReleaseKeys() {
const char *RELEASE_KEYS_VALUE = "release-keys";
return !isBadPropertyState(ANDROID_OS_BUILD_TAGS, RELEASE_KEYS_VALUE, false, true);
}
bool isFoundWrongPathPermission() {
bool result = false;
FILE *file = fopen("/proc/mounts", "r");
char mntent_strings[BUFSIZ];
if (file == NULL) {
GR_LOGE("setmntent");
return result;
}
struct mntent ent = {0};
while (NULL != getMntent(file, &ent, mntent_strings, sizeof(mntent_strings))) {
for (size_t i = 0; MG_READ_ONLY_PATH[i]; i++) {
if (strcmp((&ent)->mnt_dir, MG_READ_ONLY_PATH[i]) == 0 &&
isPresentMntOpt(&ent, "rw")) {
GR_LOGI("%s %s %s %s\n", (&ent)->mnt_fsname, (&ent)->mnt_dir, (&ent)->mnt_opts,
(&ent)->mnt_type);
result = true;
break;
}
}
memset(&ent, 0, sizeof(ent));
}
fclose(file);
return result;
}
bool isFoundDangerousProps() {
const char *BAD_DEBUGGABLE_VALUE = "1";
const char *BAD_SECURE_VALUE = "0";
const char *BAD_SYS_INITD_VALUE = "1";
const char *BAD_SERVICE_ADB_ROOT_VALUE = "1";
bool result = isBadPropertyState(ANDROID_OS_DEBUGGABLE, BAD_DEBUGGABLE_VALUE, true, true) ||
isBadPropertyState(SERVICE_ADB_ROOT, BAD_SERVICE_ADB_ROOT_VALUE, false, true) ||
isBadPropertyState(ANDROID_OS_SECURE, BAD_SECURE_VALUE, true, true) ||
isBadPropertyState(ANDROID_OS_SYS_INITD, BAD_SYS_INITD_VALUE, false, true);
return result;
}
bool isPermissiveSelinux() {
const char *BAD_VALUE = "0";
return isBadPropertyState(ANDROID_OS_BUILD_SELINUX, BAD_VALUE, false, false);
}
bool isSuExists() {
char buf[BUFSIZ];
char *str = NULL;
char *temp = NULL;
size_t size = 1; // start with size of 1 to make room for null terminator
size_t strlength;
FILE *pipe = popen("which su", "r");
if (pipe == NULL) {
GR_LOGI("pipe is null");
return false;
}
while (fgets(buf, sizeof(buf), pipe) != NULL) {
strlength = strlen(buf);
temp = realloc(str, size + strlength); // allocate room for the buf that gets appended
if (temp == NULL) {
// allocation error
GR_LOGE("Error (re)allocating memory");
pclose(pipe);
if (str != NULL) {
free(str);
}
return false;
} else {
str = temp;
}
strcpy(str + size - 1, buf);
size += strlength;
}
pclose(pipe);
GR_LOGW("A size of the result from pipe is [%zu], result:\n [%s] ", size, str);
if (str != NULL) {
free(str);
}
return size > 1 ? true : false;
}
static bool isAccessedFile(const char *path) {
int result = access(path, F_OK);
GR_LOGV("[%s] has been accessed with result: [%d]", path, result);
return result == 0 ? true : false;
}
static bool isFoundBinaryFromArray(const char *const *array, const char *binary) {
for (size_t i = 0; array[i]; ++i) {
char *checkedPath = concat2str(array[i], binary);
if (checkedPath == NULL) { // malloc failed
return false;
}
bool result = isAccessedFile(checkedPath);
free(checkedPath);
if (result) {
return result;
}
}
return false;
}
bool isAccessedSuperuserApk() {
return isAccessedFile("/system/app/Superuser.apk");
}
bool isFoundResetprop() {
return isAccessedFile("/data/magisk/resetprop");
}
bool isFoundSuBinary() {
return isFoundBinaryFromArray(MG_SU_PATH, "su");
}
bool isFoundBusyboxBinary() {
return isFoundBinaryFromArray(MG_SU_PATH, "busybox");
}
bool isFoundXposed() {
for (size_t i = 0; MG_EXPOSED_FILES[i]; ++i) {
bool result = isAccessedFile(MG_EXPOSED_FILES[i]);
if (result) {
return result;
}
}
return false;
}
bool isFoundHooks() {
bool result = false;
pid_t pid = getpid();
char maps_file_name[512];
sprintf(maps_file_name, "/proc/%d/maps", pid);
GR_LOGI("try to open [%s]", maps_file_name);
const size_t line_size = BUFSIZ;
char *line = malloc(line_size);
if (line == NULL) {
return result;
}
FILE *fp = fopen(maps_file_name, "r");
if (fp == NULL) {
free(line);
return result;
}
memset(line, 0, line_size);
const char *substrate = "com.saurik.substrate";
const char *xposed = "XposedBridge.jar";
while (fgets(line, line_size, fp) != NULL) {
const size_t real_line_size = strlen(line);
if ((real_line_size >= strlen(substrate) && strstr(line, substrate) != NULL) ||
(real_line_size >= strlen(xposed) && strstr(line, xposed) != NULL)) {
GR_LOGI("found in [%s]: [%s]", maps_file_name, line);
result = true;
break;
}
}
free(line);
fclose(fp);
return result;
}
Here is my code based on some answers here:
/**
* Checks if the phone is rooted.
*
* #return <code>true</code> if the phone is rooted, <code>false</code>
* otherwise.
*/
public static boolean isPhoneRooted() {
// get from build info
String buildTags = android.os.Build.TAGS;
if (buildTags != null && buildTags.contains("test-keys")) {
return true;
}
// check if /system/app/Superuser.apk is present
try {
File file = new File("/system/app/Superuser.apk");
if (file.exists()) {
return true;
}
} catch (Throwable e1) {
// ignore
}
return false;
}
Further to #Kevins answer, I've recently found while using his system, that the Nexus 7.1 was returning false for all three methods - No which command, no test-keys and SuperSU was not installed in /system/app.
I added this:
public static boolean checkRootMethod4(Context context) {
return isPackageInstalled("eu.chainfire.supersu", context);
}
private static boolean isPackageInstalled(String packagename, Context context) {
PackageManager pm = context.getPackageManager();
try {
pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
This is slightly less useful in some situations (if you need guaranteed root access) as it's completely possible for SuperSU to be installed on devices which don't have SU access.
However, since it's possible to have SuperSU installed and working but not in the /system/app directory, this extra case will root (haha) out such cases.
public static boolean isRootAvailable(){
Process p = null;
try{
p = Runtime.getRuntime().exec(new String[] {"su"});
writeCommandToConsole(p,"exit 0");
int result = p.waitFor();
if(result != 0)
throw new Exception("Root check result with exit command " + result);
return true;
} catch (IOException e) {
Log.e(LOG_TAG, "Su executable is not available ", e);
} catch (Exception e) {
Log.e(LOG_TAG, "Root is unavailable ", e);
}finally {
if(p != null)
p.destroy();
}
return false;
}
private static String writeCommandToConsole(Process proc, String command, boolean ignoreError) throws Exception{
byte[] tmpArray = new byte[1024];
proc.getOutputStream().write((command + "\n").getBytes());
proc.getOutputStream().flush();
int bytesRead = 0;
if(proc.getErrorStream().available() > 0){
if((bytesRead = proc.getErrorStream().read(tmpArray)) > 1){
Log.e(LOG_TAG,new String(tmpArray,0,bytesRead));
if(!ignoreError)
throw new Exception(new String(tmpArray,0,bytesRead));
}
}
if(proc.getInputStream().available() > 0){
bytesRead = proc.getInputStream().read(tmpArray);
Log.i(LOG_TAG, new String(tmpArray,0,bytesRead));
}
return new String(tmpArray);
}
Two additional ideas, if you want to check if a device is root capable from your app:
Check for the existing of the 'su' binary: run "which su" from Runtime.getRuntime().exec()
Look for the SuperUser.apk in /system/app/Superuser.apk location
Using C++ with the ndk is the best approach to detect root even if the user is using applications that hide his root such as RootCloak. I tested this code with RootCloak and I was able to detect the root even if the user is trying to hide it.
So your cpp file would like:
#include <jni.h>
#include <string>
/**
*
* function that checks for the su binary files and operates even if
* root cloak is installed
* #return integer 1: device is rooted, 0: device is not
*rooted
*/
extern "C"
JNIEXPORT int JNICALL
Java_com_example_user_root_1native_rootFunction(JNIEnv *env,jobject thiz){
const char *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"};
int counter =0;
while (counter<9){
if(FILE *file = fopen(paths[counter],"r")){
fclose(file);
return 1;
}
counter++;
}
return 0;
}
And you will call the function from your java code as follows
public class Root_detect {
/**
*
* function that calls a native function to check if the device is
*rooted or not
* #return boolean: true if the device is rooted, false if the
*device is not rooted
*/
public boolean check_rooted(){
int checker = rootFunction();
if(checker==1){
return true;
}else {
return false;
}
}
static {
System.loadLibrary("cpp-root-lib");//name of your cpp file
}
public native int rootFunction();
}
As last quarter of 2021 today, I tried to use SafetyNet regarding #HimanshiThakur 's answer. But I got an issue and opened a question in here. Still no answer.
So I decided to use RootBeer. It works perfectly but when Magisk hides the root, it doesn't work.
If you don't care this case (and many bank apps can't solve this issue too), you can use these steps:
Add this to Gradle:
implementation 'com.scottyab:rootbeer-lib:0.1.0'
Use these lines:
RootBeer rootBeer = new RootBeer(context);
if (rootBeer.isRooted()) {
//we found indication of root
} else {
//we didn't find indication of root
}
if [[ "`adb shell which su | grep -io "permission denied"`" != "permission denied" ]]; then
echo "Yes. Rooted device."
else
echo "No. Device not rooted. Only limited tasks can be performed. Done."
zenity --warning --title="Device Not Rooted" --text="The connected Android Device is <b>NOT ROOTED</b>. Only limited tasks can be performed." --no-wrap
fi
Based on some of the answers here, I merged them and also added a check if some known root-manager app is installed:
fun isProbablyRooted(context: Context, alsoIncludeCheckingRootManagerApp: Boolean = false): Boolean {
return hasRootManagerSystemApp(context) || (alsoIncludeCheckingRootManagerApp && hasRootManagerSystemApp(context))
}
fun hasRootManagerSystemApp(context: Context): Boolean {
val rootAppsPackageNames = arrayOf("com.topjohnwu.magisk", "eu.chainfire.supersu", "com.koushikdutta.superuser", "com.noshufou.android.su", "me.phh.superuser")
rootAppsPackageNames.forEach { rootAppPackageName ->
try {
context.packageManager.getApplicationInfo(rootAppPackageName, 0)
return true
} catch (e: Exception) {
}
}
return false
}
fun hasSuBinary(): Boolean {
return try {
findBinary("su")
} catch (e: Exception) {
e.printStackTrace()
false
}
}
private fun findBinary(binaryName: String): Boolean {
val paths = System.getenv("PATH")
if (!paths.isNullOrBlank()) {
val systemPlaces: List<String> = paths.split(":")
return systemPlaces.firstOrNull { File(it, binaryName).exists() } != null
}
val places = arrayOf("/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/", "/data/local/bin/",
"/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/")
return places.firstOrNull { File(it, binaryName).exists() } != null
}
manifest:
<queries>
<package android:name="com.topjohnwu.magisk" />
<package android:name="eu.chainfire.supersu" />
<package android:name="com.koushikdutta.superuser" />
<package android:name="com.noshufou.android.su" />
<package android:name="me.phh.superuser" />
</queries>
Of course, this is still a guess, like all the other solutions.
Users can install Magisk without having the device rooted, for example.
Forget all that detecting root apps and su binaries. Check for the root daemon process. This can be done from the terminal and you can run terminal commands within an app. Try this one-liner.
if [ ! "$(/system/bin/ps -A | grep -v grep | grep -c daemonsu)" = "0" ]; then echo "device is rooted"; else echo "device is not rooted"; fi
You don't need root permission to achieve this either.
Edit: Now using this method for better detection!
if [ $(ps -A | grep -e ^shell -e ^root | grep -v "\[" | tr -s ' ' | cut -d ' ' -f 9 | grep -c su) ] || [ $(which su) ]; then echo 'rooted'; else echo 'not rooted'; fi
Using google SafetyNet Attestation API you can easily check whether your device is rooted or not :
Add dependency in build.gradle(:app)
implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
Get Api key and enable Android Device Verification API using link
public static void sendSafetyNetRequest(Activity context) {
if(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context, 13000000) == ConnectionResult.SUCCESS) {
Log.e(TAG, "The SafetyNet Attestation API is available");
// TODO(developer): Change the nonce generation to include your own, used once value,
// ideally from your remote server.
String nonceData = "Safety Net Sample: " + System.currentTimeMillis();
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
Random mRandom = new SecureRandom();
byte[] bytes = new byte[24];
mRandom.nextBytes(bytes);
try {
byteStream.write(bytes);
byteStream.write(nonceData.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
byte[] nonce = byteStream.toByteArray();
SafetyNetClient client = SafetyNet.getClient(context);
Task<SafetyNetApi.AttestationResponse> task = client.attest(nonce, API_KEY_FROM_STEP_2_LINK);
task.addOnSuccessListener(context, attestationResponse -> {
/*
TODO(developer): Forward this result to your server together with
the nonce for verification.
You can also parse the JwsResult locally to confirm that the API
returned a response by checking for an 'error' field first and before
retrying the request with an exponential backoff.
NOTE: Do NOT rely on a local, client-side only check for security, you
must verify the response on a remote server!
*/
String jwsResult = attestationResponse.getJwsResult();
Log.e(TAG, "Success! SafetyNet result:\n" + jwsResult + "\n");
if (jwsResult == null) {
Log.e(TAG, "jwsResult Null");
}
final String[] jwtParts = jwsResult.split("\\.");
if (jwtParts.length == 3) {
String decodedPayload = new String(Base64.decode(jwtParts[1], Base64.DEFAULT));
Log.e(TAG, "decodedPayload : " + decodedPayload);
}
});
task.addOnFailureListener(context, e -> {
// An error occurred while communicating with the service.
String mResult = null;
if (e instanceof ApiException) {
// An error with the Google Play Services API contains some additional details.
ApiException apiException = (ApiException) e;
Util.showLog(TAG, "Error: " +
CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " +
apiException.getStatusMessage());
} else {
// A different, unknown type of error occurred.
Log.e(TAG, "ERROR! " + e.getMessage());
}
});
} else {
Log.e(TAG, "Prompt user to update Google Play services.";
}
}
`
Check your logs for decodedPayload if ctsProfileMatch and basicIntegrity both are false it means your device is rooted . The Attestation API returns a JWS response which looks like:
{ "nonce": "6pLrr9zWyl6TNzj+kpbR4LZcfPY3U2FmZXR5IE5ldCBTYW1wbGU6IDE2MTQ2NzkwMTIzNjc=", "timestampMs": 9860437986543, "apkPackageName": " your package name will be displayed here", "ctsProfileMatch": true, "apkDigestSha256": [ "base64 encoded, SHA-256 hash of the certificate used to sign requesting app" ], "basicIntegrity": true, "evaluationType": "BASIC" }
For more info check this link.
There is Safety Net Attestation API of Google play services by which we can assess the device and determine if it is rooted/tampered.
Please go through my answer to deal with rooted devices:
https://stackoverflow.com/a/58304556/3908895
if you don't want to use any 3rd party library or any random solution then just use google lib for detecting it.
Android Device Verification
response :
{
"timestampMs": 9860437986543,
"nonce": "R2Rra24fVm5xa2Mg",
"apkPackageName": "com.package.name.of.requesting.app",
"apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
certificate used to sign requesting app"],
"ctsProfileMatch": true,
"basicIntegrity": true,
}
ctsProfileMatch it gives false if the device is rooted.
ref link:
[1]: https://developer.android.com/training/safetynet/attestation
You can do this by following code :
public boolean getRootInfo() {
if (checkRootFiles() || checkTags()) {
return true;
}
return false;
}
private boolean checkRootFiles() {
boolean root = false;
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) {
root = new File(path).exists();
if (root)
break;
}
return root;
}
private boolean checkTags() {
String tag = Build.TAGS;
return tag != null && tag.trim().contains("test-keys");
}
You can also check this library RootBeer.
As of 2021 (today), looks like there's no any reliable way or method for detecting root, especially when powerful hiding tool such as MagiskHide is enabled. Most of the answers here are no longer relevant, so don't use it in production. Rely on proven check like SafetyNet, and instead of going the extra miles to detect root, I suggest to protect your app at both runtime, such as prevent debugger/instrumentation and make sure to use obfuscation.
Indeed it is interesting question and so far nobody has deserved award. I use the following code:
boolean isRooted() {
try {
ServerSocket ss = new ServerSocket(81);
ss.close();
return true;
} catch (Exception e) {
// not sure
}
return false;
}
The code is certainly not bulletproof, because network can be not available so you get an exception. If this method returns true then 99% you can be sure, otherwise just 50% that not. Networking permission can also spoil the solution.
Using my library at rootbox, it is pretty easy. Check the required code below:
//Pass true to <Shell>.start(...) call to run as superuser
Shell shell = null;
try {
shell = Shell.start(true);
} catch (IOException exception) {
exception.printStackTrace();
}
if (shell == null)
// We failed to execute su binary
return;
if (shell.isRoot()) {
// Verified running as uid 0 (root), can continue with commands
...
} else
throw Exception("Unable to gain root access. Make sure you pressed Allow/Grant in superuser prompt.");
Related
Is it possible to merge/install split APK files (AKA "app bundle"), on Android device itself, without root?
Background In the past, I've asked about sharing or backup of app-bundle / split apk files, here . This seems like an almost impossible task, which I could only figure out how to install the split APK files, and even then it's only via adb: adb install-multiple apk1 apk2 ... The problem I was told that it should be actually possible to merge multiple split APK files into one that I could install (here), but wasn't given of how to do it. This could be useful for saving it for later (backup), and because currently there is no way to install split-apk files within the device. In fact, this is such a major issue, that I don't know of any backup app that can handle split APK files (app bundle), and this include Titanium app. What I've found I took a sample app that uses app-bundles, called "AirBnb". Looking at the files it has, those are what the Play Store decided to download: So I tried to enter each. The "base" is the main one, so I skipped it to look at the others. To me it seems that all have these files within: "META-INF" "resources.arsc" "AndroidManifest.xml" in the case of the one with the "xxxhdpi", I also get "res" folder. Thing is, since those all exist in multiple places, I don't get how could I merge them. The questions What is the way to merge those all into one APK file? Is it possible to install split APK files without root and without PC ? This was possible in the past on backup apps such as Titanium, but only on normal APK files, and not app bundle (split apk). EDIT: I've set a bounty. Please, if you know of a solution, show it. Show something that you've tested to work. Either of merging split APK files, or installing them , all without root and right on the device. EDIT: Sadly all solutions here didn't work, with or without root, and that's even though I've found an app that succeeded doing it (with and without root), called "SAI (Split APKs Installer)" (I think its repository is here, found after I've put a bounty). I'm putting a new bounty. Please, whoever publishes a new answer, show that it works with and without root. Show on Github if needed (and here just the important stuff). I know this app is open sourced anyway, but it's important for me how to do it here, and share with others, as currently what's shown here isn't working, and requires root, even though it's not really needed. This time I won't grant the bounty till I see something that indeed works (previously I was short on time and granted it to the answer I thought should work).
Please check this. when we send adb install-multiple apk1 apk2 ... it calls this code install-multiple std::string install_cmd; if (_use_legacy_install()) { install_cmd = "exec:pm"; } else { install_cmd = "exec:cmd package"; } std::string cmd = android::base::StringPrintf("%s install-create -S %" PRIu64, install_cmd.c_str(), total_size); for (i = 1; i < first_apk; i++) { cmd += " " + escape_arg(argv[i]); } which in turn calls Pm.java or a new way of executing PackageManagerService code, both are similar I tried to integrate that code in my app, The problem which I faced, apk installation was not able to complete, it is due to the reason that the app needs. <uses-permission android:name="android.permission.INSTALL_PACKAGES"/> But it is only given to system-priv apps. When I executed these steps from adb shell apk installation was successful and when I created my app a system priv-app apk install was successfull. code to call new apis of PackageManager, mostly copied from Pm.java Steps in installing split apks Create a session with argument -S , return session id. (install-create, -S, 52488426) 52488426 -- total size of apks. Write split apks in that session with size , name and path (install-write, -S, 44334187, 824704264, 1_base.apk, -) (install-write, -S, 1262034, 824704264, 2_split_config.en.apk, -) (install-write, -S, 266117, 824704264, 3_split_config.hdpi.apk, -) (install-write, -S, 6626088, 824704264, 4_split_config.x86.apk, -) commit the session with session id (install-commit, 824704264) I have placed airbnb apk in my sdcard. OnePlus5:/sdcard/com.airbnb.android-1 $ ll total 51264 -rw-rw---- 1 root sdcard_rw 44334187 2019-04-01 14:20 base.apk -rw-rw---- 1 root sdcard_rw 1262034 2019-04-01 14:20 split_config.en.apk -rw-rw---- 1 root sdcard_rw 266117 2019-04-01 14:20 split_config.hdpi.apk -rw-rw---- 1 root sdcard_rw 6626088 2019-04-01 14:20 split_config.x86.apk and calling functions to install apk. final InstallParams installParams = makeInstallParams(52488426l); try { int sessionId = runInstallCreate(installParams); runInstallWrite(44334187,sessionId, "1_base.apk", "/sdcard/com.airbnb.android-1/base.apk"); runInstallWrite(1262034,sessionId, "2_split_config.en.apk", "/sdcard/com.airbnb.android-1/split_config.en.apk"); runInstallWrite(266117,sessionId, "3_split_config.hdpi.apk", "/sdcard/com.airbnb.android-1/split_config.hdpi.apk"); runInstallWrite(6626088,sessionId, "4_split_config.x86.apk", "/sdcard/com.airbnb.android-1/split_config.x86.apk"); if (doCommitSession(sessionId, false ) != PackageInstaller.STATUS_SUCCESS) { } System.out.println("Success"); } catch (RemoteException e) { e.printStackTrace(); } private int runInstallCreate(InstallParams installParams) throws RemoteException { final int sessionId = doCreateSession(installParams.sessionParams); System.out.println("Success: created install session [" + sessionId + "]"); return sessionId; } private int doCreateSession(PackageInstaller.SessionParams params) throws RemoteException { int sessionId = 0 ; try { sessionId = packageInstaller.createSession(params); } catch (IOException e) { e.printStackTrace(); } return sessionId; } private int runInstallWrite(long size, int sessionId , String splitName ,String path ) throws RemoteException { long sizeBytes = -1; String opt; sizeBytes = size; return doWriteSession(sessionId, path, sizeBytes, splitName, true /*logSuccess*/); } private int doWriteSession(int sessionId, String inPath, long sizeBytes, String splitName, boolean logSuccess) throws RemoteException { if ("-".equals(inPath)) { inPath = null; } else if (inPath != null) { final File file = new File(inPath); if (file.isFile()) { sizeBytes = file.length(); } } final PackageInstaller.SessionInfo info = packageInstaller.getSessionInfo(sessionId); PackageInstaller.Session session = null; InputStream in = null; OutputStream out = null; try { session = packageInstaller.openSession(sessionId); if (inPath != null) { in = new FileInputStream(inPath); } out = session.openWrite(splitName, 0, sizeBytes); int total = 0; byte[] buffer = new byte[65536]; int c; while ((c = in.read(buffer)) != -1) { total += c; out.write(buffer, 0, c); } session.fsync(out); if (logSuccess) { System.out.println("Success: streamed " + total + " bytes"); } return PackageInstaller.STATUS_SUCCESS; } catch (IOException e) { System.err.println("Error: failed to write; " + e.getMessage()); return PackageInstaller.STATUS_FAILURE; } finally { try { out.close(); in.close(); session.close(); } catch (IOException e) { e.printStackTrace(); } } } private int doCommitSession(int sessionId, boolean logSuccess) throws RemoteException { PackageInstaller.Session session = null; try { try { session = packageInstaller.openSession(sessionId); } catch (IOException e) { e.printStackTrace(); } session.commit(PendingIntent.getBroadcast(getApplicationContext(), sessionId, new Intent("android.intent.action.MAIN"), 0).getIntentSender()); System.out.println("install request sent"); Log.d(TAG, "doCommitSession: " + packageInstaller.getMySessions()); Log.d(TAG, "doCommitSession: after session commit "); return 1; } finally { session.close(); } } private static class InstallParams { PackageInstaller.SessionParams sessionParams; } private InstallParams makeInstallParams(long totalSize ) { final PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL); final InstallParams params = new InstallParams(); params.sessionParams = sessionParams; String opt; sessionParams.setSize(totalSize); return params; } This is the list of commands that are actually received in Pm.java when we do adb install-multiple 04-01 16:04:40.626 4886 4886 D Pm : run() called with: args = [[install-create, -S, 52488426]] 04-01 16:04:41.862 4897 4897 D Pm : run() called with: args = [[install-write, -S, 44334187, 824704264, 1_base.apk, -]] 04-01 16:04:56.036 4912 4912 D Pm : run() called with: args = [[install-write, -S, 1262034, 824704264, 2_split_config.en.apk, -]] 04-01 16:04:57.584 4924 4924 D Pm : run() called with: args = [[install-write, -S, 266117, 824704264, 3_split_config.hdpi.apk, -]] 04-01 16:04:58.842 4936 4936 D Pm : run() called with: args = [[install-write, -S, 6626088, 824704264, 4_split_config.x86.apk, -]] 04-01 16:05:01.304 4948 4948 D Pm : run() called with: args = [[install-commit, 824704264]] So for apps which are not system priv-app, I don't know how can they can install split apks. Play store being a system priv-app can use these apis and install split apks without any issues.
No root required implementation Check this git hub link: https://github.com/nkalra0123/splitapkinstall We have to create a service and pass that handle in session.commit() Intent callbackIntent = new Intent(getApplicationContext(), APKInstallService.class); PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 0, callbackIntent, 0); session.commit(pendingIntent.getIntentSender()); EDIT: Since the solution works, but not really published here, I've decided to write it before marking it as correct solution. Here's the code: manifest <manifest package="com.nitin.apkinstaller" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="#mipmap/ic_launcher" android:label="#string/app_name" android:roundIcon="#mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="#style/AppTheme" tools:ignore="AllowBackup,GoogleAppIndexingWarning"> <activity android:name=".MainActivity" android:label="#string/app_name" android:theme="#style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <service android:name=".APKInstallService"/> </application> </manifest> APKInstallService class APKInstallService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { when (if (intent.hasExtra(PackageInstaller.EXTRA_STATUS)) null else intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { Log.d("AppLog", "Requesting user confirmation for installation") val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT) confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) try { startActivity(confirmationIntent) } catch (e: Exception) { } } PackageInstaller.STATUS_SUCCESS -> Log.d("AppLog", "Installation succeed") else -> Log.d("AppLog", "Installation failed") } stopSelf() return START_NOT_STICKY } override fun onBind(intent: Intent): IBinder? { return null } } MainActivity class MainActivity : AppCompatActivity() { private lateinit var packageInstaller: PackageInstaller override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val toolbar = findViewById<Toolbar>(R.id.toolbar) setSupportActionBar(toolbar) val fab = findViewById<FloatingActionButton>(R.id.fab) fab.setOnClickListener { packageInstaller = packageManager.packageInstaller val ret = installApk("/storage/emulated/0/Download/split/") Log.d("AppLog", "onClick: return value is $ret") } } private fun installApk(apkFolderPath: String): Int { val nameSizeMap = HashMap<String, Long>() var totalSize: Long = 0 var sessionId = 0 val folder = File(apkFolderPath) val listOfFiles = folder.listFiles() try { for (listOfFile in listOfFiles) { if (listOfFile.isFile) { Log.d("AppLog", "installApk: " + listOfFile.name) nameSizeMap[listOfFile.name] = listOfFile.length() totalSize += listOfFile.length() } } } catch (e: Exception) { e.printStackTrace() return -1 } val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) installParams.setSize(totalSize) try { sessionId = packageInstaller.createSession(installParams) Log.d("AppLog","Success: created install session [$sessionId]") for ((key, value) in nameSizeMap) { doWriteSession(sessionId, apkFolderPath + key, value, key) } doCommitSession(sessionId) Log.d("AppLog","Success") } catch (e: IOException) { e.printStackTrace() } return sessionId } private fun doWriteSession(sessionId: Int, inPath: String?, sizeBytes: Long, splitName: String): Int { var inPathToUse = inPath var sizeBytesToUse = sizeBytes if ("-" == inPathToUse) { inPathToUse = null } else if (inPathToUse != null) { val file = File(inPathToUse) if (file.isFile) sizeBytesToUse = file.length() } var session: PackageInstaller.Session? = null var inputStream: InputStream? = null var out: OutputStream? = null try { session = packageInstaller.openSession(sessionId) if (inPathToUse != null) { inputStream = FileInputStream(inPathToUse) } out = session!!.openWrite(splitName, 0, sizeBytesToUse) var total = 0 val buffer = ByteArray(65536) var c: Int while (true) { c = inputStream!!.read(buffer) if (c == -1) break total += c out!!.write(buffer, 0, c) } session.fsync(out!!) Log.d("AppLog", "Success: streamed $total bytes") return PackageInstaller.STATUS_SUCCESS } catch (e: IOException) { Log.e("AppLog", "Error: failed to write; " + e.message) return PackageInstaller.STATUS_FAILURE } finally { try { out?.close() inputStream?.close() session?.close() } catch (e: IOException) { e.printStackTrace() } } } private fun doCommitSession(sessionId: Int) { var session: PackageInstaller.Session? = null try { try { session = packageInstaller.openSession(sessionId) val callbackIntent = Intent(applicationContext, APKInstallService::class.java) val pendingIntent = PendingIntent.getService(applicationContext, 0, callbackIntent, 0) session!!.commit(pendingIntent.intentSender) session.close() Log.d("AppLog", "install request sent") Log.d("AppLog", "doCommitSession: " + packageInstaller.mySessions) Log.d("AppLog", "doCommitSession: after session commit ") } catch (e: IOException) { e.printStackTrace() } } finally { session!!.close() } } }
It is possible to merge split APKs into a single APK, both manually and automatically, but forced to use a made up signing key means the APK can’t be installed as an update to the genuine app and in case the app may checks itself for tempering A detailed guide how to merge split APKs manually: https://platinmods.com/threads/how-to-turn-a-split-apk-into-a-normal-non-split-apk.76683/ A PC software to merge split APKs automatically: https://www.andnixsh.com/2020/06/sap-split-apks-packer-by-kirlif-windows.html
Run bundletool with --mode=universal This will generate an APKS file, rename extension to zip Unzip You will find a fat universal.apk file which can be installed as in the old days.
If you have root, you can use this code. Please get the read/write sdcard permission.(via runtime permissions or permission granted from settings app) before executing this code. airbnb apk was successfully installed after running this code. Calling this function with args "/split-apks/" , I have placed the airbnb split apks in a directory in /sdcard/split-apks/. installApk("/split-apks/"); public void installApk(String apkFolderPath) { PackageInstaller packageInstaller = getPackageManager().getPackageInstaller(); HashMap<String, Long> nameSizeMap = new HashMap<>(); long totalSize = 0; File folder = new File(Environment.getExternalStorageDirectory().getPath()+ apkFolderPath); File[] listOfFiles = folder.listFiles(); for (int i = 0; i < listOfFiles.length; i++) { if (listOfFiles[i].isFile()) { System.out.println("File " + listOfFiles[i].getName()); nameSizeMap.put(listOfFiles[i].getName(),listOfFiles[i].length()); totalSize += listOfFiles[i].length(); } } String su = "/system/xbin/su"; final String[] pm_install_create = new String[]{su, "-c", "pm" ,"install-create", "-S", Long.toString(totalSize) }; execute(null, pm_install_create); List<PackageInstaller.SessionInfo> sessions = packageInstaller.getAllSessions(); int sessId = sessions.get(0).getSessionId(); String sessionId = Integer.toString(sessId); for(Map.Entry<String,Long> entry : nameSizeMap.entrySet()) { String[] pm_install_write = new String[]{su, "-c", "pm" ,"install-write", "-S", Long.toString(entry.getValue()),sessionId, entry.getKey(), Environment.getExternalStorageDirectory().getPath()+apkFolderPath+ entry.getKey()}; execute(null,pm_install_write); } String[] pm_install_commit = new String[]{su, "-c", "pm" ,"install-commit", sessionId}; execute(null, pm_install_commit); } public String execute(Map<String, String> environvenmentVars, String[] cmd) { boolean DEBUG = true; if (DEBUG) Log.d("log","command is " + Arrays.toString(cmd)); try { Process process = Runtime.getRuntime().exec(cmd); if (DEBUG) Log.d("log", "process is " + process); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); if (DEBUG) Log.d("log", "bufferreader is " + reader); if (DEBUG) Log.d("log", "readline " + reader.readLine()); StringBuffer output = new StringBuffer(); char[] buffer = new char[4096]; int read; while ((read = reader.read(buffer)) > 0) { output.append(buffer, 0, read); } reader.close(); process.waitFor(); if (DEBUG) Log.d("log", output.toString()); return output.toString(); } catch (Exception e) { e.printStackTrace(); } return null; } EDIT: same code, but in Kotlin, as it's shorter: sample usage: Foo.installApk(context,fullPathToSplitApksFolder) Example: AsyncTask.execute { Foo.installApk(this#MainActivity,"/storage/emulated/0/Download/split") } Code: object Foo { #WorkerThread #JvmStatic fun installApk(context: Context, apkFolderPath: String) { val packageInstaller = context.packageManager.packageInstaller val nameSizeMap = HashMap<File, Long>() var totalSize: Long = 0 val folder = File(apkFolderPath) val listOfFiles = folder.listFiles().filter { it.isFile && it.name.endsWith(".apk") } for (file in listOfFiles) { Log.d("AppLog", "File " + file.name) nameSizeMap[file] = file.length() totalSize += file.length() } val su = "su" val pmInstallCreate = arrayOf(su, "-c", "pm", "install-create", "-S", totalSize.toString()) execute(pmInstallCreate) val sessions = packageInstaller.allSessions val sessionId = Integer.toString(sessions[0].sessionId) for ((file, value) in nameSizeMap) { val pmInstallWrite = arrayOf(su, "-c", "pm", "install-write", "-S", value.toString(), sessionId, file.name, file.absolutePath) execute(pmInstallWrite) } val pmInstallCommit = arrayOf(su, "-c", "pm", "install-commit", sessionId) execute(pmInstallCommit) } #WorkerThread #JvmStatic private fun execute(cmd: Array<String>): String? { Log.d("AppLog", "command is " + Arrays.toString(cmd)) try { val process = Runtime.getRuntime().exec(cmd) Log.d("AppLog", "process is $process") val reader = BufferedReader(InputStreamReader(process.inputStream)) Log.d("AppLog", "bufferreader is $reader") Log.d("AppLog", "readline " + reader.readLine()) val output = StringBuilder() val buffer = CharArray(4096) var read: Int while (true) { read = reader.read(buffer) if (read <= 0) break output.append(buffer, 0, read) } reader.close() process.waitFor() Log.d("AppLog", output.toString()) return output.toString() } catch (e: Exception) { e.printStackTrace() } return null } }
From an Android App Bundle, you can generate a "universal APK" using bundletool build-apks command with the --mode=universal flag. This will generate a single "fat" APK that is compatible with all devices (that your app supports). I know this isn't strictly answering your question, but trying to merge the APKs is not only a complex task, but will result in a lot of cases in something incorrect.
Well I don't know the coding part much, as I have not learnt android, but I can suggest something which you can try. If the task is just to make the split apka into one, what I do is Install the app using SAI, or Lucky patcher (since it started creating apks instead of apk, and so is able to install them) Extract as a single apk using apk extractor (the first appearing in the search, by Meher) Optional - Uninstall the app, if you only needed apk So you can look at their source code (if they are open source, otherwise something similar), and then try to make a single app to do all these processes (if you know android). Hope it helps, and please provide your app link, if you manage to create one. Thanks and cheers
What is the way to merge those all into one APK file? After installing (see question 2), use eg TotalCommander to copy the apk from 'installed apps' Is it possible to install split APK files without root and without PC ? Use any terminal app, then: pm install <split1> <split2> ...
Root Check returning true even on non rooted device [duplicate]
My app has a certain piece of functionality that will only work on a device where root is available. Rather than having this feature fail when it is used (and then show an appropriate error message to the user), I'd prefer an ability to silently check if root is available first, and if not,hide the respective options in the first place. Is there a way to do this?
Here is a class that will check for Root one of three ways. /** #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(); } } }
If you are already using Fabric/Firebase Crashlytics you can call CommonUtils.isRooted(context) This is the current implementation of that method: 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; }
The RootTools library offers simple methods to check for root: RootTools.isRootAvailable() Reference
In my application I was checking if device is rooted or not by executing "su" command. But today I've removed this part of my code. Why? Because my application became a memory killer. How? Let me tell you my story. There were some complaints that my application was slowing down devices(Of course I thought that can not be true). I tried to figure out why. So I used MAT to get heap dumps and analyze, and everything seemed perfect. But after relaunching my app many times I realized that device is really getting slower and stopping my application didn't make it faster (unless I restart device). I analyzed dump files again while device is very slow. But everything was still perfect for dump file. Then I did what must be done at first. I listed processes. $ adb shell ps Surprize; there were many processes for my application (with my application's process tag at manifest). Some of them was zombie some of them not. With a sample application which has a single Activity and executes just "su" command, I realized that a zombie process is being created on every launch of application. At first these zombies allocate 0KB but than something happens and zombie processes are holding nearly same KBs as my application's main process and they became standart processes. There is a bug report for same issue on bugs.sun.com: http://bugs.sun.com/view_bug.do?bug_id=6474073 this explains if command is not found zombies are going to be created with exec() method. But I still don't understand why and how can they become standart processes and hold significant KBs. (This is not happening all the time) You can try if you want with code sample below; String commandToExecute = "su"; executeShellCommand(commandToExecute); Simple command execution method; 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) { } } } } To sum up; I have no advice for you to determine if device is rooted or not. But if I were you I would not use Runtime.getRuntime().exec(). By the way; RootTools.isRootAvailable() causes same problem.
Update 2017 You can do it now with Google Safetynet API. The SafetyNet API provides Attestation API which helps you assess the security and compatibility of the Android environments in which your apps run. This attestation can helps to determine whether or not the particular device has been tampered with or otherwise modified. The Attestation API returns a JWS response like this { "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, } Parsing this response can help you determine if device is rooted or not Rooted devices seem to cause ctsProfileMatch=false. You can do it on client side but parsing response on server side is recommend. A basic client server archtecture with safety net API will look like this:-
Many of the answers listed here have inherent issues: Checking for test-keys is correlated with root access but doesn't necessarily guarantee it "PATH" directories should be derived from the actual "PATH" environment variable instead of being hard coded The existence of the "su" executable doesn't necessarily mean the device has been rooted The "which" executable may or may not be installed, and you should let the system resolve its path if possible Just because the SuperUser app is installed on the device does not mean the device has root access yet The RootTools library from Stericson seems to be checking for root more legitimately. It also has lots of extra tools and utilities so I highly recommend it. However, there's no explanation of how it specifically checks for root, and it may be a bit heavier than most apps really need. I've made a couple of utility methods that are loosely based on the RootTools library. If you simply want to check if the "su" executable is on the device you can use the following method: public static boolean isRootAvailable(){ for(String pathDir : System.getenv("PATH").split(":")){ if(new File(pathDir, "su").exists()) { return true; } } return false; } This method simply loops through the directories listed in the "PATH" environment variable and checks if a "su" file exists in one of them. In order to truly check for root access the "su" command must actually be run. If an app like SuperUser is installed, then at this point it may ask for root access, or if its already been granted/denied a toast may be shown indicating whether access was granted/denied. A good command to run is "id" so that you can verify that the user id is in fact 0 (root). Here's a sample method to determine whether root access has been granted: 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; } It's important to actually test running the "su" command because some emulators have the "su" executable pre-installed, but only allow certain users to access it like the adb shell. It's also important to check for the existence of the "su" executable before trying to run it, because android has been known to not properly dispose of processes that try to run missing commands. These ghost processes can run up memory consumption over time.
Root check at Java level is not a safe solution. If your app has Security Concerns to run on a Rooted device , then please use this solution. Kevin's answer works unless the phone also has an app like RootCloak . Such apps have a Handle over Java APIs once phone is rooted and they mock these APIs to return phone is not rooted. I have written a native level code based on Kevin's answer , it works even with RootCloak ! Also it does not cause any memory leak issues. #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; } In your Java code , you need to create wrapper class RootUtils to make the native calls public boolean checkRooted() { if( rootUtils.checkRootAccessMethod3() == 0 || rootUtils.checkRootAccessMethod1() == 0 || rootUtils.checkRootAccessMethod2() == 0 ) return true; return false; }
http://code.google.com/p/roottools/ If you do not want to use the jar file just use the code: 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; } Program will try to find su folder: private static boolean isRooted() { return findBinary("su"); } Example: if (isRooted()) { textView.setText("Device Rooted"); } else { textView.setText("Device Unrooted"); }
RootBeer is a root checking Android library by Scott and Matthew. It uses various checks to indicate whether device is rooted or not. Java checks CheckRootManagementApps CheckPotentiallyDangerousAppss CheckRootCloakingApps CheckTestKeys checkForDangerousProps checkForBusyBoxBinary checkForSuBinary checkSuExists checkForRWSystem Native checks We call through to our native root checker to run some of it's own checks. Native checks are typically harder to cloak, so some root cloak apps just block the loading of native libraries that contain certain key words. checkForSuBinary
Instead of using isRootAvailable() you can use isAccessGiven(). Direct from RootTools wiki: if (RootTools.isAccessGiven()) { // your app has been granted root access } RootTools.isAccessGiven() not only checks that a device is rooted, it also calls su for your app, requests permission, and returns true if your app was successfully granted root permissions. This can be used as the first check in your app to make sure that you will be granted access when you need it. Reference
Some modified builds used to set the system property ro.modversion for this purpose. Things seem to have moved on; my build from TheDude a few months ago has this: cmb#apollo:~$ adb -d shell getprop |grep build [ro.build.id]: [CUPCAKE] [ro.build.display.id]: [htc_dream-eng 1.5 CUPCAKE eng.TheDudeAbides.20090427.235325 test-keys] [ro.build.version.incremental]: [eng.TheDude.2009027.235325] [ro.build.version.sdk]: [3] [ro.build.version.release]: [1.5] [ro.build.date]: [Mon Apr 20 01:42:32 CDT 2009] [ro.build.date.utc]: [1240209752] [ro.build.type]: [eng] [ro.build.user]: [TheDude] [ro.build.host]: [ender] [ro.build.tags]: [test-keys] [ro.build.product]: [dream] [ro.build.description]: [kila-user 1.1 PLAT-RC33 126986 ota-rel-keys,release-keys] [ro.build.fingerprint]: [tmobile/kila/dream/trout:1.1/PLAT-RC33/126986:user/ota-rel-keys,release-keys] [ro.build.changelist]: [17615# end build properties] The emulator from the 1.5 SDK on the other hand, running the 1.5 image, also has root, is probably similar to the Android Dev Phone 1 (which you presumably want to allow) and has this: cmb#apollo:~$ adb -e shell getprop |grep build [ro.build.id]: [CUPCAKE] [ro.build.display.id]: [sdk-eng 1.5 CUPCAKE 148875 test-keys] [ro.build.version.incremental]: [148875] [ro.build.version.sdk]: [3] [ro.build.version.release]: [1.5] [ro.build.date]: [Thu May 14 18:09:10 PDT 2009] [ro.build.date.utc]: [1242349750] [ro.build.type]: [eng] [ro.build.user]: [android-build] [ro.build.host]: [undroid16.mtv.corp.google.com] [ro.build.tags]: [test-keys] [ro.build.product]: [generic] [ro.build.description]: [sdk-eng 1.5 CUPCAKE 148875 test-keys] [ro.build.fingerprint]: [generic/sdk/generic/:1.5/CUPCAKE/148875:eng/test-keys] As for the retail builds, I don't have one to hand, but various searches under site:xda-developers.com are informative. Here is a G1 in the Netherlands, you can see that ro.build.tags does not have test-keys, and I think that's probably the most reliable property to use.
I suggest using native code for root detection. Here is a full working example. JAVA wrapper: package com.kozhevin.rootchecks.util; import android.support.annotation.NonNull; import com.kozhevin.rootchecks.BuildConfig; public class MeatGrinder { private final static String LIB_NAME = "native-lib"; private static boolean isLoaded; private static boolean isUnderTest = false; private MeatGrinder() { } public boolean isLibraryLoaded() { if (isLoaded) { return true; } try { if(isUnderTest) { throw new UnsatisfiedLinkError("under test"); } System.loadLibrary(LIB_NAME); isLoaded = true; } catch (UnsatisfiedLinkError e) { if (BuildConfig.DEBUG) { e.printStackTrace(); } } return isLoaded; } public native boolean isDetectedDevKeys(); public native boolean isDetectedTestKeys(); public native boolean isNotFoundReleaseKeys(); public native boolean isFoundDangerousProps(); public native boolean isPermissiveSelinux(); public native boolean isSuExists(); public native boolean isAccessedSuperuserApk(); public native boolean isFoundSuBinary(); public native boolean isFoundBusyboxBinary(); public native boolean isFoundXposed(); public native boolean isFoundResetprop(); public native boolean isFoundWrongPathPermission(); public native boolean isFoundHooks(); #NonNull public static MeatGrinder getInstance() { return InstanceHolder.INSTANCE; } private static class InstanceHolder { private static final MeatGrinder INSTANCE = new MeatGrinder(); } } JNI wrapper(native-lib.c): JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedTestKeys( JNIEnv *env, jobject this ) { return (jboolean) isDetectedTestKeys(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedDevKeys( JNIEnv *env, jobject this ) { return (jboolean) isDetectedDevKeys(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isNotFoundReleaseKeys( JNIEnv *env, jobject this ) { return (jboolean) isNotFoundReleaseKeys(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundDangerousProps( JNIEnv *env, jobject this ) { return (jboolean) isFoundDangerousProps(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isPermissiveSelinux( JNIEnv *env, jobject this ) { return (jboolean) isPermissiveSelinux(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isSuExists( JNIEnv *env, jobject this ) { return (jboolean) isSuExists(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isAccessedSuperuserApk( JNIEnv *env, jobject this ) { return (jboolean) isAccessedSuperuserApk(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundSuBinary( JNIEnv *env, jobject this ) { return (jboolean) isFoundSuBinary(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundBusyboxBinary( JNIEnv *env, jobject this ) { return (jboolean) isFoundBusyboxBinary(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundXposed( JNIEnv *env, jobject this ) { return (jboolean) isFoundXposed(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundResetprop( JNIEnv *env, jobject this ) { return (jboolean) isFoundResetprop(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundWrongPathPermission( JNIEnv *env, jobject this ) { return (jboolean) isFoundWrongPathPermission(); } JNIEXPORT jboolean JNICALL Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundHooks( JNIEnv *env, jobject this ) { return (jboolean) isFoundHooks(); } constants: // Comma-separated tags describing the build, like= "unsigned,debug". const char *const ANDROID_OS_BUILD_TAGS = "ro.build.tags"; // A string that uniquely identifies this build. 'BRAND/PRODUCT/DEVICE:RELEASE/ID/VERSION.INCREMENTAL:TYPE/TAGS'. const char *const ANDROID_OS_BUILD_FINGERPRINT = "ro.build.fingerprint"; const char *const ANDROID_OS_SECURE = "ro.secure"; const char *const ANDROID_OS_DEBUGGABLE = "ro.debuggable"; const char *const ANDROID_OS_SYS_INITD = "sys.initd"; const char *const ANDROID_OS_BUILD_SELINUX = "ro.build.selinux"; //see https://android.googlesource.com/platform/system/core/+/master/adb/services.cpp#86 const char *const SERVICE_ADB_ROOT = "service.adb.root"; const char * const MG_SU_PATH[] = { "/data/local/", "/data/local/bin/", "/data/local/xbin/", "/sbin/", "/system/bin/", "/system/bin/.ext/", "/system/bin/failsafe/", "/system/sd/xbin/", "/su/xbin/", "/su/bin/", "/magisk/.core/bin/", "/system/usr/we-need-root/", "/system/xbin/", 0 }; const char * const MG_EXPOSED_FILES[] = { "/system/lib/libxposed_art.so", "/system/lib64/libxposed_art.so", "/system/xposed.prop", "/cache/recovery/xposed.zip", "/system/framework/XposedBridge.jar", "/system/bin/app_process64_xposed", "/system/bin/app_process32_xposed", "/magisk/xposed/system/lib/libsigchain.so", "/magisk/xposed/system/lib/libart.so", "/magisk/xposed/system/lib/libart-disassembler.so", "/magisk/xposed/system/lib/libart-compiler.so", "/system/bin/app_process32_orig", "/system/bin/app_process64_orig", 0 }; const char * const MG_READ_ONLY_PATH[] = { "/system", "/system/bin", "/system/sbin", "/system/xbin", "/vendor/bin", "/sbin", "/etc", 0 }; root detections from native code: struct mntent *getMntent(FILE *fp, struct mntent *e, char *buf, int buf_len) { while (fgets(buf, buf_len, fp) != NULL) { // Entries look like "/dev/block/vda /system ext4 ro,seclabel,relatime,data=ordered 0 0". // That is: mnt_fsname mnt_dir mnt_type mnt_opts mnt_freq mnt_passno. int fsname0, fsname1, dir0, dir1, type0, type1, opts0, opts1; if (sscanf(buf, " %n%*s%n %n%*s%n %n%*s%n %n%*s%n %d %d", &fsname0, &fsname1, &dir0, &dir1, &type0, &type1, &opts0, &opts1, &e->mnt_freq, &e->mnt_passno) == 2) { e->mnt_fsname = &buf[fsname0]; buf[fsname1] = '\0'; e->mnt_dir = &buf[dir0]; buf[dir1] = '\0'; e->mnt_type = &buf[type0]; buf[type1] = '\0'; e->mnt_opts = &buf[opts0]; buf[opts1] = '\0'; return e; } } return NULL; } bool isPresentMntOpt(const struct mntent *pMnt, const char *pOpt) { char *token = pMnt->mnt_opts; const char *end = pMnt->mnt_opts + strlen(pMnt->mnt_opts); const size_t optLen = strlen(pOpt); while (token != NULL) { const char *tokenEnd = token + optLen; if (tokenEnd > end) break; if (memcmp(token, pOpt, optLen) == 0 && (*tokenEnd == '\0' || *tokenEnd == ',' || *tokenEnd == '=')) { return true; } token = strchr(token, ','); if (token != NULL) { token++; } } return false; } static char *concat2str(const char *pString1, const char *pString2) { char *result; size_t lengthBuffer = 0; lengthBuffer = strlen(pString1) + strlen(pString2) + 1; result = malloc(lengthBuffer); if (result == NULL) { GR_LOGW("malloc failed\n"); return NULL; } memset(result, 0, lengthBuffer); strcpy(result, pString1); strcat(result, pString2); return result; } static bool isBadPropertyState(const char *key, const char *badValue, bool isObligatoryProperty, bool isExact) { if (badValue == NULL) { GR_LOGE("badValue may not be NULL"); return false; } if (key == NULL) { GR_LOGE("key may not be NULL"); return false; } char value[PROP_VALUE_MAX + 1]; int length = __system_property_get(key, value); bool result = false; /* A length 0 value indicates that the property is not defined */ if (length > 0) { GR_LOGI("property:[%s]==[%s]", key, value); if (isExact) { if (strcmp(value, badValue) == 0) { GR_LOGW("bad value[%s] equals to [%s] in the property [%s]", value, badValue, key); result = true; } } else { if (strlen(value) >= strlen(badValue) && strstr(value, badValue) != NULL) { GR_LOGW("bad value[%s] found in [%s] in the property [%s]", value, badValue, key); result = true; } } } else { GR_LOGI("[%s] property not found", key); if (isObligatoryProperty) { result = true; } } return result; } bool isDetectedTestKeys() { const char *TEST_KEYS_VALUE = "test-keys"; return isBadPropertyState(ANDROID_OS_BUILD_TAGS, TEST_KEYS_VALUE, true, false); } bool isDetectedDevKeys() { const char *DEV_KEYS_VALUE = "dev-keys"; return isBadPropertyState(ANDROID_OS_BUILD_TAGS, DEV_KEYS_VALUE, true, false); } bool isNotFoundReleaseKeys() { const char *RELEASE_KEYS_VALUE = "release-keys"; return !isBadPropertyState(ANDROID_OS_BUILD_TAGS, RELEASE_KEYS_VALUE, false, true); } bool isFoundWrongPathPermission() { bool result = false; FILE *file = fopen("/proc/mounts", "r"); char mntent_strings[BUFSIZ]; if (file == NULL) { GR_LOGE("setmntent"); return result; } struct mntent ent = {0}; while (NULL != getMntent(file, &ent, mntent_strings, sizeof(mntent_strings))) { for (size_t i = 0; MG_READ_ONLY_PATH[i]; i++) { if (strcmp((&ent)->mnt_dir, MG_READ_ONLY_PATH[i]) == 0 && isPresentMntOpt(&ent, "rw")) { GR_LOGI("%s %s %s %s\n", (&ent)->mnt_fsname, (&ent)->mnt_dir, (&ent)->mnt_opts, (&ent)->mnt_type); result = true; break; } } memset(&ent, 0, sizeof(ent)); } fclose(file); return result; } bool isFoundDangerousProps() { const char *BAD_DEBUGGABLE_VALUE = "1"; const char *BAD_SECURE_VALUE = "0"; const char *BAD_SYS_INITD_VALUE = "1"; const char *BAD_SERVICE_ADB_ROOT_VALUE = "1"; bool result = isBadPropertyState(ANDROID_OS_DEBUGGABLE, BAD_DEBUGGABLE_VALUE, true, true) || isBadPropertyState(SERVICE_ADB_ROOT, BAD_SERVICE_ADB_ROOT_VALUE, false, true) || isBadPropertyState(ANDROID_OS_SECURE, BAD_SECURE_VALUE, true, true) || isBadPropertyState(ANDROID_OS_SYS_INITD, BAD_SYS_INITD_VALUE, false, true); return result; } bool isPermissiveSelinux() { const char *BAD_VALUE = "0"; return isBadPropertyState(ANDROID_OS_BUILD_SELINUX, BAD_VALUE, false, false); } bool isSuExists() { char buf[BUFSIZ]; char *str = NULL; char *temp = NULL; size_t size = 1; // start with size of 1 to make room for null terminator size_t strlength; FILE *pipe = popen("which su", "r"); if (pipe == NULL) { GR_LOGI("pipe is null"); return false; } while (fgets(buf, sizeof(buf), pipe) != NULL) { strlength = strlen(buf); temp = realloc(str, size + strlength); // allocate room for the buf that gets appended if (temp == NULL) { // allocation error GR_LOGE("Error (re)allocating memory"); pclose(pipe); if (str != NULL) { free(str); } return false; } else { str = temp; } strcpy(str + size - 1, buf); size += strlength; } pclose(pipe); GR_LOGW("A size of the result from pipe is [%zu], result:\n [%s] ", size, str); if (str != NULL) { free(str); } return size > 1 ? true : false; } static bool isAccessedFile(const char *path) { int result = access(path, F_OK); GR_LOGV("[%s] has been accessed with result: [%d]", path, result); return result == 0 ? true : false; } static bool isFoundBinaryFromArray(const char *const *array, const char *binary) { for (size_t i = 0; array[i]; ++i) { char *checkedPath = concat2str(array[i], binary); if (checkedPath == NULL) { // malloc failed return false; } bool result = isAccessedFile(checkedPath); free(checkedPath); if (result) { return result; } } return false; } bool isAccessedSuperuserApk() { return isAccessedFile("/system/app/Superuser.apk"); } bool isFoundResetprop() { return isAccessedFile("/data/magisk/resetprop"); } bool isFoundSuBinary() { return isFoundBinaryFromArray(MG_SU_PATH, "su"); } bool isFoundBusyboxBinary() { return isFoundBinaryFromArray(MG_SU_PATH, "busybox"); } bool isFoundXposed() { for (size_t i = 0; MG_EXPOSED_FILES[i]; ++i) { bool result = isAccessedFile(MG_EXPOSED_FILES[i]); if (result) { return result; } } return false; } bool isFoundHooks() { bool result = false; pid_t pid = getpid(); char maps_file_name[512]; sprintf(maps_file_name, "/proc/%d/maps", pid); GR_LOGI("try to open [%s]", maps_file_name); const size_t line_size = BUFSIZ; char *line = malloc(line_size); if (line == NULL) { return result; } FILE *fp = fopen(maps_file_name, "r"); if (fp == NULL) { free(line); return result; } memset(line, 0, line_size); const char *substrate = "com.saurik.substrate"; const char *xposed = "XposedBridge.jar"; while (fgets(line, line_size, fp) != NULL) { const size_t real_line_size = strlen(line); if ((real_line_size >= strlen(substrate) && strstr(line, substrate) != NULL) || (real_line_size >= strlen(xposed) && strstr(line, xposed) != NULL)) { GR_LOGI("found in [%s]: [%s]", maps_file_name, line); result = true; break; } } free(line); fclose(fp); return result; }
Here is my code based on some answers here: /** * Checks if the phone is rooted. * * #return <code>true</code> if the phone is rooted, <code>false</code> * otherwise. */ public static boolean isPhoneRooted() { // get from build info String buildTags = android.os.Build.TAGS; if (buildTags != null && buildTags.contains("test-keys")) { return true; } // check if /system/app/Superuser.apk is present try { File file = new File("/system/app/Superuser.apk"); if (file.exists()) { return true; } } catch (Throwable e1) { // ignore } return false; }
Further to #Kevins answer, I've recently found while using his system, that the Nexus 7.1 was returning false for all three methods - No which command, no test-keys and SuperSU was not installed in /system/app. I added this: public static boolean checkRootMethod4(Context context) { return isPackageInstalled("eu.chainfire.supersu", context); } private static boolean isPackageInstalled(String packagename, Context context) { PackageManager pm = context.getPackageManager(); try { pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES); return true; } catch (NameNotFoundException e) { return false; } } This is slightly less useful in some situations (if you need guaranteed root access) as it's completely possible for SuperSU to be installed on devices which don't have SU access. However, since it's possible to have SuperSU installed and working but not in the /system/app directory, this extra case will root (haha) out such cases.
public static boolean isRootAvailable(){ Process p = null; try{ p = Runtime.getRuntime().exec(new String[] {"su"}); writeCommandToConsole(p,"exit 0"); int result = p.waitFor(); if(result != 0) throw new Exception("Root check result with exit command " + result); return true; } catch (IOException e) { Log.e(LOG_TAG, "Su executable is not available ", e); } catch (Exception e) { Log.e(LOG_TAG, "Root is unavailable ", e); }finally { if(p != null) p.destroy(); } return false; } private static String writeCommandToConsole(Process proc, String command, boolean ignoreError) throws Exception{ byte[] tmpArray = new byte[1024]; proc.getOutputStream().write((command + "\n").getBytes()); proc.getOutputStream().flush(); int bytesRead = 0; if(proc.getErrorStream().available() > 0){ if((bytesRead = proc.getErrorStream().read(tmpArray)) > 1){ Log.e(LOG_TAG,new String(tmpArray,0,bytesRead)); if(!ignoreError) throw new Exception(new String(tmpArray,0,bytesRead)); } } if(proc.getInputStream().available() > 0){ bytesRead = proc.getInputStream().read(tmpArray); Log.i(LOG_TAG, new String(tmpArray,0,bytesRead)); } return new String(tmpArray); }
Two additional ideas, if you want to check if a device is root capable from your app: Check for the existing of the 'su' binary: run "which su" from Runtime.getRuntime().exec() Look for the SuperUser.apk in /system/app/Superuser.apk location
Using C++ with the ndk is the best approach to detect root even if the user is using applications that hide his root such as RootCloak. I tested this code with RootCloak and I was able to detect the root even if the user is trying to hide it. So your cpp file would like: #include <jni.h> #include <string> /** * * function that checks for the su binary files and operates even if * root cloak is installed * #return integer 1: device is rooted, 0: device is not *rooted */ extern "C" JNIEXPORT int JNICALL Java_com_example_user_root_1native_rootFunction(JNIEnv *env,jobject thiz){ const char *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"}; int counter =0; while (counter<9){ if(FILE *file = fopen(paths[counter],"r")){ fclose(file); return 1; } counter++; } return 0; } And you will call the function from your java code as follows public class Root_detect { /** * * function that calls a native function to check if the device is *rooted or not * #return boolean: true if the device is rooted, false if the *device is not rooted */ public boolean check_rooted(){ int checker = rootFunction(); if(checker==1){ return true; }else { return false; } } static { System.loadLibrary("cpp-root-lib");//name of your cpp file } public native int rootFunction(); }
As last quarter of 2021 today, I tried to use SafetyNet regarding #HimanshiThakur 's answer. But I got an issue and opened a question in here. Still no answer. So I decided to use RootBeer. It works perfectly but when Magisk hides the root, it doesn't work. If you don't care this case (and many bank apps can't solve this issue too), you can use these steps: Add this to Gradle: implementation 'com.scottyab:rootbeer-lib:0.1.0' Use these lines: RootBeer rootBeer = new RootBeer(context); if (rootBeer.isRooted()) { //we found indication of root } else { //we didn't find indication of root }
if [[ "`adb shell which su | grep -io "permission denied"`" != "permission denied" ]]; then echo "Yes. Rooted device." else echo "No. Device not rooted. Only limited tasks can be performed. Done." zenity --warning --title="Device Not Rooted" --text="The connected Android Device is <b>NOT ROOTED</b>. Only limited tasks can be performed." --no-wrap fi
Based on some of the answers here, I merged them and also added a check if some known root-manager app is installed: fun isProbablyRooted(context: Context, alsoIncludeCheckingRootManagerApp: Boolean = false): Boolean { return hasRootManagerSystemApp(context) || (alsoIncludeCheckingRootManagerApp && hasRootManagerSystemApp(context)) } fun hasRootManagerSystemApp(context: Context): Boolean { val rootAppsPackageNames = arrayOf("com.topjohnwu.magisk", "eu.chainfire.supersu", "com.koushikdutta.superuser", "com.noshufou.android.su", "me.phh.superuser") rootAppsPackageNames.forEach { rootAppPackageName -> try { context.packageManager.getApplicationInfo(rootAppPackageName, 0) return true } catch (e: Exception) { } } return false } fun hasSuBinary(): Boolean { return try { findBinary("su") } catch (e: Exception) { e.printStackTrace() false } } private fun findBinary(binaryName: String): Boolean { val paths = System.getenv("PATH") if (!paths.isNullOrBlank()) { val systemPlaces: List<String> = paths.split(":") return systemPlaces.firstOrNull { File(it, binaryName).exists() } != null } val places = arrayOf("/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/", "/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/") return places.firstOrNull { File(it, binaryName).exists() } != null } manifest: <queries> <package android:name="com.topjohnwu.magisk" /> <package android:name="eu.chainfire.supersu" /> <package android:name="com.koushikdutta.superuser" /> <package android:name="com.noshufou.android.su" /> <package android:name="me.phh.superuser" /> </queries> Of course, this is still a guess, like all the other solutions. Users can install Magisk without having the device rooted, for example.
Forget all that detecting root apps and su binaries. Check for the root daemon process. This can be done from the terminal and you can run terminal commands within an app. Try this one-liner. if [ ! "$(/system/bin/ps -A | grep -v grep | grep -c daemonsu)" = "0" ]; then echo "device is rooted"; else echo "device is not rooted"; fi You don't need root permission to achieve this either. Edit: Now using this method for better detection! if [ $(ps -A | grep -e ^shell -e ^root | grep -v "\[" | tr -s ' ' | cut -d ' ' -f 9 | grep -c su) ] || [ $(which su) ]; then echo 'rooted'; else echo 'not rooted'; fi
Using google SafetyNet Attestation API you can easily check whether your device is rooted or not : Add dependency in build.gradle(:app) implementation 'com.google.android.gms:play-services-safetynet:17.0.0' Get Api key and enable Android Device Verification API using link public static void sendSafetyNetRequest(Activity context) { if(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context, 13000000) == ConnectionResult.SUCCESS) { Log.e(TAG, "The SafetyNet Attestation API is available"); // TODO(developer): Change the nonce generation to include your own, used once value, // ideally from your remote server. String nonceData = "Safety Net Sample: " + System.currentTimeMillis(); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); Random mRandom = new SecureRandom(); byte[] bytes = new byte[24]; mRandom.nextBytes(bytes); try { byteStream.write(bytes); byteStream.write(nonceData.getBytes()); } catch (IOException e) { e.printStackTrace(); } byte[] nonce = byteStream.toByteArray(); SafetyNetClient client = SafetyNet.getClient(context); Task<SafetyNetApi.AttestationResponse> task = client.attest(nonce, API_KEY_FROM_STEP_2_LINK); task.addOnSuccessListener(context, attestationResponse -> { /* TODO(developer): Forward this result to your server together with the nonce for verification. You can also parse the JwsResult locally to confirm that the API returned a response by checking for an 'error' field first and before retrying the request with an exponential backoff. NOTE: Do NOT rely on a local, client-side only check for security, you must verify the response on a remote server! */ String jwsResult = attestationResponse.getJwsResult(); Log.e(TAG, "Success! SafetyNet result:\n" + jwsResult + "\n"); if (jwsResult == null) { Log.e(TAG, "jwsResult Null"); } final String[] jwtParts = jwsResult.split("\\."); if (jwtParts.length == 3) { String decodedPayload = new String(Base64.decode(jwtParts[1], Base64.DEFAULT)); Log.e(TAG, "decodedPayload : " + decodedPayload); } }); task.addOnFailureListener(context, e -> { // An error occurred while communicating with the service. String mResult = null; if (e instanceof ApiException) { // An error with the Google Play Services API contains some additional details. ApiException apiException = (ApiException) e; Util.showLog(TAG, "Error: " + CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " + apiException.getStatusMessage()); } else { // A different, unknown type of error occurred. Log.e(TAG, "ERROR! " + e.getMessage()); } }); } else { Log.e(TAG, "Prompt user to update Google Play services."; } } ` Check your logs for decodedPayload if ctsProfileMatch and basicIntegrity both are false it means your device is rooted . The Attestation API returns a JWS response which looks like: { "nonce": "6pLrr9zWyl6TNzj+kpbR4LZcfPY3U2FmZXR5IE5ldCBTYW1wbGU6IDE2MTQ2NzkwMTIzNjc=", "timestampMs": 9860437986543, "apkPackageName": " your package name will be displayed here", "ctsProfileMatch": true, "apkDigestSha256": [ "base64 encoded, SHA-256 hash of the certificate used to sign requesting app" ], "basicIntegrity": true, "evaluationType": "BASIC" } For more info check this link.
There is Safety Net Attestation API of Google play services by which we can assess the device and determine if it is rooted/tampered. Please go through my answer to deal with rooted devices: https://stackoverflow.com/a/58304556/3908895
if you don't want to use any 3rd party library or any random solution then just use google lib for detecting it. Android Device Verification response : { "timestampMs": 9860437986543, "nonce": "R2Rra24fVm5xa2Mg", "apkPackageName": "com.package.name.of.requesting.app", "apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the certificate used to sign requesting app"], "ctsProfileMatch": true, "basicIntegrity": true, } ctsProfileMatch it gives false if the device is rooted. ref link: [1]: https://developer.android.com/training/safetynet/attestation
You can do this by following code : public boolean getRootInfo() { if (checkRootFiles() || checkTags()) { return true; } return false; } private boolean checkRootFiles() { boolean root = false; 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) { root = new File(path).exists(); if (root) break; } return root; } private boolean checkTags() { String tag = Build.TAGS; return tag != null && tag.trim().contains("test-keys"); } You can also check this library RootBeer.
As of 2021 (today), looks like there's no any reliable way or method for detecting root, especially when powerful hiding tool such as MagiskHide is enabled. Most of the answers here are no longer relevant, so don't use it in production. Rely on proven check like SafetyNet, and instead of going the extra miles to detect root, I suggest to protect your app at both runtime, such as prevent debugger/instrumentation and make sure to use obfuscation.
Indeed it is interesting question and so far nobody has deserved award. I use the following code: boolean isRooted() { try { ServerSocket ss = new ServerSocket(81); ss.close(); return true; } catch (Exception e) { // not sure } return false; } The code is certainly not bulletproof, because network can be not available so you get an exception. If this method returns true then 99% you can be sure, otherwise just 50% that not. Networking permission can also spoil the solution.
Using my library at rootbox, it is pretty easy. Check the required code below: //Pass true to <Shell>.start(...) call to run as superuser Shell shell = null; try { shell = Shell.start(true); } catch (IOException exception) { exception.printStackTrace(); } if (shell == null) // We failed to execute su binary return; if (shell.isRoot()) { // Verified running as uid 0 (root), can continue with commands ... } else throw Exception("Unable to gain root access. Make sure you pressed Allow/Grant in superuser prompt.");
Check su exists i.e. Device is rooted or not
This question is possibly a duplicate but I have gone through all the answers and noticed that they won't be working anymore. private static boolean checkRootMethod1() { String buildTags = android.os.Build.TAGS; return buildTags != null && buildTags.contains("test-keys"); } Method-1 failed 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" }; for (String path : paths) { if (new File(path).exists()) return true; } return false; } Method 2 also failed because "SuperSu" is not system app anymore it can be uninstall and su file is now present on separate folder SU/bin/su 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(); } } method 3 also failed because there is no "which" file on android devices nowadays RootTools.isavailable(); also failed My question is can I detect device rooted by checking whether the su file is inside the Su/bin/su folder? Is this the right way to detect rooted device?
add "/su/bin/su" to paths variable of checkRootMethod2() as to method 3, you should also call "which" by using "/system/bin/which"
Is there any way for me to check if root has been granted before requesting root, so it can be explained to the user?
I swear I've seen this done in other apps, but I can't think of any examples: I would like to check if my app has been granted root or not (not request). If not, I would like to explain what's about to happen BEFORE requesting root (triggering the super user dialogue box). Is there any way to do this? The only thing I can think of is to save a preference indicating that they've read the explanation before I check, but that's not ideal. Suggestions?
Try this piece of code: /** * Checks if the device is rooted. * * #return <code>true</code> if the device is rooted, <code>false</code> otherwise. */ public static boolean isRooted() { // get from build info String buildTags = android.os.Build.TAGS; if (buildTags != null && buildTags.contains("test-keys")) { return true; } // check if /system/app/Superuser.apk is present try { File file = new File("/system/app/Superuser.apk"); if (file.exists()) { return true; } } catch (Exception e1) { // ignore } // try executing commands return canExecuteCommand("/system/xbin/which su") || canExecuteCommand("/system/bin/which su") || canExecuteCommand("which su"); } // executes a command on the system private static boolean canExecuteCommand(String command) { boolean executedSuccesfully; try { Runtime.getRuntime().exec(command); executedSuccesfully = true; } catch (Exception e) { executedSuccesfully = false; } return executedSuccesfully; }
commumicating between windows app and android app
I'm sorry if this is a very general question but I don't know where to start so I'm looking for ideas. I have a windows app (music score editing) and I'm currently porting it to Andriod which is coming along well. I would like to add the feature than documents created in the windows app can be sent to the users android tablet. I was wondering, how would I write some kind of listener on Android that the windows side could open a socket or something to and send data across to it, assuming both are on the same local network. thanks
I think sending files directly over a local network isn't the best approach. You are prone to many user complaints that the sharing isn't working.. and this will mostly be due to their own network configuration issues. Why not use a service like DropBox to implement file sharing? Services like DropBox offer simple API that can be used in apps in order to save files into a remote folder, and read files from a remote folder. This way, users will not have to be in the same network at all.. and most of the heavy-lifting of implementing file sharing will be done by a service that is focused around that. Addition: If you don't want to require an account for a separate service like DropBox, consider this approach: Implement a very simple DropBox-like service on your own web server. Make a simple script that will allow users to upload a file to your server anonymously via HTTP. After upload, they will receive a 5 digit id for this file, or some other link they could share. When using this id or link from the 2nd app, the file could be downloaded (again via HTTP). If you delete files automatically from the server after a few hours, you will not run out of space. You can implement such a service with about 20 lines of PHP code. And the required apps code is extremely simple (since it only relies on HTTP). If you're worried about the costs of a web server, you can get one from about $5/month or even use a free service like Google App Engine (free if your bandwidth+space requirements are low). Code example for the file upload. Downloading should be simple enough to do alone. Regarding the periodical file delete - the obvious approach is cron but I think it's easy to manage without it. Whenever you accept a new upload (in the PHP script), go over all the downloads and delete old ones.
i wrote a small thing so my windows app can find an instance of my android app running on the local network, here it is. this is the android code first import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.Arrays; import android.os.AsyncTask; import android.util.Log; public class TabSyncServer extends AsyncTask<Void, Void, Void> { ServerSocket mServerSocket = null; Socket mSocket = null; DataInputStream mDataInputStream = null; DataOutputStream mDataOutputStream = null; #Override protected void onPreExecute() { try { mServerSocket = new ServerSocket(2112); //System.out.println("Listening :2112"); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } #Override protected Void doInBackground(Void... args) { byte[] bytebuf = new byte[1024]; while (true) { try { mSocket = mServerSocket.accept(); mDataInputStream = new DataInputStream(mSocket.getInputStream()); mDataOutputStream = new DataOutputStream(mSocket.getOutputStream()); Log.d("TabSyncServer", "ip: " + mSocket.getInetAddress()); mDataInputStream.read(bytebuf); String str = new String(bytebuf, "UTF8"); Log.d("TabSyncServer", "message: " + str); if(str.contains("Hello Android")) { Log.d("TabSyncServer", "sending reply"); mDataOutputStream.writeBytes("Hello Windows"); } // } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if (mSocket != null) { try { mSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (mDataInputStream != null) { try { mDataInputStream.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (mDataOutputStream != null) { try { mDataOutputStream.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } } and the windows MFC code void CMainFrame::OnBrowseMobile() { CMobileSync* con = new CMobileSync(); CString ipaddr_base; int my_last_digit; if(!con->getMyIP(ipaddr_base, my_last_digit)) { setMobilePath("Can't find local network"); return; } for(int i=1 ; i<98 ; i++) { if(i==my_last_digit) continue; // don;t check self CString ipaddr; ipaddr.Format("%s.%d", ipaddr_base, i); bool res = con->ConnectToHost(ipaddr); if(res) { res = con->SendMsg ("Hello Android"); if(res) { TRACE1("send ok %s\n",ipaddr.GetBuffer()); #define RD_BUF_LEN 80 char buffer[RD_BUF_LEN]; if(con->ListenOnPortBlocking(buffer, RD_BUF_LEN)) { if(strncmp(buffer, "Hello Windows", 12)==0) { TRACE1("reply ok %s", buffer); setMobilePath(ipaddr); con->CloseConnection (); return; } } } else { TRACE("send FAILED\n"); } } con->CloseConnection (); } setMobilePath("No TabTrax on local network"); } #include "stdafx.h" #include <winsock.h> #include "MobileSync.h" #define TTPORT 2112 bool CMobileSync::getMyIP(CString& ipaddr_front, int& ipaddr_lastdigit) { char szBuffer[1024]; #ifdef WIN32 WSADATA wsaData; WORD wVersionRequested = MAKEWORD(2, 0); if(::WSAStartup(wVersionRequested, &wsaData) != 0) return false; #endif if(gethostname(szBuffer, sizeof(szBuffer)) == SOCKET_ERROR) { #ifdef WIN32 WSACleanup(); #endif return false; } struct hostent *host = gethostbyname(szBuffer); if(host == NULL) { #ifdef WIN32 WSACleanup(); #endif return false; } //Obtain the computer's IP unsigned char b1, b2, b3, b4; b1 = ((struct in_addr *)(host->h_addr))->S_un.S_un_b.s_b1; b2 = ((struct in_addr *)(host->h_addr))->S_un.S_un_b.s_b2; b3 = ((struct in_addr *)(host->h_addr))->S_un.S_un_b.s_b3; b4 = ((struct in_addr *)(host->h_addr))->S_un.S_un_b.s_b4; ipaddr_front.Format("%d.%d.%d", b1, b2, b3); ipaddr_lastdigit = b4; #ifdef WIN32 WSACleanup(); #endif return true; } //CONNECTTOHOST – Connects to a remote host bool CMobileSync::ConnectToHost(const char* IPAddress) { //Start up Winsock… WSADATA wsadata; int error = WSAStartup(0x0202, &wsadata); //Did something happen? if (error) return false; //Did we get the right Winsock version? if (wsadata.wVersion != 0x0202) { WSACleanup(); //Clean up Winsock return false; } //Fill out the information needed to initialize a socket… SOCKADDR_IN target; //Socket address information target.sin_family = AF_INET; // address family Internet target.sin_port = htons (TTPORT); //Port to connect on target.sin_addr.s_addr = inet_addr (IPAddress); //Target IP mSocket = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); //Create socket if (mSocket == INVALID_SOCKET) { return false; //Couldn't create the socket } //Try connecting... if (connect(mSocket, (SOCKADDR *)&target, sizeof(target)) == SOCKET_ERROR) { return false; //Couldn't connect } return true; //Success } //CLOSECONNECTION – shuts down the socket and closes any connection on it void CMobileSync::CloseConnection () { //Close the socket if it exists if (mSocket) closesocket(mSocket); mSocket=0; WSACleanup(); //Clean up Winsock } int CMobileSync::SendMsg (char* szpText, int buflen) { if(buflen==0) buflen = strlen(szpText); int ret = send(mSocket, szpText, buflen, 0); TRACE1("CMobileSync::SendMsg sent %d bytes\n", ret); return ret; } WSADATA w; //LISTENONPORT – Listens on a specified port for incoming connections //or data bool CMobileSync::ListenOnPortBlocking(char* buffer, int buflen) { //Now we can start listening (allowing as many connections as possible to //be made at the same time using SOMAXCONN). You could specify any //integer value equal to or lesser than SOMAXCONN instead for custom //purposes). The function will not //return until a connection request is //made // listen(s, SOMAXCONN); memset(buffer, 0, sizeof(buffer)); //Clear the buffer int iTimeout = 1600; setsockopt( mSocket, SOL_SOCKET, SO_RCVTIMEO, (const char *)&iTimeout, sizeof(iTimeout)); //Put the incoming text into our buffer int ret = recv (mSocket, buffer, buflen-1, 0); //Don't forget to clean up with CloseConnection()! if(ret != SOCKET_ERROR) return true; int err = WSAGetLastError(); return false; } its not tested extensively but it is running this maybe useful to someone