Android App Penetration Testing & Frida Hooks

Frida Masterclass: How to Bypass Android Root Detection in 10 Easy Steps

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Cat and Mouse Game of Root Detection

Android’s open nature provides unparalleled flexibility, yet it also creates security challenges. Root detection mechanisms are implemented by developers to prevent applications from running on rooted devices, primarily for security-sensitive apps like banking, DRM-protected content, or games to deter cheating. These checks typically look for indicators of a modified operating system, such as the presence of the su binary, Magisk modules, or altered system properties. However, for penetration testers, security researchers, or even curious developers, bypassing these checks is a critical skill for deeper analysis and understanding application behavior.

This masterclass will guide you through using Frida, a dynamic instrumentation toolkit, to systematically identify and bypass common Android root detection mechanisms. Frida allows you to inject custom scripts into running processes, modify functions, and observe behavior in real-time, making it an invaluable tool in your mobile security arsenal.

Prerequisites and Environment Setup

Before diving into the hooks, ensure you have the necessary tools installed:

  • ADB (Android Debug Bridge): For interacting with your Android device.
  • Python 3 and pip: For installing Frida-tools.
  • Frida-tools: Install using pip install frida-tools.
  • Android Device/Emulator: A rooted device is ideal for understanding detection, but a non-rooted device is necessary to verify the bypass. An emulator (e.g., AVD, Genymotion) can also work.
  • Frida Server: Download the appropriate Frida server binary for your device’s architecture (e.g., frida-server-*-android-arm64) from the Frida releases page.

Step 1: Setting Up Frida Server on Your Device

Push the Frida server to your device and make it executable:

adb push /path/to/frida-server /data/local/tmp/frida-server
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"

Verify it’s running by listing processes:

frida-ps -U

Identifying and Bypassing Root Checks

Step 2: Target Application Analysis

Identify the package name of the application you want to bypass. For this example, let’s assume our target app is com.example.secureapp.

adb shell pm list packages | grep secureapp

Step 3: Understanding Common Root Detection Vectors

Apps typically check for root using a combination of methods:

  • File Existence: Checking for /system/bin/su, /system/xbin/su, /sbin/su, or Magisk-related files.
  • Package Names: Looking for root management apps like Magisk Manager or SuperSU.
  • System Properties: Checking ro.build.tags for “test-keys” or other suspicious properties.
  • Command Execution: Running which su or similar commands.
  • Native Libraries: Custom C/C++ code for more robust checks.

Step 4: Hooking java.io.File.exists()

Many apps check for root by verifying the existence of common root binaries. We can hook java.io.File.exists() to always return false for these specific paths.

// root_bypass.js
Java.perform(function() {
    var File = Java.use('java.io.File');
    File.exists.implementation = function() {
        var path = this.getAbsolutePath();
        console.log('Checking path: ' + path);
        if (path.includes('/su') || 
            path.includes('magisk') || 
            path.includes('busybox')) {
            console.log('Bypassing exists() for root path: ' + path);
            return false;
        }
        return this.exists();
    };
});

Run with: frida -U -l root_bypass.js com.example.secureapp

Step 5: Bypassing Runtime.exec() for Command Execution Checks

Some apps execute commands like which su to detect root. We can hook Runtime.exec() to prevent these commands from returning expected root indicators.

// ... inside Java.perform(function() { ...
    var Runtime = Java.use('java.lang.Runtime');
    Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmd) {
        var command = Java.cast(cmd, Java.array(Java.use('java.lang.String'), cmd.length));
        console.log('Executing command: ' + command.join(' '));
        if (command.includes('which su') || 
            command.includes('su') || 
            command.includes('id')) {
            console.log('Bypassing exec() for root command: ' + command.join(' '));
            return null; // Prevent command from executing or return harmless output
        }
        return this.exec(cmd);
    };
});

Step 6: Hooking PackageManager.getPackageInfo() for Root App Detection

Apps might check for the presence of root management apps like Magisk Manager. Hooking getPackageInfo() can hide these packages.

// ... inside Java.perform(function() { ...
    var PackageManager = Java.use('android.app.ApplicationPackageManager');
    PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {
        console.log('getPackageInfo for: ' + packageName);
        if (packageName.includes('com.topjohnwu.magisk') || 
            packageName.includes('eu.chainfire.supersu')) {
            console.log('Bypassing getPackageInfo for root package: ' + packageName);
            throw PackageManager.NameNotFoundException.$new(); // Simulate package not found
        }
        return this.getPackageInfo(packageName, flags);
    };
});

Step 7: Bypassing System Property Checks

Rooted devices often have specific system properties. We can hook System.getProperty() or related methods to spoof these values.

