Android App Penetration Testing & Frida Hooks

Automating Root Bypass: Building a Dynamic Frida Script for Android App Pen Testing

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Android Root Detection

Android applications often implement root detection mechanisms to enhance security, protect intellectual property, and comply with licensing agreements. Rooted devices, while offering users greater control, pose a significant risk to app developers as they can bypass security controls, inject malicious code, and access sensitive data. For penetration testers, however, bypassing these checks is a critical step in assessing an application’s true security posture, identifying vulnerabilities that might otherwise remain hidden.

Traditional methods of bypassing root detection often involve manual patching of APKs, using Xposed modules, or complex reverse engineering. While effective, these methods can be time-consuming and difficult to adapt to different applications or updated detection logic. This article will guide you through building a dynamic and robust Frida script to automate the bypass of common Android root detection techniques, streamlining your mobile application penetration testing workflow.

Why Frida for Root Bypass?

Frida is a dynamic instrumentation toolkit that allows you to inject snippets of JavaScript or your own library into native apps on Windows, macOS, Linux, iOS, Android, and QNX. Its ability to hook into functions, inspect memory, and modify execution flow at runtime makes it an unparalleled tool for security research, reverse engineering, and bypassing security mechanisms like root detection without modifying the application binary itself.

Key advantages of Frida for root bypass:

  • Dynamic Instrumentation: Modify app behavior during runtime without recompilation.
  • Cross-Platform: Works seamlessly across various Android versions and architectures.
  • Powerful API: Provides comprehensive APIs for hooking Java and native functions, memory manipulation, and interacting with the application context.
  • Rapid Prototyping: Write and test bypass logic quickly using JavaScript.

Common Root Detection Techniques

Before we build our bypass script, it’s essential to understand the common methods applications use to detect root. A robust Frida script should ideally address a combination of these:

  1. File and Directory Checks

    Applications often look for the presence of common root-related binaries or directories:

    • /system/bin/su
    • /system/xbin/su
    • /sbin/su
    • /data/local/su
    • /system/app/Superuser.apk
    • /data/data/com.noshufou.android.su
    • /system/xbin/busybox
    • /dev/bus/usb/001/001 (for USB debugging/ADB)
  2. Package Checks

    Checking for the installation of known root management apps:

    • com.noshufou.android.su (Superuser)
    • eu.chainfire.supersu (SuperSU)
    • com.topjohnwu.magisk (Magisk Manager)
  3. Property Checks

    Inspecting system properties for indicators of a rooted or emulator environment:

    • ro.boot.flash.locked (often 0 on rooted devices)
    • ro.secure (often 0 on rooted devices)
    • ro.debuggable (often 1 on development/rooted devices)
    • ro.build.tags (test-keys on custom ROMs/rooted devices)
  4. Command Execution Checks

    Executing commands like su -c id or which su and analyzing the output for indicators of root access.

  5. Signature/Certificate Checks

    Verifying the integrity of system applications or certificates, which can be altered on rooted devices.

Building a Dynamic Frida Root Bypass Script

Our Frida script will leverage the Java.perform context to hook into critical Android APIs. We’ll build a modular script that can be easily extended.

1. Initializing the Frida Script

All Frida Android Java hooks start within Java.perform.

