marginaldeer

char nick[5] = "marg";

char full_nick[13] = "marginaldeer";

printf("https://github.com/%s\n",full_nick);

printf("%s\x40marginaldeer\x2ecom\n",nick);

puts("63DD 76E6 3428 1285 CD8B E3F5 7509 1985 DF70 E945");

Bypassing Root Detection and Emulator Checks on Android

Feb 23, 2026 • android,frida,root-detection,pentest,mobile

This is part six of my Android pentest series. We’ve covered the lab, SSL pinning, method hooking, native hooking, and APK reverse engineering. Now let’s deal with the thing that stops most of those techniques from working in the first place — root detection and emulator checks.

Almost every app I assess on mobile engagements has some form of environment detection. Banking apps, healthcare apps, anything handling sensitive data. The app launches, detects you’re on a rooted device or emulator, and either exits or degrades functionality. This is annoying when you need to actually test the app.

Let’s break all of it.

How Root Detection Works

Root detection isn’t one technique — it’s a pile of heuristics. Apps check multiple indicators and combine the results. Here’s what they typically look for.

File-Based Checks

The most common approach. Apps look for files that exist on rooted devices:

// Typical root detection code you'll find when decompiling
private boolean isRooted() {
    String[] paths = {
        "/system/app/Superuser.apk",
        "/system/xbin/su",
        "/system/bin/su",
        "/sbin/su",
        "/data/local/xbin/su",
        "/data/local/bin/su",
        "/data/local/su",
        "/su/bin/su",
        "/system/bin/.ext/.su",
        "/system/etc/.has_su_daemon",
        "/system/etc/.installed_su_daemon"
    };
    
    for (String path : paths) {
        if (new File(path).exists()) return true;
    }
    return false;
}

Package-Based Checks

Apps query the package manager for known root management apps:

private boolean hasRootPackages() {
    String[] packages = {
        "com.topjohnwu.magisk",
        "eu.chainfire.supersu",
        "com.koushikdutta.superuser",
        "com.thirdparty.superuser",
        "com.yellowes.su",
        "com.noshufou.android.su"
    };
    
    PackageManager pm = getPackageManager();
    for (String pkg : packages) {
        try {
            pm.getPackageInfo(pkg, 0);
            return true;
        } catch (NameNotFoundException e) {}
    }
    return false;
}

Command Execution

Some apps try to actually run su:

private boolean canRunSu() {
    try {
        Runtime.getRuntime().exec("su");
        return true;
    } catch (Exception e) {
        return false;
    }
}

System Properties

Checking build properties for test-keys, custom ROMs, or debug builds:

private boolean hasTestKeys() {
    String buildTags = android.os.Build.TAGS;
    return buildTags != null && buildTags.contains("test-keys");
}

private boolean isDebuggable() {
    return (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}

SafetyNet / Play Integrity

The nuclear option. Google’s server-side attestation checks device integrity, bootloader status, and software state. This is the hardest to bypass because the attestation happens server-side.

Emulator Detection

Apps use overlapping techniques to detect emulators:

Build Properties

private boolean isEmulator() {
    return Build.FINGERPRINT.contains("generic")
        || Build.MODEL.contains("google_sdk")
        || Build.MODEL.contains("Emulator")
        || Build.MODEL.contains("Android SDK built for x86")
        || Build.MANUFACTURER.contains("Genymotion")
        || Build.BRAND.startsWith("generic")
        || Build.DEVICE.startsWith("generic")
        || Build.HARDWARE.contains("goldfish")
        || Build.HARDWARE.contains("ranchu")
        || Build.PRODUCT.contains("sdk")
        || Build.PRODUCT.contains("vbox86p");
}

Hardware Indicators

// Sensor check - emulators have fewer/fake sensors
SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE);
int sensorCount = sm.getSensorList(Sensor.TYPE_ALL).size();
// Real devices typically have 15+ sensors, emulators have < 5

// Telephony check
TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
String phoneNumber = tm.getLine1Number();
// Emulators often return "15555215554" or similar

// Battery check - emulators report specific battery states

Timing and Performance Checks