// ... inside Java.perform(function() { ...
    var System = Java.use('java.lang.System');
    System.getProperty.overload('java.lang.String').implementation = function(key) {
        // console.log('System.getProperty: ' + key);
        if (key === 'ro.build.tags') {
            console.log('Bypassing ro.build.tags');
            return 'release-keys'; // Spoof to non-test keys
        }
        if (key === 'ro.secure') {
            console.log('Bypassing ro.secure');
            return '1'; // Spoof to secure
        }
        return this.getProperty(key);
    };
});

Step 8: Hooking Custom Root Check Methods (Dynamic Discovery)

For more sophisticated apps, root checks might be encapsulated in custom methods. Use frida-trace to identify these methods first, then apply a hook.

frida-trace -U -f com.example.secureapp -i "*RootCheck*" -i "*detectRoot*"

Once a method like com.example.secureapp.RootChecker.isDeviceRooted() is identified:

// ... inside Java.perform(function() { ...
    var RootChecker = Java.use('com.example.secureapp.RootChecker');
    RootChecker.isDeviceRooted.implementation = function() {
        console.log('Hooked isDeviceRooted(), returning false!');
        return false;
    };
});

Step 9: Advanced Bypasses – Native Library Checks

Some robust root detections are implemented in native (C/C++) libraries. Frida’s Interceptor.attach can hook native functions. For example, if a native library calls access("/system/bin/su", F_OK):

// ... inside Java.perform(function() { ...
    var module = Process.findModuleByName('libc.so'); // Or the app's native library
    if (module) {
        var accessPtr = module.findExportByName('access');
        if (accessPtr) {
            Interceptor.attach(accessPtr, {
                onEnter: function(args) {
                    this.path = args[0].readUtf8String();
                    // console.log('Native access() called with: ' + this.path);
                    if (this.path.includes('/su') || this.path.includes('magisk')) {
                        console.log('Bypassing native access() for root path: ' + this.path);
                        this.doBypass = true;
                    }
                },
                onLeave: function(retval) {
                    if (this.doBypass) {
                        retval.replace(0); // Return 0 (success) or -1 (failure) depending on context
                    }
                }
            });
        }
    }
});

Step 10: Combining and Automating Your Bypass Script

Consolidate all your successful hooks into a single Frida script. This script can then be used to consistently bypass the app’s root detection. Keep iterating: if one hook fails, examine the new detection mechanism the app might be using and add a corresponding hook.

// full_root_bypass.js
Java.perform(function() {
    // Step 4 hook
    var File = Java.use('java.io.File');
    File.exists.implementation = function() {
        var path = this.getAbsolutePath();
        if (path.includes('/su') || path.includes('magisk') || path.includes('busybox')) {
            return false;
        }
        return this.exists();
    };

    // Step 5 hook
    var Runtime = Java.use('java.lang.Runtime');
    Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmd) {
        var command = Java.cast(cmd, Java.array(Java.use('java.lang.String'), cmd.length));
        if (command.includes('which su') || command.includes('su')) {
            return null;
        }
        return this.exec(cmd);
    };

    // Step 6 hook
    var PackageManager = Java.use('android.app.ApplicationPackageManager');
    PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {
        if (packageName.includes('com.topjohnwu.magisk') || packageName.includes('eu.chainfire.supersu')) {
            throw PackageManager.NameNotFoundException.$new();
        }
        return this.getPackageInfo(packageName, flags);
    };

    // Step 7 hook
    var System = Java.use('java.lang.System');
    System.getProperty.overload('java.lang.String').implementation = function(key) {
        if (key === 'ro.build.tags') {
            return 'release-keys';
        }
        if (key === 'ro.secure') {
            return '1';
        }
        return this.getProperty(key);
    };

    // Example Step 8 hook (if identified)
    // var RootChecker = Java.use('com.example.secureapp.RootChecker');
    // RootChecker.isDeviceRooted.implementation = function() {
    //     return false;
    // };

    // Example Step 9 hook (native)
    var module = Process.findModuleByName('libc.so');
    if (module) {
        var accessPtr = module.findExportByName('access');
        if (accessPtr) {
            Interceptor.attach(accessPtr, {
                onEnter: function(args) {
                    this.path = args[0].readUtf8String();
                    if (this.path && (this.path.includes('/su') || this.path.includes('magisk'))) {
                        this.doBypass = true;
                    }
                },
                onLeave: function(retval) {
                    if (this.doBypass) {
                        retval.replace(0);
                    }
                }
            });
        }
    }
});

To run the comprehensive script:

frida -U -l full_root_bypass.js -f com.example.secureapp --no-pause

Conclusion

Bypassing Android root detection is a dynamic and iterative process. While these 10 steps cover many common techniques, sophisticated applications may employ obfuscation, anti-Frida measures, or unique native checks. The key is to adopt a systematic approach: observe, identify, hook, and refine. Frida’s versatility makes it an indispensable tool for navigating these security challenges, empowering you to gain deeper insights into application behavior on Android devices. Remember to always use these techniques ethically and legally for authorized security testing.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner