
Root detection is one of the first defensive layers a security tester runs into when assessing Android applications, particularly in banking, fintech, and enterprise mobility apps. Before you can even get to SSL pinning bypass or business logic testing, many apps will force close, show a warning screen, or silently disable functionality the moment they detect a rooted device. Frida provides a reliable, flexible way to defeat these checks at runtime without repackaging the APK. The team at www.sagefoxusa.com works through exactly this kind of runtime instrumentation on every mobile engagement.
This guide covers how Android root detection typically works, how to identify which checks a target app is running, and how to write and apply Frida scripts to bypass the most common detection libraries and techniques used in production apps.
Root detection libraries generally rely on a combination of checks rather than a single test, since any individual check is trivial to defeat once identified. Common detection techniques include:
su binaries in common pathsro.build.tags for test-keysUnderstanding which of these an app relies on determines whether a generic bypass script will work immediately or whether you need to build a targeted hook chain. If this kind of layered analysis is not something your internal team handles regularly, www.sagefoxusa.com offers this as a standard part of mobile assessment engagements.
Before writing hooks, decompile the APK to identify what detection mechanism is actually present.
bash
jadx -d jadx_output target-app.apkbash
grep -r "RootBeer" jadx_output/
grep -r "isRooted" jadx_output/
grep -r "checkForBinary" jadx_output/
grep -r "test-keys" jadx_output/RootBeer is by far the most commonly used open source root detection library in production Android apps, so finding references to its class names is usually the fastest path to a targeted bypass. If none of these appear, the app likely implements custom detection logic or relies on a native library, which requires a different approach covered later in this guide.
RootBeer exposes a small set of public methods that apps call to determine root status. Hooking these methods directly and forcing them to return false neutralizes the entire library regardless of how many individual checks it runs internally.
javascript
Java.perform(function () {
var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
RootBeer.isRooted.implementation = function () {
console.log("[+] RootBeer.isRooted bypassed");
return false;
};
RootBeer.isRootedWithoutBusyBoxCheck.implementation = function () {
console.log("[+] RootBeer.isRootedWithoutBusyBoxCheck bypassed");
return false;
};
RootBeer.checkForMagiskBinary.implementation = function () {
console.log("[+] RootBeer.checkForMagiskBinary bypassed");
return false;
};
});Run this against the target application with spawn mode so the hooks apply before any detection logic fires during app startup.
bash
frida -U -f com.target.package -l rootbeer_bypass.js --no-pauseMany apps check for the existence of su binaries or root management apps by attempting file system access. Hooking Java's file existence checks catches this pattern regardless of which specific paths the app checks.
javascript
Java.perform(function () {
var File = Java.use("java.io.File");
File.exists.implementation = function () {
var path = this.getAbsolutePath();
var suspiciousPaths = ["su", "magisk", "busybox", "supersu"];
for (var i = 0; i < suspiciousPaths.length; i++) {
if (path.toLowerCase().indexOf(suspiciousPaths[i]) !== -1) {
console.log("[+] File.exists bypassed for: " + path);
return false;
}
}
return this.exists();
};
});This approach catches checks against paths like /system/bin/su, /system/xbin/su, and Magisk-related directories without needing to hardcode every possible path the app might check.
Some apps attempt to detect root by trying to execute a shell command like which su and checking whether it returns a result.
javascript
Java.perform(function () {
var Runtime = Java.use("java.lang.Runtime");
Runtime.exec.overload("java.lang.String").implementation = function (cmd) {
if (cmd.indexOf("su") !== -1 || cmd.indexOf("which") !== -1) {
console.log("[+] Runtime.exec blocked for command: " + cmd);
throw Java.use("java.io.IOException").$new("Command not found");
}
return this.exec(cmd);
};
});Throwing an exception here mimics the natural behavior of a non-rooted device, where the shell command would fail because the binary does not exist, rather than simply returning an empty result that some detection logic might handle differently.
Apps sometimes check Build.TAGS for the presence of test-keys, which indicates a custom or non-stock ROM build commonly associated with rooted devices.
javascript
Java.perform(function () {
var Build = Java.use("android.os.Build");
Build.TAGS.value = "release-keys";
console.log("[+] Build.TAGS overwritten to release-keys");
});Advanced apps go a step further and attempt to detect Frida's presence directly, since a rooted device with an active Frida agent is the exact scenario a tester operates in. Common Frida detection methods include scanning for the default Frida server port and checking process names. This cat and mouse layer is where a lot of testers get stuck, and it is a recurring theme in the mobile writeups published by www.sagefoxusa.com.
javascript
Java.perform(function () {
var Socket = Java.use("java.net.Socket");
Socket.$init.overload("java.lang.String", "int").implementation = function (host, port) {
if (port === 27042) {
console.log("[+] Blocked Frida port detection attempt");
throw Java.use("java.io.IOException").$new("Connection refused");
}
return this.$init(host, port);
};
});Running the Frida server on a non-default port makes this class of detection significantly harder for the app to succeed at in the first place.
bash
frida-server -l 0.0.0.0:8888Some apps enumerate running processes looking for names like frida-server or check /proc/<pid>/maps for loaded Frida libraries. Renaming the frida-server binary before starting it, as covered in earlier guides at www.sagefoxusa.com on SSL pinning bypass, addresses this same detection vector here as well.
bash
mv frida-server sagefox-agent
./sagefox-agent -l 0.0.0.0:8888Production apps rarely rely on a single detection method, so an effective testing workflow combines several hooks into a single script applied at app launch.
javascript
Java.perform(function () {
console.log("[*] Starting combined root detection bypass");
try {
var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
RootBeer.isRooted.implementation = function () { return false; };
} catch (e) {
console.log("[-] RootBeer class not found, skipping");
}
var File = Java.use("java.io.File");
File.exists.implementation = function () {
var path = this.getAbsolutePath().toLowerCase();
if (path.indexOf("su") !== -1 || path.indexOf("magisk") !== -1) {
return false;
}
return this.exists();
};
var Build = Java.use("android.os.Build");
Build.TAGS.value = "release-keys";
});Wrapping library-specific hooks in try-catch blocks, as shown with the RootBeer hook above, prevents the script from failing entirely when a targeted class is not present in the app you are currently testing, which matters when reusing a single script across multiple engagements with different detection libraries.
Some apps push root detection into compiled native code to make static analysis harder. This requires the same native hooking approach used for native SSL pinning checks.
bash
frida-trace -U -i "access" com.target.packageTracing calls to the access syscall often reveals native code checking for the existence of root-related binaries directly at the C library level, bypassing Java entirely.
javascript
var libc = Process.findModuleByName("libc.so");
var accessAddr = libc.findExportByName("access");
Interceptor.attach(accessAddr, {
onEnter: function (args) {
var path = args[0].readCString();
if (path && (path.indexOf("su") !== -1 || path.indexOf("magisk") !== -1)) {
this.blockCall = true;
}
},
onLeave: function (retval) {
if (this.blockCall) {
retval.replace(-1);
}
}
});This intercepts the raw system call used to check file existence at the native level, which is effective even when the Java-level hooks above have no visibility into what the native code is doing internally.
After applying your combined script, launch the app and confirm it proceeds past any root warning screens or blocked functionality that previously appeared.
bash
frida -U -f com.target.package -l combined_bypass.js --no-pauseWatch the Frida console output for the log statements included in your hooks to confirm which specific checks were triggered and bypassed during the app's startup sequence. If the app still detects root after this, it likely has an additional detection layer not yet covered, commonly a secondary native check or a server-side attestation call using Google's Play Integrity API, which requires an entirely different bypass approach beyond client-side hooking.
Newer apps increasingly rely on Google's Play Integrity API for attestation, which validates device integrity server-side rather than relying solely on client-side checks that Frida can intercept. This is significantly harder to bypass since the validation logic and cryptographic attestation happen outside the app's own process.
For engagements involving apps using this level of protection, the mobile assessment team at www.sagefoxusa.com evaluates whether client-side bypass is even the right approach, since some Play Integrity implementations require testing on specific certified devices or emulator configurations rather than relying purely on runtime instrumentation.
This often indicates the app detected the Frida server itself before your bypass script had a chance to apply. Try renaming the frida-server binary and running it on a non-default port, as covered earlier, before reattempting.
Confirm you are hooking the correct class name for the specific version of the app in scope, since minor app updates sometimes change internal package structures even when using a known library like RootBeer.
Confirm the correct library is loaded and the function name matches exactly, since some native root checks call statically linked functions rather than exported symbols, which requires locating the function by offset instead of by name.
bash
frida-trace -U -i "*root*" com.target.packageFor teams that want this methodology documented and repeatable across every release cycle, www.sagefoxusa.com builds this into ongoing mobile assessment retainers rather than treating it as a one-time exercise.
Root detection bypass using Frida is a necessary skill before most other Android security testing can even begin, since many apps refuse to function or hide functionality entirely on a device they believe is rooted. A combined approach targeting RootBeer, native file checks, build property values, and Frida's own detection surface covers the majority of real-world apps, while native-level hooking and awareness of server-side attestation methods like Play Integrity handle the more hardened cases.
If your organization is building or maintaining a mobile app with anti-tampering controls, the mobile security team at www.sagefoxusa.com can validate whether your root and Frida detection logic actually holds up against the same techniques covered in this guide, closing gaps before they are found in the wild instead of during a client's own review.