Some sophisticated apps measure execution timing. Emulators are slower at certain operations like floating point math or file I/O, giving them away.

Bypassing Everything with Frida

Now the fun part. We have a few approaches depending on how much time you want to spend.

Quick Win: Hook the Detection Methods Directly

If you’ve decompiled the APK and found the detection methods, just override them:

Java.perform(function() {
    var rootCheck = Java.use("com.target.security.RootDetector");
    
    rootCheck.isRooted.implementation = function() {
        console.log("[*] isRooted() called - returning false");
        return false;
    };
    
    rootCheck.isEmulator.implementation = function() {
        console.log("[*] isEmulator() called - returning false");
        return false;
    };
});

This works when the detection is in one class. But most serious implementations spread checks across multiple classes or use obfuscated names.

Comprehensive: Hook the Underlying APIs

Instead of finding each detection method, hook the system APIs that all detection relies on:

File.exists() bypass:

Java.perform(function() {
    var File = Java.use("java.io.File");
    
    var blacklist = [
        "su", "Superuser.apk", "magisk", "busybox",
        ".has_su_daemon", ".installed_su_daemon"
    ];
    
    File.exists.implementation = function() {
        var path = this.getAbsolutePath();
        
        for (var i = 0; i < blacklist.length; i++) {
            if (path.indexOf(blacklist[i]) !== -1) {
                console.log("[*] File.exists(" + path + ") -> false");
                return false;
            }
        }
        return this.exists();
    };
});

PackageManager bypass:

Java.perform(function() {
    var PM = Java.use("android.app.ApplicationPackageManager");
    
    var blockedPkgs = [
        "com.topjohnwu.magisk",
        "eu.chainfire.supersu",
        "com.koushikdutta.superuser",
        "com.noshufou.android.su",
        "com.thirdparty.superuser"
    ];
    
    PM.getPackageInfo.overload("java.lang.String", "int").implementation = function(pkg, flags) {
        if (blockedPkgs.indexOf(pkg) !== -1) {
            console.log("[*] Hiding package: " + pkg);
            throw Java.use("android.content.pm.PackageManager$NameNotFoundException").$new(pkg);
        }
        return this.getPackageInfo(pkg, flags);
    };
});

Build properties bypass:

Java.perform(function() {
    var Build = Java.use("android.os.Build");
    
    // Spoof to look like a real device
    Build.FINGERPRINT.value = "google/walleye/walleye:11/RP1A.200720.009/6720564:user/release-keys";
    Build.MODEL.value = "Pixel 2";
    Build.MANUFACTURER.value = "Google";
    Build.BRAND.value = "google";
    Build.DEVICE.value = "walleye";
    Build.PRODUCT.value = "walleye";
    Build.HARDWARE.value = "walleye";
    Build.TAGS.value = "release-keys";
    
    console.log("[*] Build properties spoofed to Pixel 2");
});

Runtime.exec bypass (block su execution):

Java.perform(function() {
    var Runtime = Java.use("java.lang.Runtime");
    
    Runtime.exec.overload("java.lang.String").implementation = function(cmd) {
        if (cmd.indexOf("su") !== -1 || cmd.indexOf("which") !== -1) {
            console.log("[*] Blocked exec: " + cmd);
            throw Java.use("java.io.IOException").$new("Permission denied");
        }
        return this.exec(cmd);
    };
    
    // Also catch the String[] overload
    Runtime.exec.overload("[Ljava.lang.String;").implementation = function(cmds) {
        var cmdStr = cmds.join(" ");
        if (cmdStr.indexOf("su") !== -1) {
            console.log("[*] Blocked exec: " + cmdStr);
            throw Java.use("java.io.IOException").$new("Permission denied");
        }
        return this.exec(cmds);
    };
});

The Combined Script

In practice I use a single script that combines all these bypasses. Save this as bypass-detection.js:

