Files
frida-interception-and-unpi…/android/android-disable-root-detection.js

443 lines
18 KiB
JavaScript

/**************************************************************************************************
*
* This script defines a large set of root detection bypasses for Android. Hooks included here
* block detection of many known root indicators, including file paths, package names, commands,
* notably binaries, and system properties.
*
* Enable DEBUG_MODE to see debug output for each bypassed check.
*
* Source available at https://github.com/httptoolkit/frida-interception-and-unpinning/
* SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-FileCopyrightText: Tim Perry <tim@httptoolkit.com>
* SPDX-FileCopyrightText: Riyad Mondal
*
*************************************************************************************************/
Java.perform(() => {
let loggedRootDetectionWarning = false;
function logFirstRootDetection() {
if (!loggedRootDetectionWarning) {
console.log(" => Blocked possible root detection checks. Enable DEBUG_MODE for more details.");
loggedRootDetectionWarning = true;
}
}
const LIB_C = Process.findModuleByName("libc.so");
const BUILD_FINGERPRINT_REGEX = /^([\w.-]+\/[\w.-]+\/[\w.-]+):([\w.]+\/[\w.-]+\/[\w.-]+):(\w+\/[\w,.-]+)$/;
const CONFIG = {
secureProps: {
"ro.secure": "1",
"ro.debuggable": "0",
"ro.build.type": "user",
"ro.build.tags": "release-keys"
}
};
const ROOT_INDICATORS = {
paths: new Set([
"/data/local/bin/su",
"/data/local/su",
"/data/local/xbin/su",
"/dev/com.koushikdutta.superuser.daemon/",
"/sbin/su",
"/su/bin/su",
"/system/bin/su",
"/system/xbin/su",
"/system/sbin/su",
"/vendor/bin/su",
"/data/adb/su/bin/su",
"/system/bin/failsafe/su",
"/system/bin/.ext/.su",
"/system/bin/.ext/su",
"/system/bin/failsafe/su",
"/system/sd/xbin/su",
"/system/usr/we-need-root/su",
"/cache/su",
"/data/su",
"/dev/su",
"/data/adb/magisk",
"/sbin/.magisk",
"/cache/.disable_magisk",
"/dev/.magisk.unblock",
"/cache/magisk.log",
"/data/adb/magisk.img",
"/data/adb/magisk.db",
"/data/adb/magisk_simple",
"/init.magisk.rc",
"/system/app/Superuser.apk",
"/system/etc/init.d/99SuperSUDaemon",
"/system/xbin/daemonsu",
"/system/xbin/ku.sud",
"/data/adb/ksu",
"/data/adb/ksud",
"/system/xbin/busybox",
"/system/app/Kinguser.apk"
]),
packages: new Set([
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license",
"com.dimonvideo.luckypatcher",
"com.chelpus.lackypatch",
"com.ramdroid.appquarantine",
"com.ramdroid.appquarantinepro",
"com.topjohnwu.magisk",
"me.weishu.kernelsu"
]),
commands: new Set([
"su",
"which su",
"whereis su",
"locate su",
"find / -name su",
"mount",
"magisk",
"/system/bin/su",
"/system/xbin/su",
"/sbin/su",
"/su/bin/su"
]),
binaries: new Set([
"su",
"busybox",
"magisk",
"supersu",
"ksud",
"daemonsu"
])
};
function isRootIndicatorPath(path) {
const lowercasePath = path.toLowerCase();
return ROOT_INDICATORS.paths.has(path) ||
lowercasePath.includes("magisk") ||
lowercasePath.endsWith("/su") ||
lowercasePath.includes("/su/");
}
function bypassNativeFileCheck() {
const fopen = LIB_C.findExportByName("fopen");
if (fopen) {
Interceptor.attach(fopen, {
onEnter(args) {
this.path = args[0].readUtf8String();
},
onLeave(retval) {
if (retval.toInt32() !== 0) {
if (isRootIndicatorPath(this.path)) {
if (DEBUG_MODE) {
console.log(`Blocked possible root-detection: fopen ${this.path}`);
} else logFirstRootDetection();
retval.replace(ptr(0x0));
}
}
}
});
}
const access = LIB_C.findExportByName("access");
if (access) {
Interceptor.attach(access, {
onEnter(args) {
this.path = args[0].readUtf8String();
},
onLeave(retval) {
if (retval.toInt32() === 0) {
if (isRootIndicatorPath(this.path)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: access ${this.path}`);
} else logFirstRootDetection();
retval.replace(ptr(-1));
}
}
}
});
}
const stat = LIB_C.findExportByName("stat");
if (stat) {
Interceptor.attach(stat, {
onEnter(args) {
this.path = args[0].readUtf8String();
},
onLeave(retval) {
if (isRootIndicatorPath(this.path)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: stat ${this.path}`);
} else logFirstRootDetection();
retval.replace(ptr(-1));
}
}
});
}
const lstat = LIB_C.findExportByName("lstat");
if (lstat) {
Interceptor.attach(lstat, {
onEnter(args) {
this.path = args[0].readUtf8String();
},
onLeave(retval) {
if (isRootIndicatorPath(this.path)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: lstat ${this.path}`);
} else logFirstRootDetection();
retval.replace(ptr(-1));
}
}
});
}
}
function bypassJavaFileCheck() {
function isRootIndicatorFile(file) {
const path = file.getAbsolutePath();
const filename = file.getName();
return ROOT_INDICATORS.paths.has(path) ||
path.includes("magisk") ||
filename === "su";
}
const UnixFileSystem = Java.use("java.io.UnixFileSystem");
UnixFileSystem.checkAccess.implementation = function(file, access) {
if (isRootIndicatorFile(file)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: filesystem access check for ${file.getAbsolutePath()}`);
} else logFirstRootDetection();
return false;
}
return this.checkAccess(file, access);
};
const File = Java.use("java.io.File");
File.exists.implementation = function() {
if (isRootIndicatorFile(this)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: file exists check for ${this.getAbsolutePath()}`);
} else logFirstRootDetection();
return false;
}
return this.exists();
};
File.length.implementation = function() {
if (isRootIndicatorFile(this)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: file length check for ${this.getAbsolutePath()}`);
} else logFirstRootDetection();
return 0;
}
return this.length();
};
const FileInputStream = Java.use("java.io.FileInputStream");
FileInputStream.$init.overload('java.io.File').implementation = function(file) {
if (isRootIndicatorFile(file)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: file stream for ${file.getAbsolutePath()}`);
} else logFirstRootDetection();
throw Java.use("java.io.FileNotFoundException").$new(file.getAbsolutePath());
}
return this.$init(file);
};
}
function spoofBuildProperties() {
const Build = Java.use("android.os.Build");
// We do a little work to make the minimum changes required to hide in the BUILD fingerprint,
// but otherwise keep matching the real device wherever possible.
const realFingerprint = Build.FINGERPRINT.value;
const fingerprintMatch = BUILD_FINGERPRINT_REGEX.exec(realFingerprint);
let fixedFingerprint;
if (fingerprintMatch) {
let [, device, versions, tags] = BUILD_FINGERPRINT_REGEX.exec(realFingerprint);
tags = 'user/release-keys'; // Should always be the case in production builds
if (device.includes('generic') || device.includes('sdk') || device.includes('lineage')) {
device = 'google/raven/raven';
}
fixedFingerprint = `${device}:${versions}:${tags}`;
} else {
console.warn(`Unexpected BUILD fingerprint format: ${realFingerprint}`);
// This should never happen in theory (the format is standard), but just in case,
// we use this fallback fingerprint:
fixedFingerprint = "google/crosshatch/crosshatch:10/QQ3A.200805.001/6578210:user/release-keys";
}
const fields = {
"TAGS": "release-keys",
"TYPE": "user",
"FINGERPRINT": fixedFingerprint
};
Object.entries(fields).forEach(([field, value]) => {
const fieldObj = Build.class.getDeclaredField(field);
fieldObj.setAccessible(true);
fieldObj.set(null, value);
});
}
function bypassPropertyChecks() {
const system_property_get = LIB_C.findExportByName("__system_property_get");
if (system_property_get) {
Interceptor.attach(system_property_get, {
onEnter(args) {
this.key = args[0].readCString();
this.ret = args[1];
},
onLeave(retval) {
const secureValue = CONFIG.secureProps[this.key];
if (secureValue !== undefined) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: system_property_get ${this.key}`);
} else logFirstRootDetection();
const valuePtr = Memory.allocUtf8String(secureValue);
Memory.copy(this.ret, valuePtr, secureValue.length + 1);
}
}
});
}
const Runtime = Java.use('java.lang.Runtime');
Runtime.exec.overload('java.lang.String').implementation = function(cmd) {
if (cmd.startsWith("getprop ")) {
const prop = cmd.split(" ")[1];
if (CONFIG.secureProps[prop]) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: getprop ${prop}`);
} else logFirstRootDetection();
return null;
}
}
return this.exec(cmd);
};
}
function bypassRootPackageCheck() {
const ApplicationPackageManager = Java.use("android.app.ApplicationPackageManager");
// Android 13 added PackageInfoFlags variants of both of these, and it's the int
// variants that delegate to those rather than the other way around, so an app calling
// the newer API directly is only covered if we patch both:
const FLAG_TYPES = ['int', 'android.content.pm.PackageManager$PackageInfoFlags'];
const patchOverload = (method, argTypes, buildReplacement) => {
let overload;
try {
overload = method.overload(...argTypes);
} catch (e) {
return; // Not present on this Android version
}
overload.implementation = buildReplacement();
};
FLAG_TYPES.forEach((flagsType) => {
patchOverload(ApplicationPackageManager.getPackageInfo,
['java.lang.String', flagsType], () => function (packageName, flags) {
if (ROOT_INDICATORS.packages.has(packageName)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: package info for ${packageName}`);
} else logFirstRootDetection();
packageName = "invalid.example.nonexistent.package";
}
return this.getPackageInfo.overload('java.lang.String', flagsType)
.call(this, packageName, flags);
});
patchOverload(ApplicationPackageManager.getInstalledPackages,
[flagsType], () => function (flags) {
const packages = this.getInstalledPackages.overload(flagsType).call(this, flags);
const filteredPackages = packages.toArray()
.filter(pkg => !ROOT_INDICATORS.packages.has(pkg.packageName?.value));
return Java.use("java.util.ArrayList").$new(Java.use("java.util.Arrays").asList(filteredPackages));
});
});
}
function bypassShellCommands() {
const ProcessBuilder = Java.use('java.lang.ProcessBuilder');
ProcessBuilder.command.overload('java.util.List').implementation = function(commands) {
const cmdArray = commands.toArray();
if (cmdArray.length > 0) {
const cmd = cmdArray[0].toString();
if (ROOT_INDICATORS.commands.has(cmd) || (cmdArray.length > 1 && ROOT_INDICATORS.binaries.has(cmdArray[1].toString()))) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: ProcessBuilder with ${cmdArray.join(' ')}`);
} else logFirstRootDetection();
return this.command(Java.use("java.util.Arrays").asList([""]));
}
}
return this.command(commands);
};
const Runtime = Java.use('java.lang.Runtime');
Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmdArray) {
if (cmdArray.length > 0) {
const cmd = cmdArray[0];
if (ROOT_INDICATORS.commands.has(cmd) || (cmdArray.length > 1 && ROOT_INDICATORS.binaries.has(cmdArray[1]))) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: Runtime.exec for ${cmdArray.join(' ')}`);
} else logFirstRootDetection();
return this.exec([""]);
}
}
return this.exec(cmdArray);
};
const ProcessImpl = Java.use("java.lang.ProcessImpl");
ProcessImpl.start.implementation = function(cmdArray, env, dir, redirects, redirectErrorStream) {
if (cmdArray.length > 0) {
const cmd = cmdArray[0].toString();
const arg = cmdArray.length > 1 ? cmdArray[1].toString() : "";
if (ROOT_INDICATORS.commands.has(cmd) || ROOT_INDICATORS.binaries.has(arg)) {
if (DEBUG_MODE) {
console.debug(`Blocked possible root detection: ProcessImpl.start for ${cmdArray.join(' ')}`);
} else logFirstRootDetection();
return ProcessImpl.start.call(this, [Java.use("java.lang.String").$new("")], env, dir, redirects, redirectErrorStream);
}
}
return ProcessImpl.start.call(this, cmdArray, env, dir, redirects, redirectErrorStream);
};
}
// Android's internals shift between releases, and these bypasses are independent of one
// another, so one failing must not take the rest of them down with it:
const BYPASSES = {
'native file checks': bypassNativeFileCheck,
'Java file checks': bypassJavaFileCheck,
'build properties': spoofBuildProperties,
'system property checks': bypassPropertyChecks,
'root package checks': bypassRootPackageCheck,
'shell commands': bypassShellCommands
};
const failures = Object.entries(BYPASSES).filter(([name, applyBypass]) => {
try {
applyBypass();
return false;
} catch (error) {
console.warn(`[!] Could not hook ${name} to disable root detection: ${error}`);
return true;
}
});
if (failures.length === Object.keys(BYPASSES).length) {
console.error("\n !!! Error setting up root detection bypass !!!");
} else {
console.log("== Disabled Android root detection ==");
}
});