Advanced Root Detection Bypass

Happy Jester
8 min readJun 25, 2024

--

بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ

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)

Checking Code for Su Binary

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.

Source

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();
in this we can see access() try to access su

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 device

Dynamic 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() and call_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 -

Source

Source

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

we can see it store 0x38 lets see what is that mean in syscall table

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

--

--