mirror of
https://github.com/httptoolkit/frida-interception-and-unpinning.git
synced 2026-09-17 06:32:25 +02:00
443 lines
18 KiB
JavaScript
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 ==");
|
|
}
|
|
});
|