Introduction: The Evolving Cat-and-Mouse Game
In the realm of Android security, a perpetual arms race exists between app developers aiming to secure their applications against rooted devices and users seeking full control over their hardware. Google’s SafetyNet Attestation API, now largely superseded by the Play Integrity API, stands as a primary defense mechanism, designed to verify the integrity of an Android device and its software environment. Magisk, the de-facto standard for Android root, offers powerful features like Magisk Hide (or its successor, Zygisk with DenyList), attempting to conceal root from apps. However, sophisticated apps, especially those in banking, gaming, or DRM-protected content, often employ advanced root detection techniques that even Magisk struggles to circumvent. This is where Frida, a dynamic instrumentation toolkit, becomes an indispensable tool for penetration testers and researchers.
This article delves into advanced strategies for bypassing Android root detection mechanisms using Frida. We’ll explore common detection vectors and craft targeted Frida scripts to dynamically alter application behavior, effectively hiding root and allowing apps to function on a compromised device.
Understanding Android Root Detection Vectors
Before we can bypass root detection, we must understand how applications typically identify a rooted environment. Detection methods can vary widely in complexity and location (Java vs. Native code):
1. File-Based Checks
- Presence of su binary: Checking paths like
/system/bin/su,/system/xbin/su,/sbin/su. - Magisk-specific directories: Looking for
/data/adb,/magisk. - Other common root files:
/system/app/Superuser.apk,/data/local/tmp/su, etc.
2. Property-Based Checks
- System properties: Querying
ro.debuggable(should be ‘0’ for production),ro.boot.flash.locked(should be ‘1’ for locked bootloader),ro.build.tags(should not contain ‘test-keys’).
3. Package-Based Checks
- Installed root management apps: Searching for packages like
com.topjohnwu.magisk(Magisk Manager),eu.chainfire.supersu(SuperSU).
4. Library/Framework Checks
- Detecting Xposed/LSPosed/Zygisk: Identifying injected modules or frameworks that modify the Android runtime.
5. Native Code Checks
- System call hooking: Intercepting low-level functions like
access(),stat(),open()to check for root files, often performed in C/C++ libraries to evade Java-level instrumentation. - Integrity checks: Verifying library checksums or code integrity.
6. SafetyNet / Play Integrity API Attestation
- Server-side validation: This is the hardest to bypass as it relies on Google’s remote servers to verify device integrity. Client-side Frida can only prevent the app from *triggering* the attestation or modify its *response* if not properly signed.
Setting Up Your Frida Environment
To begin, ensure you have Frida installed on your host machine and frida-server running on your Android device (rooted or using an emulator):
# On your host machine (assuming Python and pip are installed)pip install frida-tools# On your Android device (rooted)adb push /path/to/frida-server /data/local/tmp/frida-serverchmod 755 /data/local/tmp/frida-serveradb shell /data/local/tmp/frida-server
Identify the target application’s package name (e.g., com.example.bankingapp) and ensure it’s running or ready to launch.
Frida Strategies: Bypassing Common Root Checks
1. Hiding Root-Related Files and Directories
Apps often call java.io.File.exists() or similar methods to check for root binaries. We can hook these methods to return false for known root paths.
Java.perform(function() { var File = Java.use('java.io.File'); File.exists.implementation = function() { var path = this.getPath(); console.log('File.exists() called for: ' + path); if (path.includes('/su') || path.includes('/magisk') || path.contains('/data/adb')) { console.warn('Blocking File.exists() for root path: ' + path); return false; } return this.exists(); }; File.canRead.implementation = function() { var path = this.getPath(); console.log('File.canRead() called for: ' + path); if (path.includes('/su') || path.includes('/magisk') || path.contains('/data/adb')) { console.warn('Blocking File.canRead() for root path: ' + path); return false; } return this.canRead(); };});
To run this script: frida -U -f com.example.bankingapp -l hide_files.js --no-pause
2. Spoofing System Properties
Applications check system properties to determine if the device is debuggable or has an unlocked bootloader. We can manipulate these values.
Java.perform(function() { var SystemProperties = Java.use('android.os.SystemProperties'); SystemProperties.get.overload('java.lang.String').implementation = function(key) { console.log('SystemProperties.get() called for: ' + key); if (key === 'ro.debuggable') { console.warn('Spoofing ro.debuggable to 0'); return '0'; } if (key === 'ro.boot.flash.locked') { console.warn('Spoofing ro.boot.flash.locked to 1'); return '1'; } if (key === 'ro.build.tags') { console.warn('Spoofing ro.build.tags to release-keys'); return 'release-keys'; } return this.get(key); }; // For Build.TAGS, which might be cached or accessed directly var Build = Java.use('android.os.Build'); Object.defineProperty(Build, 'TAGS', { get: function() { console.warn('Spoofing Build.TAGS to release-keys'); return 'release-keys'; } });});
Run with: frida -U -f com.example.bankingapp -l spoof_props.js --no-pause
3. Concealing Root Management Packages
Apps use PackageManager to list installed applications. We can filter out known root packages.
Java.perform(function() { var PackageManager = Java.use('android.app.ApplicationPackageManager'); PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) { console.log('getPackageInfo() called for: ' + packageName); if (packageName === 'com.topjohnwu.magisk' || packageName === 'eu.chainfire.supersu') { console.warn('Blocking getPackageInfo() for root package: ' + packageName); // Throw an exception as if the package doesn't exist throw Java.use('android.content.pm.PackageManager$NameNotFoundException').$new(packageName + ' not found'); } return this.getPackageInfo(packageName, flags); }; // Also hook queryIntentActivities for broader coverage PackageManager.queryIntentActivities.overload('android.content.Intent', 'int').implementation = function(intent, flags) { var result = this.queryIntentActivities(intent, flags); var filteredResult = Java.use('java.util.ArrayList').$new(); for (var i = 0; i < result.size(); i++) { var info = result.get(i); if (info.activityInfo.packageName !== 'com.topjohnwu.magisk' && info.activityInfo.packageName !== 'eu.chainfire.supersu') { filteredResult.add(info); } } if (filteredResult.size() !== result.size()) { console.warn('Filtered out root-related activities from queryIntentActivities.'); } return filteredResult; };});
Run with: frida -U -f com.example.bankingapp -l hide_packages.js --no-pause
4. Native Hooking for Deeper Evasion
For checks implemented in native libraries (e.g., using libc.so functions like access, stat, open), Frida’s Interceptor.attach() is crucial. This requires identifying the specific native functions being called. For instance, to block access() calls to /system/bin/su:
Interceptor.attach(Module.findExportByName('libc.so', 'access'), { onEnter: function(args) { this.path = Memory.readUtf8String(args[0]); // console.log('access() called for: ' + this.path); if (this.path.includes('/su') || this.path.includes('/magisk') || this.path.includes('/data/adb')) { console.warn('Blocking native access() to root path: ' + this.path); this.block = true; } }, onLeave: function(retval) { if (this.block) { retval.replace(0); // Return 0 (success) or -1 (failure) with errno ESRCH (No such process) // Or just return 0 to indicate success for the file not existing check // Usually, returning -1 and setting errno to ENOENT (No such file or directory) is more realistic var errno = Module.findExportByName('libc.so', '__errno'); if (errno) { Memory.writeS32(errno, 2); // ENOENT } retval.replace(-1); } }});
This example demonstrates how to intercept native calls. Identifying the exact native functions and their arguments often requires reverse engineering the native library using tools like Ghidra or IDA Pro.
Dealing with Play Integrity API (Formerly SafetyNet)
Bypassing Play Integrity API’s server-side attestation is exceedingly difficult, as Google’s servers are the ultimate arbiters of device integrity. Frida’s role here is primarily client-side: to prevent the application from *triggering* the attestation in the first place, or to modify any client-side components that might react to the attestation result.
- Hooking Google Play Services APIs: Identify and hook calls to
SafetyNetApiorPlayIntegrityManagerwithin the app. By returning a pre-defined
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 →