fixed frida script loading and bypass

This commit is contained in:
Jan Kammerath committed 2026-10-31 18:21:26 +01:00
1 parent f231c97db1
commit b392670d1d
4 files changed
+511 -195

No files matched your search

+64 -19
View File
@@ -53,35 +53,40 @@ fn main() {
std::process::exit(2);
};
let seconds: u64 = positional.next().and_then(|s| s.parse().ok()).unwrap_or(90);
let path = PathBuf::from(&input);
let pkg = Package::open(&path).expect("open package");
// Extract the APK(s) to install.
let tmp = std::env::temp_dir().join(format!("bogusmips-net-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let mut apks = Vec::new();
let mut package_name = None;
if pkg.kind == PackageKind::Apk {
apks.push(path.clone());
if let Some(pkg_id) = input.strip_prefix("pkg:") {
package_name = Some(pkg_id.to_owned());
} else {
for (i, archive) in pkg.archives.iter().enumerate().skip(1) {
if let Some(file) = archive.source_file {
let data = pkg.read_file(file).expect("read inner apk");
let out = tmp.join(format!("part{i}.apk"));
std::fs::write(&out, &data.bytes).unwrap();
apks.push(out);
let path = PathBuf::from(&input);
let pkg = Package::open(&path).expect("open package");
// Extract the APK(s) to install.
if pkg.kind == PackageKind::Apk {
apks.push(path.clone());
} else {
for (i, archive) in pkg.archives.iter().enumerate().skip(1) {
if let Some(file) = archive.source_file {
let data = pkg.read_file(file).expect("read inner apk");
let out = tmp.join(format!("part{i}.apk"));
std::fs::write(&out, &data.bytes).unwrap();
apks.push(out);
}
}
}
}
for archive in &pkg.archives {
if let Some(m) = &archive.manifest {
if let Some(p) = &m.package {
package_name = Some(p.clone());
break;
for archive in &pkg.archives {
if let Some(m) = &archive.manifest {
if let Some(p) = &m.package {
package_name = Some(p.clone());
break;
}
}
}
}
eprintln!("package: {package_name:?}, {} apk(s)", apks.len());
let controller_pkg = package_name.clone();
let mut controller = NetworkController::new();
std::fs::write(tmp.join("ca.pem"), controller.ca_pem()).unwrap();
@@ -94,12 +99,19 @@ fn main() {
let deadline = Instant::now() + Duration::from_secs(seconds);
let mut last_phase = String::new();
let mut last_log_len = 0;
let mut last_count = 0;
while Instant::now() < deadline {
let (phase, error, log) = controller.status_snapshot();
if phase.label() != last_phase {
last_phase = phase.label().to_owned();
eprintln!("[{last_phase}] {}", log.last().cloned().unwrap_or_default());
eprintln!("[{last_phase}]");
}
if log.len() > last_log_len {
for line in &log[last_log_len..] {
eprintln!(" log: {line}");
}
last_log_len = log.len();
}
if let Some(error) = error {
eprintln!("FAILED: {error}");
@@ -120,6 +132,39 @@ fn main() {
std::thread::sleep(Duration::from_millis(500));
}
for (spec, st) in controller.script_statuses() {
eprintln!("script {spec}: {st:?}");
}
let diag_pkg = controller_pkg.unwrap_or_else(|| "de.clara.sharing.app".to_owned());
if let Some(sdk) = controller.sdk() {
let cmd = format!(
"PID=$(pidof {diag_pkg}); echo PID=$PID; ls -laZ /data/local/tmp/bogusmips*; cat /data/local/tmp/bogusmips-frida.log; if [ -n \"$PID\" ]; then grep -iE 'bogusmips|frida' /proc/$PID/maps; logcat -d --pid=$PID | tail -n 80; fi"
);
if let Ok(out) = std::process::Command::new(sdk.adb())
.args(["shell", "su", "root", "sh", "-c", &cmd])
.output()
{
eprintln!("--- process & gadget diagnostics ---\n{}", String::from_utf8_lossy(&out.stdout));
}
if let Ok(out) = std::process::Command::new(sdk.adb())
.args(["logcat", "-d"])
.output()
{
let text = String::from_utf8_lossy(&out.stdout);
eprintln!("--- relevant logcat ---");
for line in text.lines().filter(|l| {
let low = l.to_ascii_lowercase();
low.contains("bogusmips")
|| low.contains("frida")
|| low.contains("androidruntime")
|| low.contains(&diag_pkg)
|| low.contains("fatal")
}) {
eprintln!(" {line}");
}
}
}
let flows = controller.capture().snapshot();
let https = flows.iter().filter(|f| f.scheme == bogusmips::net::capture::Scheme::Https).count();
eprintln!(
+287 -126
View File
@@ -53,6 +53,13 @@ pub fn gadget_url(version: &str, abi: &str) -> String {
)
}
/// GitHub release URL of the standalone `frida-inject` binary `.xz` for `abi`.
pub fn inject_url(version: &str, abi: &str) -> String {
format!(
"https://github.com/frida/frida/releases/download/{version}/frida-inject-{version}-android-{abi}.xz"
)
}
/// Directory where downloaded gadgets are cached between sessions.
pub fn cache_dir() -> PathBuf {
if let Some(dir) = std::env::var_os("XDG_CACHE_HOME") {
@@ -91,6 +98,32 @@ pub fn ensure_gadget(cache_dir: &Path, abi: &str) -> Result<PathBuf, String> {
Ok(dest)
}
/// Ensures the decompressed standalone `frida-inject` binary for `abi` exists
/// in `cache_dir`, downloading and decompressing it on first use.
pub fn ensure_inject(cache_dir: &Path, abi: &str) -> Result<PathBuf, String> {
let version = frida_version();
let dest = cache_dir.join(format!("frida-inject-{version}-android-{abi}"));
if dest.metadata().map(|m| m.len() > 0).unwrap_or(false) {
return Ok(dest);
}
std::fs::create_dir_all(cache_dir).map_err(|e| e.to_string())?;
let url = inject_url(&version, abi);
let compressed = cache_dir.join(format!("frida-inject-{version}-android-{abi}.xz"));
download(&url, &compressed)?;
let bytes = std::fs::read(&compressed).map_err(|e| e.to_string())?;
let mut out = Vec::new();
lzma_rs::xz_decompress(&mut std::io::Cursor::new(bytes), &mut out)
.map_err(|e| format!("could not decompress frida-inject: {e}"))?;
let _ = std::fs::remove_file(&compressed);
if out.get(..4) != Some(ELF_MAGIC) {
return Err("the downloaded frida-inject binary is not a valid ELF executable".into());
}
std::fs::write(&dest, &out).map_err(|e| e.to_string())?;
Ok(dest)
}
const ELF_MAGIC: &[u8] = b"\x7fELF";
/// A user-defined Frida script entry (CodeShare slug, URL, or local `.js` path)
@@ -477,8 +510,61 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
let mut out = String::from(
r#"(function () {
function safeSet(obj, prop, val) {
if (!obj) return false;
try {
obj[prop] = val;
if (obj[prop] === val) return true;
} catch (e) {}
try {
Object.defineProperty(obj, prop, {
value: val,
writable: true,
configurable: true,
enumerable: true
});
return obj[prop] === val;
} catch (e) {}
return false;
}
function makeDelegate(target, overrides) {
if (!target) return overrides;
var allSucceeded = true;
for (var k in overrides) {
if (Object.prototype.hasOwnProperty.call(overrides, k)) {
if (!safeSet(target, k, overrides[k])) {
allSucceeded = false;
}
}
}
if (allSucceeded) return target;
if (typeof Proxy === 'function') {
try {
return new Proxy({}, {
get: function (_dummy, prop) {
if (Object.prototype.hasOwnProperty.call(overrides, prop)) {
return overrides[prop];
}
var v = target[prop];
return (typeof v === 'function') ? v.bind(target) : v;
},
set: function (_dummy, prop, val) {
overrides[prop] = val;
safeSet(target, prop, val);
return true;
},
has: function (_dummy, prop) {
return (prop in overrides) || (prop in target);
}
});
} catch (e) {}
}
return target;
}
if (typeof globalThis.__bogusmipsWrapped !== 'undefined') return;
globalThis.__bogusmipsWrapped = true;
safeSet(globalThis, '__bogusmipsWrapped', true);
var origSetTimeout = globalThis.setTimeout;
var origSetImmediate = globalThis.setImmediate;
var FridaFile = (typeof File === 'function') ? File : null;
@@ -537,53 +623,59 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
return parts.join(' ');
}
var origConsoleLog = console.log ? console.log.bind(console) : function () {};
var origConsoleWarn = console.warn ? console.warn.bind(console) : origConsoleLog;
var origConsoleError = console.error ? console.error.bind(console) : origConsoleLog;
console.log = function () {
writeOutput(formatArgs(arguments));
try { origConsoleLog.apply(console, arguments); } catch (e) {}
};
console.warn = function () {
writeOutput(formatArgs(arguments));
try { origConsoleWarn.apply(console, arguments); } catch (e) {}
};
console.error = function () {
writeOutput(formatArgs(arguments));
try { origConsoleError.apply(console, arguments); } catch (e) {}
};
if (typeof send === 'function') {
var origSend = send;
globalThis.send = function (message, data) {
try {
var text = (typeof message === 'string') ? message : JSON.stringify(message);
writeOutput(text);
} catch (e) {}
var origConsoleLog = (typeof console !== 'undefined' && console.log) ? console.log.bind(console) : function () {};
var origConsoleWarn = (typeof console !== 'undefined' && console.warn) ? console.warn.bind(console) : origConsoleLog;
var origConsoleError = (typeof console !== 'undefined' && console.error) ? console.error.bind(console) : origConsoleLog;
var bogusmipsConsole = makeDelegate(typeof console !== 'undefined' ? console : {}, {
log: function () {
writeOutput(formatArgs(arguments));
try { origConsoleLog.apply(console, arguments); } catch (e) {}
},
warn: function () {
writeOutput(formatArgs(arguments));
try { origConsoleWarn.apply(console, arguments); } catch (e) {}
},
error: function () {
writeOutput(formatArgs(arguments));
try { origConsoleError.apply(console, arguments); } catch (e) {}
}
});
var origSend = (typeof globalThis.send === 'function') ? globalThis.send : null;
var bogusmipsSend = function (message, data) {
try {
var text = (typeof message === 'string') ? message : JSON.stringify(message);
writeOutput(text);
} catch (e) {}
if (origSend) {
try { return origSend(message, data); } catch (e) {}
};
}
}
};
safeSet(globalThis, 'send', bogusmipsSend);
writeOutput('[bogusmips:gadget:loaded]');
// 2. Polyfill Frida 12 Memory.* helpers used by older CodeShare scripts
// (e.g. fdciabdul/frida-multiple-bypass).
var bogusmipsMemory = (typeof Memory !== 'undefined') ? Memory : {};
if (typeof Memory !== 'undefined') {
var memOverrides = {};
if (typeof Memory.readCString !== 'function') {
Memory.readCString = function (p) {
memOverrides.readCString = function (p) {
try { return ptr(p).readCString(); } catch (e) { return ''; }
};
}
if (typeof Memory.readUtf8String !== 'function') {
Memory.readUtf8String = function (p) {
memOverrides.readUtf8String = function (p) {
try { return ptr(p).readUtf8String(); } catch (e) { return ''; }
};
}
if (typeof Memory.writeUtf8String !== 'function') {
Memory.writeUtf8String = function (p, _s) {
memOverrides.writeUtf8String = function (p, _s) {
// Never overwrite caller string literals in-place (.rodata -> SIGSEGV).
return p;
};
}
bogusmipsMemory = makeDelegate(Memory, memOverrides);
}
// 3. Guard Module.findExportByName & Interceptor.attach against hooking
@@ -595,35 +687,41 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
'access': true, 'faccessat': true, 'stat': true, 'lstat': true, 'fstat': true,
'__system_property_get': true, '__system_property_read': true, 'system': true
};
var bogusmipsModule = (typeof Module !== 'undefined') ? Module : {};
if (origFindExport) {
Module.findExportByName = function (moduleName, exportName) {
var addr = origFindExport(moduleName, exportName);
try {
var mod = moduleName ? ('' + moduleName).toLowerCase() : '';
var sym = '' + exportName;
if ((mod === '' || mod.indexOf('libc.so') !== -1) && unsafeLibcSymbols[sym] && addr) {
unsafeNativeAddrs[addr.toString()] = sym;
}
} catch (e) {}
return addr;
};
bogusmipsModule = makeDelegate(Module, {
findExportByName: function (moduleName, exportName) {
var addr = origFindExport(moduleName, exportName);
try {
var mod = moduleName ? ('' + moduleName).toLowerCase() : '';
var sym = '' + exportName;
if ((mod === '' || mod.indexOf('libc.so') !== -1) && unsafeLibcSymbols[sym] && addr) {
unsafeNativeAddrs[addr.toString()] = sym;
}
} catch (e) {}
return addr;
}
});
}
var bogusmipsInterceptor = (typeof Interceptor !== 'undefined') ? Interceptor : {};
if (typeof Interceptor !== 'undefined' && typeof Interceptor.attach === 'function') {
var origAttach = Interceptor.attach.bind(Interceptor);
var attachedAddrs = {};
Interceptor.attach = function (target, callbacksOrProbe, data) {
try {
if (!target) return { detach: function () {} };
var key = target.toString();
if (key === '0x0' || key === '0' || unsafeNativeAddrs[key] || attachedAddrs[key]) {
bogusmipsInterceptor = makeDelegate(Interceptor, {
attach: function (target, callbacksOrProbe, data) {
try {
if (!target) return { detach: function () {} };
var key = target.toString();
if (key === '0x0' || key === '0' || unsafeNativeAddrs[key] || attachedAddrs[key]) {
return { detach: function () {} };
}
attachedAddrs[key] = true;
return origAttach(target, callbacksOrProbe, data);
} catch (e) {
return { detach: function () {} };
}
attachedAddrs[key] = true;
return origAttach(target, callbacksOrProbe, data);
} catch (e) {
return { detach: function () {} };
}
};
});
}
// 4. Do NOT touch `Java` at t = 0ms (during LD_PRELOAD, before ART is
@@ -632,15 +730,19 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
var shimsInstalled = false;
var origPerform = null;
var origUse = null;
var bogusmipsJava = null;
var bogusmipsSetTimeout = origSetTimeout;
var bogusmipsSetImmediate = origSetImmediate;
var envHooked = {};
function applyBaselineEnvHooks() {
if (!origUse) return;
var useFn = (bogusmipsJava && bogusmipsJava.use) ? bogusmipsJava.use : origUse;
function hookReturn(className, method, value) {
var key = className + '#' + method;
if (envHooked[key]) return;
try {
var cls = Java.use(className);
var cls = useFn(className);
if (!cls || !cls[method]) return;
cls[method].overloads.forEach(function (ov) {
ov.implementation = function () { return value; };
@@ -666,6 +768,63 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
hookReturn('com.scottyab.rootbeer.RootBeerNative', 'checkForRoot', 0);
hookReturn('com.scottyab.rootbeer.RootBeerNative', 'isLibraryLoaded', false);
// Spoof Google Play Store installer origin & bypass Google Play PairIP license checks.
hookReturn('android.app.ApplicationPackageManager', 'getInstallerPackageName', 'com.android.vending');
['getInstallingPackageName', 'getInitiatingPackageName', 'getOriginatingPackageName'].forEach(function (m) {
hookReturn('android.content.pm.InstallSourceInfo', m, 'com.android.vending');
});
['initializeLicenseCheck', 'connectToLicensingService', 'startErrorDialogActivity', 'handleError'].forEach(function (m) {
hookReturn('com.pairip.licensecheck.LicenseClient', m, undefined);
hookReturn('com.pairip.licensecheck3.LicenseClient', m, undefined);
});
['com.pairip.licensecheck.LicenseActivity', 'com.pairip.licensecheck3.LicenseActivity'].forEach(function (c) {
var key = c + '#onCreate';
if (!envHooked[key]) {
try {
var actCls = origUse(c);
if (actCls && actCls.onCreate) {
actCls.onCreate.overloads.forEach(function (ov) {
ov.implementation = function (b) {
try { this.onCreate(b); } catch (e) {}
try { this.finish(); } catch (e) {}
};
});
envHooked[key] = true;
}
} catch (e) {}
}
});
if (!envHooked['File.exists']) {
try {
var JavaFile = origUse('java.io.File');
var blockedNames = {
'su': true, 'busybox': true, 'supersu': true, 'daemonsu': true,
'Superuser.apk': true, 'KingoUser.apk': true, 'SuperSu.apk': true,
'magisk': true, 'magisk.db': true, 'magisk.img': true, 'ksud': true,
'qemud': true, 'qemu_pipe': true
};
JavaFile.exists.implementation = function () {
try {
var name = '' + this.getName();
if (blockedNames[name]) {
return false;
}
var path = '' + this.getAbsolutePath();
if (path.indexOf('/su') !== -1 || path.indexOf('magisk') !== -1 || path.indexOf('superuser') !== -1) {
if (path === '/system/xbin/su' || path === '/system/bin/su' || path === '/sbin/su' ||
path === '/vendor/bin/su' || path === '/data/local/xbin/su' || path === '/data/local/bin/su' ||
path === '/data/local/su' || path === '/su/bin/su') {
return false;
}
}
} catch (e) {}
return this.exists();
};
envHooked['File.exists'] = true;
} catch (e) {}
}
if (!envHooked['Build.TAGS']) {
try {
var Build = origUse('android.os.Build');
@@ -675,27 +834,9 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
} catch (e) {}
}
if (!envHooked['File.exists']) {
try {
var JFile = Java.use('java.io.File');
var badNames = {
'su': true, 'busybox': true, 'supersu': true, 'Superuser.apk': true,
'KingoUser.apk': true, 'SuperSu.apk': true, 'magisk': true, 'daemonsu': true
};
JFile.exists.implementation = function () {
try {
var name = '' + this.getName();
if (badNames[name]) return false;
} catch (e) {}
return this.exists();
};
envHooked['File.exists'] = true;
} catch (e) {}
}
if (!envHooked['SystemProperties']) {
try {
var SystemProperties = Java.use('android.os.SystemProperties');
var SystemProperties = useFn('android.os.SystemProperties');
var safe = {
'ro.debuggable': '0',
'ro.secure': '1',
@@ -726,45 +867,21 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
origUse = Java.use.bind(Java);
try { javaLog = origUse('android.util.Log'); } catch (e) {}
// Allow reflection Field.set(null, val) on static final fields (e.g.
// Build.TAGS in fdciabdul/frida-multiple-bypass) to fall back to Frida's
// JNI field setter instead of throwing IllegalAccessException on Android 12+.
try {
var Field = origUse('java.lang.reflect.Field');
Field.set.overload('java.lang.Object', 'java.lang.Object').implementation = function (obj, val) {
try {
return this.set(obj, val);
} catch (e) {
try {
var clsName = '' + this.getDeclaringClass().getName();
var fldName = '' + this.getName();
var w = origUse(clsName);
if (w && w[fldName]) w[fldName].value = val;
} catch (e2) {}
}
};
} catch (e) {}
// Methods that need in-place overload guarding:
// - java.lang.String#contains is blocked completely (causes infinite
// recursion inside frida-java-bridge).
// - File/Runtime/ProcessBuilder/SystemProperties/PackageManager methods
// are guarded with blockAll=false so scripts like fdciabdul/frida-multiple-bypass
// that assign `.implementation` multiple times on the same overload
// update the JS callback in-place without crashing ART.
// Bootstrap methods whose ART trampolines cause infinite recursion or
// extreme slowdowns when replaced on Android 14/15 (API 34/35).
var blockedClassMethods = {
'java.lang.String': ['contains']
};
var guardedClassMethods = {
'java.lang.String': ['contains'],
'java.io.File': ['exists'],
'java.io.UnixFileSystem': ['checkAccess'],
'java.io.BufferedReader': ['readLine'],
'java.io.BufferedReader': ['readLine']
};
var guardedClassMethods = {
'android.os.SystemProperties': ['get'],
'android.app.ApplicationPackageManager': ['getPackageInfo', 'getInstallerPackageName'],
'java.lang.Runtime': ['exec'],
'java.lang.ProcessImpl': ['start'],
'java.lang.ProcessBuilder': ['start'],
'java.lang.ProcessManager': ['exec'],
'android.os.SystemProperties': ['get'],
'android.app.ApplicationPackageManager': ['getPackageInfo']
'java.lang.ProcessManager': ['exec']
};
var hookedJavaOverloads = {};
@@ -808,17 +925,17 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
}
if (typeof m.overload === 'function') {
var origOverload = m.overload.bind(m);
m.overload = function () {
safeSet(m, 'overload', function () {
var args = Array.prototype.slice.call(arguments);
var ov = origOverload.apply(m, args);
return guardOverload(className, methodName, ov, args.join(','), blockAll);
};
});
}
m.__bogusmipsMethodGuarded = true;
safeSet(m, '__bogusmipsMethodGuarded', true);
} catch (e) {}
}
Java.use = function (className) {
var customUse = function (className) {
var name = '' + className;
var cls = origUse(name);
if (!cls) return cls;
@@ -837,35 +954,52 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
return cls;
};
Java.enumerateLoadedClassesSync = function () {
var customEnumerate = function () {
return [];
};
Java.perform = function (fn) {
var customPerform = function (fn) {
try {
origPerform(function () {
try {
fn();
} catch (innerErr) {
console.log('[frida] ' + innerErr);
bogusmipsConsole.log('[frida] ' + innerErr);
}
});
} catch (e) {
console.log('[frida] ' + e);
bogusmipsConsole.log('[frida] ' + e);
}
};
globalThis.setTimeout = function (cb, delay) {
bogusmipsJava = makeDelegate(Java, {
use: customUse,
enumerateLoadedClassesSync: customEnumerate,
perform: customPerform
});
bogusmipsSetTimeout = function (cb, delay) {
var d = (typeof delay === 'number' && delay > 0) ? delay : 0;
if (d === 0) {
if (typeof cb === 'function') {
customPerform(cb);
}
return 0;
}
return origSetTimeout(function () {
if (typeof cb !== 'function') return;
Java.perform(cb);
customPerform(cb);
}, d);
};
safeSet(globalThis, 'setTimeout', bogusmipsSetTimeout);
if (typeof origSetImmediate === 'function') {
globalThis.setImmediate = function (cb) {
return globalThis.setTimeout(cb, 0);
bogusmipsSetImmediate = function (cb) {
if (typeof cb === 'function') {
customPerform(cb);
}
return 0;
};
safeSet(globalThis, 'setImmediate', bogusmipsSetImmediate);
}
}
@@ -875,11 +1009,27 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
for (spec, source) in user_scripts {
let label = spec.replace('\\', "\\\\").replace('\'', "\\'");
// Guard unguarded reflection `.set(null, ...)` on static final Build
// fields and recursive `ProcessImpl.start.apply` in older CodeShare
// scripts (e.g. fdciabdul/frida-multiple-bypass).
let patched = source
.replace(
"TAGS.set(null, \"release-keys\");",
"try { TAGS.set(null, \"release-keys\"); } catch (_e) {}",
)
.replace(
"FINGERPRINT.set(null,",
"if (false) FINGERPRINT.set(null,",
)
.replace(
"ProcessImpl.start.apply(this, arguments)",
"this.start.apply(this, arguments)",
);
out.push_str(&format!(
r#" {{
spec: '{label}',
run: function () {{
{source}
run: function (send, console, setTimeout, setImmediate, Memory, Module, Interceptor, Java) {{
{patched}
}}
}},
"#
@@ -889,7 +1039,7 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
out.push_str(
r#" ];
var delays = [1000, 2000, 3500, 5500, 8000];
var delays = [0, 200, 800, 1800, 3500, 6000];
var attempt = 0;
function bootAfterArt() {
try {
@@ -898,21 +1048,32 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
applyBaselineEnvHooks();
for (var i = 0; i < scripts.length; i++) {
var s = scripts[i];
console.log('[bogusmips:script:start] ' + s.spec);
bogusmipsConsole.log('[bogusmips:script:start] ' + s.spec);
try {
s.run();
console.log('[bogusmips:script:ok] ' + s.spec);
s.run(
bogusmipsSend,
bogusmipsConsole,
bogusmipsSetTimeout,
bogusmipsSetImmediate,
bogusmipsMemory,
bogusmipsModule,
bogusmipsInterceptor,
bogusmipsJava
);
bogusmipsConsole.log('[bogusmips:script:ok] ' + s.spec);
} catch (scriptErr) {
console.log('[bogusmips:script:error] ' + s.spec + ' | ' + scriptErr);
bogusmipsConsole.log('[bogusmips:script:error] ' + s.spec + ' | ' + scriptErr);
}
}
// Re-apply baseline hooks for classes (e.g. RootBeer) loaded lazily.
origSetTimeout(function () {
try { origPerform(applyBaselineEnvHooks); } catch (e) {}
}, 1500);
}, 500);
origSetTimeout(function () {
try { origPerform(applyBaselineEnvHooks); } catch (e) {}
}, 3500);
}, 1800);
origSetTimeout(function () {
try { origPerform(applyBaselineEnvHooks); } catch (e) {}
}, 3800);
});
} catch (artErr) {
attempt += 1;
@@ -920,12 +1081,12 @@ pub fn build_gadget_script(user_scripts: &[(String, String)]) -> String {
origSetTimeout(bootAfterArt, delays[attempt] - delays[attempt - 1]);
} else {
for (var j = 0; j < scripts.length; j++) {
console.log('[bogusmips:script:error] ' + scripts[j].spec + ' | ' + artErr);
bogusmipsConsole.log('[bogusmips:script:error] ' + scripts[j].spec + ' | ' + artErr);
}
}
}
}
origSetTimeout(bootAfterArt, delays[0]);
bootAfterArt();
})();
"#,
);
+29 -2
View File
@@ -144,6 +144,25 @@ impl Status {
self.log(format!("✖ Frida script error ({spec}): {err}"));
return;
}
if trimmed.starts_with('[') && trimmed.starts_with("[bogusmips:") {
self.log(format!("[frida] {trimmed}"));
return;
}
if trimmed.starts_with("{\"type\":\"error\"") {
let err = trimmed
.split("\"description\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or(trimmed)
.to_owned();
for (_, state) in &mut self.scripts {
if !matches!(state, ScriptState::Active { .. }) {
*state = ScriptState::Error(err.clone());
}
}
self.log(format!("✖ Frida runtime error: {err}"));
return;
}
// Any output emitted by the script in the target process confirms the
// script is running and increments its message counter.
@@ -382,7 +401,8 @@ fn run_session(
status
.lock()
.unwrap()
.log("Downloading the frida gadget…".to_owned());
.log("Preparing Frida runtime (frida-inject + gadget)…".to_owned());
let inject = bypass::ensure_inject(&cache, frida_abi)?;
let gadget = bypass::ensure_gadget(&cache, frida_abi)?;
let mut loaded = Vec::with_capacity(config.frida_scripts.len());
for spec in &config.frida_scripts {
@@ -405,7 +425,7 @@ fn run_session(
}
let combined = bypass::build_gadget_script(&loaded);
emulator.clear_logcat();
let res = emulator.install_frida_script(&gadget, package, &combined)?;
let res = emulator.install_frida_script(&inject, &gadget, package, &combined)?;
{
let mut s = status.lock().unwrap();
for (canonical, _) in &loaded {
@@ -418,6 +438,13 @@ fn run_session(
step!(Phase::InstallingBypass, &label, install_frida());
}
step!(Phase::Launching, "Launching the app", emulator.launch(package));
if needs_frida {
step!(
Phase::InstallingBypass,
"Attaching Frida runtime to app process",
emulator.attach_frida_script(package)
);
}
}
set_phase(Phase::Capturing, "Capturing traffic");
+131 -48
View File
@@ -308,15 +308,32 @@ impl Emulator {
/// Switches adbd to root and remounts the system partition.
pub fn enable_root(&self) -> Step {
let out = self.adb(&["root"])?;
if out.to_lowercase().contains("cannot run as root") {
return Err("this emulator image cannot be rooted (use a non-Play-Store image)".into());
}
// adbd restarts; wait for it to come back.
std::thread::sleep(Duration::from_secs(2));
let _ = self.adb(&["wait-for-device"]);
let _ = self.adb(&["remount"]);
Ok("root enabled".into())
let mut last_err = String::new();
for attempt in 0..3 {
match self.adb(&["root"]) {
Ok(out) => {
if out.to_lowercase().contains("cannot run as root") {
return Err(
"this emulator image cannot be rooted (use a non-Play-Store image)"
.into(),
);
}
std::thread::sleep(Duration::from_secs(2));
let _ = self.adb(&["wait-for-device"]);
let _ = self.adb(&["remount"]);
return Ok("root enabled".into());
}
Err(e) => {
last_err = e;
if attempt + 1 < 3 {
std::thread::sleep(Duration::from_secs(1));
let _ = self.adb(&["wait-for-device"]);
}
}
}
}
Err(last_err)
}
/// Pushes the CA (PEM) and installs it as a system trust anchor.
@@ -356,7 +373,13 @@ impl Emulator {
} else {
args.push("install".into());
}
args.extend(["-r".into(), "-t".into(), "-g".into()]);
args.extend([
"-r".into(),
"-t".into(),
"-g".into(),
"-i".into(),
"com.android.vending".into(),
]);
for apk in apks {
args.push(apk.to_string_lossy().into_owned());
}
@@ -403,41 +426,56 @@ impl Emulator {
.map(|s| s.trim().to_owned())
}
/// Pushes `gadget_local`, its config, and the assembled `script_js` for
/// the user-selected Frida scripts, then sets `wrap.<package>` so
/// `frida-gadget` is `LD_PRELOAD`ed on app launch.
/// Pushes `inject_local` (`frida-inject`), `gadget_local` (`frida-gadget`),
/// and the assembled `script_js` to the device, prepares the device
/// environment, and arms a background injector for `package`.
pub fn install_frida_script(
&self,
inject_local: &Path,
gadget_local: &Path,
package: &str,
script_js: &str,
) -> Step {
const INJECT: &str = "/data/local/tmp/bogusmips-frida-inject";
const GADGET: &str = "/data/local/tmp/bogusmips-gadget.so";
const SCRIPT: &str = "/data/local/tmp/bogusmips-bypass.js";
// frida accepts either form; push both so it is found regardless.
const CONFIG: &str = "/data/local/tmp/bogusmips-gadget.config";
const CONFIG_SO: &str = "/data/local/tmp/bogusmips-gadget.config.so";
const ARM_SCRIPT: &str = "/data/local/tmp/bogusmips_frida_arm.sh";
const ATTACH_SCRIPT: &str = "/data/local/tmp/bogusmips_frida_attach.sh";
let local = gadget_local.to_string_lossy();
self.adb(&["push", &local, GADGET])?;
let inject_str = inject_local.to_string_lossy();
self.adb(&["push", &inject_str, INJECT])?;
let gadget_str = gadget_local.to_string_lossy();
self.adb(&["push", &gadget_str, GADGET])?;
self.push_bytes(script_js.as_bytes(), SCRIPT)?;
let cfg = bypass::gadget_config(SCRIPT);
self.push_bytes(cfg.as_bytes(), CONFIG)?;
self.push_bytes(cfg.as_bytes(), CONFIG_SO)?;
let attach_sh = frida_attach_script(package);
self.push_bytes(attach_sh.as_bytes(), ATTACH_SCRIPT)?;
let arm_sh = frida_arm_script(package);
self.push_bytes(arm_sh.as_bytes(), ARM_SCRIPT)?;
let out = self
.adb(&["shell", "su", "root", "sh", ARM_SCRIPT])
.or_else(|_| self.adb(&["shell", "sh", ARM_SCRIPT]))?;
if out.contains("ARMED") {
Ok("Frida gadget armed".into())
Ok("Frida runtime armed".into())
} else {
Err(format!("could not arm Frida gadget: {out}"))
Err(format!("could not arm Frida runtime: {out}"))
}
}
/// Attaches `frida-inject` to the running `package` process immediately
/// after launch so scripts execute in the live ART runtime.
pub fn attach_frida_script(&self, package: &str) -> Step {
const ATTACH_SCRIPT: &str = "/data/local/tmp/bogusmips_frida_attach.sh";
let _ = package;
self.adb(&["shell", "su", "root", "sh", ATTACH_SCRIPT])
.or_else(|_| self.adb(&["shell", "sh", ATTACH_SCRIPT]))
}
/// Clears the device's logcat ring buffer and on-device Frida telemetry log
/// before launching the instrumented app so subsequent
/// [`Self::frida_log_lines`] calls only return fresh output.
@@ -472,10 +510,9 @@ impl Emulator {
}
}
/// Shell script executed as `root` on the device to label `/data/local/tmp`
/// and the Frida gadget files so `untrusted_app` can traverse `/data/local/tmp`,
/// map `bogusmips-gadget.so`, read its config/script, and append telemetry to
/// [`bypass::FRIDA_DEVICE_LOG_PATH`].
/// Shell script executed as `root` on the device to set up the Frida runtime
/// environment (`/data/local/tmp/bogusmips-frida-inject`, gadget, script, and
/// telemetry log) and hide `/system/xbin/su` from app mount namespaces.
pub fn frida_arm_script(package: &str) -> String {
let prop = format!("wrap.{package}");
let prop_short = if prop.len() > 31 {
@@ -484,44 +521,88 @@ pub fn frida_arm_script(package: &str) -> String {
None
};
let short_cmd = prop_short
.map(|s| format!("setprop \"{s}\" \"LD_PRELOAD=/data/local/tmp/bogusmips-gadget.so\" 2>/dev/null\n"))
.map(|s| format!("setprop \"{s}\" \"\" 2>/dev/null\n"))
.unwrap_or_default();
let log_path = bypass::FRIDA_DEVICE_LOG_PATH;
format!(
r#"GADGET=/data/local/tmp/bogusmips-gadget.so
r#"INJECT=/data/local/tmp/bogusmips-frida-inject
GADGET=/data/local/tmp/bogusmips-gadget.so
SCRIPT=/data/local/tmp/bogusmips-bypass.js
CONFIG=/data/local/tmp/bogusmips-gadget.config
CONFIG_SO=/data/local/tmp/bogusmips-gadget.config.so
LOG={log_path}
EMPTY_XBIN=/data/local/tmp/bogusmips-empty-xbin
killall -9 bogusmips-frida-inject 2>/dev/null
: > "$LOG"
chmod 777 /data/local/tmp
chmod 755 "$GADGET"
chmod 755 "$INJECT" "$GADGET"
chmod 644 "$SCRIPT" "$CONFIG" "$CONFIG_SO"
chmod 666 "$LOG"
# On Android 14/15, untrusted_app is denied `search` on shell_data_file
# directories; relabel /data/local/tmp to system_data_file, the gadget
# payloads to system_file, and the telemetry log to apk_data_file.
chcon u:object_r:system_data_file:s0 /data/local/tmp 2>/dev/null
chcon u:object_r:system_file:s0 "$GADGET" "$SCRIPT" "$CONFIG" "$CONFIG_SO" 2>/dev/null
chcon u:object_r:system_file:s0 "$INJECT" "$GADGET" "$SCRIPT" "$CONFIG" "$CONFIG_SO" 2>/dev/null
chcon u:object_r:apk_data_file:s0 "$LOG" 2>/dev/null
setenforce 0 2>/dev/null
# Prevent unspecialized app process (USAP) pool from bypassing wrap.<package>.
setprop persist.device_config.runtime_native.usap_pool_enabled false 2>/dev/null
setprop persist.sys.usap_pool_enabled false 2>/dev/null
# Hide /system/xbin/su inside zygote and app mount namespaces while keeping
# /proc/1/ns/mnt (init + adbd) untouched so `adb shell su root` keeps working.
mkdir -p -m 755 "$EMPTY_XBIN"
chown root:root "$EMPTY_XBIN" 2>/dev/null
chcon u:object_r:system_file:s0 "$EMPTY_XBIN" 2>/dev/null
INIT_NS=$(readlink /proc/1/ns/mnt 2>/dev/null)
SEEN=" $INIT_NS "
for P in /proc/[0-9]*; do
NS=$(readlink "$P/ns/mnt" 2>/dev/null)
[ -z "$NS" ] && continue
case "$SEEN" in *" $NS "*) continue;; esac
SEEN="$SEEN$NS "
nsenter -t "${{P#/proc/}}" -m -- mount --bind "$EMPTY_XBIN" /system/xbin 2>/dev/null
done
# Hide /system/xbin/su from untrusted_app (other) while keeping root:shell
# access intact for `adb shell su root`.
chown root:shell /system/xbin 2>/dev/null
chmod 750 /system/xbin 2>/dev/null
setprop "{prop}" "LD_PRELOAD=$GADGET"
{short_cmd}am force-stop "{package}" 2>/dev/null
# Clear any wrap.<package> property so the app forks cleanly from Zygote
# instead of entering WrapperInit (which triggers seccomp SIGSYS in idmap2).
setprop "{prop}" "" 2>/dev/null
{short_cmd}pm set-installer "{package}" com.android.vending 2>/dev/null
am force-stop "{package}" 2>/dev/null
echo ARMED"#
)
}
/// Shell script executed as `root` right after launching `package` to attach
/// `frida-inject` to the running app process and stream script output to
/// [`bypass::FRIDA_DEVICE_LOG_PATH`].
pub fn frida_attach_script(package: &str) -> String {
let log_path = bypass::FRIDA_DEVICE_LOG_PATH;
format!(
r#"INJECT=/data/local/tmp/bogusmips-frida-inject
SCRIPT=/data/local/tmp/bogusmips-bypass.js
LOG={log_path}
PKG="{package}"
PID=""
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do
PID=$(pidof "$PKG" 2>/dev/null)
for P in $PID; do
PID="$P"
break
done
if [ -n "$PID" ]; then
break
fi
sleep 0.1
done
if [ -z "$PID" ]; then
echo "[bogusmips:script:error] $PKG | target process did not start" >> "$LOG"
exit 1
fi
setenforce 0 2>/dev/null
"$INJECT" -p "$PID" -s "$SCRIPT" --eternalize >> "$LOG" 2>&1
RC=$?
echo "ATTACHED:$PID:RC=$RC""#
)
}
impl Drop for Emulator {
fn drop(&mut self) {
self.clear_proxy();
@@ -619,15 +700,17 @@ mod tests {
}
#[test]
fn frida_arm_script_labels_tmp_dir_and_sets_wrap_prop() {
let script = frida_arm_script("de.clara.sharing.app");
assert!(script.contains("chcon u:object_r:system_data_file:s0 /data/local/tmp"));
assert!(script.contains("chcon u:object_r:system_file:s0"));
assert!(script.contains("setenforce 0"));
assert!(script.contains("usap_pool_enabled false"));
assert!(script.contains("chmod 750 /system/xbin"));
assert!(script.contains("setprop \"wrap.de.clara.sharing.app\""));
assert!(script.contains("echo ARMED"));
fn frida_arm_and_attach_scripts_setup_runtime_and_hide_su() {
let arm = frida_arm_script("de.clara.sharing.app");
assert!(arm.contains("chcon u:object_r:system_data_file:s0 /data/local/tmp"));
assert!(arm.contains("chcon u:object_r:system_file:s0"));
assert!(arm.contains("mount --bind \"$EMPTY_XBIN\" /system/xbin"));
assert!(arm.contains("setprop \"wrap.de.clara.sharing.app\" \"\""));
assert!(arm.contains("echo ARMED"));
let attach = frida_attach_script("de.clara.sharing.app");
assert!(attach.contains("bogusmips-frida-inject"));
assert!(attach.contains("ATTACHED:"));
}
#[test]