Android App Penetration Testing & Frida Hooks

Live Hacking: Bypassing Financial App Root Detection with Frida and Dynamic Analysis

Google AdSense Native Placement - Horizontal Top-Post banner

The Challenge of Root Detection in Financial Apps

Financial applications on Android often implement robust security measures to protect user data and prevent fraud. One of the most common and critical of these is root detection. A rooted device, while offering greater user control, also exposes the device to potential security vulnerabilities, making it a less secure environment for sensitive operations like banking. Applications use root detection to refuse execution or limit functionality on such devices, aiming to mitigate risks from malware, insecure configurations, or malicious user actions.

However, for security researchers and penetration testers, bypassing these very same root detection mechanisms is crucial. It allows us to perform in-depth security audits, identify other vulnerabilities, and ensure that the application’s core logic remains secure even if its initial root checks are circumvented. This article delves into how to dynamically bypass common root detection techniques in Android financial applications using Frida, a powerful dynamic instrumentation toolkit.

Why Bypass Root Detection? A Penetration Tester’s Perspective

The primary goal of a penetration test is to uncover vulnerabilities that an attacker could exploit. When an app enforces root detection, it essentially creates a barrier to comprehensive testing. Without bypassing it, a tester might miss critical vulnerabilities in the app’s business logic, API communication, or data storage that only become apparent when the app is fully operational on a rooted environment. By demonstrating a bypass, we not only highlight a potential gap in the app’s defense-in-depth strategy but also enable a more thorough security assessment.

Understanding Common Root Detection Methods

Android applications employ various strategies to detect root access. These often include a combination of the following:

  • File System Checks: Looking for common root-related binaries or files, such as /system/bin/su, /system/xbin/su, /sbin/su, /data/local/tmp/su, or the presence of specific Magisk/SuperSU files.
  • Package Checks: Detecting known root management apps like Magisk Manager or SuperSU by checking for their package names (e.g., com.topjohnwu.magisk, eu.chainfire.supersu).
  • Property Checks: Examining system properties that indicate a rooted or emulated environment, such as ro.build.tags containing “test-keys” or ro.secure being 0.
  • Permissions and Capabilities: Attempting to execute commands that require root privileges (e.g., id command looking for UID 0).
  • SELinux Status: Checking the SELinux enforcement status.
  • Signing Certificate Checks: While less common for root detection, modified apps might have different signing certificates, which some apps verify.

Setting Up Your Environment for Dynamic Analysis

Before we can start bypassing, we need a properly configured environment:

  1. Rooted Android Device or Emulator: A rooted device (physical or emulator like Genymotion/Android Studio’s AVD) is essential. Ensure Magisk is installed for easy root management.
  2. ADB (Android Debug Bridge): Install ADB on your workstation and ensure it can communicate with your device (adb devices).
  3. Frida-Server on Device:
  4. # Download the appropriate frida-server for your device's architecture (e.g., arm64) from GitHub releases:https://github.com/frida/frida/releasesadb push /path/to/frida-server /data/local/tmp/frida-serveradb shell"chmod 755 /data/local/tmp/frida-server"adb shell"/data/local/tmp/frida-server &"
  5. Frida-Tools on Workstation: Install via pip: pip install frida-tools

Dynamic Analysis and Identifying Root Checks with Frida

The first step is often to identify *how* the app is detecting root. While static analysis (decompiling the APK) can reveal clues, dynamic analysis with Frida provides real-time insights.

You can use frida-trace to monitor common APIs:

frida-trace -U -f com.example.financialapp -i "*File*exists*" -i "*Runtime*exec*" -i "*PackageManager*getPackageInfo*" -i "*Property*get*"

This command will attach to the specified app (-f to spawn and attach, -U for USB device) and log calls to methods containing “File”, “exists”, “Runtime”, “exec”, “PackageManager”, “getPackageInfo”, “Property”, or “get”. Observe the output when the app starts. You’ll likely see calls to java.io.File.exists() checking for /system/bin/su or /system/xbin/su.

Crafting Frida Bypass Scripts

Once you identify the checks, you can write Frida scripts to intercept and modify their behavior. Here’s a comprehensive script to bypass several common root detection methods:

