feat: ship commercial FridaBox workspace

This commit is contained in:
Mahdi Karzari
2026-07-20 04:25:09 +03:30
parent 5140bfc6a7
commit 165c014d75
32 changed files with 1666 additions and 307 deletions
+1
View File
@@ -13,6 +13,7 @@ android {
aidlPackagedList "android/app/IServiceConnection.aidl"
aidlPackagedList "android/app/IBinderSession.aidl"
aidlPackagedList "android/accounts/IAccountManagerResponse.aidl"
buildFeatures {
aidl true
@@ -0,0 +1,5 @@
package android.app;
/** @hide */
interface IBinderSession {
}
@@ -18,9 +18,10 @@
package android.app;
import android.content.ComponentName;
import android.app.IBinderSession;
/** @hide */
interface IServiceConnection {
void connected(in ComponentName name, IBinder service);
void connected(in ComponentName name, IBinder service, IBinderSession session, boolean dead);
}
@@ -0,0 +1,13 @@
package black.android.app;
import android.content.ComponentName;
import android.os.IBinder;
import top.niunaijun.blackreflection.annotation.BClassName;
import top.niunaijun.blackreflection.annotation.BMethod;
@BClassName("android.app.IServiceConnection")
public interface IServiceConnectionL {
@BMethod
void connected(ComponentName ComponentName0, IBinder IBinder1);
}
@@ -1,6 +1,7 @@
package top.niunaijun.blackbox.fake.delegate;
import android.app.IServiceConnection;
import android.app.IBinderSession;
import android.content.ComponentName;
import android.content.Intent;
import android.os.IBinder;
@@ -9,6 +10,7 @@ import android.os.RemoteException;
import java.util.HashMap;
import java.util.Map;
import black.android.app.BRIServiceConnectionL;
import black.android.app.BRIServiceConnectionO;
import top.niunaijun.blackbox.utils.compat.BuildCompat;
@@ -48,7 +50,6 @@ public class ServiceConnectionDelegate extends IServiceConnection.Stub {
return delegate;
}
@Override
public void connected(ComponentName name, IBinder service) throws RemoteException {
connected(name, service, false);
}
@@ -57,7 +58,17 @@ public class ServiceConnectionDelegate extends IServiceConnection.Stub {
if (BuildCompat.isOreo()) {
BRIServiceConnectionO.get(mConn).connected(mComponentName, service, dead);
} else {
mConn.connected(name, service);
BRIServiceConnectionL.get(mConn).connected(mComponentName, service);
}
}
@Override
public void connected(ComponentName name, IBinder service, IBinderSession session, boolean dead)
throws RemoteException {
if (android.os.Build.VERSION.SDK_INT >= 36) {
mConn.connected(mComponentName, service, session, dead);
} else {
connected(name, service, dead);
}
}
}
@@ -2,6 +2,7 @@ package top.niunaijun.blackbox.instrumentation;
import android.util.Log;
import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
/** Loads Frida Gadget at most once in the current Linux process. */
@@ -25,8 +26,17 @@ public final class FridaGadgetLoader {
if (!ATTEMPTED.compareAndSet(false, true)) return false;
try {
InstrumentationStatusStore.recordBinding();
Log.i(TAG, "Loading Frida Gadget for " + GuestRuntimeRegistry.getGuestProcessName());
System.loadLibrary("frida-gadget");
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
if (InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)) {
String scriptPath = InstrumentationSettings.getScriptPathForPackage(packageName);
File runtime = LocalScriptGadgetRuntime.prepare(packageName, scriptPath);
Log.i(TAG, "Loading on-device Frida agent for " + GuestRuntimeRegistry.getGuestProcessName());
System.load(runtime.getAbsolutePath());
} else {
Log.i(TAG, "Loading Frida Gadget listener for " + GuestRuntimeRegistry.getGuestProcessName());
System.loadLibrary("frida-gadget");
}
loaded = true;
InstrumentationStatusStore.recordLoaded();
Log.i(TAG, "Frida Gadget loaded");
@@ -13,6 +13,12 @@ public final class InstrumentationSettings {
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 static final String PACKAGE_MODE_PREFIX = "package_mode_";
private static final String PACKAGE_SCRIPT_PREFIX = "package_script_";
public static final String MODE_COMPUTER = "computer";
public static final String MODE_LOCAL_SCRIPT = "local_script";
public static final String MODE_CLEAN = "clean";
private InstrumentationSettings() {
}
@@ -27,13 +33,56 @@ public final class InstrumentationSettings {
}
public static boolean isEnabledForPackage(String packageName) {
SharedPreferences preferences = preferences();
return preferences.getBoolean(KEY_ENABLED, true)
&& preferences.getBoolean(PACKAGE_PREFIX + packageName, true);
return preferences().getBoolean(KEY_ENABLED, true)
&& !MODE_CLEAN.equals(getModeForPackage(packageName));
}
public static void setEnabledForPackage(String packageName, boolean enabled) {
preferences().edit().putBoolean(PACKAGE_PREFIX + packageName, enabled).commit();
setModeForPackage(packageName, enabled ? MODE_COMPUTER : MODE_CLEAN);
}
public static String getModeForPackage(String packageName) {
SharedPreferences preferences = preferences();
String mode = preferences.getString(PACKAGE_MODE_PREFIX + packageName, null);
if (isValidMode(mode)) return mode;
return preferences.getBoolean(PACKAGE_PREFIX + packageName, true)
? MODE_COMPUTER : MODE_CLEAN;
}
public static void setModeForPackage(String packageName, String mode) {
String safeMode = isValidMode(mode) ? mode : MODE_COMPUTER;
preferences().edit()
.putString(PACKAGE_MODE_PREFIX + packageName, safeMode)
.putBoolean(PACKAGE_PREFIX + packageName, !MODE_CLEAN.equals(safeMode))
.commit();
}
public static String getScriptPathForPackage(String packageName) {
return preferences().getString(PACKAGE_SCRIPT_PREFIX + packageName, null);
}
public static void setScriptPathForPackage(String packageName, String path) {
SharedPreferences.Editor editor = preferences().edit();
if (path == null || path.trim().isEmpty()) {
editor.remove(PACKAGE_SCRIPT_PREFIX + packageName);
} else {
editor.putString(PACKAGE_SCRIPT_PREFIX + packageName, path);
}
editor.commit();
}
public static void clearPackage(String packageName) {
preferences().edit()
.remove(PACKAGE_PREFIX + packageName)
.remove(PACKAGE_MODE_PREFIX + packageName)
.remove(PACKAGE_SCRIPT_PREFIX + packageName)
.commit();
}
private static boolean isValidMode(String mode) {
return MODE_COMPUTER.equals(mode)
|| MODE_LOCAL_SCRIPT.equals(mode)
|| MODE_CLEAN.equals(mode);
}
public static int getBasePort() {
@@ -17,21 +17,42 @@ public final class InstrumentationStatusStore {
public static void recordBinding() {
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
String state;
if (!GuestRuntimeRegistry.isInstrumentationEnabled()) {
state = "disabled";
} else if (InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)) {
state = "loading_local_script";
} else {
state = "waiting_for_attach";
}
preferences().edit()
.putString("runtime_package", packageName)
.putString("runtime_process", GuestRuntimeRegistry.getGuestProcessName())
.putInt("runtime_user_id", GuestRuntimeRegistry.getGuestUserId())
.putInt("runtime_vpid", GuestRuntimeRegistry.getVirtualProcessId())
.putString("runtime_source", GuestRuntimeRegistry.getGuestSourceDir())
.putString("runtime_class_loader", classLoaderDescription())
.putBoolean("runtime_enabled", GuestRuntimeRegistry.isInstrumentationEnabled())
.putString("runtime_state", GuestRuntimeRegistry.isInstrumentationEnabled()
? "waiting_for_attach" : "disabled")
.putString("runtime_mode", mode)
.putString("runtime_script", InstrumentationSettings.getScriptPathForPackage(packageName))
.putString("runtime_state", state)
.putString("runtime_error", null)
.putLong("runtime_timestamp", GuestRuntimeRegistry.getInitializationTimestamp())
.commit();
}
private static String classLoaderDescription() {
ClassLoader loader = GuestRuntimeRegistry.getGuestClassLoader();
if (loader == null) return null;
return loader.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(loader));
}
public static void recordLoaded() {
preferences().edit().putString("runtime_state", "loaded").putString("runtime_error", null).commit();
String mode = preferences().getString("runtime_mode", InstrumentationSettings.MODE_COMPUTER);
String state = InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)
? "local_script_active" : "computer_attached";
preferences().edit().putString("runtime_state", state).putString("runtime_error", null).commit();
}
public static void recordError(String error) {
@@ -0,0 +1,114 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import top.niunaijun.blackbox.BlackBoxCore;
/** Prepares a private Gadget copy configured to autonomously load one guest agent. */
final class LocalScriptGadgetRuntime {
private static final String AGENT_ROOT = "fridabox-agents";
private static final String AGENT_NAME = "agent.js";
private static final String RUNTIME_NAME = "libfridabox-agent.so";
private static final String CONFIG_NAME = "libfridabox-agent.config.so";
private LocalScriptGadgetRuntime() {
}
static File prepare(String packageName, String scriptPath) throws IOException {
Context context = BlackBoxCore.getContext();
if (scriptPath == null || scriptPath.trim().isEmpty()) {
throw new IOException("No on-device JavaScript agent is selected");
}
File root = new File(context.getFilesDir(), AGENT_ROOT).getCanonicalFile();
File script = new File(scriptPath).getCanonicalFile();
if (!isInside(root, script) || !AGENT_NAME.equals(script.getName()) || !script.isFile()) {
throw new IOException("Selected JavaScript agent is outside FridaBox private storage");
}
if (!script.setReadable(true, true) || !script.setWritable(false, false)) {
throw new IOException("Unable to secure the selected JavaScript agent");
}
File directory = script.getParentFile();
File source = new File(context.getApplicationInfo().nativeLibraryDir, "libfrida-gadget.so");
if (!source.isFile()) throw new IOException("Packaged Frida Gadget is missing");
File runtime = new File(directory, RUNTIME_NAME);
if (!runtime.isFile() || runtime.length() != source.length()) {
copyAtomically(source, runtime);
}
if (!runtime.setReadable(true, false)
|| !runtime.setExecutable(true, false)
|| !runtime.setWritable(false, false)) {
throw new IOException("Unable to secure private Frida Gadget permissions");
}
File config = new File(directory, CONFIG_NAME);
writeUtf8Atomically(config, buildConfig(packageName));
return runtime;
}
static String buildConfig(String packageName) {
return "{\n" +
" \"interaction\": {\n" +
" \"type\": \"script\",\n" +
" \"path\": \"" + AGENT_NAME + "\",\n" +
" \"on_change\": \"reload\",\n" +
" \"parameters\": { \"package\": \"" + json(packageName) + "\" }\n" +
" },\n" +
" \"runtime\": \"qjs\",\n" +
" \"teardown\": \"minimal\"\n" +
"}\n";
}
static boolean isInside(File root, File child) {
String rootPath = root.getAbsolutePath();
String childPath = child.getAbsolutePath();
return childPath.startsWith(rootPath + File.separator);
}
private static String json(String value) {
if (value == null) return "";
return value.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\n", "\\n").replace("\r", "\\r");
}
private static void copyAtomically(File source, File destination) throws IOException {
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
if (temporary.exists() && !temporary.delete()) throw new IOException("Unable to replace temporary Gadget");
try (FileInputStream input = new FileInputStream(source);
FileOutputStream output = new FileOutputStream(temporary)) {
byte[] buffer = new byte[128 * 1024];
int count;
while ((count = input.read(buffer)) >= 0) output.write(buffer, 0, count);
output.getFD().sync();
}
replace(temporary, destination);
}
private static void writeUtf8Atomically(File destination, String value) throws IOException {
byte[] expected = value.getBytes(StandardCharsets.UTF_8);
if (destination.isFile() && destination.length() == expected.length) {
byte[] current = new byte[expected.length];
try (FileInputStream input = new FileInputStream(destination)) {
if (input.read(current) == current.length && java.util.Arrays.equals(current, expected)) return;
}
}
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
try (FileOutputStream output = new FileOutputStream(temporary)) {
output.write(expected);
output.getFD().sync();
}
replace(temporary, destination);
}
private static void replace(File temporary, File destination) throws IOException {
if (destination.exists() && !destination.delete()) throw new IOException("Unable to replace " + destination.getName());
if (!temporary.renameTo(destination)) throw new IOException("Unable to install " + destination.getName());
}
}
@@ -0,0 +1,25 @@
package top.niunaijun.blackbox.instrumentation;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.io.File;
public class LocalScriptGadgetRuntimeTest {
@Test
public void configUsesAutonomousScriptInteraction() {
String config = LocalScriptGadgetRuntime.buildConfig("sample.\"guest");
assertTrue(config.contains("\"type\": \"script\""));
assertTrue(config.contains("\"path\": \"agent.js\""));
assertTrue(config.contains("sample.\\\"guest"));
}
@Test
public void privatePathCheckRejectsSiblingPrefix() {
File root = new File("/data/user/0/host/files/fridabox-agents");
assertTrue(LocalScriptGadgetRuntime.isInside(root, new File(root, "guest/agent.js")));
assertFalse(LocalScriptGadgetRuntime.isInside(root, new File(root.getPath() + "-other/agent.js")));
}
}
+25 -1
View File
@@ -3,6 +3,16 @@ plugins {
alias(libs.plugins.jetbrains.kotlin.android)
}
def releaseStoreFile = System.getenv("FRIDABOX_RELEASE_STORE_FILE")
def releaseStorePassword = System.getenv("FRIDABOX_RELEASE_STORE_PASSWORD")
def releaseKeyAlias = System.getenv("FRIDABOX_RELEASE_KEY_ALIAS")
def releaseKeyPassword = System.getenv("FRIDABOX_RELEASE_KEY_PASSWORD")
def releaseSigningValues = [releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword]
def releaseSigningConfigured = releaseSigningValues.every { it != null && !it.trim().isEmpty() }
if (releaseSigningValues.any { it != null && !it.trim().isEmpty() } && !releaseSigningConfigured) {
throw new GradleException("Release signing is incomplete. Set all four FRIDABOX_RELEASE_* environment variables.")
}
android {
namespace 'top.niunaijun.blackboxa'
@@ -28,10 +38,24 @@ android {
}
}
signingConfigs {
if (releaseSigningConfigured) {
release {
storeFile file(releaseStoreFile)
storePassword releaseStorePassword
keyAlias releaseKeyAlias
keyPassword releaseKeyPassword
}
}
}
buildTypes {
release {
signingConfig signingConfigs.debug
if (releaseSigningConfigured) {
signingConfig signingConfigs.release
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+12 -12
View File
@@ -17,18 +17,24 @@
android:allowBackup="false"
android:extractNativeLibs="true"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/ic_fridabox_app"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@drawable/ic_fridabox_app"
android:supportsRtl="true"
android:theme="@style/Theme.BlackBox"
android:theme="@style/Theme.FridaBox"
android:enableOnBackInvokedCallback="true"
tools:replace="android:allowBackup"
tools:targetApi="n">
<activity
android:name=".fridabox.FridaBoxActivity"
android:exported="false" />
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".view.fake.FollowMyLocationOverlay"
android:exported="false" />
@@ -36,15 +42,9 @@
<activity android:name=".view.gms.GmsManagerActivity" />
<activity
android:name=".view.main.WelcomeActivity"
android:exported="true"
android:exported="false"
android:launchMode="singleTop"
android:theme="@style/WelcomeTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android:theme="@style/Theme.FridaBox" />
<activity android:name=".view.list.ListActivity" />
<activity android:name=".view.fake.FakeManagerActivity" />
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<gradient android:angle="315" android:startColor="#1A2942" android:centerColor="#141D30" android:endColor="#111827" />
<corners android:radius="28dp" />
<stroke android:width="1dp" android:color="#334862" />
</shape>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF05070B" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6z" />
</vector>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M3,13h4l2.4,-7.2L14,20l3,-7h4v-2h-5.3l-1.5,3.5L9.6,1L5.5,11H3z" />
</vector>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M19.4,13a7.7,7.7 0,0 0,0.1 -1,7.7 7.7,0 0,0 -0.1,-1l2.1,-1.7 -2,-3.4 -2.5,1a8,8 0,0 0,-1.7 -1L15,3h-4l-0.4,2.8a8,8 0,0 0,-1.7 1l-2.5,-1 -2,3.4L6.6,11a7.7,7.7 0,0 0,-0.1 1,7.7 7.7,0 0,0 0.1,1l-2.1,1.7 2,3.4 2.5,-1a8,8 0,0 0,1.7 1L11,21h4l0.4,-2.8a8,8 0,0 0,1.7 -1l2.5,1 2,-3.4zM13,15.5a3.5,3.5 0,1 1,0 -7,3.5 3.5,0 0,1 0,7z" />
</vector>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M3,3h8v8H3zM13,3h8v5h-8zM13,10h8v11h-8zM3,13h8v8H3z" />
</vector>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
<path android:fillColor="#080C14" android:pathData="M18,4 H90 C97.7,4 104,10.3 104,18 V90 C104,97.7 97.7,104 90,104 H18 C10.3,104 4,97.7 4,90 V18 C4,10.3 10.3,4 18,4 Z" />
<path android:fillColor="#64E8E8" android:pathData="M43,26 L22,47 C18.1,50.9 18.1,57.1 22,61 L43,82 L50,75 L29,54 L50,33 Z" />
<path android:fillColor="#9B87F5" android:pathData="M65,26 L86,47 C89.9,50.9 89.9,57.1 86,61 L65,82 L58,75 L79,54 L58,33 Z" />
<path android:fillColor="#F8FAFC" android:pathData="M49,44 h10 v8 h-10z M49,57 h10 v8 h-10z" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="32dp" android:height="32dp" android:viewportWidth="32" android:viewportHeight="32">
<path android:fillColor="#64E8E8" android:pathData="M12.2,5.5 L4.2,13.5 C2.8,14.9 2.8,17.1 4.2,18.5 L12.2,26.5 L15,23.7 L7.3,16 L15,8.3 Z" />
<path android:fillColor="#9B87F5" android:pathData="M19.8,5.5 L28,13.5 C29.4,14.9 29.4,17.1 28,18.5 L19.8,26.5 L17,23.7 L24.7,16 L17,8.3 Z" />
<path android:fillColor="#F8FAFC" android:pathData="M14.2,12.2 h3.6 v3 h-3.6z M14.2,16.8 h3.6 v3 h-3.6z" />
</vector>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fridabox_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/fb_background"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/fb_background"
android:elevation="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="72dp"
android:background="@color/fb_background"
android:contentInsetStart="18dp"
android:contentInsetEnd="18dp"
app:logo="@drawable/ic_fridabox_mark"
app:logoDescription="@string/fb_brand"
app:subtitle="@string/fb_brand_tagline"
app:subtitleTextColor="@color/fb_text_secondary"
app:title="@string/fb_brand"
app:titleTextColor="@color/fb_text_primary" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/content_scroll"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:fillViewport="true"
android:overScrollMode="never"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/app_bar">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="420dp">
<LinearLayout
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical"
android:paddingStart="@dimen/fb_content_margin"
android:paddingTop="@dimen/fb_content_top"
android:paddingEnd="@dimen/fb_content_margin"
android:paddingBottom="@dimen/fb_content_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="@dimen/fb_max_content_width" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="0dp"
android:layout_height="3dp"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="@color/fb_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/app_bar"
app:trackColor="@color/fb_surface" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/import_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="18dp"
android:layout_marginBottom="18dp"
android:letterSpacing="0"
android:text="@string/fb_import_apk"
android:textAllCaps="false"
android:textColor="@color/fb_black"
app:backgroundTint="@color/fb_primary"
app:icon="@drawable/ic_fb_add"
app:iconTint="@color/fb_black"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="72dp"
android:background="@color/fb_surface"
app:itemActiveIndicatorStyle="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
app:itemIconTint="@color/fb_text_primary"
app:itemTextColor="@color/fb_text_primary"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/fridabox_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/nav_workspace" android:icon="@drawable/ic_fb_workspace" android:title="@string/fb_nav_workspace" />
<item android:id="@+id/nav_runtime" android:icon="@drawable/ic_fb_runtime" android:title="@string/fb_nav_runtime" />
<item android:id="@+id/nav_settings" android:icon="@drawable/ic_fb_settings" android:title="@string/fb_nav_settings" />
</menu>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fb_content_margin">32dp</dimen>
<dimen name="fb_content_top">28dp</dimen>
<dimen name="fb_content_bottom">120dp</dimen>
<dimen name="fb_card_radius">24dp</dimen>
<dimen name="fb_max_content_width">920dp</dimen>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="fb_background">#080C14</color>
<color name="fb_surface">#111827</color>
<color name="fb_surface_high">#182235</color>
<color name="fb_surface_tint">#1D2940</color>
<color name="fb_primary">#64E8E8</color>
<color name="fb_primary_dark">#1AAFB5</color>
<color name="fb_secondary">#9B87F5</color>
<color name="fb_text_primary">#F8FAFC</color>
<color name="fb_text_secondary">#9CAEC5</color>
<color name="fb_outline">#2B3A52</color>
<color name="fb_success">#42D39A</color>
<color name="fb_warning">#F5BF5B</color>
<color name="fb_error">#FB7185</color>
<color name="fb_black">#05070B</color>
<color name="fb_transparent">#00000000</color>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fb_content_margin">18dp</dimen>
<dimen name="fb_content_top">20dp</dimen>
<dimen name="fb_content_bottom">112dp</dimen>
<dimen name="fb_card_radius">22dp</dimen>
<dimen name="fb_max_content_width">840dp</dimen>
</resources>
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fb_brand">FridaBox</string>
<string name="fb_brand_tagline">Mobile instrumentation workspace</string>
<string name="fb_nav_workspace">Workspace</string>
<string name="fb_nav_runtime">Runtime</string>
<string name="fb_nav_settings">Settings</string>
<string name="fb_import_apk">Import APK</string>
<string name="fb_hero_eyebrow">PRIVATE · NON-ROOT · ARM64</string>
<string name="fb_hero_title">Instrument apps. Keep control.</string>
<string name="fb_hero_body">Run trusted JavaScript agents directly on the device, connect from a computer, or launch a clean guest—per app.</string>
<string name="fb_guest_workspace">Guest workspace</string>
<string name="fb_guest_workspace_hint">Each guest keeps its own launch mode and agent.</string>
<string name="fb_no_guests">Your workspace is empty</string>
<string name="fb_no_guests_body">Import one ARM64 base APK to create a private FridaBox guest.</string>
<string name="fb_mode_local">On-device</string>
<string name="fb_mode_computer">Computer</string>
<string name="fb_mode_clean">Clean</string>
<string name="fb_mode_local_title">On-device agent</string>
<string name="fb_mode_local_body">The selected JavaScript runs automatically before app startup. No cable or controller is required.</string>
<string name="fb_mode_computer_title">Computer attach</string>
<string name="fb_mode_computer_body">Startup pauses at Frida Gadget until you attach from a computer.</string>
<string name="fb_mode_clean_title">Clean launch</string>
<string name="fb_mode_clean_body">Starts the guest without loading Frida Gadget.</string>
<string name="fb_select_script">Select JavaScript</string>
<string name="fb_replace_script">Replace agent</string>
<string name="fb_launch">Launch</string>
<string name="fb_more">Manage</string>
<string name="fb_no_script">No JavaScript agent selected</string>
<string name="fb_script_ready">Agent ready</string>
<string name="fb_runtime_title">Runtime</string>
<string name="fb_runtime_subtitle">Latest guest process and instrumentation state</string>
<string name="fb_settings_title">Settings</string>
<string name="fb_settings_subtitle">Connection defaults and product information</string>
<string name="fb_save_settings">Save settings</string>
<string name="fb_global_instrumentation">Allow instrumentation</string>
<string name="fb_advanced_logs">Show advanced runtime details</string>
<string name="fb_base_port">Gadget base port</string>
<string name="fb_scan_count">Port discovery range</string>
<string name="fb_security_title">Run only agents you trust</string>
<string name="fb_security_body">An on-device agent executes inside the virtual guest process and has the same access as that process. FridaBox stores a private byte-for-byte copy of the selected file.</string>
<string name="fb_about_title">Independent by design</string>
<string name="fb_about_body">FridaBox combines a private Android virtualization layer with Frida Gadget. It does not modify imported APKs and it does not require root.</string>
<string name="fb_copy_command">Copy command</string>
<string name="fb_refresh">Refresh</string>
<string name="fb_choose_trusted_title">Choose a trusted JavaScript agent?</string>
<string name="fb_choose_trusted_body">The file will be copied into FridaBox private storage and executed automatically whenever this guest starts in On-device mode.</string>
<string name="fb_choose">Choose file</string>
<string name="fb_cancel">Cancel</string>
<string name="fb_computer_launch_title">Ready to attach from a computer?</string>
<string name="fb_computer_launch_body">The guest will pause before its Application starts. Connect with Frida to resume it.</string>
<string name="fb_clear_data">Clear guest data</string>
<string name="fb_runtime_details">Runtime details</string>
<string name="fb_remove_guest">Remove guest</string>
<string name="fb_remove_title">Remove this guest?</string>
<string name="fb_remove_body">The virtual app, its virtual data, and its saved on-device agent will be removed from FridaBox. The Android PackageManager installation is not changed.</string>
<string name="fb_remove">Remove</string>
<string name="fb_details">App details</string>
</resources>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.FridaBox" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/fb_primary</item>
<item name="colorPrimaryVariant">@color/fb_primary_dark</item>
<item name="colorOnPrimary">@color/fb_black</item>
<item name="colorSecondary">@color/fb_secondary</item>
<item name="colorSecondaryVariant">@color/fb_secondary</item>
<item name="colorOnSecondary">@color/fb_black</item>
<item name="colorSurface">@color/fb_surface</item>
<item name="colorOnSurface">@color/fb_text_primary</item>
<item name="colorError">@color/fb_error</item>
<item name="android:colorAccent">@color/fb_primary</item>
<item name="android:windowBackground">@color/fb_background</item>
<item name="android:navigationBarColor">@color/fb_surface</item>
<item name="android:statusBarColor">@color/fb_background</item>
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
<item name="android:fontFamily">sans</item>
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
<item name="materialAlertDialogTheme">@style/ThemeOverlay.FridaBox.Dialog</item>
</style>
<style name="ThemeOverlay.FridaBox.Dialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="colorSurface">@color/fb_surface_high</item>
<item name="colorOnSurface">@color/fb_text_primary</item>
<item name="colorPrimary">@color/fb_primary</item>
</style>
</resources>
+30
View File
@@ -52,3 +52,33 @@ Build and test:
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.
## Production release
The release variant enables R8 optimization, code shrinking, and resource
shrinking. It never falls back to the Android debug signing key.
Build an unsigned release artifact for later signing:
```powershell
$env:FRIDABOX_NDK_PROJECT_DIR='D:\FridaBoxBuild\Bcore'
.\gradlew.bat :app:assembleRelease
```
For a signed production build, provide all four variables before running the
same task:
```powershell
$env:FRIDABOX_RELEASE_STORE_FILE='D:\secure\fridabox-release.jks'
$env:FRIDABOX_RELEASE_STORE_PASSWORD='<store password>'
$env:FRIDABOX_RELEASE_KEY_ALIAS='fridabox'
$env:FRIDABOX_RELEASE_KEY_PASSWORD='<key password>'
.\gradlew.bat :app:assembleRelease
```
If only some signing variables are present, configuration fails instead of
producing an ambiguously signed artifact. Keep the keystore and credentials
outside the repository and CI logs.
The ARM64 release output is written to
`app/build/outputs/apk/release/FridaBox_4.0.0_arm64-v8a-release.apk`.
+12
View File
@@ -33,6 +33,18 @@ 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.
## On-device mode
Computer-side forwarding and controller tools are not used in On-device mode.
FridaBox creates a private Gadget copy with an adjacent Script-interaction
configuration and a relative `agent.js` path. Gadget loads that script before
returning to guest Application creation. The selected script is stored per
package and reused automatically until it is replaced or the guest is removed.
Use Computer mode when the script needs an interactive host, RPC calls, or
observable `send()` messages. Use On-device mode for autonomous hooks and
in-process behavior.
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.
+51
View File
@@ -68,3 +68,54 @@ Runtime validation passed on a Samsung SM-S928B running ARM64 Android 16/API 36:
The command and log transcript, including two device-discovered fixes, is in
`docs/device-validation.log`.
## Commercial workspace and per-app mode validation
Validation date: 2026-07-20
The redesigned FridaBox launcher and all three per-app modes were validated on
the same Samsung SM-S928B, ARM64 Android 16/API 36 device with the imported
`com.paeezanstudio.pesarkhande` 3.3.7 guest.
- The independent FridaBox launcher, icon, dark product theme, responsive
workspace cards, bottom navigation, import action, and selected-mode states
rendered correctly at 1080 x 2340.
- `pesarkhande-agent.js` (198,960 bytes, SHA-256
`41dd04f7a6a4b8de47fcd94ee5646f43effd8f73b36eda64d46a65f4f304fa49`)
was selected through Android's document picker and copied without modification.
- On-device mode loaded the private Gadget and Script configuration without a
controller, then returned to `beforeCreateApplication`; the Unity game reached
its interactive home screen.
- Runtime reported `local_script_active`, package
`com.paeezanstudio.pesarkhande`, virtual user ID 0, virtual process slot 1,
the private source APK, and `dalvik.system.PathClassLoader`.
- Computer mode paused before `beforeCreateApplication`. Direct
`frida -U gadget` attachment resumed the guest and enumerated 416 native
modules; the first five were `app_process64`, `linker64`,
`libandroid_runtime.so`, `libbinder.so`, and `libcutils.so`.
- Clean mode recycled the main guest PID from 26464 to 27819, emitted
`Instrumentation disabled for this guest process`, opened no Gadget listener,
and launched the game normally.
- A stale cross-process SharedPreferences cache initially made Runtime display
`Waiting for computer` for a successful on-device launch. Multi-process reload
semantics fixed the display; the persisted state was already correct.
- The private JavaScript file is mode 0400 and the private Gadget executable is
mode 0555 at launch. Android 16 no longer reports the writable-executable
warning for the FridaBox Gadget copy.
Final automated builds and tests passed:
```powershell
.\gradlew.bat :app:assembleDebug :app:assembleRelease :Bcore:testDebugUnitTest :app:testDebugUnitTest
```
Artifacts:
- debug: 21,049,981 bytes, SHA-256
`80e70b33fca741e4f805aa233cdfaf5bc6fe2030e93c8fd825611eb5c407c917`;
- release: 13,266,764 bytes, SHA-256
`caa2218194fcbe91c10d0d29a74b7401aaed53f340b8a0d6668321ddae48ddfb`.
The release artifact was intentionally unsigned because no production keystore
was supplied. `apksigner` confirmed the debug APK verifies and the release APK
does not contain a debug signature.
+47 -21
View File
@@ -1,27 +1,53 @@
# 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.
FridaBox keeps every imported application inside its private virtual workspace.
Importing an APK does not install that package into Android's real PackageManager.
Debug builds expose **Install demo guest**. The action installs the generated
`com.qm4rs.fridabox.sample` APK only into BlackBox. Then run:
## Import an application
```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
```
1. Open **Workspace** and tap **Import APK**.
2. Select one ARM64 base `.apk` through Android's document picker.
3. FridaBox validates the APK, records its SHA-256, and creates a private guest.
After startup resumes, press the sample button. The visible result should be
`1337`, demonstrating that the guest ClassLoader was selected.
Split-only packages (`.apks`, `.xapk`, and `.apkm`) and unsupported native ABIs
are rejected.
**Clear virtual app data** and **Remove from virtual space** affect only the
BlackBox virtual environment. They do not invoke Android's real package manager.
## Choose a launch mode
Every guest remembers one of three independent modes:
- **On-device**: select a trusted `.js` file once. FridaBox stores a byte-for-byte
private copy, records its SHA-256, and loads it through Frida Gadget's Script
interaction before the guest Application starts. No cable, computer, Frida CLI,
port forwarding, or controller process is required on later launches.
- **Computer**: starts the loopback-only Gadget listener and pauses before the
guest Application. Attach through the normal Frida workflow, for example:
```text
frida -U gadget -l path/to/agent.js
```
- **Clean**: recycles the virtual process and launches without loading Frida
Gadget. The selected on-device agent is retained for future use.
Switching modes always stops the previous guest process before the next launch.
## On-device agents
Select **On-device**, tap **Select JavaScript**, review the trust warning, and
choose a `.js` file up to 16 MiB. The original file is not modified. The private
copy and the private Gadget executable are made read-only before execution.
An autonomous script has no computer-side message handler. Calls such as `send()`
may be intentionally unobserved, while hooks, replacements, Java calls, native
interceptors, and in-app overlays continue to run in the guest process.
## Runtime and management
The **Runtime** tab reports the latest package, process, virtual user ID, virtual
process slot, source APK, ClassLoader, selected mode, state, and latest error.
Use **Manage** on an app card for app details, runtime details, virtual data
clearing, or removal from the private workspace.
Only run JavaScript agents you trust. An on-device agent executes with the same
access as the virtual guest process.
+104
View File
@@ -161,3 +161,107 @@ 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.
-------------------------------------------------------------------------------
2026-07-20 - Commercial UI and per-app execution modes
-------------------------------------------------------------------------------
Device reconfirmation:
> adb shell getprop ro.product.cpu.abi
arm64-v8a
> adb shell getprop ro.build.version.release
16
> adb shell getprop ro.build.version.sdk
36
> adb install -r -t app\build\outputs\apk\debug\FridaBox_4.0.0_arm64-v8a-debug.apk
Performing Streamed Install
Success
The branded launcher was inspected by screenshot and UIAutomator at 1080 x
2340. FridaBox opened directly into Workspace; the selected mode had a distinct
state, the header was not clipped, and Import APK did not overlap guest actions.
On-device agent import:
> adb push D:\Reverse\Game-Hacking\pesarkhande\release\pesarkhande-agent.js /sdcard/Download/pesarkhande-agent.js
1 file pushed, 198960 bytes
> Get-FileHash -Algorithm SHA256 D:\Reverse\Game-Hacking\pesarkhande\release\pesarkhande-agent.js
41dd04f7a6a4b8de47fcd94ee5646f43effd8f73b36eda64d46a65f4f304fa49
The UI selected On-device -> Select JavaScript -> pesarkhande-agent.js and
displayed the same SHA-256 prefix. The private configuration was:
{
"interaction": {
"type": "script",
"path": "agent.js",
"on_change": "reload",
"parameters": { "package": "com.paeezanstudio.pesarkhande" }
},
"runtime": "qjs",
"teardown": "minimal"
}
Autonomous launch evidence (no Frida client was attached):
07-20 02:46:59.510 760 760 I FridaBox.Gadget: Loading on-device Frida agent for com.paeezanstudio.pesarkhande
07-20 02:46:59.562 760 760 D nativeloader: Load /data/user/0/com.qm4rs.fridabox/files/fridabox-agents/com.paeezanstudio.pesarkhande/libfridabox-agent.so ...: ok
07-20 02:46:59.569 760 760 I FridaBox.Gadget: Frida Gadget loaded
07-20 02:46:59.570 760 760 D BlackBoxLoader: beforeCreateApplication: pkg com.paeezanstudio.pesarkhande, processName com.paeezanstudio.pesarkhande,userID:0
The top resumed activity was ProxyActivity$P0 and the Unity guest reached its
interactive home screen. Private file permissions after launch:
- agent.js: `-r--------`, 198960 bytes
- libfridabox-agent.config.so: `-rw-------`, 210 bytes
- libfridabox-agent.so: `-r-xr-xr-x`, 25212656 bytes
Runtime snapshot:
- runtime_state: local_script_active
- runtime_package: com.paeezanstudio.pesarkhande
- runtime_process: com.paeezanstudio.pesarkhande:Metrica
- runtime_user_id: 0
- runtime_vpid: 1
- runtime_source: /data/user/0/com.qm4rs.fridabox/blackbox/data/app/com.paeezanstudio.pesarkhande/base.apk
- runtime_class_loader: dalvik.system.PathClassLoader@6c0723b
- runtime_error: none
Computer mode regression:
07-20 02:30:02.989 26464 26464 I FridaBox.Gadget: Loading Frida Gadget listener for com.paeezanstudio.pesarkhande
07-20 02:30:03.026 26464 26487 I Frida: Listening on 127.0.0.1 TCP port 27042
No `beforeCreateApplication` line existed before attachment.
> frida -U gadget -q -e "send({type:'fridabox-probe',pid:Process.id,moduleCount:Process.enumerateModules().length,firstModules:Process.enumerateModules().slice(0,5).map(function(m){return m.name;})})"
message: {'type': 'send', 'payload': {'type': 'fridabox-probe', 'pid': 26464, 'moduleCount': 416, 'firstModules': ['app_process64', 'linker64', 'libandroid_runtime.so', 'libbinder.so', 'libcutils.so']}} data: None
07-20 02:31:04.593 26464 26464 I FridaBox.Gadget: Frida Gadget loaded
07-20 02:31:04.594 26464 26464 D BlackBoxLoader: beforeCreateApplication: pkg com.paeezanstudio.pesarkhande, processName com.paeezanstudio.pesarkhande,userID:0
Clean mode regression after virtual-process recycling:
- previous main PID: 26464
- clean main PID: 27819
07-20 02:33:27.289 27819 27819 I FridaBox.Gadget: Instrumentation disabled for this guest process
07-20 02:33:27.290 27819 27819 D BlackBoxLoader: beforeCreateApplication: pkg com.paeezanstudio.pesarkhande, processName com.paeezanstudio.pesarkhande,userID:0
There was no Gadget listener or Gadget load in the clean process. The saved
agent remained available, and the final device state was restored to On-device.
Final build:
> .\gradlew.bat :app:assembleDebug :app:assembleRelease :Bcore:testDebugUnitTest :app:testDebugUnitTest
BUILD SUCCESSFUL
- debug APK: 21049981 bytes, SHA-256 80e70b33fca741e4f805aa233cdfaf5bc6fe2030e93c8fd825611eb5c407c917
- release APK: 13266764 bytes, SHA-256 caa2218194fcbe91c10d0d29a74b7401aaed53f340b8a0d6668321ddae48ddfb
- debug signature verification: passed
- release signature verification: intentionally unsigned; no debug key fallback