Java.perform(function() {
    console.log("[*] Root/Emulator detection bypass loaded");
    
    // ===== File.exists bypass =====
    var File = Java.use("java.io.File");
    var blacklistedFiles = [
        "su", "Superuser", "magisk", "busybox", "daemonsu",
        ".has_su_daemon", ".installed_su_daemon", "supersu",
        "frida", "xposed"
    ];
    
    File.exists.implementation = function() {
        var path = this.getAbsolutePath();
        for (var i = 0; i < blacklistedFiles.length; i++) {
            if (path.toLowerCase().indexOf(blacklistedFiles[i]) !== -1) {
                console.log("[*] Hiding file: " + path);
                return false;
            }
        }
        return this.exists();
    };
    
    // ===== Build property spoofing =====
    var Build = Java.use("android.os.Build");
    Build.FINGERPRINT.value = "google/walleye/walleye:11/RP1A.200720.009/6720564:user/release-keys";
    Build.MODEL.value = "Pixel 2";
    Build.MANUFACTURER.value = "Google";
    Build.BRAND.value = "google";
    Build.DEVICE.value = "walleye";
    Build.PRODUCT.value = "walleye";
    Build.HARDWARE.value = "walleye";
    Build.TAGS.value = "release-keys";
    Build.HOST.value = "wphr1.hot.corp.google.com";
    console.log("[*] Build properties spoofed");
    
    // ===== Package manager bypass =====
    var PM = Java.use("android.app.ApplicationPackageManager");
    var hiddenPkgs = [
        "com.topjohnwu.magisk", "eu.chainfire.supersu",
        "com.koushikdutta.superuser", "com.noshufou.android.su",
        "com.thirdparty.superuser", "com.yellowes.su",
        "com.zachspong.temprootremovejb", "com.ramdroid.appquarantine",
        "de.robv.android.xposed.installer", "com.saurik.substrate"
    ];
    
    PM.getPackageInfo.overload("java.lang.String", "int").implementation = function(pkg, flags) {
        if (hiddenPkgs.indexOf(pkg) !== -1) {
            console.log("[*] Hiding package: " + pkg);
            throw Java.use("android.content.pm.PackageManager$NameNotFoundException").$new(pkg);
        }
        return this.getPackageInfo(pkg, flags);
    };
    
    // ===== Runtime.exec bypass =====
    var Runtime = Java.use("java.lang.Runtime");
    var blockedCmds = ["su", "which su", "busybox", "magisk"];
    
    Runtime.exec.overload("java.lang.String").implementation = function(cmd) {
        for (var i = 0; i < blockedCmds.length; i++) {
            if (cmd.indexOf(blockedCmds[i]) !== -1) {
                console.log("[*] Blocked exec: " + cmd);
                throw Java.use("java.io.IOException").$new("Permission denied");
            }
        }
        return this.exec(cmd);
    };
    
    // ===== System.getProperty bypass =====
    var System = Java.use("java.lang.System");
    System.getProperty.overload("java.lang.String").implementation = function(key) {
        if (key === "ro.debuggable" || key === "ro.secure") {
            console.log("[*] Spoofed property: " + key);
            return key === "ro.secure" ? "1" : "0";
        }
        return this.getProperty(key);
    };
    
    console.log("[*] All bypasses active");
});

Run it:

frida -U -f com.target.app -l bypass-detection.js --no-pause

Dealing with Commercial Frameworks

When you run into apps that use commercial protection SDKs, the generic bypass above might not cut it. Here’s what to look for.

RootBeer

Popular open-source root detection library. Easy to spot because the package name is com.scottyab.rootbeer.

Java.perform(function() {
    var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
    
    RootBeer.isRooted.implementation = function() { return false; };
    RootBeer.isRootedWithBusyBoxCheck.implementation = function() { return false; };
    RootBeer.detectRootManagementApps.implementation = function() { return false; };
    RootBeer.detectPotentiallyDangerousApps.implementation = function() { return false; };
    RootBeer.detectTestKeys.implementation = function() { return false; };
    RootBeer.checkForBusyBoxBinary.implementation = function() { return false; };
    RootBeer.checkForSuBinary.implementation = function() { return false; };
    RootBeer.checkSuExists.implementation = function() { return false; };
    RootBeer.checkForRWPaths.implementation = function() { return false; };
    RootBeer.checkForDangerousProps.implementation = function() { return false; };
    RootBeer.checkForRootNative.implementation = function() { return false; };
    RootBeer.detectRootCloakingApps.implementation = function() { return false; };
    RootBeer.isSelinuxFlagInEnabled.implementation = function() { return false; };
    RootBeer.checkForMagiskBinary.implementation = function() { return false; };
    
    console.log("[*] RootBeer fully bypassed");
});

