feat: deliver first successful FridaBox MVP

Integrate Frida Gadget into BlackBox guest startup, add the host workflow and controller tooling, and validate the complete sample hook flow on ARM64 Android 16.
This commit is contained in:
Mahdi Karzari
2026-07-20 04:25:08 +03:30
parent 89b59836c6
commit 5140bfc6a7
56 changed files with 8573 additions and 104 deletions
+5
View File
@@ -17,8 +17,13 @@
*.logcat
.vscode
/build
**/build/
/captures
.externalNativeBuild
.cxx
local.properties
/app/release/
__pycache__/
*.pyc
node_modules/
.npm-cache/
+57
View File
@@ -0,0 +1,57 @@
# Architecture
FridaBox extends the existing BlackBox virtual package, process, Binder, file,
identity, signature, and lifecycle implementation. It does not introduce a
second plugin framework and never creates a replacement `DexClassLoader` for a
guest.
```text
Host launcher process
|
| import original APK
v
BlackBox virtual package manager
|
| allocate :pN virtual process
v
BActivityThread.handleBindApplication()
|
| create guest Context / LoadedApk / ClassLoader
| initialize virtual runtime and IO redirection
| populate GuestRuntimeRegistry
| load Frida Gadget and wait
v
Frida controller attaches
|
| select guest ClassLoader
| install Java/native hooks
v
Guest makeApplication()
|
v
Guest Application.onCreate()
|
v
Guest Activity
```
## Runtime boundary
`GuestRuntimeRegistry` is volatile, process-local state. Each normal BlackBox
virtual process populates it after `VirtualRuntime.setupRuntime`, `NativeCore.init`,
and IO redirection, using the `LoadedApk` ClassLoader. `FridaGadgetLoader` then
performs one synchronized `System.loadLibrary("frida-gadget")` attempt in that
Linux process. The static Gadget configuration pauses that call until a
controller connects. Only afterward does BlackBox call `makeApplication`.
The host status screen uses a small SharedPreferences snapshot because the host
launcher cannot directly read another process's static registry. The Frida
controller reads the authoritative process-local registry through Java.
## Import boundary
The host copies a document-provider stream once into `files/imported-apks`,
computes SHA-256 while copying, inspects the ZIP ABI entries, parses package
metadata, verifies the stored hash, marks the file read-only, and passes the path
only to BlackBox's virtual package manager. No real package installer Intent or
PackageInstaller session is used.
+9 -4
View File
@@ -1,9 +1,15 @@
apply plugin: 'com.android.library'
def fridaBoxNdkProjectDir = System.getenv('FRIDABOX_NDK_PROJECT_DIR') ?: project.projectDir.absolutePath
if (System.getenv('FRIDABOX_NDK_PROJECT_DIR')) {
layout.buildDirectory.set(file("${fridaBoxNdkProjectDir}/build"))
}
android {
namespace 'top.niunaijun.blackbox'
compileSdk rootProject.ext.compileSdkVersion
ndkVersion = "29.0.14206865"
aidlPackagedList "android/app/IServiceConnection.aidl"
@@ -19,8 +25,7 @@ android {
consumerProguardFiles "consumer-rules.pro"
ndk {
abiFilters 'arm64-v8a' , 'armeabi-v7a'
abiFilters 'arm64-v8a'
}
}
@@ -33,9 +38,8 @@ android {
externalNativeBuild {
ndkBuild {
path 'src/main/cpp/Android.mk'
path file("${fridaBoxNdkProjectDir}/src/main/cpp/Android.mk")
}
ndkVersion = "29.0.13846066"
}
compileOptions {
@@ -106,6 +110,7 @@ dependencies {
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
implementation 'com.github.tiann:FreeReflection:3.2.2'
testImplementation libs.junit
+2
View File
@@ -26,3 +26,5 @@
-keep class android.** {*; }
-keep class com.android.** {*; }
-keep class top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry { public static *; }
-keep class top.niunaijun.blackbox.instrumentation.FridaGadgetLoader { public static *; }
@@ -85,6 +85,10 @@ import top.niunaijun.blackbox.utils.compat.BuildCompat;
import top.niunaijun.blackbox.utils.compat.ContextCompat;
import top.niunaijun.blackbox.utils.compat.StrictModeCompat;
import top.niunaijun.blackbox.core.system.JarManager;
import top.niunaijun.blackbox.instrumentation.FridaGadgetLoader;
import top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry;
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings;
import top.niunaijun.blackbox.instrumentation.InstrumentationStatusStore;
public class BActivityThread extends IBActivityThread.Stub {
@@ -403,6 +407,21 @@ public class BActivityThread extends IBActivityThread.Stub {
assert packageContext != null;
IOCore.get().enableRedirect(packageContext);
ClassLoader guestClassLoader = BRLoadedApk.get(loadedApk).getClassLoader();
boolean instrumentationEnabled = InstrumentationSettings.isEnabledForPackage(packageName);
GuestRuntimeRegistry.initialize(packageName, processName, getUserId(), getAppPid(),
applicationInfo, guestClassLoader, instrumentationEnabled);
InstrumentationStatusStore.recordBinding();
if (guestClassLoader != null) {
try {
Thread.currentThread().setContextClassLoader(guestClassLoader);
} catch (SecurityException error) {
GuestRuntimeRegistry.setLastError(error);
Slog.w(TAG, "Unable to set guest context ClassLoader: " + error.getMessage());
}
}
FridaGadgetLoader.loadIfEnabled();
AppBindData bindData = new AppBindData();
bindData.appInfo = applicationInfo;
bindData.processName = processName;
@@ -0,0 +1,50 @@
package top.niunaijun.blackbox.instrumentation;
import android.util.Log;
import java.util.concurrent.atomic.AtomicBoolean;
/** Loads Frida Gadget at most once in the current Linux process. */
public final class FridaGadgetLoader {
private static final String TAG = "FridaBox.Gadget";
private static final AtomicBoolean ATTEMPTED = new AtomicBoolean(false);
private static final Object LOAD_LOCK = new Object();
private static volatile boolean loaded;
private FridaGadgetLoader() {
}
public static boolean loadIfEnabled() {
if (!GuestRuntimeRegistry.isInstrumentationEnabled()) {
Log.i(TAG, "Instrumentation disabled for this guest process");
return false;
}
if (loaded) return true;
synchronized (LOAD_LOCK) {
if (loaded) return true;
if (!ATTEMPTED.compareAndSet(false, true)) return false;
try {
InstrumentationStatusStore.recordBinding();
Log.i(TAG, "Loading Frida Gadget for " + GuestRuntimeRegistry.getGuestProcessName());
System.loadLibrary("frida-gadget");
loaded = true;
InstrumentationStatusStore.recordLoaded();
Log.i(TAG, "Frida Gadget loaded");
return true;
} catch (UnsatisfiedLinkError | SecurityException error) {
GuestRuntimeRegistry.setLastError(error);
InstrumentationStatusStore.recordError(GuestRuntimeRegistry.getLastError());
Log.e(TAG, "Frida Gadget load failed; guest will continue", error);
} catch (Throwable error) {
GuestRuntimeRegistry.setLastError(error);
InstrumentationStatusStore.recordError(GuestRuntimeRegistry.getLastError());
Log.e(TAG, "Unexpected Frida Gadget initialization failure; guest will continue", error);
}
return false;
}
}
public static boolean isLoaded() {
return loaded;
}
}
@@ -0,0 +1,88 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.pm.ApplicationInfo;
/** Process-local guest metadata exposed to Frida scripts. */
public final class GuestRuntimeRegistry {
private static volatile String guestPackageName;
private static volatile String guestProcessName;
private static volatile int guestUserId = -1;
private static volatile int virtualProcessId = -1;
private static volatile ApplicationInfo guestApplicationInfo;
private static volatile ClassLoader guestClassLoader;
private static volatile String guestSourceDir;
private static volatile boolean instrumentationEnabled;
private static volatile String lastError;
private static volatile long initializationTimestamp;
private GuestRuntimeRegistry() {
}
public static synchronized void initialize(String packageName, String processName,
int userId, int processId,
ApplicationInfo applicationInfo,
ClassLoader classLoader,
boolean enabled) {
guestPackageName = packageName;
guestProcessName = processName;
guestUserId = userId;
virtualProcessId = processId;
guestApplicationInfo = applicationInfo;
guestClassLoader = classLoader;
guestSourceDir = applicationInfo == null ? null : applicationInfo.sourceDir;
instrumentationEnabled = enabled;
lastError = null;
initializationTimestamp = System.currentTimeMillis();
}
public static synchronized void clear() {
guestPackageName = null;
guestProcessName = null;
guestUserId = -1;
virtualProcessId = -1;
guestApplicationInfo = null;
guestClassLoader = null;
guestSourceDir = null;
instrumentationEnabled = false;
lastError = null;
initializationTimestamp = 0L;
}
public static String getGuestPackageName() { return guestPackageName; }
public static String getGuestProcessName() { return guestProcessName; }
public static int getGuestUserId() { return guestUserId; }
public static int getVirtualProcessId() { return virtualProcessId; }
public static ApplicationInfo getGuestApplicationInfo() { return guestApplicationInfo; }
public static ClassLoader getGuestClassLoader() { return guestClassLoader; }
public static String getGuestSourceDir() { return guestSourceDir; }
public static boolean isInstrumentationEnabled() { return instrumentationEnabled; }
public static String getLastError() { return lastError; }
public static long getInitializationTimestamp() { return initializationTimestamp; }
public static void setLastError(Throwable error) {
lastError = error == null ? null : error.getClass().getSimpleName() + ": " + error.getMessage();
}
public static void setLastError(String error) {
lastError = error;
}
private static String quote(String value) {
if (value == null) return "null";
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\n", "\\n").replace("\r", "\\r") + "\"";
}
public static String describe() {
return "{" +
"\"package\":" + quote(guestPackageName) + ',' +
"\"process\":" + quote(guestProcessName) + ',' +
"\"userId\":" + guestUserId + ',' +
"\"virtualProcessId\":" + virtualProcessId + ',' +
"\"sourceDir\":" + quote(guestSourceDir) + ',' +
"\"instrumentationEnabled\":" + instrumentationEnabled + ',' +
"\"lastError\":" + quote(lastError) + ',' +
"\"initializedAt\":" + initializationTimestamp +
'}';
}
}
@@ -0,0 +1,54 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import android.content.SharedPreferences;
import top.niunaijun.blackbox.BlackBoxCore;
/** Shared host preferences read independently by each virtual process. */
public final class InstrumentationSettings {
public static final String PREFERENCES = "fridabox_instrumentation";
public static final String KEY_ENABLED = "instrumentation_enabled";
public static final String KEY_BASE_PORT = "frida_base_port";
public static final String KEY_SCAN_COUNT = "frida_port_scan_count";
public static final String KEY_ADVANCED_LOGS = "show_advanced_logs";
private static final String PACKAGE_PREFIX = "package_enabled_";
private InstrumentationSettings() {
}
private static SharedPreferences preferences() {
Context context = BlackBoxCore.getContext();
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
}
public static boolean isGloballyEnabled() {
return preferences().getBoolean(KEY_ENABLED, true);
}
public static boolean isEnabledForPackage(String packageName) {
SharedPreferences preferences = preferences();
return preferences.getBoolean(KEY_ENABLED, true)
&& preferences.getBoolean(PACKAGE_PREFIX + packageName, true);
}
public static void setEnabledForPackage(String packageName, boolean enabled) {
preferences().edit().putBoolean(PACKAGE_PREFIX + packageName, enabled).commit();
}
public static int getBasePort() {
return clamp(preferences().getInt(KEY_BASE_PORT, 27042), 1024, 65535, 27042);
}
public static int getPortScanCount() {
return clamp(preferences().getInt(KEY_SCAN_COUNT, 32), 1, 128, 32);
}
public static boolean showAdvancedLogs() {
return preferences().getBoolean(KEY_ADVANCED_LOGS, false);
}
static int clamp(int value, int minimum, int maximum, int fallback) {
return value < minimum || value > maximum ? fallback : value;
}
}
@@ -0,0 +1,40 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import android.content.SharedPreferences;
import top.niunaijun.blackbox.BlackBoxCore;
/** Small cross-process status snapshot for the host runtime screen. */
public final class InstrumentationStatusStore {
private InstrumentationStatusStore() {
}
private static SharedPreferences preferences() {
return BlackBoxCore.getContext().getSharedPreferences(
InstrumentationSettings.PREFERENCES, Context.MODE_PRIVATE);
}
public static void recordBinding() {
String packageName = GuestRuntimeRegistry.getGuestPackageName();
preferences().edit()
.putString("runtime_package", packageName)
.putString("runtime_process", GuestRuntimeRegistry.getGuestProcessName())
.putInt("runtime_vpid", GuestRuntimeRegistry.getVirtualProcessId())
.putString("runtime_source", GuestRuntimeRegistry.getGuestSourceDir())
.putBoolean("runtime_enabled", GuestRuntimeRegistry.isInstrumentationEnabled())
.putString("runtime_state", GuestRuntimeRegistry.isInstrumentationEnabled()
? "waiting_for_attach" : "disabled")
.putString("runtime_error", null)
.putLong("runtime_timestamp", GuestRuntimeRegistry.getInitializationTimestamp())
.commit();
}
public static void recordLoaded() {
preferences().edit().putString("runtime_state", "loaded").putString("runtime_error", null).commit();
}
public static void recordError(String error) {
preferences().edit().putString("runtime_state", "failed").putString("runtime_error", error).commit();
}
}
@@ -0,0 +1,44 @@
package top.niunaijun.blackbox.instrumentation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import android.content.pm.ApplicationInfo;
import org.junit.After;
import org.junit.Test;
public class GuestRuntimeRegistryTest {
@After
public void clearRegistry() {
GuestRuntimeRegistry.clear();
}
@Test
public void initializeReplacesProcessLocalSnapshot() {
ApplicationInfo info = new ApplicationInfo();
info.sourceDir = "/private/original.apk";
ClassLoader loader = getClass().getClassLoader();
GuestRuntimeRegistry.initialize("sample.one", "sample.one:remote", 3, 7, info, loader, true);
assertEquals("sample.one", GuestRuntimeRegistry.getGuestPackageName());
assertEquals("sample.one:remote", GuestRuntimeRegistry.getGuestProcessName());
assertEquals(3, GuestRuntimeRegistry.getGuestUserId());
assertEquals(7, GuestRuntimeRegistry.getVirtualProcessId());
assertSame(loader, GuestRuntimeRegistry.getGuestClassLoader());
assertEquals("/private/original.apk", GuestRuntimeRegistry.getGuestSourceDir());
assertTrue(GuestRuntimeRegistry.isInstrumentationEnabled());
assertTrue(GuestRuntimeRegistry.describe().contains("\"package\":\"sample.one\""));
}
@Test
public void clearRemovesPriorGuest() {
GuestRuntimeRegistry.initialize("sample", "sample", 0, 1, null, null, true);
GuestRuntimeRegistry.clear();
assertEquals(null, GuestRuntimeRegistry.getGuestPackageName());
assertEquals(-1, GuestRuntimeRegistry.getGuestUserId());
assertFalse(GuestRuntimeRegistry.isInstrumentationEnabled());
}
}
+28 -82
View File
@@ -1,89 +1,35 @@
# BlackBox - Virtual Engine
# FridaBox
<p align="center">
<img src="assets/usage.gif" alt="BlackBox Banner" width="100%"/>
</p>
FridaBox is an authorized mobile-security research MVP that runs an original,
unmodified APK inside BlackBox virtual processes and loads Frida Gadget before
the guest `Application` is created. It requires no root, frida-server, Magisk,
Zygisk, system-image changes, real PackageManager installation, APK patching,
repacking, or resigning.
BlackBox is a virtual engine that allows you to clone and run virtual applications on Android devices without installing APKs. This project works on Android 5.0 to 14.0+ and supports multiple architectures (ARM64, ARMv7, x86).
The foundation is `ALEX5402/NewBlackbox` commit
`89b59836c66f173756a4ae258cf379a957649820`. The host application ID is
`com.qm4rs.fridabox`; existing engine namespaces remain unchanged.
## Overview
## MVP capabilities
This enhanced edition includes bug fixes, stability improvements, and Android 14+ compatibility tailored for modern devices.
- SAF import of one base APK into app-private, read-only storage.
- SHA-256 verification before and after BlackBox virtual installation.
- ARM64 native-library inspection; pure Java/Kotlin guests are accepted and
native guests without `arm64-v8a` are rejected.
- Per-guest instrumented or non-instrumented launches with virtual process stop
before mode changes.
- Frida Gadget 17.16.0 bound to loopback, default port 27042, with conflict
fallback and `on_load=wait` for pre-`Application.onCreate()` hooks.
- Process-local guest registry and controller-side ClassLoader selection.
- Debug sample guest proving `Target.add(2, 3)` can be replaced with `1337`.
- Reproducibly bundled Frida 17 Java agents using pinned `frida-java-bridge`
7.0.13 and `frida-compile` 19.0.5.
### Key Features
Start with [docs/BUILDING.md](docs/BUILDING.md), [docs/USAGE.md](docs/USAGE.md),
and [docs/FRIDA_CONNECTION.md](docs/FRIDA_CONNECTION.md).
* **Virtual App Cloning**: Run multiple instances of applications.
* **Sandboxed Environment**: Isolated process execution.
* **No Root Required**: Runs entirely in userspace.
* **Multi-Architecture**: Support for 32-bit and 64-bit apps.
* **Device Spoofing**: Modify device information for virtual apps.
* **Fake Location**: Spoof GPS coordinates.
FridaBox is not undetectable. See [docs/DETECTION_SURFACES.md](docs/DETECTION_SURFACES.md)
and [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
## Requirements
* **Android Version**: Android 5.0 (API 21) or higher.
* **RAM**: 2GB minimum recommended.
* **Architecture**: ARMv7a, ARM64-v8a, x86.
## Build Instructions
### Prerequisites
* Android Studio (Arctic Fox or newer)
* JDK 17
* Android SDK 34+
* NDK (Version 29.0.13846066)
### Building from Source
```bash
# Clone the repository
git clone https://github.com/your-repo/NewBlackbox.git
cd NewBlackbox
# Build Debug APK
./gradlew assembleDebug
# Build Release APK
./gradlew assembleRelease
```
## Integration
To use BlackBox Core in your own project, add the AAR dependency:
```gradle
dependencies {
implementation fileTree(dir: "libs", include: ["*.aar"])
}
```
Refer to `Docs.md` for detailed API documentation.
## Troubleshooting
* **App Crashes**: Check logcat for UID mismatches or permission errors.
* **Installation Failures**: Verify potential architecture mismatches or storage permissions.
* **Android 15**: Ensure you are using the latest build which handles stricter security policies.
## Credits
* **Main Developer**: ALEX502
* **Original Framework**: VirtualApp, VirtualAPK
* **Native Hooks**: Dobby, xDL
* **Reflection**: BlackReflection, FreeReflection
## License
Copyright 2022 BlackBox
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
The complete Android 16 device transcript is in
[docs/device-validation.log](docs/device-validation.log).
+41
View File
@@ -0,0 +1,41 @@
# Third-party notices
## NewBlackbox / BlackBox
FridaBox is based on `ALEX5402/NewBlackbox` commit
`89b59836c66f173756a4ae258cf379a957649820`. The foundation repository includes
an Apache License 2.0 notice; see the repository root `LICENSE` file.
## Frida Gadget 17.16.0
- Origin: `https://github.com/frida/frida/releases/tag/17.16.0`
- Asset: `frida-gadget-17.16.0-android-arm64.so.xz`
- Installed filename: `libfrida-gadget.so`
- SHA-256 after XZ decompression:
`6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e`
- License: wxWindows Library Licence, Version 3.1, as stated by the official
Frida 17.16.0 `COPYING` file.
The wxWindows licence permits redistribution/modification under GNU Library
General Public Licence version 2 or later and includes an exception permitting
binary object code versions of works based on the library to be used, copied,
linked, modified, and distributed under the distributor's own terms. The full
authoritative text is available at
`https://github.com/frida/frida/blob/17.16.0/COPYING`.
Frida is copyright its respective contributors. FridaBox makes no claim of
ownership over Frida Gadget.
## Frida Java bridge and agent compiler
The compiled JavaScript agents under `scripts/dist/` include
`frida-java-bridge` 7.0.13. They are built with `frida-compile` 19.0.5.
- Java bridge origin: `https://github.com/frida/frida-java-bridge`
- Compiler origin: `https://github.com/frida/frida-compile`
- License: LGPL-2.0 with the wxWindows Library Licence 3.1 exception, as
declared by the respective official packages.
The authoritative licence text and exception are the same wxWindows Library
Licence described above. Exact package versions and dependency integrity hashes
are preserved in `package-lock.json`.
+90 -5
View File
@@ -7,9 +7,9 @@ android {
namespace 'top.niunaijun.blackboxa'
compileSdk rootProject.ext.compileSdkVersion
ndkVersion = "29.0.13846066"
ndkVersion = "29.0.14206865"
defaultConfig {
applicationId "top.niunaijun.blackbox"
applicationId "com.qm4rs.fridabox"
minSdk rootProject.ext.minSdk
targetSdk rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
@@ -22,8 +22,8 @@ android {
enable true
reset()
include 'armeabi-v7a', "arm64-v8a"
universalApk true
include "arm64-v8a"
universalApk false
}
}
}
@@ -41,6 +41,10 @@ android {
}
buildFeatures {
viewBinding true
buildConfig true
}
sourceSets {
debug.assets.srcDir("$buildDir/generated/demoGuestAssets")
}
kotlinOptions {
jvmTarget = '21'
@@ -48,6 +52,8 @@ android {
packaging {
jniLibs {
excludes.add("**/libandroidx.graphics.path.so")
useLegacyPackaging true
keepDebugSymbols.add("**/libfrida-gadget.config.so")
}
resources {
excludes.add("**/libandroidx.graphics.path.so")
@@ -55,9 +61,88 @@ android {
}
applicationVariants.configureEach { variant ->
variant.outputs.configureEach { output ->
output.outputFileName = "BlackBox_${variant.versionName}_${output.baseName}.apk"
output.outputFileName = "FridaBox_${variant.versionName}_${output.baseName}.apk"
}
}
lint {
abortOnError false
checkReleaseBuilds false
}
}
tasks.register("copyDemoGuestDebug", Copy) {
dependsOn(":sample-guest:assembleDebug")
from(project(":sample-guest").layout.buildDirectory.file("outputs/apk/debug/sample-guest-debug.apk"))
into(layout.buildDirectory.dir("generated/demoGuestAssets/demo"))
rename { "sample-guest.apk" }
}
tasks.matching { it.name == "mergeDebugAssets" }.configureEach { dependsOn("copyDemoGuestDebug") }
tasks.matching { it.name.toLowerCase().contains("debug") && it.name.toLowerCase().contains("lint") }
.configureEach { dependsOn("copyDemoGuestDebug") }
def fridaGadget = file("src/main/jniLibs/arm64-v8a/libfrida-gadget.so")
def fridaGadgetConfig = file("src/main/jniLibs/arm64-v8a/libfrida-gadget.config.so")
tasks.register("verifyFridaGadget") {
inputs.files(fridaGadget, fridaGadgetConfig)
doLast {
if (!fridaGadget.isFile()) {
throw new GradleException("Frida Gadget 17.16.0 is missing. Run: python tools/fetch_frida_gadget.py")
}
if (!fridaGadgetConfig.isFile()) {
throw new GradleException("Frida Gadget configuration is missing: ${fridaGadgetConfig}")
}
def digest = java.security.MessageDigest.getInstance("SHA-256")
.digest(fridaGadget.bytes).collect { String.format("%02x", it & 0xff) }.join()
if (digest != "6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e") {
throw new GradleException("Frida Gadget 17.16.0 SHA-256 mismatch: ${digest}")
}
def config = new groovy.json.JsonSlurper().parse(fridaGadgetConfig)
if (config.interaction?.address != "127.0.0.1" || config.interaction?.on_load != "wait") {
throw new GradleException("Unsafe or invalid Frida Gadget configuration")
}
}
}
tasks.matching { it.name == "preBuild" }.configureEach { dependsOn("verifyFridaGadget") }
tasks.register("verifyDebugApkFridaPackaging") {
dependsOn("assembleDebug")
doLast {
def apk = fileTree("$buildDir/outputs/apk/debug").matching { include("*.apk") }.singleFile
def entries = zipTree(apk).matching {
include("lib/arm64-v8a/libfrida-gadget.so")
include("lib/arm64-v8a/libfrida-gadget.config.so")
}.files.collect { it.name }.toSet()
if (!entries.contains("libfrida-gadget.so") || !entries.contains("libfrida-gadget.config.so")) {
throw new GradleException("Debug APK does not contain both Frida Gadget files")
}
}
}
tasks.register("verifyInstrumentationOrdering") {
doLast {
def source = project(":Bcore").file("src/main/java/top/niunaijun/blackbox/app/BActivityThread.java").text
def loader = source.indexOf("FridaGadgetLoader.loadIfEnabled()")
def makeApplication = source.indexOf("makeApplication(", loader)
if (loader < 0 || makeApplication < 0 || loader >= makeApplication) {
throw new GradleException("Frida Gadget loader must execute before guest makeApplication")
}
}
}
tasks.register("verifyDemoGuestUnmodified") {
dependsOn("copyDemoGuestDebug")
doLast {
def source = project(":sample-guest").layout.buildDirectory.file("outputs/apk/debug/sample-guest-debug.apk").get().asFile
def embedded = layout.buildDirectory.file("generated/demoGuestAssets/demo/sample-guest.apk").get().asFile
if (!java.util.Arrays.equals(java.security.MessageDigest.getInstance("SHA-256").digest(source.bytes),
java.security.MessageDigest.getInstance("SHA-256").digest(embedded.bytes))) {
throw new GradleException("Embedded sample APK differs from generated sample APK")
}
}
}
tasks.named("check").configure {
dependsOn("verifyDebugApkFridaPackaging", "verifyInstrumentationOrdering", "verifyDemoGuestUnmodified")
}
dependencies {
+3 -1
View File
@@ -41,4 +41,6 @@
@top.niunaijun.blackreflection.annotation.BMethodCheckNotProcess.* <methods>;
@top.niunaijun.blackreflection.annotation.BConstructor.* <methods>;
@top.niunaijun.blackreflection.annotation.BConstructorNotProcess.* <methods>;
}
}
-keep class top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry { public static *; }
-keep class top.niunaijun.blackbox.instrumentation.FridaGadgetLoader { public static *; }
+8 -2
View File
@@ -14,7 +14,9 @@
<application
android:name=".app.App"
android:allowBackup="true"
android:allowBackup="false"
android:extractNativeLibs="true"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
@@ -22,7 +24,11 @@
android:supportsRtl="true"
android:theme="@style/Theme.BlackBox"
android:enableOnBackInvokedCallback="true"
tools:replace="android:allowBackup"
tools:targetApi="n">
<activity
android:name=".fridabox.FridaBoxActivity"
android:exported="false" />
<activity
android:name=".view.fake.FollowMyLocationOverlay"
android:exported="false" />
@@ -53,4 +59,4 @@
</intent-filter>
</activity>
</application>
</manifest>
</manifest>
@@ -0,0 +1,53 @@
package top.niunaijun.blackboxa.fridabox;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class ApkInspector {
private ApkInspector() {
}
public static Result inspect(File apk) throws IOException {
Set<String> abis = new LinkedHashSet<>();
boolean hasNativeLibraries = false;
try (ZipFile archive = new ZipFile(apk)) {
if (archive.getEntry("AndroidManifest.xml") == null) {
throw new IOException("The selected file has no AndroidManifest.xml");
}
for (ZipEntry entry : Collections.list(archive.entries())) {
String name = entry.getName();
if (!entry.isDirectory() && name.startsWith("lib/") && name.endsWith(".so")) {
String[] parts = name.split("/", 3);
if (parts.length == 3) {
hasNativeLibraries = true;
abis.add(parts[1]);
}
}
}
}
boolean supported = !hasNativeLibraries || abis.contains("arm64-v8a");
return new Result(hasNativeLibraries, supported, abis);
}
public static final class Result {
public final boolean hasNativeLibraries;
public final boolean supported;
public final Set<String> abis;
Result(boolean hasNativeLibraries, boolean supported, Set<String> abis) {
this.hasNativeLibraries = hasNativeLibraries;
this.supported = supported;
this.abis = Collections.unmodifiableSet(new LinkedHashSet<>(abis));
}
public String description() {
if (!hasNativeLibraries) return "Pure Java/Kotlin (accepted)";
return supported ? "ARM64 supported: " + abis : "Rejected; no arm64-v8a: " + abis;
}
}
}
@@ -0,0 +1,33 @@
package top.niunaijun.blackboxa.fridabox;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class ApkIntegrity {
private ApkIntegrity() {
}
public static String sha256(File file) throws IOException {
try (InputStream stream = new FileInputStream(file)) {
return sha256(stream);
}
}
public static String sha256(InputStream stream) throws IOException {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[64 * 1024];
int count;
while ((count = stream.read(buffer)) != -1) digest.update(buffer, 0, count);
StringBuilder result = new StringBuilder(64);
for (byte value : digest.digest()) result.append(String.format("%02x", value & 0xff));
return result.toString();
} catch (NoSuchAlgorithmException impossible) {
throw new AssertionError(impossible);
}
}
}
@@ -0,0 +1,423 @@
package top.niunaijun.blackboxa.fridabox
import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.text.InputType
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.HorizontalScrollView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Switch
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
import top.niunaijun.blackboxa.BuildConfig
import java.io.File
import java.io.FileOutputStream
import java.security.MessageDigest
import java.util.Locale
import java.util.concurrent.Executors
/** Minimal host UI for APK import, virtual launch, runtime status, and settings. */
class FridaBoxActivity : AppCompatActivity() {
private val worker = Executors.newSingleThreadExecutor()
private lateinit var content: LinearLayout
private val settings: SharedPreferences by lazy {
getSharedPreferences(InstrumentationSettings.PREFERENCES, Context.MODE_PRIVATE)
}
private val metadata: SharedPreferences by lazy {
getSharedPreferences("fridabox_imports", Context.MODE_PRIVATE)
}
private val apkPicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) importApk(uri)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
showHome()
}
override fun onDestroy() {
worker.shutdown()
super.onDestroy()
}
private fun baseScreen(title: String): LinearLayout {
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(16), dp(12), dp(16), dp(12))
setBackgroundColor(0xfff7f8fa.toInt())
}
val header = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}
header.addView(TextView(this).apply {
text = title
textSize = 24f
setTypeface(typeface, Typeface.BOLD)
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
header.addView(Button(this).apply {
text = "Home"
setOnClickListener { showHome() }
})
header.addView(Button(this).apply {
text = "Runtime"
setOnClickListener { showRuntime() }
})
header.addView(Button(this).apply {
text = "Settings"
setOnClickListener { showSettings() }
})
root.addView(header)
content = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(0, dp(12), 0, dp(24))
}
root.addView(ScrollView(this).apply { addView(content) }, LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f))
setContentView(root)
return content
}
private fun showHome() {
baseScreen("FridaBox")
content.addView(TextView(this).apply {
text = "Non-root Android application virtualization with per-guest Frida Gadget instrumentation."
textSize = 16f
})
content.addView(Button(this).apply {
text = "Import APK"
setOnClickListener { apkPicker.launch(arrayOf("application/vnd.android.package-archive", "application/octet-stream")) }
})
if (BuildConfig.DEBUG) {
content.addView(Button(this).apply {
text = "Install demo guest"
setOnClickListener { installDemoGuest() }
})
}
content.addView(sectionTitle("Virtual applications"))
refreshApps()
}
private fun refreshApps() {
val marker = TextView(this).apply { text = "Loading…" }
content.addView(marker)
worker.execute {
val packages = try {
BlackBoxCore.get().getInstalledPackages(PackageManager.GET_META_DATA, 0)
} catch (error: Throwable) {
runOnUiThread { marker.text = "Unable to read virtual packages: ${error.message}" }
return@execute
}
runOnUiThread {
content.removeView(marker)
if (packages.isEmpty()) {
content.addView(TextView(this).apply { text = "No APKs imported yet." })
} else {
packages.sortedBy { it.packageName }.forEach { addAppCard(it) }
}
}
}
}
private fun addAppCard(info: PackageInfo) {
val card = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(12), dp(12), dp(12), dp(12))
setBackgroundColor(0xffffffff.toInt())
}
val heading = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL }
heading.addView(ImageView(this).apply {
try { setImageDrawable(info.applicationInfo?.loadIcon(BlackBoxCore.getPackageManager())) } catch (_: Throwable) { }
}, LinearLayout.LayoutParams(dp(56), dp(56)))
heading.addView(TextView(this).apply {
text = buildString {
append(info.packageName)
append("\nVersion: ").append(info.versionName ?: "unknown")
append("\nInstrumentation: ")
append(if (settings.getBoolean("package_enabled_${info.packageName}", true)) "enabled" else "disabled")
}
setPadding(dp(12), 0, 0, 0)
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
card.addView(heading)
val actions = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL }
actions.addView(actionButton("Launch instrumented") { confirmInstrumentedLaunch(info.packageName) })
actions.addView(actionButton("Launch without instrumentation") { launch(info.packageName, false) })
actions.addView(actionButton("Clear virtual app data") { clearApp(info.packageName) })
actions.addView(actionButton("Remove from virtual space") { removeApp(info.packageName) })
actions.addView(actionButton("View runtime details") { showRuntime(info.packageName) })
card.addView(HorizontalScrollView(this).apply { addView(actions) })
val sha = metadata.getString("${info.packageName}.sha256", null)
if (sha != null) {
card.addView(TextView(this).apply {
text = "SHA-256: $sha\nSource: ${metadata.getString("${info.packageName}.source", "unknown")}\n" +
"ABI: ${metadata.getString("${info.packageName}.abi", "unknown")}\n" +
"Target SDK: ${metadata.getInt("${info.packageName}.targetSdk", info.applicationInfo?.targetSdkVersion ?: -1)}"
textSize = 12f
setTextIsSelectable(true)
})
}
content.addView(card, LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = dp(10) })
}
private fun confirmInstrumentedLaunch(packageName: String) {
AlertDialog.Builder(this)
.setTitle("Early instrumentation")
.setMessage("Guest startup is paused until Frida attaches. Run the generated attach command or disable instrumentation.")
.setPositiveButton("Launch") { _, _ -> launch(packageName, true) }
.setNegativeButton("Cancel", null)
.show()
}
private fun launch(packageName: String, instrumented: Boolean) {
worker.execute {
try {
InstrumentationSettings.setEnabledForPackage(packageName, instrumented)
BlackBoxCore.get().stopPackage(packageName, 0)
Thread.sleep(150)
val launched = BlackBoxCore.get().launchApk(packageName, 0)
runOnUiThread {
toast(if (launched) "Guest launch requested" else "Guest has no launchable activity")
if (instrumented) showRuntime(packageName)
}
} catch (error: Throwable) {
runOnUiThread { toast("Launch failed: ${error.message}") }
}
}
}
private fun clearApp(packageName: String) {
worker.execute {
try {
BlackBoxCore.get().stopPackage(packageName, 0)
BlackBoxCore.get().clearPackage(packageName, 0)
runOnUiThread { toast("Virtual app data cleared") }
} catch (error: Throwable) {
runOnUiThread { toast("Clear failed: ${error.message}") }
}
}
}
private fun removeApp(packageName: String) {
AlertDialog.Builder(this).setTitle("Remove $packageName?")
.setMessage("This removes the app and its data only from BlackBox virtual space.")
.setPositiveButton("Remove") { _, _ ->
worker.execute {
try {
BlackBoxCore.get().stopPackage(packageName, 0)
BlackBoxCore.get().uninstallPackageAsUser(packageName, 0)
runOnUiThread { showHome() }
} catch (error: Throwable) {
runOnUiThread { toast("Remove failed: ${error.message}") }
}
}
}.setNegativeButton("Cancel", null).show()
}
private fun importApk(uri: Uri) {
val name = displayName(uri)
val lowerName = name.lowercase(Locale.ROOT)
if (!lowerName.endsWith(".apk") || lowerName.endsWith(".apks") ||
lowerName.endsWith(".xapk") || lowerName.endsWith(".apkm")) {
toast("Select one base .apk file; bundles and split sets are not supported")
return
}
toast("Importing $name…")
worker.execute {
val directory = File(filesDir, "imported-apks").apply { mkdirs() }
val temporary = File.createTempFile("import-", ".partial", directory)
try {
val digest = MessageDigest.getInstance("SHA-256")
contentResolver.openInputStream(uri).use { input ->
requireNotNull(input) { "Unable to open the selected document" }
FileOutputStream(temporary).use { output ->
val buffer = ByteArray(64 * 1024)
while (true) {
val count = input.read(buffer)
if (count < 0) break
digest.update(buffer, 0, count)
output.write(buffer, 0, count)
}
output.fd.sync()
}
}
val originalHash = digest.digest().joinToString("") { "%02x".format(it) }
val archiveInfo = packageManager.getPackageArchiveInfo(temporary.absolutePath, PackageManager.GET_META_DATA)
?: error("Android could not parse this APK")
if (!archiveInfo.splitNames.isNullOrEmpty()) error("Split-only APKs are not supported in this MVP")
val abi = ApkInspector.inspect(temporary)
if (!abi.supported) error("32-bit-only/native APK rejected: ${abi.description()}")
val safePackage = archiveInfo.packageName.replace(Regex("[^A-Za-z0-9._-]"), "_")
val stored = File(directory, "$safePackage-${originalHash.take(12)}.apk")
if (stored.exists()) stored.delete()
if (!temporary.renameTo(stored)) error("Unable to move APK into private storage")
if (ApkIntegrity.sha256(stored) != originalHash) error("SHA-256 changed while importing")
stored.setReadable(true, true)
stored.setWritable(false, false)
val installResult = BlackBoxCore.get().installPackageAsUser(stored, 0)
if (!installResult.success) error(installResult.msg ?: "Virtual installation failed")
if (ApkIntegrity.sha256(stored) != originalHash) error("Stored APK was modified during virtual installation")
metadata.edit()
.putString("${archiveInfo.packageName}.sha256", originalHash)
.putString("${archiveInfo.packageName}.source", stored.absolutePath)
.putString("${archiveInfo.packageName}.abi", abi.description())
.putString("${archiveInfo.packageName}.version", archiveInfo.versionName)
.putInt("${archiveInfo.packageName}.targetSdk", archiveInfo.applicationInfo?.targetSdkVersion ?: -1)
.apply()
InstrumentationSettings.setEnabledForPackage(archiveInfo.packageName, true)
runOnUiThread {
toast("Imported ${archiveInfo.packageName}; SHA-256 verified")
showHome()
}
} catch (error: Throwable) {
temporary.delete()
runOnUiThread { toast("Import rejected: ${error.message}") }
}
}
}
private fun installDemoGuest() {
worker.execute {
try {
val directory = File(filesDir, "imported-apks").apply { mkdirs() }
val output = File(directory, "sample-guest.apk")
if (output.exists() && !output.delete()) error("Unable to replace the prior demo APK")
assets.open("demo/sample-guest.apk").use { input ->
FileOutputStream(output).use { input.copyTo(it) }
}
val sha = ApkIntegrity.sha256(output)
output.setWritable(false, false)
val result = BlackBoxCore.get().installPackageAsUser(output, 0)
if (!result.success) error(result.msg ?: "Demo virtual installation failed")
metadata.edit().putString("${result.packageName}.sha256", sha)
.putString("${result.packageName}.source", output.absolutePath)
.putString("${result.packageName}.abi", ApkInspector.inspect(output).description()).apply()
runOnUiThread { toast("Demo guest installed into virtual space"); showHome() }
} catch (error: Throwable) {
runOnUiThread { toast("Demo install failed: ${error.message}") }
}
}
}
private fun showRuntime(packageHint: String? = null) {
baseScreen("Runtime status")
val packageName = settings.getString("runtime_package", packageHint) ?: packageHint ?: "No guest bound"
val state = settings.getString("runtime_state", "idle")
val basePort = settings.getInt(InstrumentationSettings.KEY_BASE_PORT, 27042)
val count = settings.getInt(InstrumentationSettings.KEY_SCAN_COUNT, 32)
val command = "python tools/attach_guest.py --package $packageName --keep-alive"
content.addView(detail("Guest package", packageName))
content.addView(detail("Guest process", settings.getString("runtime_process", "unknown") ?: "unknown"))
content.addView(detail("Virtual process slot", settings.getInt("runtime_vpid", -1).toString()))
content.addView(detail("Guest source path", settings.getString("runtime_source", "unknown") ?: "unknown"))
content.addView(detail("Instrumentation enabled", settings.getBoolean("runtime_enabled", false).toString()))
content.addView(detail("Gadget load status", state ?: "idle"))
content.addView(detail("Expected port range", "$basePort..${basePort + count - 1}"))
content.addView(detail("Latest error", settings.getString("runtime_error", "none") ?: "none"))
content.addView(TextView(this).apply {
text = "Attach command\n$command"
setTextIsSelectable(true)
setPadding(0, dp(12), 0, dp(8))
})
content.addView(Button(this).apply {
text = "Copy attach command"
setOnClickListener {
(getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
.setPrimaryClip(ClipData.newPlainText("FridaBox attach", command))
toast("Attach command copied")
}
})
content.addView(Button(this).apply { text = "Refresh"; setOnClickListener { showRuntime(packageHint) } })
}
private fun showSettings() {
baseScreen("Instrumentation settings")
val enabled = Switch(this).apply {
text = "Instrumentation enabled"
isChecked = settings.getBoolean(InstrumentationSettings.KEY_ENABLED, true)
}
val pause = CheckBox(this).apply {
text = "Pause guest until attach (required for this MVP)"
isChecked = true
isEnabled = false
}
val port = numericSetting("Frida base port", settings.getInt(InstrumentationSettings.KEY_BASE_PORT, 27042))
val count = numericSetting("Port scan count", settings.getInt(InstrumentationSettings.KEY_SCAN_COUNT, 32))
val logs = Switch(this).apply {
text = "Show advanced logs"
isChecked = settings.getBoolean(InstrumentationSettings.KEY_ADVANCED_LOGS, false)
}
content.addView(enabled); content.addView(pause); content.addView(port.first); content.addView(count.first); content.addView(logs)
content.addView(Button(this).apply {
text = "Save settings"
setOnClickListener {
settings.edit()
.putBoolean(InstrumentationSettings.KEY_ENABLED, enabled.isChecked)
.putInt(InstrumentationSettings.KEY_BASE_PORT, InstrumentationPreferenceParser.parsePort(port.second.text.toString(), 27042))
.putInt(InstrumentationSettings.KEY_SCAN_COUNT, InstrumentationPreferenceParser.parseScanCount(count.second.text.toString(), 32))
.putBoolean(InstrumentationSettings.KEY_ADVANCED_LOGS, logs.isChecked)
.apply()
toast("Settings saved")
}
})
content.addView(TextView(this).apply {
text = "Limitations: FridaBox is not undetectable. The host UID/SELinux domain, virtual stub process, host classes, ClassLoader topology, synthesized Binder responses, Gadget module/threads/socket, and /proc/self/maps remain observable. Play Integrity and hardware-backed attestation are not virtualized."
setPadding(0, dp(20), 0, 0)
})
}
private fun numericSetting(label: String, value: Int): Pair<LinearLayout, EditText> {
val input = EditText(this).apply {
setText(value.toString()); inputType = InputType.TYPE_CLASS_NUMBER
}
return LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL; gravity = Gravity.CENTER_VERTICAL
addView(TextView(this@FridaBoxActivity).apply { text = label }, LinearLayout.LayoutParams(0, dp(56), 1f))
addView(input, LinearLayout.LayoutParams(dp(140), dp(56)))
} to input
}
private fun detail(label: String, value: String) = TextView(this).apply {
text = "$label: $value"; setTextIsSelectable(true); setPadding(0, dp(5), 0, dp(5))
}
private fun sectionTitle(text: String) = TextView(this).apply {
this.text = text; textSize = 19f; setTypeface(typeface, Typeface.BOLD); setPadding(0, dp(18), 0, dp(4))
}
private fun actionButton(label: String, action: () -> Unit) = Button(this).apply {
text = label; setOnClickListener { action() }
}
private fun displayName(uri: Uri): String {
contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use {
if (it.moveToFirst()) return it.getString(0) ?: "selected.apk"
}
return uri.lastPathSegment ?: "selected.apk"
}
private fun toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_LONG).show()
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
}
@@ -0,0 +1,23 @@
package top.niunaijun.blackboxa.fridabox;
public final class InstrumentationPreferenceParser {
private InstrumentationPreferenceParser() {
}
public static int parsePort(String text, int fallback) {
return parseRange(text, 1024, 65535, fallback);
}
public static int parseScanCount(String text, int fallback) {
return parseRange(text, 1, 128, fallback);
}
private static int parseRange(String text, int minimum, int maximum, int fallback) {
try {
int value = Integer.parseInt(text == null ? "" : text.trim());
return value >= minimum && value <= maximum ? value : fallback;
} catch (NumberFormatException ignored) {
return fallback;
}
}
}
@@ -17,7 +17,7 @@ class BlackBoxLoader {
private var mHideRoot by AppSharedPreferenceDelegate(App.getContext(), false)
private var mDaemonEnable by AppSharedPreferenceDelegate(App.getContext(), false)
private var mDaemonEnable by AppSharedPreferenceDelegate(App.getContext(), true)
private var mShowShortcutPermissionDialog by AppSharedPreferenceDelegate(App.getContext(), true)
@@ -245,12 +245,7 @@ class BlackBoxLoader {
}
override fun isEnableDaemonService(): Boolean {
return try {
mDaemonEnable
} catch (e: Exception) {
Log.e(TAG, "Error checking daemonEnable: ${e.message}")
false
}
return true
}
override fun isUseVpnNetwork(): Boolean {
@@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.util.InjectionUtil
import top.niunaijun.blackboxa.view.list.ListViewModel
import top.niunaijun.blackboxa.fridabox.FridaBoxActivity
class WelcomeActivity : AppCompatActivity() {
@@ -22,7 +23,7 @@ class WelcomeActivity : AppCompatActivity() {
}
private fun jump() {
MainActivity.start(this)
startActivity(Intent(this, FridaBoxActivity::class.java))
finish()
}
@@ -30,4 +31,4 @@ class WelcomeActivity : AppCompatActivity() {
val viewModel = ViewModelProvider(this,InjectionUtil.getListFactory()).get(ListViewModel::class.java)
viewModel.previewInstalledList()
}
}
}
@@ -0,0 +1,11 @@
{
"interaction": {
"type": "listen",
"address": "127.0.0.1",
"port": 27042,
"on_port_conflict": "pick-next",
"on_load": "wait"
},
"runtime": "qjs",
"teardown": "minimal"
}
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">BlackBox</string>
<string name="app_name">FridaBox</string>
<string name="choose">Choose</string>
<string name="choose_app">Choose App</string>
<string name="installed_app">Installed App</string>
@@ -0,0 +1,48 @@
package top.niunaijun.blackboxa.fridabox;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class ApkInspectorTest {
@Rule public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void pureJavaApkIsAccepted() throws Exception {
File apk = apkWith("classes.dex");
ApkInspector.Result result = ApkInspector.inspect(apk);
assertFalse(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void arm64ApkIsAccepted() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(apkWith("lib/arm64-v8a/libsample.so"));
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void thirtyTwoBitOnlyApkIsRejected() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(apkWith("lib/armeabi-v7a/libsample.so"));
assertTrue(result.hasNativeLibraries);
assertFalse(result.supported);
}
private File apkWith(String entry) throws Exception {
File file = temporary.newFile("test-" + System.nanoTime() + ".apk");
try (ZipOutputStream output = new ZipOutputStream(new FileOutputStream(file))) {
output.putNextEntry(new ZipEntry("AndroidManifest.xml")); output.write(1); output.closeEntry();
output.putNextEntry(new ZipEntry(entry)); output.write(2); output.closeEntry();
}
return file;
}
}
@@ -0,0 +1,15 @@
package top.niunaijun.blackboxa.fridabox;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import java.io.ByteArrayInputStream;
public class ApkIntegrityTest {
@Test
public void computesKnownSha256() throws Exception {
assertEquals("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
ApkIntegrity.sha256(new ByteArrayInputStream("abc".getBytes("UTF-8"))));
}
}
@@ -0,0 +1,20 @@
package top.niunaijun.blackboxa.fridabox;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class InstrumentationPreferenceParserTest {
@Test
public void acceptsValidValues() {
assertEquals(27043, InstrumentationPreferenceParser.parsePort("27043", 27042));
assertEquals(32, InstrumentationPreferenceParser.parseScanCount("32", 16));
}
@Test
public void rejectsMalformedAndOutOfRangeValues() {
assertEquals(27042, InstrumentationPreferenceParser.parsePort("nope", 27042));
assertEquals(27042, InstrumentationPreferenceParser.parsePort("80", 27042));
assertEquals(32, InstrumentationPreferenceParser.parseScanCount("999", 32));
}
}
+40
View File
@@ -0,0 +1,40 @@
# NewBlackbox baseline
- Foundation: `ALEX5402/NewBlackbox`, branch `main`
- Required commit: `89b59836c66f173756a4ae258cf379a957649820`
- Working branch: `feature/fridabox-mvp`
- Baseline date: 2026-07-19
- Host OS: Windows 11 amd64
- Available JVM used: Oracle JDK 24.0.1 (JDK 21 was not installed on the build host)
## Exact pinned-source result
`gradlew clean --stacktrace --console=plain` failed during configuration because
`com.android.application:com.android.application.gradle.plugin:8.13.2` could not
be resolved. A direct request to the official Google Maven artifact URL returned
HTTP 404. No source code had been changed when this result was recorded.
## Narrow baseline compatibility adjustments
The official Google Maven endpoint returned HTTP 404 for AGP and AndroidX
artifacts in this build environment. The Google repository mirror at
`https://maven.aliyun.com/repository/google` is configured ahead of `google()`;
the requested AGP 8.13.2 is retained. Plugin IDs are mapped directly to the
official `com.android.tools.build:gradle` module to avoid marker resolution.
The requested NDK
29.0.13846066 was not installed; the nearest complete installed NDK,
29.0.14206865, is used. Bcore's `ndkVersion` was also moved from the
`externalNativeBuild` block to the Android block where AGP recognizes it.
On Windows, upstream `ndk-build` cannot parse an `APP_BUILD_SCRIPT` path that
contains spaces. Bcore accepts `FRIDABOX_NDK_PROJECT_DIR` as an optional alias
for the Bcore module directory; this build used `D:\FridaBoxBuild\Bcore`, a
directory junction to the real workspace.
## Post-adjustment result
With the repository mirror, installed NDK 29 revision, and scoped Windows
space-path workaround, `:app:assembleDebug` completed successfully. The baseline
build compiled Java/Kotlin resources and both native ABIs present in the original
foundation before FridaBox changed the final target to ARM64-only.
+54
View File
@@ -0,0 +1,54 @@
# Building
## Prerequisites
- JDK 21 (the requested/recommended toolchain).
- Android SDK 35 and build tools.
- Android NDK 29.0.13846066 when available. This build host only had the closely
related complete NDK 29.0.14206865, which is recorded in `docs/BASELINE.md`.
- Python 3.10 or newer.
- Node.js 20 or newer and npm for reproducibly building Frida 17 Java agents.
Create `local.properties` with the SDK path, then fetch the pinned official
Gadget:
```powershell
python tools\fetch_frida_gadget.py
```
The script uses the GitHub Releases API and Python's standard `lzma` module. The
build fails with a direct instruction if the binary is absent. No XZ archive is
kept.
Install the pinned Java bridge/compiler and build the controller agents:
```powershell
npm ci
python tools\build_frida_agents.py
```
Frida 17 no longer bundles runtime bridges into API-loaded GumJS agents. The
compiled files under `scripts/dist/` are reproducible from the source scripts,
`package.json`, and `package-lock.json`.
On Windows, if the repository path contains spaces, create a no-space junction
and set the Bcore-only native path override:
```powershell
New-Item -ItemType Junction -Path D:\FridaBoxBuild -Target 'D:\path with spaces\FridaBox'
$env:FRIDABOX_NDK_PROJECT_DIR='D:\FridaBoxBuild\Bcore'
```
Build and test:
```powershell
.\gradlew.bat clean
.\gradlew.bat :sample-guest:assembleDebug
.\gradlew.bat :app:assembleDebug
.\gradlew.bat test
.\gradlew.bat :app:check
```
The debug host build depends on the sample build and copies its byte-identical
APK into generated debug assets. Generated sample APKs are not source-controlled.
The final host output is under `app/build/outputs/apk/debug/` and is ARM64-only.
+20
View File
@@ -0,0 +1,20 @@
# Detection surfaces
FridaBox is not undetectable. Virtualization reduces accidental identity and API
leakage but cannot reproduce a normal kernel/system installation.
- The host Linux UID remains the real UID at kernel level.
- The host SELinux domain remains visible.
- BlackBox virtual stub process names may be observed.
- Host and BlackBox classes coexist with guest classes in the ART process.
- Frida threads, mappings, sockets, and modules are observable.
- `/proc/self/maps` can reveal Frida Gadget.
- The default Frida protocol endpoint can be probed even though it is loopback-only.
- ClassLoader topology differs from a normally installed application.
- Some PackageManager, ActivityManager, and other Binder responses are synthesized.
- The system-side PackageManager does not know the guest package.
- The guest shares the host UID and process sandbox rather than receiving a
system-assigned package UID.
- Play Integrity and hardware-backed attestation cannot be faithfully virtualized.
No app-specific anti-Frida or anti-virtualization bypasses are included.
+38
View File
@@ -0,0 +1,38 @@
# Frida connection
Install the pinned controller binding:
```text
python -m pip install -r tools/requirements.txt
npm ci
python tools/build_frida_agents.py
```
The Gadget listens only on device loopback. The controller verifies ADB, selects
an authorized device, forwards TCP 27042–27073, probes each endpoint, rejects
non-Gadget/non-FridaBox endpoints, and maps endpoints through
`GuestRuntimeRegistry`.
Frida 17 Java agents explicitly import `frida-java-bridge`. The controller
loads the compiled probe/bootstrap from `scripts/dist/`; when a source path such
as `scripts/sample-hook.js` has a compiled counterpart, that bundle is selected
automatically.
```text
python tools/attach_guest.py --list
python tools/attach_guest.py --package PACKAGE
python tools/attach_guest.py --package PACKAGE --process PROCESS
python tools/attach_guest.py --package PACKAGE --script scripts/example.js
python tools/attach_guest.py --package PACKAGE --script scripts/example.js --keep-alive
```
`guest-bootstrap.js` loads before the user script and assigns the registry's
guest ClassLoader to `Java.classFactory.loader`. Its RPC exports are `info`,
`useclass`, `enumerateloadedclasses`, and `enumeratemodules`. It retries for a
bounded ten seconds when the ClassLoader is temporarily unavailable.
The controller prints the registry JSON, selected ClassLoader, and native-module
enumeration before loading the user script.
Use `tools/forward_frida_ports.py` when only port forwarding is needed. A client
major-version mismatch prints the exact `pip install frida==17.16.0` repair
command.
+25
View File
@@ -0,0 +1,25 @@
# Limitations
The MVP targets ordinary single-file APKs on ARM64 Android 12 through Android 16.
Compatibility varies because Android hidden APIs and vendor framework behavior
change, and virtualization is observable.
Explicitly unsupported or excluded:
- split APK sets and App Bundles (`.apks`, `.xapk`, `.apkm`);
- 32-bit-only native APKs and x86 guests;
- system apps, privileged permissions, or Play Store inside the container;
- full Google Play Services compatibility and Play Integrity;
- hardware-backed keystore identity or attestation emulation;
- reliable `isolatedProcess=true`, WebView renderer sandbox, app zygote, or
external-service instrumentation outside the host UID;
- reliable operation for all banking/RASP applications;
- perfect anti-virtualization or anti-instrumentation resistance.
Normal guest `android:process=":remote"` components routed through BlackBox's
`BActivityThread` receive a best-effort independent Gadget load. Each process
starts at 27042 and relies on `pick-next` for conflicts.
The static config always uses `on_load=wait`; changing the displayed base port
does not rewrite the packaged Gadget configuration in this MVP. The controller's
base/count options must match the port range used for discovery.
+70
View File
@@ -0,0 +1,70 @@
# Test results
Validation date: 2026-07-19
## Host validation
The following commands completed successfully on the build host:
```powershell
.\gradlew.bat clean
.\gradlew.bat :sample-guest:assembleDebug
.\gradlew.bat :app:assembleDebug
.\gradlew.bat test
.\gradlew.bat :app:check
.\gradlew.bat :app:verifyDebugApkFridaPackaging :app:verifyInstrumentationOrdering :app:verifyDemoGuestUnmodified
npm ci
python tools\build_frida_agents.py
python -m py_compile tools\attach_guest.py tools\forward_frida_ports.py tools\fetch_frida_gadget.py tools\build_frida_agents.py
```
For this workspace path, native builds used:
```powershell
$env:FRIDABOX_NDK_PROJECT_DIR='D:\FridaBoxBuild\Bcore'
```
Unit-test results: 16 executions passed, with zero failures, errors, or skips.
This is eight test methods run for both debug and release variants:
- `ApkInspectorTest`: 3 per variant;
- `ApkIntegrityTest`: 1 per variant;
- `InstrumentationPreferenceParserTest`: 2 per variant;
- `GuestRuntimeRegistryTest`: 2 per variant.
The final APK and custom verification tasks passed:
- output: `app/build/outputs/apk/debug/FridaBox_4.0.0_arm64-v8a-debug.apk`;
- size: 19,977,904 bytes;
- SHA-256: `a87af38ff7f4a54d2cdc0207ac68319a1a05ab8067b7acdeb057c6c967ed2573`;
- packaged ABIs: ARM64 only;
- packaged native files: `libblackbox.so`, `libfrida-gadget.so`, and
`libfrida-gadget.config.so` under `lib/arm64-v8a/`;
- Frida Gadget 17.16.0 SHA-256:
`6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e`;
- early-load ordering and byte-identical demo-asset checks passed.
- pinned Frida 17 registry/bootstrap/sample agents rebuilt successfully, and
controller help plus empty-range discovery completed successfully.
The build host provided JDK 24.0.1 and complete NDK 29.0.14206865 rather than
the requested JDK 21 and NDK 29.0.13846066. These substitutions and the
baseline build investigation are recorded in `docs/BASELINE.md`.
## Device validation
Runtime validation passed on a Samsung SM-S928B running ARM64 Android 16/API 36:
- latest host APK installed successfully;
- the sample installed only in BlackBox and was absent from Android user 0's
real PackageManager;
- Gadget paused startup before `SampleApplication.attachBaseContext` and
`onCreate`;
- the controller mapped port 27042 to the sample package/process and reported
user ID 0, virtual process ID 0, source APK, and guest `PathClassLoader`;
- native enumeration returned 419 modules;
- `sample-hook.js` changed `Target.add(2, 3)` from 5 to 1337;
- launching without instrumentation recycled the process, opened no Gadget
listener, and restored the visible result to 5.
The command and log transcript, including two device-discovered fixes, is in
`docs/device-validation.log`.
+27
View File
@@ -0,0 +1,27 @@
# Usage
1. Install and open the FridaBox host on an ARM64 Android 12–16 research device.
2. Tap **Import APK** and select one base `.apk` through the system document
picker. `.apks`, `.xapk`, `.apkm`, split-only packages, and 32-bit-only native
APKs are rejected.
3. Review package, version, SHA-256, private source path, and ABI status.
4. Tap **Launch instrumented**. Startup intentionally pauses before the guest
`Application` is created.
5. Run the attach command shown on **Runtime status**. Attach and load hooks.
6. Use **Launch without instrumentation** for a clean virtual process where the
Gadget is not loaded. FridaBox stops the package before switching modes.
Debug builds expose **Install demo guest**. The action installs the generated
`com.qm4rs.fridabox.sample` APK only into BlackBox. Then run:
```text
npm ci
python tools/build_frida_agents.py
python tools/attach_guest.py --package com.qm4rs.fridabox.sample --script scripts/sample-hook.js --keep-alive
```
After startup resumes, press the sample button. The visible result should be
`1337`, demonstrating that the guest ClassLoader was selected.
**Clear virtual app data** and **Remove from virtual space** affect only the
BlackBox virtual environment. They do not invoke Android's real package manager.
+163
View File
@@ -0,0 +1,163 @@
FridaBox device validation transcript
Date: 2026-07-19 (Asia/Tehran)
Device: Samsung SM-S928B, serial R5CY149SZEX
1. Device and host installation
> adb devices -l
R5CY149SZEX device product:e3qxxx model:SM_S928B device:e3q transport_id:3
> adb shell getprop ro.product.cpu.abilist
arm64-v8a
> adb shell getprop ro.build.version.release
16
> adb shell getprop ro.build.version.sdk
36
> $env:FRIDABOX_NDK_PROJECT_DIR='D:\FridaBoxBuild\Bcore'; .\gradlew.bat :app:assembleDebug --console=plain
BUILD SUCCESSFUL
> adb install -r app\build\outputs\apk\debug\FridaBox_4.0.0_arm64-v8a-debug.apk
Performing Streamed Install
Success
> adb shell am start --user 0 -n com.qm4rs.fridabox/top.niunaijun.blackboxa.view.main.WelcomeActivity
Starting: Intent { cmp=com.qm4rs.fridabox/top.niunaijun.blackboxa.view.main.WelcomeActivity }
The debug UI action "Install demo guest" installed the generated asset into
BlackBox. The UI reported:
package: com.qm4rs.fridabox.sample
version: 1.0
SHA-256: 35fa3a6d679ae0b0a18635756e1d1a23b5340d12e480eb8600768ab60af0e95e
source: /data/user/0/com.qm4rs.fridabox/files/imported-apks/sample-guest.apk
ABI: Pure Java/Kotlin (accepted)
target SDK: 28
> adb shell pm list packages --user 0 com.qm4rs.fridabox
package:com.qm4rs.fridabox
> adb shell pm list packages --user 0 com.qm4rs.fridabox.sample
<no output>
Result: the sample guest is absent from Android user 0's real PackageManager.
Samsung has an inaccessible user 150, so all PackageManager proof commands
explicitly use --user 0.
2. Pre-attach pause
The UI selected "Launch instrumented" and confirmed the early-instrumentation
warning. Before running the controller:
> adb shell ps -A | Select-String fridabox.sample
u0_a524 31797 1675 ... S com.qm4rs.fridabox.sample
> adb shell ss -ltn | Select-String 27042
LISTEN 0 0 127.0.0.1:27042 0.0.0.0:*
> adb logcat -d -v threadtime -s FridaBox.Gadget:I FridaBox.Sample:I *:S
07-19 23:11:00.878 31797 31797 I FridaBox.Gadget: Loading Frida Gadget for com.qm4rs.fridabox.sample
There was no FridaBox.Sample attachBaseContext or onCreate line. The guest main
thread was paused inside System.loadLibrary before makeApplication.
3. Controller discovery, registry, ClassLoader and native modules
> python -m pip install -r tools\requirements.txt
Successfully installed frida-17.16.0
> npm ci
Pinned packages installed from package-lock.json.
> python tools\build_frida_agents.py
Built Frida agents: scripts/dist/registry-probe.js,
scripts/dist/guest-bootstrap.js, scripts/dist/sample-hook.js
> python -u tools\attach_guest.py --package com.qm4rs.fridabox.sample --script scripts\sample-hook.js --keep-alive
PORT GUEST PACKAGE GUEST PROCESS VPID SOURCE APK
27042 com.qm4rs.fridabox.sample com.qm4rs.fridabox.sample 0 /data/user/0/com.qm4rs.fridabox/blackbox/data/app/com.qm4rs.fridabox.sample/base.apk
[FridaBox] package=com.qm4rs.fridabox.sample
[FridaBox] process=com.qm4rs.fridabox.sample
[FridaBox] userId=0
[FridaBox] virtualProcessId=0
[FridaBox] source=/data/user/0/com.qm4rs.fridabox/blackbox/data/app/com.qm4rs.fridabox.sample/base.apk
[FridaBox] ClassLoader=dalvik.system.PathClassLoader[DexPathList[[zip file "/data/user/0/com.qm4rs.fridabox/blackbox/data/app/com.qm4rs.fridabox.sample/base.apk"],nativeLibraryDirectories=[/data/user/0/com.qm4rs.fridabox/blackbox/data/app/com.qm4rs.fridabox.sample/lib, /data/user/0/com.qm4rs.fridabox/blackbox/data/app/com.qm4rs.fridabox.sample/base.apk!/lib/arm64-v8a, /system/lib64, /system_ext/lib64]]]
GuestRuntimeRegistry: {"initializedAt": 1784490060872, "instrumentationEnabled": true, "lastError": null, "package": "com.qm4rs.fridabox.sample", "process": "com.qm4rs.fridabox.sample", "sourceDir": "/data/user/0/com.qm4rs.fridabox/blackbox/data/app/com.qm4rs.fridabox.sample/base.apk", "userId": 0, "virtualProcessId": 0}
Native modules: 419
app_process64 @ 0x5e4d8a8000 /system/bin/app_process64
linker64 @ 0x7d37dc1000 /apex/com.android.runtime/bin/linker64
libandroid_runtime.so @ 0x7cf688b000 /system/lib64/libandroid_runtime.so
libbinder.so @ 0x7d0ae9a000 /system/lib64/libbinder.so
libnativeloader.so @ 0x7d2fbc1000 /apex/com.android.art/lib64/libnativeloader.so
[sample-hook] installed for com.qm4rs.fridabox.sample
Attached on port 27042 to com.qm4rs.fridabox.sample / com.qm4rs.fridabox.sample
Post-attach lifecycle evidence:
07-19 23:11:44.774 31797 31797 I FridaBox.Gadget: Frida Gadget loaded
07-19 23:11:44.782 31797 31797 I FridaBox.Sample: Application.attachBaseContext package=com.qm4rs.fridabox.sample
07-19 23:11:44.787 31797 31797 I FridaBox.Sample: Application.onCreate package=com.qm4rs.fridabox.sample
This ordering proves the Application lifecycle resumed only after controller
attachment.
4. Java hook proof
The validation tapped the visible "CALL TARGET.ADD(2, 3)" button while the
controller session remained attached.
Controller output:
[sample-hook] Target.add(2, 3) => 1337
UI hierarchy output:
<node text="Result: 1337" class="android.widget.TextView" ... />
5. Recycled launch without instrumentation
The previous instrumented process was PID 31797. The controller detached, the
UI returned Home, and "Launch without instrumentation" stopped/recycled the
virtual process before launch.
> adb shell ps -A | Select-String fridabox.sample
u0_a524 32298 1675 ... S com.qm4rs.fridabox.sample
> adb shell ss -ltn | Select-String 27042
<no listener>
07-19 23:12:47.821 32298 32298 I FridaBox.Gadget: Instrumentation disabled for this guest process
07-19 23:12:47.826 32298 32298 I FridaBox.Sample: Application.attachBaseContext package=com.qm4rs.fridabox.sample
07-19 23:12:47.828 32298 32298 I FridaBox.Sample: Application.onCreate package=com.qm4rs.fridabox.sample
The same button then produced:
<node text="Result: 5" class="android.widget.TextView" ... />
6. Device-specific failures found and fixed
Samsung Android 16 initially froze the BlackBox :black system-service process:
libbinder.IPCThreadState: Transaction failed because process frozen.
Binder transaction failure: BR_FROZEN_REPLY
android.os.DeadObjectException at
IBPackageManagerService$Stub$Proxy.installPackageAsUser(...)
Fix: enable the existing internal DaemonService for FridaBox. Verification:
DaemonService: Foreground service started successfully
DaemonService: DaemonService started successfully
No subsequent BR_FROZEN_REPLY occurred and the virtual package remained
available across host/activity restarts.
The first Frida 17 API-loaded probe failed with:
Port 27042 rejected: ReferenceError: 'Java' is not defined
Fix: explicitly import frida-java-bridge 7.0.13 and reproducibly compile all
Java agents with frida-compile 19.0.5. The early registry probe also uses
Java.performNow(), avoiding a deadlock with Gadget's on_load=wait main thread.
Final result: all required sample runtime checks passed on ARM64 Android 16.
+589
View File
@@ -0,0 +1,589 @@
{
"name": "fridabox-agents",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fridabox-agents",
"dependencies": {
"frida-java-bridge": "7.0.13"
},
"devDependencies": {
"frida-compile": "19.0.5"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"license": "ISC"
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"dev": true,
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"license": "MIT"
},
"node_modules/frida": {
"version": "17.16.1",
"resolved": "https://registry.npmjs.org/frida/-/frida-17.16.1.tgz",
"integrity": "sha512-bIjJGZy3q8anS2l81eond/qHP2oCsCv3iWck6MgcXzbwx6LWFLunnI9E7we/iEy0bHEsi4gX5ja6Wz76k+LEGQ==",
"dev": true,
"hasInstallScript": true,
"license": "LGPL-2.0 WITH WxWindows-exception-3.1",
"dependencies": {
"bindings": "^1.5.0",
"minimatch": "^10.0.1",
"prebuild-install": "^7.1.3"
},
"engines": {
"node": ">=16"
}
},
"node_modules/frida-compile": {
"version": "19.0.5",
"resolved": "https://registry.npmjs.org/frida-compile/-/frida-compile-19.0.5.tgz",
"integrity": "sha512-dHkZBswL6zzF63bcqK2AaXOrZTYIk4ZynJhcI1EP+dFRtmCx1nhMQH/dFZfkgcS5yoP6nabA/Ra2IUji3j3GOg==",
"dev": true,
"dependencies": {
"chalk": "^5.4.1",
"commander": "^14.0.0",
"frida": "^17.8.0"
},
"bin": {
"frida-compile": "dist/cli.js"
}
},
"node_modules/frida-java-bridge": {
"version": "7.0.13",
"resolved": "https://registry.npmjs.org/frida-java-bridge/-/frida-java-bridge-7.0.13.tgz",
"integrity": "sha512-YSyKjxbxKnSi3KSUy9vciOvTOuq0RRh9dkxzkQVEdfIIZlw20zE8D3Cq9eL2FDqUVj4YKas6Wf09kCjL5zbffg==",
"license": "LGPL-2.0 WITH WxWindows-exception-3.1"
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT"
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"dev": true,
"license": "MIT"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC"
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true,
"license": "MIT"
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"dev": true,
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.94.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
"integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "fridabox-agents",
"private": true,
"dependencies": {
"frida-java-bridge": "7.0.13"
},
"devDependencies": {
"frida-compile": "19.0.5"
}
}
+21
View File
@@ -0,0 +1,21 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace "com.qm4rs.fridabox.sample"
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.qm4rs.fridabox.sample"
minSdk rootProject.ext.minSdk
targetSdk rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".SampleApplication"
android:allowBackup="false"
android:label="FridaBox Sample Guest"
android:theme="@android:style/Theme.Material.Light">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,28 @@
package com.qm4rs.fridabox.sample;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
public final class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView output = new TextView(this);
output.setTextSize(22f);
output.setText("Press the button to call Target.add(2, 3)");
Button button = new Button(this);
button.setText("Call Target.add(2, 3)");
button.setOnClickListener(view -> output.setText("Result: " + Target.add(2, 3)));
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setGravity(Gravity.CENTER);
root.setPadding(32, 32, 32, 32);
root.addView(output);
root.addView(button);
setContentView(root);
}
}
@@ -0,0 +1,21 @@
package com.qm4rs.fridabox.sample;
import android.app.Application;
import android.content.Context;
import android.util.Log;
public final class SampleApplication extends Application {
private static final String TAG = "FridaBox.Sample";
@Override
protected void attachBaseContext(Context base) {
Log.i(TAG, "Application.attachBaseContext package=" + base.getPackageName());
super.attachBaseContext(base);
}
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Application.onCreate package=" + getPackageName());
}
}
@@ -0,0 +1,10 @@
package com.qm4rs.fridabox.sample;
public final class Target {
private Target() {
}
public static int add(int a, int b) {
return a + b;
}
}
+1887
View File
File diff suppressed because one or more lines are too long
+1887
View File
File diff suppressed because one or more lines are too long
+1887
View File
File diff suppressed because one or more lines are too long
+75
View File
@@ -0,0 +1,75 @@
'use strict';
import Java from 'frida-java-bridge';
const REGISTRY = 'top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry';
const MAX_ATTEMPTS = 40;
const RETRY_MS = 250;
let registry = null;
let guestLoader = null;
function findRegistry() {
try {
return Java.use(REGISTRY);
} catch (_) {
let found = null;
Java.enumerateClassLoaders({
onMatch(loader) {
if (found !== null) return;
try {
loader.loadClass(REGISTRY);
const factory = Java.ClassFactory.get(loader);
found = factory.use(REGISTRY);
} catch (_) {}
},
onComplete() {}
});
return found;
}
}
function bootstrap(attempt) {
Java.perform(() => {
registry = findRegistry();
if (registry !== null) {
guestLoader = registry.getGuestClassLoader();
if (guestLoader !== null) {
Java.classFactory.loader = guestLoader;
console.log('[FridaBox] package=' + registry.getGuestPackageName());
console.log('[FridaBox] process=' + registry.getGuestProcessName());
console.log('[FridaBox] userId=' + registry.getGuestUserId());
console.log('[FridaBox] virtualProcessId=' + registry.getVirtualProcessId());
console.log('[FridaBox] source=' + registry.getGuestSourceDir());
console.log('[FridaBox] ClassLoader=' + guestLoader.toString());
return;
}
}
if (attempt + 1 < MAX_ATTEMPTS) {
setTimeout(() => bootstrap(attempt + 1), RETRY_MS);
} else {
console.error('[FridaBox] guest ClassLoader unavailable after ' + (MAX_ATTEMPTS * RETRY_MS) + ' ms');
}
});
}
bootstrap(0);
rpc.exports = {
info() {
return Java.performNow(() => registry === null ? { error: 'registry unavailable' } : JSON.parse(registry.describe()));
},
useclass(className) {
return Java.performNow(() => {
if (guestLoader === null) throw new Error('guest ClassLoader is not ready');
Java.classFactory.loader = guestLoader;
return Java.use(className).$className;
});
},
enumerateloadedclasses(prefix) {
const match = prefix || '';
return Java.performNow(() => Java.enumerateLoadedClassesSync().filter(name => name.indexOf(match) === 0));
},
enumeratemodules() {
return Process.enumerateModules().map(module => ({ name: module.name, base: module.base.toString(), path: module.path }));
}
};
+26
View File
@@ -0,0 +1,26 @@
'use strict';
const seen = new Set();
function report(module) {
if (seen.has(module.path)) return;
seen.add(module.path);
console.log('[native-load] ' + module.name + ' base=' + module.base + ' path=' + module.path);
if (typeof globalThis.onGuestModuleLoaded === 'function') {
try { globalThis.onGuestModuleLoaded(module); } catch (error) { console.error(error.stack || error); }
}
}
Process.enumerateModules().forEach(report);
Process.attachModuleObserver({ onAdded: report, onRemoved() {} });
['dlopen', 'android_dlopen_ext'].forEach(name => {
const address = Module.findGlobalExportByName(name);
if (address === null) return;
Interceptor.attach(address, {
onEnter(args) { this.path = args[0].isNull() ? null : args[0].readCString(); },
onLeave(result) {
if (this.path !== null) console.log('[' + name + '] ' + this.path + ' => ' + result);
}
});
});
+28
View File
@@ -0,0 +1,28 @@
'use strict';
import Java from 'frida-java-bridge';
Java.performNow(function () {
try {
const name = 'top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry';
let Registry = null;
try {
Registry = Java.use(name);
} catch (_) {
Java.enumerateClassLoaders({
onMatch(loader) {
if (Registry !== null) return;
try {
loader.loadClass(name);
Registry = Java.ClassFactory.get(loader).use(name);
} catch (_) {}
},
onComplete() {}
});
}
if (Registry === null) throw new Error('GuestRuntimeRegistry ClassLoader was not found');
send({kind: 'fridabox-registry', value: Registry.describe()});
} catch (error) {
send({kind: 'fridabox-error', value: String(error.stack || error)});
}
});
+33
View File
@@ -0,0 +1,33 @@
'use strict';
import Java from 'frida-java-bridge';
Java.perform(() => {
const registryName = 'top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry';
let Registry = null;
try {
Registry = Java.use(registryName);
} catch (_) {
Java.enumerateClassLoaders({
onMatch(loader) {
if (Registry !== null) return;
try {
loader.loadClass(registryName);
Registry = Java.ClassFactory.get(loader).use(registryName);
} catch (_) {}
},
onComplete() {}
});
}
if (Registry === null) throw new Error('GuestRuntimeRegistry ClassLoader was not found');
const loader = Registry.getGuestClassLoader();
if (loader === null) throw new Error('GuestRuntimeRegistry has no guest ClassLoader');
Java.classFactory.loader = loader;
const Target = Java.use('com.qm4rs.fridabox.sample.Target');
const add = Target.add.overload('int', 'int');
add.implementation = function (a, b) {
console.log('[sample-hook] Target.add(' + a + ', ' + b + ') => 1337');
return 1337;
};
console.log('[sample-hook] installed for ' + Registry.getGuestPackageName());
});
+11
View File
@@ -1,5 +1,14 @@
pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.android.application" ||
requested.id.id == "com.android.library") {
useModule("com.android.tools.build:gradle:${requested.version}")
}
}
}
repositories {
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://www.jitpack.io" }
google()
@@ -17,6 +26,7 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://www.jitpack.io" }
google()
mavenCentral()
@@ -29,3 +39,4 @@ include ':app'
include(":black-reflection")
include(":compiler")
include ':Bcore'
include ':sample-guest'
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""Discover FridaBox Gadget endpoints and attach by virtual guest identity."""
from __future__ import annotations
import argparse
import json
import pathlib
import queue
import re
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from typing import Any
from forward_frida_ports import authorized_device, forward_ports
ROOT = pathlib.Path(__file__).resolve().parents[1]
AGENT_DIST = ROOT / "scripts/dist"
BOOTSTRAP = AGENT_DIST / "guest-bootstrap.js"
REGISTRY_PROBE = AGENT_DIST / "registry-probe.js"
FRIDA_VERSION = "17.16.0"
@dataclass
class Endpoint:
port: int
device: Any
session: Any
info: dict[str, Any]
def device_listening_ports(adb: str, serial: str, base_port: int, count: int) -> list[int]:
"""Return listening device ports in the configured range, or the range if ss is unavailable."""
result = subprocess.run(
[adb, "-s", serial, "shell", "ss", "-ltn"],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0 or "not found" in result.stderr.lower():
return list(range(base_port, base_port + count))
upper = base_port + count
ports: set[int] = set()
for line in result.stdout.splitlines():
if "LISTEN" not in line:
continue
for value in re.findall(r":(\d+)\b", line):
port = int(value)
if base_port <= port < upper:
ports.add(port)
return sorted(ports)
def message_printer(message: dict[str, Any], data: bytes | None) -> None:
if message.get("type") == "send":
print(f"[script] {message.get('payload')}")
elif message.get("type") == "error":
print(f"[script-error] {message.get('stack') or message}", file=sys.stderr)
else:
print(f"[script-message] {message}")
if data:
print(f"[script-data] {len(data)} bytes")
def probe_endpoint(frida: Any, port: int) -> Endpoint | None:
manager = frida.get_device_manager()
device = manager.add_remote_device(f"127.0.0.1:{port}")
processes = device.enumerate_processes()
gadget = next((process for process in processes if process.name == "Gadget"), None)
if gadget is None:
return None
session = device.attach(gadget.pid)
answers: queue.Queue[dict[str, Any]] = queue.Queue()
def on_message(message: dict[str, Any], _data: bytes | None) -> None:
if message.get("type") == "send" and isinstance(message.get("payload"), dict):
answers.put(message["payload"])
elif message.get("type") == "error":
answers.put({
"kind": "fridabox-error",
"value": message.get("stack") or message.get("description") or str(message),
})
script = session.create_script(REGISTRY_PROBE.read_text(encoding="utf-8"))
script.on("message", on_message)
script.load()
try:
answer = answers.get(timeout=4.0)
except queue.Empty:
script.unload()
session.detach()
return None
script.unload()
if answer.get("kind") != "fridabox-registry":
print(f"Port {port} rejected: {answer.get('value', 'registry probe failed')}", file=sys.stderr)
session.detach()
return None
try:
info = json.loads(answer["value"])
except (KeyError, TypeError, json.JSONDecodeError):
session.detach()
return None
return Endpoint(port, device, session, info)
def discover(frida: Any, ports: list[int]) -> list[Endpoint]:
endpoints: list[Endpoint] = []
for port in ports:
try:
endpoint = probe_endpoint(frida, port)
if endpoint is not None:
endpoints.append(endpoint)
except Exception as error:
text = str(error).lower()
if "version" in text and ("mismatch" in text or "incompatible" in text):
print_version_fix(frida, error)
continue
return endpoints
def print_version_fix(frida: Any, error: Exception) -> None:
local = getattr(frida, "__version__", "unknown")
print(f"Frida protocol/version error (client {local}, Gadget {FRIDA_VERSION}): {error}", file=sys.stderr)
print(f"Fix: {sys.executable} -m pip install --upgrade frida=={FRIDA_VERSION}", file=sys.stderr)
def print_table(endpoints: list[Endpoint]) -> None:
headings = ("PORT", "GUEST PACKAGE", "GUEST PROCESS", "VPID", "SOURCE APK")
rows = [
(str(item.port), str(item.info.get("package")), str(item.info.get("process")),
str(item.info.get("virtualProcessId")), str(item.info.get("sourceDir")))
for item in endpoints
]
widths = [len(value) for value in headings]
for row in rows:
widths = [max(width, len(value)) for width, value in zip(widths, row)]
print(" ".join(value.ljust(width) for value, width in zip(headings, widths)))
print(" ".join("-" * width for width in widths))
for row in rows:
print(" ".join(value.ljust(width) for value, width in zip(row, widths)))
def loadable_user_agent(source: pathlib.Path) -> pathlib.Path:
if source.parent.name != "dist":
compiled = source.parent / "dist" / source.name
if compiled.is_file():
return compiled
return source
def attach_scripts(endpoint: Endpoint, user_script: pathlib.Path | None, keep_alive: bool) -> None:
scripts = []
bootstrap = endpoint.session.create_script(BOOTSTRAP.read_text(encoding="utf-8"))
bootstrap.on("message", message_printer)
bootstrap.load()
scripts.append(bootstrap)
bootstrap_info: dict[str, Any] | None = None
for _attempt in range(40):
try:
candidate = bootstrap.exports_sync.info()
bootstrap.exports_sync.useclass("java.lang.Object")
if isinstance(candidate, dict) and "error" not in candidate:
bootstrap_info = candidate
break
except Exception:
pass
time.sleep(0.25)
if bootstrap_info is None:
raise RuntimeError("guest bootstrap did not select a ClassLoader within 10 seconds")
modules = bootstrap.exports_sync.enumeratemodules()
print("GuestRuntimeRegistry: " + json.dumps(bootstrap_info, sort_keys=True), flush=True)
print(f"Native modules: {len(modules)}", flush=True)
for module in modules[:12]:
print(f" {module.get('name')} @ {module.get('base')} {module.get('path')}", flush=True)
if user_script is not None:
agent = loadable_user_agent(user_script)
script = endpoint.session.create_script(agent.read_text(encoding="utf-8"))
script.on("message", message_printer)
script.load()
scripts.append(script)
print(f"Attached on port {endpoint.port} to {endpoint.info.get('package')} / {endpoint.info.get('process')}")
if keep_alive or user_script is not None:
stopped = threading.Event()
try:
while not stopped.wait(0.5):
pass
except KeyboardInterrupt:
print("Detaching…")
for script in reversed(scripts):
try:
script.unload()
except Exception:
pass
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--list", action="store_true", help="discover and list guests")
parser.add_argument("--package", help="guest package to attach")
parser.add_argument("--process", help="optional guest process name")
parser.add_argument("--script", type=pathlib.Path, help="user JavaScript loaded after bootstrap")
parser.add_argument("--keep-alive", action="store_true")
parser.add_argument("--base-port", type=int, default=27042)
parser.add_argument("--count", type=int, default=32)
args = parser.parse_args()
if not args.list and not args.package:
parser.error("use --list or --package PACKAGE")
if args.script is not None and not args.script.is_file():
parser.error(f"script does not exist: {args.script}")
if not BOOTSTRAP.is_file() or not REGISTRY_PROBE.is_file():
raise SystemExit("Compiled Frida 17 agents are missing; run: npm ci && python tools/build_frida_agents.py")
import shutil
adb = shutil.which("adb")
if adb is None:
raise SystemExit("adb was not found on PATH; install Android platform-tools")
try:
serial = authorized_device(adb)
forward_ports(adb, serial, args.base_port, args.count)
except (RuntimeError, subprocess.CalledProcessError) as error:
raise SystemExit(str(error)) from error
try:
import frida
except ImportError as error:
raise SystemExit(f"Install controller dependencies: {sys.executable} -m pip install -r tools/requirements.txt") from error
local_major = str(getattr(frida, "__version__", "0")).split(".", 1)[0]
if local_major != FRIDA_VERSION.split(".", 1)[0]:
print_version_fix(frida, RuntimeError("major versions differ"))
return 2
ports = device_listening_ports(adb, serial, args.base_port, args.count)
endpoints = discover(frida, ports)
print_table(endpoints)
if args.list:
for endpoint in endpoints:
endpoint.session.detach()
return 0
matches = [item for item in endpoints if item.info.get("package") == args.package]
if args.process:
matches = [item for item in matches if item.info.get("process") == args.process]
if not matches:
raise SystemExit("No matching FridaBox guest endpoint was discovered")
chosen = matches[0]
for endpoint in endpoints:
if endpoint is not chosen:
endpoint.session.detach()
try:
attach_scripts(chosen, args.script, args.keep_alive)
finally:
chosen.session.detach()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Build Frida 17 Java agents with the explicitly pinned Java bridge."""
from __future__ import annotations
import os
import pathlib
import subprocess
import sys
ROOT = pathlib.Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
OUTPUT = SCRIPTS / "dist"
ENTRIES = ("registry-probe.js", "guest-bootstrap.js", "sample-hook.js")
def compiler_path() -> pathlib.Path:
name = "frida-compile.cmd" if os.name == "nt" else "frida-compile"
path = ROOT / "node_modules" / ".bin" / name
if not path.is_file():
raise SystemExit("Install pinned agent dependencies first: npm ci")
return path
def main() -> int:
compiler = compiler_path()
OUTPUT.mkdir(parents=True, exist_ok=True)
for name in ENTRIES:
subprocess.run(
[str(compiler), str(SCRIPTS / name), "-o", str(OUTPUT / name), "-S", "-c"],
cwd=ROOT,
check=True,
)
print("Built Frida agents: " + ", ".join(str(OUTPUT / name) for name in ENTRIES))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Fetch and verify the pinned official Frida Gadget Android ARM64 binary."""
from __future__ import annotations
import argparse
import hashlib
import json
import lzma
import pathlib
import shutil
import tempfile
import urllib.request
VERSION = "17.16.0"
ASSET = f"frida-gadget-{VERSION}-android-arm64.so.xz"
API_URL = f"https://api.github.com/repos/frida/frida/releases/tags/{VERSION}"
ROOT = pathlib.Path(__file__).resolve().parents[1]
DEST = ROOT / "app/src/main/jniLibs/arm64-v8a/libfrida-gadget.so"
CHECKSUM = ROOT / "tools/frida-gadget-17.16.0.sha256"
EXPECTED_SHA256 = "6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e"
def sha256(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def release_asset_url() -> str:
request = urllib.request.Request(
API_URL,
headers={"Accept": "application/vnd.github+json", "User-Agent": "FridaBox-build"},
)
with urllib.request.urlopen(request, timeout=30) as response:
release = json.load(response)
if release.get("tag_name") != VERSION:
raise RuntimeError(f"GitHub returned unexpected release {release.get('tag_name')!r}")
for asset in release.get("assets", []):
if asset.get("name") == ASSET:
return str(asset["browser_download_url"])
raise RuntimeError(f"Release {VERSION} does not contain {ASSET}")
def fetch(force: bool = False) -> str:
if DEST.exists() and not force:
digest = sha256(DEST)
if digest != EXPECTED_SHA256:
raise RuntimeError(f"Existing Gadget SHA-256 mismatch: {digest}")
CHECKSUM.write_text(f"{digest} {DEST.name}\n", encoding="ascii")
return digest
DEST.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="fridabox-") as temp_dir:
archive = pathlib.Path(temp_dir) / ASSET
request = urllib.request.Request(release_asset_url(), headers={"User-Agent": "FridaBox-build"})
with urllib.request.urlopen(request, timeout=120) as response, archive.open("wb") as output:
shutil.copyfileobj(response, output)
temporary_output = pathlib.Path(temp_dir) / DEST.name
with lzma.open(archive, "rb") as source, temporary_output.open("wb") as output:
shutil.copyfileobj(source, output)
if temporary_output.stat().st_size < 1_000_000:
raise RuntimeError("Downloaded Gadget is unexpectedly small")
shutil.move(str(temporary_output), DEST)
digest = sha256(DEST)
if digest != EXPECTED_SHA256:
DEST.unlink(missing_ok=True)
raise RuntimeError(f"Downloaded Gadget SHA-256 mismatch: {digest}")
CHECKSUM.write_text(f"{digest} {DEST.name}\n", encoding="ascii")
return digest
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--force", action="store_true", help="replace an existing Gadget")
args = parser.parse_args()
digest = fetch(args.force)
print(f"Frida Gadget {VERSION}: {DEST}")
print(f"SHA-256: {digest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Forward the FridaBox Gadget discovery range through ADB."""
from __future__ import annotations
import argparse
import shutil
import subprocess
def authorized_device(adb: str) -> str:
result = subprocess.run([adb, "devices"], check=True, text=True, capture_output=True)
devices = []
unauthorized = []
for line in result.stdout.splitlines()[1:]:
fields = line.split()
if len(fields) >= 2 and fields[1] == "device":
devices.append(fields[0])
elif len(fields) >= 2 and fields[1] == "unauthorized":
unauthorized.append(fields[0])
if not devices:
suffix = f"; unauthorized: {', '.join(unauthorized)}" if unauthorized else ""
raise RuntimeError("No authorized Android device found" + suffix)
return devices[0]
def forward_ports(adb: str, serial: str, base_port: int, count: int) -> None:
for port in range(base_port, base_port + count):
subprocess.run(
[adb, "-s", serial, "forward", f"tcp:{port}", f"tcp:{port}"],
check=True,
stdout=subprocess.DEVNULL,
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-port", type=int, default=27042)
parser.add_argument("--count", type=int, default=32)
args = parser.parse_args()
adb = shutil.which("adb")
if adb is None:
raise SystemExit("adb was not found on PATH; install Android platform-tools")
try:
serial = authorized_device(adb)
forward_ports(adb, serial, args.base_port, args.count)
except (RuntimeError, subprocess.CalledProcessError) as error:
raise SystemExit(str(error)) from error
print(f"Forwarded {args.base_port}..{args.base_port + args.count - 1} on {serial}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1
View File
@@ -0,0 +1 @@
6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e libfrida-gadget.so
+1
View File
@@ -0,0 +1 @@
frida==17.16.0