Root Detection is a security mechanism that protects Android applications from running on devices with superuser privileges. Banking, payment, and enterprise applications block or restrict functionality on rooted devices, because root access removes Android sandbox restrictions and opens up the possibility of traffic interception, process memory reading, and data tampering. According to OWASP Mobile Top 10 (2024), the absence of Root Detection falls under category M8 (Security Decisions via Untrusted Inputs). Root Detection is built on a combination of static file system checks and dynamic runtime behavior analysis.
Key Takeaways
Root Detection is a software mechanism that detects the presence of root access on an Android device. Root access provides full control over the operating system, allowing applications and scripts to execute commands with UID 0. On a rooted device, application isolation (Android Sandbox) is lost, making it possible to intercept keyboard input, read SQLite databases of other applications, inject code into processes, and replace SSL certificates in the trusted store.
For financial and enterprise applications, running on a rooted device presents an unacceptable risk: an attacker gains access to tokens, session keys, and personal data. Regulators, including the PCI Security Standards Council, require payment applications to detect and respond to root access. In response, Android developers embed Root Detection as part of a proactive protection strategy.
There are two approaches to detection: static, which analyzes the file system and installed packages, and dynamic, which performs runtime checks. A combined approach is considered the most reliable, as it covers different bypass vectors. According to a study by NowSecure (2025), 76% of banking applications in the Google Play top 100 contain some form of Root Detection.
Static methods execute at application startup and check for signs of root access left by rooting tools in the file system. These methods do not require executing privileged commands and work within the context of a normal application.
The main indicator of root access is the presence of the su executable file in standard paths: /system/bin/su, /system/xbin/su, /sbin/su, /su/bin/su. The application checks for the file's existence via File.exists() or a native implementation of access() from libc. Additionally, you can try to execute su --version or su -c id and check the exit code.
Typical applications for managing root access: Superuser, SuperSU, Magisk Manager, KingRoot. Their presence is checked via PackageManager.getPackageInfo() or by reading the /data/app/ directory. Packages to check: com.topjohnwu.magisk, eu.chainfire.supersu, com.noshufou.android.su, com.thirdparty.superuser, com.koushikdutta.superuser, com.zacharee1.systemuituner.
Android stores system state information in system properties, accessible via System.getProperty and Build.TAGS. If Build.TAGS contains test-keys instead of release-keys, this indicates a custom firmware, often with root access. Additionally, ro.build.tags, ro.debuggable, and ro.secure are checked by reading /system/build.prop.
public class RootDetectionChecker {
private static final String[] SU_PATHS = {
"/system/bin/su",
"/system/xbin/su",
"/sbin/su",
"/su/bin/su",
"/system/sd/xbin/su"
};
public boolean checkRootByFiles() {
for (String path : SU_PATHS) {
if (new File(path).exists()) {
return true;
}
}
return false;
}
public boolean checkRootByPackages(Context ctx) {
String[] packages = {
"com.topjohnwu.magisk",
"eu.chainfire.supersu",
"com.noshufou.android.su",
"com.koushikdutta.superuser"
};
for (String pkg : packages) {
try {
ctx.getPackageManager().getPackageInfo(pkg, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
// package not found
}
}
return false;
}
}
Dynamic methods execute during application runtime and analyze the execution environment. Unlike static methods, they can detect rooting hidden through Magisk Hide or Zygisk, since they check system behavior rather than just file system structure.
With root access, some system partitions are mounted with the rw (read-write) flag instead of ro (read-only). The application reads /proc/mounts and checks that /system is mounted as ro. If /system is mounted as rw, this indicates a modified system. Additionally, the presence of /su mounting via Magisk is checked.
Android Safe Mode disables third-party applications, including root managers. A properly implemented Root Detection can check whether the device is running in safe mode. If the application detects that root managers are not visible but the su binary exists, this is an indicator of Magisk Hide.
Attempting to execute su -c id via ProcessBuilder or Runtime.exec is a direct test of root access. However, Magisk can intercept this call. A more reliable approach is checking via native code: opening /proc/1/limits or /proc/self/maps and analyzing the UID of running processes. If the application can obtain UID 0 or read files accessible only to root, the device is compromised.
public boolean checkRootDynamically() {
// Build flags check
String buildTags = Build.TAGS;
if (buildTags != null && buildTags.contains("test-keys")) {
return true;
}
// Checking /system mount
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("/proc/mounts"))
);
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("/system")
&& line.contains("rw")) {
reader.close();
return true;
}
}
reader.close();
} catch (IOException e) {
// error reading mounts
}
return false;
}
Root Detection implemented in Java can be easily bypassed through Xposed modules or Frida, which intercept Java methods and replace return values. A native implementation in C++ via JNI is significantly more resilient: dynamic analysis tools operating at the Java level cannot see native libc calls such as stat, access, popen, and dlopen.
#include <unistd.h>
#include <sys/stat.h>
#include <cstring>
#include <vector>
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_checker_RootCheck_nativeCheck(
JNIEnv* env, jobject instance) {
std::vector<const char*> paths = {
"/system/bin/su",
"/system/xbin/su",
"/sbin/su",
"/data/local/su"
};
struct stat st;
for (const char* path : paths) {
if (stat(path, &st) == 0) {
return JNI_TRUE;
}
}
return JNI_FALSE;
}
Native checking does not use the Java API, making it invisible to bypass tools operating at the Dalvik/ART level. For additional protection, it is recommended to store constants (path list) not in a read-only section but to compute them through simple reversible functions. The stat call from libc directly accesses the Linux kernel, bypassing Java wrappers, and cannot be intercepted through Xposed.
Protection developers need to understand existing bypass methods in order to build a robust detection system. Each bypass method requires a countermeasure at the appropriate level.
Magisk is the most popular rooting tool on Android 9–14. Magisk Hide hides the presence of su from /proc and spoofs path check results. Magisk operates at the kernel level and intercepts stat() and access() before the application sees them. Countermeasure: checking for Magisk itself via the presence of /sbin/.magisk or checking through reading the application's own maps — Magisk injects its library into every process.
Frida is a dynamic instrumentation tool that can intercept native functions via Ptrace or Dobby. Frida replaces the return value of any check, spoofing the stat result to ENOENT. Countermeasure: verifying the integrity of native functions by computing a checksum of instructions in memory and detecting Frida through analysis of /proc/self/maps for the presence of frida-agent.so or frida-helper.
Root Detection implemented in Java can be removed in 2–3 minutes: the APK is decompiled using apktool, the return value of the method is changed to false in smali code, the APK is rebuilt and resigned. Countermeasure: moving critical logic to native code and verifying the application's digital signature at runtime via the Signature API or comparing the APK hash against a reference on the server.
Effective Root Detection is built on a multi-layered architecture. No single method provides sufficient protection on its own. A combination of static and dynamic checks, native code, and server-side verification provides maximum resilience.
Do not rely solely on client-side checking. Send Root Detection results to the server along with a one-time session token. The server decides whether to block or restrict functionality. This prevents attacks at the API level, where the client application may be modified while the server remains a trusted party.
Root Detection code must be obfuscated. If an attacker sees a clear sequence of su path checks in jadx, bypassing it will take minutes. Use ProGuard or DexGuard to obfuscate control flow and encrypt strings. Obfuscation increases the time required to analyze protection code from minutes to hours.
The list of checked paths, packages, and indicators should be updated with every application release. New rooting and bypass tools appear monthly. A static list that has not changed in a year will not detect modern methods. It is recommended to load current signatures from the server at application startup before performing checks.
Frequently Asked Questions
Root Detection protects against running an application on a device where the Android sandbox is disabled. On a rooted device, any application can read data from other applications. Banking and payment applications are required to block operation on rooted devices per PCI DSS requirements and OWASP Mobile Security recommendations.
Magisk Hide uses a mount namespace mechanism. For each process in the exclusion list, Magisk creates an isolated namespace where the su binary is invisible. System calls stat, access, and open in this namespace do not see Magisk files. Magisk can be detected by checking for the presence of /proc/self/maps and searching for magisk dumps.
Yes, if the application does not verify its code integrity. Through Frida, you can intercept the Java check method and force it to return false. The countermeasure is a native implementation of critical logic in C++ and integrity verification via DEX file hash. Without obfuscation, any Java-based Root Detection can be bypassed in 5–10 minutes.
SafetyNet (deprecated) and Play Integrity API are server-side checks from Google that verify device integrity. They include checking the bootloader, system signature, and root status. Play Integrity API is the recommended replacement for SafetyNet, providing three levels: BASIC, DEVICE, and STRONG. Client-side Root Detection complements server-side attestation.
Install the application on a real rooted device (e.g., a Pixel with Magisk). Check whether the block triggers. Then try to hide root via Magisk Hide for your application and restart the test. For in-depth testing, use Frida to intercept target methods and ensure that native protection cannot be bypassed.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also