ProGuard/R8 Obfuscated Detection

When root detection is obfuscated, you won’t find obvious class names. The approach changes:

  1. Search for string constants — Decompile with jadx and grep for "/system/xbin/su" or "test-keys". Even with obfuscated method names, the strings are usually still there.

  2. Trace File.exists — Run the comprehensive File.exists hook and watch what paths get checked at startup. The detection code will reveal itself.

  3. Hook at the decision point — Find where the app shows the “rooted device detected” dialog or calls finish(). Hook that and prevent it.

// Find the kill switch
Java.perform(function() {
    var Activity = Java.use("android.app.Activity");
    
    Activity.finish.implementation = function() {
        console.log("[!] finish() blocked on: " + this.getClass().getName());
        console.log(Java.use("android.util.Log").getStackTraceString(
            Java.use("java.lang.Exception").$new()
        ));
        // Don't call finish - keep the app alive
    };
});

The stack trace will show you exactly which class triggered the kill, even if it’s obfuscated.

Frida Detection

Some apps detect Frida itself. Common methods:

  • Scanning /proc/self/maps for frida libraries
  • Checking for frida-server on default port (27042)
  • Looking for frida-agent in memory
  • Checking for the frida thread name

Bypass Frida detection:

// Hide frida from /proc/self/maps
var openPtr = Module.findExportByName("libc.so", "open");
var readPtr = Module.findExportByName("libc.so", "read");

Interceptor.attach(openPtr, {
    onEnter: function(args) {
        this.path = Memory.readUtf8String(args[0]);
        this.isMaps = this.path && this.path.indexOf("/proc/") !== -1 
                      && this.path.indexOf("/maps") !== -1;
    },
    onLeave: function(retval) {}
});

// Change frida-server default port
// Start frida-server on a non-default port:
// ./frida-server -l 0.0.0.0:1337
// Then connect: frida -H 127.0.0.1:1337 -f com.target.app

For stubborn anti-Frida checks, start frida-server with a renamed binary:

# On the device
cp frida-server /data/local/tmp/myserver
chmod 755 /data/local/tmp/myserver
/data/local/tmp/myserver &

MagiskHide and Shamiko

If you’re on a real rooted device (not an emulator), Magisk’s built-in hide features handle a lot of this automatically.

Zygisk + DenyList (Magisk 24+):

  1. Enable Zygisk in Magisk settings
  2. Configure the DenyList with the target app
  3. The app won’t see root

Shamiko (Magisk module):

  • Mounts over root-indicating files
  • Hides Magisk itself from detection
  • Works alongside Zygisk DenyList

For most assessments I use Magisk + Shamiko as the baseline and layer Frida hooks on top for anything that gets through.

Practical Workflow

Here’s how I approach root/emulator detection on a real engagement:

  1. Install the app and launch it — See what happens. Does it crash? Show a warning? Silently degrade?

  2. Decompile and searchgrep -r "isRooted\|rootbeer\|safetynet\|isEmulator" jadx-output/ to find the detection logic.

  3. Try the generic bypass script — Run bypass-detection.js. This handles ~70% of apps.

  4. If that fails, trace the APIs — Hook File.exists, Runtime.exec, PackageManager.getPackageInfo and log everything at startup. You’ll see exactly what the app checks.

  5. Hook the decision point — Find where the app acts on the detection result and neutralize it there.

  6. For SafetyNet/Play Integrity — Use a real rooted device with Magisk + Shamiko. Emulators can’t pass hardware attestation.

Most apps fall to step 3 or 4. The rest require targeted hooking but the techniques above give you everything you need to find the right hooks.

If you enjoyed this post please consider subscribing to the feed!