Java.perform(function() {    console.log('[+] Frida Root Bypass Script Loaded');    // Your bypass logic goes here});

2. Bypassing File-Based Checks

We’ll hook the java.io.File constructor and the exists() method to intercept checks for root-related files.

    var File = Java.use('java.io.File');    var paths = [        '/system/bin/su',        '/system/xbin/su',        '/sbin/su',        '/data/local/su',        '/system/app/Superuser.apk',        '/data/data/com.noshufou.android.su',        '/system/xbin/busybox',        '/dev/bus/usb/001/001',        '/system/sbin/su',        '/vendor/bin/su'    ];    File.exists.implementation = function() {        var path = this.getAbsolutePath();        if (paths.indexOf(path) > -1) {            console.log('[-] Root file detected via exists(): ' + path + ', returning false.');            return false;        }        return this.exists();    };    File.$init.overload('java.lang.String').implementation = function(path) {        if (paths.indexOf(path) > -1) {            console.log('[-] Root file detected via File constructor: ' + path + ', returning a dummy file.');            return this.$init('/dummy/path/not/exists'); // Initialize with a non-existent path        }        return this.$init(path);    };

3. Bypassing Package Checks

Hooking PackageManager.getPackageInfo and getApplicationInfo to hide root management packages.

    var PackageManager = Java.use('android.app.ApplicationPackageManager');    var rootPackages = [        'com.noshufou.android.su',        'eu.chainfire.supersu',        'com.topjohnwu.magisk',        'com.koushikdutta.rommanager',        'com.ramdroid.appquarantine'    ];    PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {        if (rootPackages.indexOf(packageName) > -1) {            console.log('[-] Root package detected via getPackageInfo: ' + packageName + ', throwing an exception.');            throw PackageManager.NameNotFoundException.$new('Package ' + packageName + ' not found.');        }        return this.getPackageInfo(packageName, flags);    };    PackageManager.getApplicationInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {        if (rootPackages.indexOf(packageName) > -1) {            console.log('[-] Root package detected via getApplicationInfo: ' + packageName + ', throwing an exception.');            throw PackageManager.NameNotFoundException.$new('Package ' + packageName + ' not found.');        }        return this.getApplicationInfo(packageName, flags);    };

4. Bypassing Property Checks

Intercepting android.os.SystemProperties.get and modifying android.os.Build.TAGS.

    var SystemProperties = Java.use('android.os.SystemProperties');    SystemProperties.get.overload('java.lang.String').implementation = function(key) {        if (key === 'ro.build.tags') {            console.log('[-] Property ' + key + ' requested, returning release-keys.');            return 'release-keys';        }        if (key === 'ro.debuggable' || key === 'ro.secure') {            console.log('[-] Property ' + key + ' requested, returning 0.');            return '0';        }        return this.get(key);    };    SystemProperties.get.overload('java.lang.String', 'java.lang.String').implementation = function(key, defaultValue) {        if (key === 'ro.build.tags') {            console.log('[-] Property ' + key + ' requested, returning release-keys.');            return 'release-keys';        }        if (key === 'ro.debuggable' || key === 'ro.secure') {            console.log('[-] Property ' + key + ' requested, returning 0.');            return '0';        }        return this.get(key, defaultValue);    };    // Also hook Build.TAGS directly if an app uses it    var Build = Java.use('android.os.Build');    Object.defineProperty(Build, 'TAGS', {        get: function() {            console.log('[-] Build.TAGS accessed, returning release-keys.');            return 'release-keys';        }    });

5. Bypassing Command Execution Checks

Hooking java.lang.Runtime.exec to prevent execution of root-checking commands.

    var Runtime = Java.use('java.lang.Runtime');    Runtime.exec.overload('java.lang.String').implementation = function(command) {        if (command.indexOf('su') > -1 || command.indexOf('busybox') > -1) {            console.log('[-] Command execution detected: ' + command + ', returning dummy process.');            // Return a dummy Process object or throw an exception            // For simplicity, we'll return a process that immediately finishes            return Java.use('java.lang.Process').$new();        }        return this.exec(command);    };    Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmdarray) {        var command = Java.cast(cmdarray[0], Java.use('java.lang.String'));        if (command.indexOf('su') > -1 || command.indexOf('busybox') > -1) {            console.log('[-] Command execution detected (array): ' + command + ', returning dummy process.');            return Java.use('java.lang.Process').$new();        }        return this.exec(cmdarray);    };

Putting it all Together: The Comprehensive Script

Here’s a consolidated version of the Frida script:

// frida_root_bypass.jsJava.perform(function() {    console.log('[+] Frida Root Bypass Script Loaded');    // --- File-based Checks Bypass ---    var File = Java.use('java.io.File');    var rootPaths = [        '/system/bin/su', '/system/xbin/su', '/sbin/su', '/data/local/su',        '/system/app/Superuser.apk', '/data/data/com.noshufou.android.su',        '/system/xbin/busybox', '/dev/bus/usb/001/001', '/system/sbin/su',        '/vendor/bin/su', '/su/bin/su', '/su/xbin/su', '/data/su'    ];    File.exists.implementation = function() {        var path = this.getAbsolutePath();        if (rootPaths.indexOf(path) > -1) {            console.log('[-] Root file detected via exists(): ' + path + ', returning false.');            return false;        }        return this.exists();    };    File.$init.overload('java.lang.String').implementation = function(path) {        if (rootPaths.indexOf(path) > -1) {            console.log('[-] Root file detected via File constructor: ' + path + ', initializing with a dummy path.');            return this.$init('/dummy/path/not/exists');        }        return this.$init(path);    };    File.$init.overload('java.lang.String', 'java.lang.String').implementation = function(parent, child) {        var path = parent + '/' + child;        if (rootPaths.indexOf(path) > -1) {            console.log('[-] Root file detected via File constructor (parent, child): ' + path + ', initializing with a dummy path.');            return this.$init('/dummy/path/not/exists');        }        return this.$init(parent, child);    };    // --- Package-based Checks Bypass ---    var PackageManager = Java.use('android.app.ApplicationPackageManager');    var rootPackages = [        'com.noshufou.android.su', 'eu.chainfire.supersu', 'com.topjohnwu.magisk',        'com.koushikdutta.rommanager', 'com.ramdroid.appquarantine',        'com.devadvance.rootverifier', 'com.joeykrim.rootcheck'    ];    PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {        if (rootPackages.indexOf(packageName) > -1) {            console.log('[-] Root package detected via getPackageInfo: ' + packageName + ', throwing exception.');            throw PackageManager.NameNotFoundException.$new('Package ' + packageName + ' not found.');        }        return this.getPackageInfo(packageName, flags);    };    PackageManager.getApplicationInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {        if (rootPackages.indexOf(packageName) > -1) {            console.log('[-] Root package detected via getApplicationInfo: ' + packageName + ', throwing exception.');            throw PackageManager.NameNotFoundException.$new('Package ' + packageName + ' not found.');        }        return this.getApplicationInfo(packageName, flags);    };    // --- Property-based Checks Bypass ---    var SystemProperties = Java.use('android.os.SystemProperties');    SystemProperties.get.overload('java.lang.String').implementation = function(key) {        if (key === 'ro.build.tags') {            console.log('[-] Property ' + key + ' requested, returning release-keys.');            return 'release-keys';        }        if (key === 'ro.debuggable' || key === 'ro.secure') {            console.log('[-] Property ' + key + ' requested, returning 0.');            return '0';        }        return this.get(key);    };    SystemProperties.get.overload('java.lang.String', 'java.lang.String').implementation = function(key, defaultValue) {        if (key === 'ro.build.tags') {            console.log('[-] Property ' + key + ' requested, returning release-keys.');            return 'release-keys';        }        if (key === 'ro.debuggable' || key === 'ro.secure') {            console.log('[-] Property ' + key + ' requested, returning 0.');            return '0';        }        return this.get(key, defaultValue);    };    var Build = Java.use('android.os.Build');    Object.defineProperty(Build, 'TAGS', {        get: function() {            console.log('[-] Build.TAGS accessed, returning release-keys.');            return 'release-keys';        }    });    // --- Command Execution Checks Bypass ---    var Runtime = Java.use('java.lang.Runtime');    Runtime.exec.overload('java.lang.String').implementation = function(command) {        if (command.indexOf('su') > -1 || command.indexOf('busybox') > -1 || command.indexOf('which su') > -1) {            console.log('[-] Command execution detected: ' + command + ', returning dummy process.');            // Return a dummy Process object that implies success but does nothing            var process = Java.use('java.lang.Process').$new();            Java.scheduleOnMainThread(function() {                Java.cast(process, Java.use('java.lang.Object')).wait(10); // Simulate brief execution            });            return process;        }        return this.exec(command);    };    Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmdarray) {        var command = Java.cast(cmdarray[0], Java.use('java.lang.String'));        if (command.indexOf('su') > -1 || command.indexOf('busybox') > -1 || command.indexOf('which su') > -1) {            console.log('[-] Command execution detected (array): ' + command + ', returning dummy process.');            var process = Java.use('java.lang.Process').$new();            Java.scheduleOnMainThread(function() {                Java.cast(process, Java.use('java.lang.Object')).wait(10);            });            return process;        }        return this.exec(cmdarray);    };    // Add more advanced hooks here as needed, e.g., native library hooking});

Running Your Frida Script

To use this script, save it as frida_root_bypass.js. Ensure you have the Frida server running on your Android device (either physically connected via USB or on an emulator) and the Frida client installed on your host machine.

First, find the package name of the target application. For example, if the app is called

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