Advanced Root Detection Bypass
بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ
Introduction
In the rapidly evolving world of mobile security, root detection has become a critical feature for many applications to ensure the integrity and security of the device. However, bypassing these root detection mechanisms is sometimes necessary for legitimate reasons, such as development and testing. In this article, I will share advanced techniques for root detection bypass by using Frida.
What is Rooting
Rooting is the process of gaining root access to the Android operating system, allowing users to have elevated permissions. This can enable advanced customization and control over the device but also introduces security vulnerabilities.
Root Detection Mechanisms
Developers use various methods to detect if a device is rooted. These methods include checking for the presence of root binaries, modifications to the system partition.
The common way to detect if the device is rooted or no, is Searching for the su
binary in the system paths. If the binary exists, then the app will flag the device immediately as rooted. such as /system/bin/su /system/xbin/su
Simple Root Detection Bypass (Uncrackable)
To bypass something like this we have many ways but we will use Frida to hook those Functions.
- Attach Frida to the target app.
- Use a Frida script to bypass root checks dynamically.
frida -U -f owasp.mstg.uncrackable2 -l .\bypass.js
Java.perform(function (){
let b = Java.use("sg.vantagepoint.a.b");
b.a.implementation = function () {
return false;
};
b.b.implementation = function () {
return false;
};
b.c.implementation = function () {
return false;
};
})
Proof of Concepts
Advanced Root Detection Bypass
In this example app use one function called detectRoot()
in source code, but after hook this function it only return A number, so this time we need to check Native Library how it works and how it can detect Root by checking for su
Binary
Time to Bypass
First: lets use Frida for hook detectRoot()
and see what it Returns.
Step 1 (Scripting)
Java.perform(function(){
let MainActivity = Java.use("com.fatalsec.inappprotections.MainActivity")
MainActivity.detectRoot.implementation = function(){
var result = this.detectRoot();
console.log(`Method Called Returned: ${result}`);
return result;
}
})
Nothing happened so lets see what inside the Libs we need to use apktool for decompile the apk
What is so files?
The Android NDK (Native Development Kit) compiles this code into .so files.
Application Binary Interface (ABI): The ABI defines exactly how your app’s machine code is expected to interact with the system at runtime. The NDK builds .so files against these definitions.
I’ll use Ghidra in my case you can use whatever you want
First: we need to load thelib.so
in Ghidra and see what happen inside it
after loading it Press “D” to decompile all Functions
after we see the Functions nothing interesting there let’s see Imports maybe found something there
What these Imports?
- Access
The access() function in libc.so is used to check a file’s accessibility in terms of the current user’s permissions.
function hookImportedFunction(){
Interceptor.attach(Module.findExportByName("libc.so", "access"),{
onEnter: function(args){
console.log(`Access ${args[0].readCString()}`)
}
})
}
hookImportedFunction();
So in Step 2 we need to check all those Functions but we need to use Linker64 to make output be specific on our app
- fopen
The fopen() function in libc.so is used to opens a file specified by
pathname.
- stat
The stat() function in libc.so is used to retrieves information about a file and stores it in a
struct stat
object.
- strstr
The strstr() function in libc.so is used to csearches for the first occurrence of a substring within a string.
- Linker64
The
/system/bin/linker64
file on Android systems is a dynamic linker used to load and link native executables and shared libraries. It plays a crucial role in the execution of native code on Android deviceDynamic Linker: It’s a dynamic linker because it performs dynamic linking, allowing the linking of libraries at runtime rather than during the compile time.
do_dlopen()
andcall_constructor
functions are essential parts of the dynamic linking process, responsible for loading shared libraries and invoking their constructors
do_dlopen()
is a function used to open and load shared libraries dynamically at runtime.
call_constructor
function is responsible for invoking the constructors of a shared library after it has been loaded.The dynamic linker needs to call these constructors to ensure that the shared library sets up its internal state correctly.
Step 2 (Scripting)
Let’s do it one by one and patch it
- Access it trying to use
su
Interceptor.attach(Module.findExportByName("libc.so", "access"),{
onEnter: function(args){
if (args[0].readCString().indexOf("/su") >= 0 ){
args[0].writeUtf8String("/dosen't/exsit")
}
console.log(`Access ${args[0].readCString()}`)
}
})
- Stat if Access can’t be found
su The app Will try to use Stat to check Selinux files
Note: to Change something in Memory u need to change Permission of it using this line
Memory.protect(args[0], Process.pointerSize,”rwx”);
Interceptor.attach(Module.findExportByName("libc.so", "stat"),{
onEnter: function(args){
if (args[0].readCString().indexOf("/selinux") >= 0 ){
Memory.protect(args[0], Process.pointerSize,"rwx");
args[0].writeUtf8String("/dosen't/nopath")
}
console.log(`Stat: ${args[0].readCString()}`)
}
})
- Fopen && StrStr next app will try to access
/proc/self/attr/prev
and search for 2 things zygote as we can see in Ghidra it indicator there untrusted app process in loaded also Fopen try to open/proc/self/mountinfo
to check if magisk there
Interceptor.attach(Module.findExportByName("libc.so", "fopen"),{
onEnter: function(args){
console.log(`Fopen: ${args[0].readCString()}`)
}
})
Interceptor.attach(Module.findExportByName("libc.so", "strstr"),{
onEnter: function(args){
if (args[1].readCString().indexOf("zygote") >= 0){
args[1].writeUtf8String("Potato");
}
if (args[1].readCString().indexOf("magisk") >= 0){
args[1].writeUtf8String("Potato");
}
console.log(`Strstr: Orignal Output: ${args[0].readCString()}, Patched Output: ${args[1].readCString()}`);
}
})
After all this app still detect Rooting last thing we can search for something called SysCalls
What is SysCalls ?
A system call is a routine that allows a user application to request actions that require special privileges. Adding system calls is one of several ways to extend the functions provided by the kernel.
Arch/ABI Instruction System Ret Ret Error
call # val val2
───────────────────────────────────────────────────────
alpha callsys v0 v0 a4 a3
arc trap0 r8 r0 - -
arm/OABI swi NR - r0 - -
arm/EABI swi 0x0 r7 r0 r1 -
arm64 svc #0 w8 x0 x1 -
Lets search for svc
in Ghidra but First we need to know hex by using Convertor “01 00 00 D4” lets search using this value
okay now we need Syscalls address’s What Next ??
From our list of Function are loaded we can see Function called __open_2
- _open_2
The open() or openat() system call opens the file specified by pathname. and we can see it called before Svc (syscalls) to refer wihch file will be opend
to verified this we need to check last mov before first syscall
and as we can see this function store filename in arg[1]
Step 3 (Scripting)
to bypass this point we need to use base.address of Library and add the offset of every Svc call in open_2 Function and we have 5 of them the offset will be last 4 bits from every svc of them.
All we did in this point overwrite the Filename in arg[1] of open2 function to be /nothing
function hookSvc(base_addr){ //base_addr = native_mod.base from Linker 64 function
Interceptor.attach(base_addr.add(0x00001f8c),function(){
var path = this.context.x1.readCString(); // x1 Means Arg[1]
this.context.x1.writeUtf8String("/nothing");
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001fa8),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001fc4),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001fe0),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001ffc),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
}
Bypassed
Full Script
// ----------------------------------------------------------Hook Linker64.so----------------------------------------------------------
var do_dlopen = null;
var call_constructor = null;
Process.findModuleByName('linker64').enumerateSymbols().forEach(function(symbol) {
if (symbol.name.indexOf("do_dlopen") >= 0) {
do_dlopen = symbol.address;
} else if (symbol.name.indexOf("call_constructor") >= 0) {
call_constructor = symbol.address;
}
});
if (do_dlopen !== null) {
var lib_Loaded = 0;
Interceptor.attach(do_dlopen, {
onEnter: function(args) {
var libpath = this.context.x0.readCString();
if (libpath.indexOf("libinappprotections.so") >= 0) {
Interceptor.attach(call_constructor, {
onEnter: function(args) {
if (lib_Loaded == 0) {
var native_mod = Process.findModuleByName("libinappprotections.so");
console.log(`inappprotections Lib is loaded at ${native_mod.base}`);
hookImportedFunction(); //call Imports function
hookSvc(native_mod.base); // call svc function
}
lib_Loaded = 1;
}
});
}
}
});
}
// ----------------------------------------------------------Hook RootDetection Function----------------------------------------------------------
Java.perform(function(){
let MainActivity = Java.use("com.fatalsec.inappprotections.MainActivity")
MainActivity.detectRoot.implementation = function(){
var result = this.detectRoot();
console.log(`Method Called Returned: ${result}`);
return result;
}
})
// ----------------------------------------------------------Hook Libc.so Functions----------------------------------------------------------
function hookImportedFunction(){
Interceptor.attach(Module.findExportByName("libc.so", "stat"),{
onEnter: function(args){
if (args[0].readCString().indexOf("/selinux") >= 0 ){
Memory.protect(args[0], Process.pointerSize,"rwx");
args[0].writeUtf8String("/dosen't/nopath")
}
console.log(`Stat: ${args[0].readCString()}`)
}
})
Interceptor.attach(Module.findExportByName("libc.so", "fopen"),{
onEnter: function(args){
console.log(`Fopen: ${args[0].readCString()}`)
}
})
Interceptor.attach(Module.findExportByName("libc.so", "strstr"),{
onEnter: function(args){
if (args[1].readCString().indexOf("zygote") >= 0){
args[1].writeUtf8String("Potato");
}
if (args[1].readCString().indexOf("magisk") >= 0){
args[1].writeUtf8String("Potato");
}
console.log(`Strstr: Orignal Output: ${args[0].readCString()}, Patched Output: ${args[1].readCString()}`);
}
})
Interceptor.attach(Module.findExportByName("libc.so", "access"),{
onEnter: function(args){
if (args[0].readCString().indexOf("/su") >= 0 ){
args[0].writeUtf8String("/dosen't/exsit")
}
console.log(`Access ${args[0].readCString()}`)
}
})
}
// ----------------------------------------------------------Hook SysCalls----------------------------------------------------------
function hookSvc(base_addr){
Interceptor.attach(base_addr.add(0x00001f8c),function(){
var path = this.context.x1.readCString();
this.context.x1.writeUtf8String("/nothing");
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001fa8),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001fc4),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001fe0),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
Interceptor.attach(base_addr.add(0x00001ffc),function(){
this.context.x1.writeUtf8String("/nothing");
var path = this.context.x1.readCString();
console.log(`SVC: ${path}`);
})
}
Requirements
- Frida
- Apktool
- Ghidra
- Android SDK Platform-Tools
- Text Editor
- Jadx
- Rooted Device
Arm64