mirror of
https://github.com/QM4RS/FridaBox.git
synced 2026-10-01 13:32:01 +02:00
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:
@@ -17,8 +17,13 @@
|
||||
*.logcat
|
||||
.vscode
|
||||
/build
|
||||
**/build/
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/app/release/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
node_modules/
|
||||
.npm-cache/
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
}
|
||||
+40
@@ -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();
|
||||
}
|
||||
}
|
||||
+44
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Vendored
+3
-1
@@ -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 *; }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
+23
@@ -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,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"))));
|
||||
}
|
||||
}
|
||||
+20
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Generated
+589
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "fridabox-agents",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"frida-java-bridge": "7.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"frida-compile": "19.0.5"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+1887
File diff suppressed because one or more lines are too long
Vendored
+1887
File diff suppressed because one or more lines are too long
Vendored
+1887
File diff suppressed because one or more lines are too long
@@ -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 }));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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)});
|
||||
}
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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'
|
||||
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -0,0 +1 @@
|
||||
6bf149e5d1c5ec701e7b822cab57bb243f1c2a03318fa974fe373ee711a9ed9e libfrida-gadget.so
|
||||
@@ -0,0 +1 @@
|
||||
frida==17.16.0
|
||||
Reference in New Issue
Block a user