Introduction to Frida for Android Penetration Testing
In the evolving landscape of mobile security, dynamic instrumentation tools like Frida have become indispensable for penetration testers and security researchers. Frida allows you to inject custom scripts into running processes on Android, iOS, Windows, macOS, and Linux, enabling powerful runtime analysis and modification. This article delves into advanced Frida techniques, specifically focusing on how to bypass common root detection and SSL pinning mechanisms in Android applications, and provides a comparative overview with Xposed.
Android applications often employ security measures to prevent tampering and ensure data integrity. Root detection identifies if a device is rooted, while SSL pinning ensures that the application only communicates with servers presenting a known, valid certificate. Bypassing these controls is a critical step in many Android application penetration tests, allowing testers to analyze network traffic, modify application logic, and uncover vulnerabilities.
Understanding and Bypassing Root Detection
Root detection mechanisms are implemented by developers to prevent applications from running on potentially compromised devices. Common methods include checking for the existence of su binaries, analyzing installed packages for root-related apps (e.g., SuperSU, Magisk), checking for read/write access to sensitive directories, or executing shell commands to verify root status.
Common Root Detection Checks
- File Presence: Looking for files like
/system/app/Superuser.apk,/sbin/su,/system/bin/su,/system/xbin/su. - Package Names: Checking for installed packages like
com.noshufou.android.su,eu.chainfire.supersu. - Test-Keys in Build Tags: Analyzing
android.os.Build.TAGSfortest-keys. - Read/Write Permissions: Attempting to write to protected locations like
/system. - Command Execution: Running
which suoridto check user privileges.
Frida Script for Generic Root Detection Bypass
A comprehensive Frida script can hook various methods associated with these checks and modify their return values. This script targets common Java APIs used for root detection.
Java.perform(function() { var File = Java.use('java.io.File'); var PackageManager = Java.use('android.app.PackageManager'); var String = Java.use('java.lang.String'); var ProcessBuilder = Java.use('java.lang.ProcessBuilder'); var Runtime = Java.use('java.lang.Runtime'); var BufferedReader = Java.use('java.io.BufferedReader'); var InputStreamReader = Java.use('java.io.InputStreamReader'); // 1. Hooking File.exists() for common root files File.exists.implementation = function() { var path = this.getPath(); if (path.indexOf('su') != -1 || path.indexOf('busybox') != -1 || path.indexOf('xposed') != -1 || path.indexOf('magisk') != -1 || path.indexOf('Superuser.apk') != -1) { console.log('[-] Bypassing root check (File.exists): ' + path); return false; } return this.exists(); }; // 2. Hooking PackageManager for root packages PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pkg, flags) { if (pkg.indexOf('noshufou.android.su') != -1 || pkg.indexOf('eu.chainfire.supersu') != -1 || pkg.indexOf('com.koushikdutta.superuser') != -1 || pkg.indexOf('com.topjohnwu.magisk') != -1) { console.log('[-] Bypassing root check (getPackageInfo): ' + pkg); throw Java.use('android.content.pm.PackageManager$NameNotFoundException').$new(); } return this.getPackageInfo(pkg, flags); }; // 3. Hooking Runtime.exec() for shell commands (e.g., 'which su') Runtime.exec.overload('java.lang.String').implementation = function(cmd) { if (cmd.indexOf('su') != -1 || cmd.indexOf('which') != -1 || cmd.indexOf('busybox') != -1 || cmd.indexOf('id') != -1) { console.log('[-] Bypassing root check (Runtime.exec): ' + cmd); return null; // Return null or a mock process } return this.exec(cmd); }; Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmdArray) { for (var i = 0; i < cmdArray.length; i++) { if (cmdArray[i].indexOf('su') != -1 || cmdArray[i].indexOf('which') != -1 || cmdArray[i].indexOf('busybox') != -1 || cmdArray[i].indexOf('id') != -1) { console.log('[-] Bypassing root check (Runtime.exec array): ' + JSON.stringify(cmdArray)); return null; } } return this.exec(cmdArray); }; // 4. Hooking various methods related to build tags and specific properties var Build = Java.use('android.os.Build'); Build.TAGS.value = 'release-keys'; // Modify 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('[-] Bypassing ro.build.tags check'); return 'release-keys'; } return this.get(key); }; SystemProperties.get.overload('java.lang.String', 'java.lang.String').implementation = function(key, def) { if (key === 'ro.build.tags') { console.log('[-] Bypassing ro.build.tags check (default value)'); return 'release-keys'; } return this.get(key, def); }; // Additional hooks for Magisk-specific detection (example: /proc/self/maps) var BufferedReader = Java.use('java.io.BufferedReader'); BufferedReader.readLine.implementation = function() { var line = this.readLine(); if (line != null && (line.indexOf('magisk') != -1 || line.indexOf('xposed') != -1)) { console.log('[-] Bypassing Magisk/Xposed detection (BufferedReader): ' + line); return ''; // Return an empty string or modify the line } return line; };});
Deployment and Execution
To use this script, ensure you have Frida server running on your Android device and Frida tools installed on your host machine.
- Push Frida Server to Device:
adb push /path/to/frida-server /data/local/tmp/ - Make it Executable:
adb shellAndroid 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 →