Many users of Android devices will eventually root their device for one reason or another. The term root in this case means to gain root privileges on your device. Android, like most other phone operating systems, prefers you to not have root control over your device. Presumably this is for your own protection, but the question of why they don't want you having root is for a different discussion. I had the opportunity to study an application that was refusing to run on my device and only displaying a vague error message informing me that my device was not supported. What follows is a brief summary of what I discovered about root checks, and how I bypassed them for the purposes of interoperability.
After installing this application and receiving the error message on the screen, I needed to grab a copy of the program. While I don't know of a good way to do this directly (I don't trust third party apk download sites), I do have a copy of it on my phone. Thus, the most direct approach is to use the Android adb application to grab it. If you don't have adb installed, see this life hacker article.
In general, applications on your Android phone will be installed under /data/app. Unfortunately, you might have to go ahead and look at the names as the naming conventions used don't necessarily make it immediately obvious which directory to pull. This can be done by first getting interactive on your phone, then directory listing:
$ adb connect 192.168.1.117
connected to 192.168.1.117:5555
$ adb shell
root@phone:/ # cd /data/app
root@phone:/data/app # ls
com.amazon.kindle-1
com.android.chrome-1
com.google.android.calendar-1
...
Once you find the application, you can pull it with adb as well. For example:
$ adb pull /data/app/com.google.android.music-1
This will download the entire data directory. It will contain the apk and any extra libraries or files that are needed to run it. Next, we want to reverse this apk into something more readable. A discussion of the structure of apk files is better suited for a different article. The bottom line here is that the apk file can, for the most part, be reversed back into something human readable. For the purposes of this article, I will note that jd-gui worked mostly, however it failed to reverse some of application. Where jd-gui failed, a nice tool from Penn State University called dare succeeded.
With the application reversed into source code, I discovered the following functions (note, I have added the function names myself):
isRooted
public static boolean isRooted()
{
boolean z3;
label_1:
{
if (isRootedKeysCheck() == false)
{
if (hssSU() == false)
{
if (hasSU2() == false)
{
z3 = false;
break label_1;
}
}
}
z3 = true;
} //end label_1:
return z3;
}
Function 1 was simply a combination of 3 functions. It acted as the single call that the application had to make to determine if it was operating on a rooted phone.
isRootedKeysCheck
private static boolean isRootedKeysCheck()
{
String r0;
boolean z1;
r0 = Build.TAGS;
label_2:
{
if (r0 != null)
{
if (r0.contains("test-keys") != false)
{
z1 = true;
break label_2;
}
}
z1 = false;
} //end label_2:
return z1;
}
This function checks the build tag for your install. It turns out that many custom ROM builds will use the Android test-keys for their builds, while most if not all the OEM installs of Android will use release-keys. The build-tag itself is simply a free-form text field that is by default populated with some information about your install. In this case, it turns out many Android ROMs (and perhaps the generic build script itself) will place a string representing which keys were used in this build. The key thing to note here is that there is nothing restricting you from updating this text field so that it does not contain "test-keys".
isRootedKeysCheck Bypass
First off, check if your tag already has this or not. It is possible you don't need to work to bypass this check. Android will likely tell you what types of keys you've used under the "about" menu. The full answer requires the following steps:
- Open an adb shell onto the phone
- Re-mount the /system drive to be read-write
- mount -o rw,remount /system
- Edit /system/build.prop
- Change the "ro.build.tags" entry to anything aside from "test-keys" ("release-keys" would work just fine)
- Re-mount the /system drive read only (this is important on my device, otherwise the config didn't actually update)
- mount -o ro,remount /system
- Reboot
Here's an example:
$ adb shell
root@phone:/ # mount | grep system
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,errors=panic,data=ordered 0 0
root@phone:/ # mount -o rw,remount /system
root@phone:/ # mount | grep system
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 rw,seclabel,relatime,errors=panic,data=ordered 0 0
root@phone:/ # vi /system/build.prop
<< Set ro.build.tags=release-keys >>
root@phone:/ # mount -o ro,remount /system
root@phone:/ # reboot
That's it. Once your phone reboots you should successfully pass this check.
hasSU
private static boolean hasSU()
{
boolean z0;
String[] r0;
int i8, i9;
z0 = true;
r0 = new String[9];
r0[(int) 0] = "/system/app/Superuser.apk";
r0[(int) 1] = "/sbin/su";
r0[2] = "/system/bin/su";
r0[3] = "/system/xbin/su";
r0[4] = "/data/local/xbin/su";
r0[5] = "/data/local/bin/su";
r0[6] = "/system/sd/xbin/su";
r0[7] = "/system/bin/failsafe/su";
r0[8] = "/data/local/su";
i8 = r0.length;
i9 = (int) 0;
label_3:
{
while (i9 < i8)
{
if ((new File(r0[i9])).exists() == false)
{
i9 = i9 + 1;
}
else
{
break label_3;
}
}
z0 = false;
} //end label_3:
return z0;
}
This check has an array of static locations to check for super user binaries or packages. When you root your phone, you need some way to switch into root context. This is usually done through the su command (switch user). Some root packages also provide an Android user land application to ease configuration and utilization of root. The irony here being, if you have root, you can very easily change these.
hasSU Bypass
Utilize the same list that the application uses, and ensure that you do not have these binaries. The way I went about this was to simply use adb shell to check for these files. If I found a file (and I found a few), I renamed them to something else. Renaming them could be problematic if you have other applications that rely on having root. In my case, this was not an issue, but just be aware that if you do change the locations or names it might affect those applications as well.
The list of files being checked for is:
- /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
hasSU2
private static boolean hasSU2()
{
boolean z0;
Object n0;
Runtime r0;
String[] r2;
Process $r4, r13;
String $r9;
z0 = true;
n0 = null;
label_6:
{
label_5:
{
label_4:
{
try
{
r0 = Runtime.getRuntime();
r2 = new String[2];
r2[0] = "/system/xbin/which";
r2[1] = "su";
$r4 = r0.exec(r2);
}
catch (Throwable $r11)
{
r13 = n0;
break label_4;
}
try
{
$r9 = (new BufferedReader(new InputStreamReader($r4.getInputStream()))).readLine();
break label_5;
}
catch (Throwable $r16)
{
r13 = $r4;
}
} //end label_4:
if (r13 != null)
{
r13.destroy();
}
z0 = false;
break label_6;
} //end label_5:
if ($r9 == null)
{
if ($r4 != null)
{
$r4.destroy();
}
z0 = false;
}
else
{
if ($r4 != null)
{
$r4.destroy();
}
}
} //end label_6:
return z0;
}
This method confused jd-gui for some reason. However, dare/soot was able to decompile it just fine, albeit a little gross looking. A careful read through this indicates that the application runs the command "/system/xbin/which su". In linux, the "which" command is used to locate which version of a particular executable will be utilized when you run a command. Due to Linux's (and really most modern operating systems) use of a $PATH variable, there may be a few commands with the same name. Thus, when you type "su" into the command prompt, without any other information or path, Linux will attempt to discover a binary named "su" that it can execute using the $PATH variable. Bottom line here is that this check is basically a more generalized su existence check than the other one. It will catch the situation where a su binary exists in the user's $PATH but not in one of the locations statically mentioned in the previous function.
hasSU2 bypass
This check can also fairly easily be bypassed the same way hasSU was bypassed. This time, you connect via adb shell, re-mount the file system to read/write, then run the same command "/system/xbin/which su". If which doesn't exist, you're already done. If any results come back, simply move or rename that file until running the which command returns no results. Done.
Conclusion
While this research was done against an Android phone, it is likely that the same checks or very similar checks would be a default means to discover rooted Android devices such as TVs and Tablets. Further, discovering that an Android device has been rooted is a cat and mouse game where the power often times is in the hands of the device owner. I was surprised to discover the detection techniques implemented were quite minimal and novice. I fully expect to see more advanced root detection techniques emerge at some point and cause the game to move back to device owners.