Java.perform(function() {    var File = Java.use('java.io.File');    var String = Java.use('java.lang.String');    var ProcessBuilder = Java.use('java.lang.ProcessBuilder');    var Runtime = Java.use('java.lang.Runtime');    var System = Java.use('java.lang.System');    var BufferedReader = Java.use('java.io.BufferedReader');    var InputStreamReader = Java.use('java.io.InputStreamReader');    var PackageManager = Java.use('android.content.pm.PackageManager');    var Build = Java.use('android.os.Build');    var SU_PATHS = [        "/data/local/su",        "/data/local/bin/su",        "/data/local/xbin/su",        "/sbin/su",        "/su/bin/su",        "/system/bin/su",        "/system/bin/.ext/su",        "/system/bin/failsafe/su",        "/system/sd/xbin/su",        "/system/usr/we-need-root/su",        "/system/xbin/su",        "/cache/su",        "/data/su",        "/dev/su"    ];    var SU_PACKAGES = [        "com.noshufou.android.su",        "com.noshufou.android.su.elite",        "eu.chainfire.supersu",        "com.koushikdutta.superuser",        "com.thirdparty.superuser",        "com.topjohnwu.magisk"    ];    var ROOT_PROPERTIES = [        "ro.build.tags",        "ro.debuggable",        "ro.secure"    ];    console.log("[*] Attaching root detection bypass...");    // Hooking File.exists() for common su binaries    File.exists.implementation = function() {        var path = this.getAbsolutePath();        if (SU_PATHS.indexOf(path) > -1) {            console.log("[+] Bypassing File.exists() for root path: " + path);            return false;        }        return this.exists();    };    // Hooking Runtime.exec() for 'su' commands    Runtime.exec.overload('java.lang.String').implementation = function(cmd) {        if (cmd.includes("su") || cmd.includes("which su") || cmd.includes("id")) {            console.log("[+] Bypassing Runtime.exec() for root command: " + cmd);            // Return a dummy process that indicates no root            return Java.cast(Java.use('java.lang.Process').$new(), Java.use('java.lang.Process'));        }        return this.exec(cmd);    };    Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmdArray) {        var cmd = cmdArray.join(' ');        if (cmd.includes("su") || cmd.includes("which su") || cmd.includes("id")) {            console.log("[+] Bypassing Runtime.exec() for root command array: " + cmd);            return Java.cast(Java.use('java.lang.Process').$new(), Java.use('java.lang.Process'));        }        return this.exec(cmdArray);    };    // Hooking PackageManager.getPackageInfo() for root packages    PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {        if (SU_PACKAGES.indexOf(packageName) > -1) {            console.log("[+] Bypassing getPackageInfo() for root package: " + packageName);            throw Java.use('android.content.pm.PackageManager$NameNotFoundException').$new("Package not found");        }        return this.getPackageInfo(packageName, flags);    };    // Hooking System.getProperty() or Build.TAGS for system properties    System.getProperty.overload('java.lang.String').implementation = function(propertyName) {        if (ROOT_PROPERTIES.indexOf(propertyName) > -1) {            console.log("[+] Bypassing System.getProperty() for root property: " + propertyName);            if (propertyName === "ro.build.tags") return "release-keys";            if (propertyName === "ro.debuggable") return "0";            if (propertyName === "ro.secure") return "1";        }        return this.getProperty(propertyName);    };    Object.defineProperty(Build, 'TAGS', {        get: function() {            var originalValue = this.TAGS.value;            if (originalValue.includes("test-keys")) {                console.log("[+] Bypassing Build.TAGS: changed 'test-keys' to 'release-keys'");                return originalValue.replace("test-keys", "release-keys");            }            return originalValue;        }    });    // Hooking getprop command execution through BufferedReader    BufferedReader.readLine.implementation = function() {        var line = this.readLine();        if (line != null && (line.includes("ro.build.tags=test-keys") || line.includes("ro.debuggable=1") || line.includes("ro.secure=0"))) {            console.log("[+] Bypassing BufferedReader.readLine() for root property check: " + line);            if (line.includes("ro.build.tags")) return "ro.build.tags=release-keys";            if (line.includes("ro.debuggable")) return "ro.debuggable=0";            if (line.includes("ro.secure")) return "ro.secure=1";        }        return line;    };    console.log("[*] Root detection bypass script loaded.");});

Explanation of the Script:

  • File.exists(): Intercepts calls to check for the existence of common su binaries. If a known root path is requested, it returns false, effectively hiding the binary.
  • Runtime.exec(): Hooks execution of commands. If the app tries to run su, which su, or id, it returns a dummy Process object, preventing the actual execution and making the app believe root commands failed or are not present.
  • PackageManager.getPackageInfo(): Prevents the app from finding common root management packages by throwing a NameNotFoundException.
  • System.getProperty() & Build.TAGS: Modifies system properties like ro.build.tags, ro.debuggable, and ro.secure to reflect non-rooted values.
  • BufferedReader.readLine(): Catches scenarios where apps execute getprop commands and read the output line by line. It modifies known root-indicating lines on the fly.

Executing the Bypass

Save the above script as bypass_root.js. Then, execute it using Frida:

frida -U -f com.example.financialapp -l bypass_root.js --no-pause

The --no-pause flag allows the application to start immediately, which is crucial for bypassing early root checks. Observe the console output for [+] Bypassing... messages, indicating successful interception.

After running the script, the financial application should now launch and function correctly on your rooted device, allowing you to proceed with further security testing.

Conclusion

Bypassing root detection is a fundamental technique for Android penetration testers. While root detection is a valid security control, it is rarely foolproof and can often be circumvented with dynamic instrumentation tools like Frida. By understanding common root detection mechanisms and skillfully crafting Frida scripts, security professionals can gain full access to applications on rooted devices, enabling comprehensive security audits and the discovery of deeper vulnerabilities. This process underscores the importance of a multi-layered security approach, as relying solely on root detection can leave applications vulnerable to determined attackers or skilled testers.

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