Compare commits

Author SHA1 Message Date
dependabot[bot]andGitHub 9bbd38df36 build(deps): bump actions/setup-java in the github-actions group
Bumps the github-actions group with 1 update: [actions/setup-java](https://github.com/actions/setup-java).


Updates `actions/setup-java` from 5 to 6
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-31 22:06:38 +00:00
dependabot[bot]andGitHub 330c634715 build(deps): bump org.tukaani:xz to 1.12 (#16)
Apply the bug-fix update after a successful Android CI run.
2026-07-28 19:30:44 +03:30
dependabot[bot]andGitHub 4f4e04aafa build(deps): bump ConstraintLayout to 2.2.1 (#13)
Apply the patch update after a successful Android CI run.
2026-07-28 19:30:35 +03:30
dependabot[bot]andGitHub 16f51cf1d5 build(deps): bump androidx.appcompat:appcompat to 1.7.1 (#15)
Update AppCompat dependencies after a successful Android CI run.
2026-07-28 19:21:03 +03:30
Mahdi Karzari c89c6c4f81 Release FridaBox 4.2.0 with runtime bridges 2026-07-28 18:35:19 +03:30
Mahdi Karzari 4bfc6b5a42 Fix runtime ABI selection across release variants 2026-07-28 13:12:44 +03:30
Mahdi Karzari 3b771faa21 Release FridaBox 4.1.0 for all Android ABIs 2026-07-28 12:52:59 +03:30
Mr.KarzariandGitHub bb608b4663 Merge pull request #12 from QM4RS/agent/gadget-manager-liquid-glass
Agent/gadget manager liquid glass
2026-07-27 16:56:35 +03:30
Mahdi Karzari ca9b32026d Add liquid bubble menu animations 2026-07-27 16:46:30 +03:30
Mahdi Karzari 4d0789ecd2 Add downloadable gadgets and liquid glass launcher 2026-07-27 16:03:29 +03:30
Mahdi Karzari 1d9ff51165 Update README branding with project logo 2026-07-27 14:13:25 +03:30
Mahdi Karzari 44cf537d89 Add floating log output menu and fix several issues 2026-07-27 14:04:49 +03:30
Mr.KarzariandGitHub e4b4f6e350 Fix home header logo and title layout (#11)
Use the current logo asset and a fixed-size custom toolbar layout so the home title and tagline stay aligned.
2026-07-23 22:40:22 +03:30
Mr.KarzariandGitHub 00cc4ebde3 build: consolidate compatible dependency updates (#10)
Update compatible CI actions, Kotlin, and Android test dependencies; repair sdkmanager discovery on current Ubuntu runners; and group future Dependabot updates.
2026-07-23 21:53:24 +03:30
Mahdi Karzari ab88654013 Icon updated 2026-07-23 21:20:26 +03:30
105 changed files with 10505 additions and 604 deletions
+11
View File
@@ -5,6 +5,13 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
android-test-dependencies:
patterns:
- "androidx.test.*"
update-types:
- minor
- patch
- package-ecosystem: npm
directory: /
schedule:
@@ -16,3 +23,7 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
github-actions:
patterns:
- "*"
+8 -7
View File
@@ -21,32 +21,33 @@ jobs:
steps:
- name: Check out source
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '21'
cache: gradle
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.12'
cache: pip
cache-dependency-path: tools/requirements.txt
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
- name: Install Android SDK components
run: |
yes | sdkmanager --licenses >/dev/null || true
sdkmanager \
SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
yes | "$SDKMANAGER" --licenses >/dev/null || true
"$SDKMANAGER" \
'platforms;android-35' \
'build-tools;35.0.0' \
'ndk;29.0.14206865'
@@ -75,7 +76,7 @@ jobs:
- name: Upload APKs and reports
if: always()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: fridabox-${{ github.sha }}
if-no-files-found: warn
+2
View File
@@ -27,3 +27,5 @@ __pycache__/
*.pyc
node_modules/
.npm-cache/
/release-secrets/
/.release-work/
+2 -2
View File
@@ -26,7 +26,7 @@ android {
consumerProguardFiles "consumer-rules.pro"
ndk {
abiFilters 'arm64-v8a'
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
@@ -106,7 +106,7 @@ dependencies {
implementation 'com.google.android.material:material:1.3.0'
implementation(project(":black-reflection"))
annotationProcessor(project(":compiler"))
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
+1 -1
View File
@@ -1,4 +1,4 @@
APP_ABI := armeabi-v7a arm64-v8a
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
APP_PLATFORM := android-24
APP_STL := c++_static
APP_OPTIM := release
Binary file not shown.
Binary file not shown.
@@ -68,6 +68,7 @@ import top.niunaijun.blackbox.utils.StackTraceFilter;
import top.niunaijun.blackbox.utils.SocialMediaAppCrashPrevention;
import top.niunaijun.blackbox.utils.DexCrashPrevention;
import top.niunaijun.blackbox.utils.NativeCrashPrevention;
import top.niunaijun.blackbox.utils.ProcessAbi;
import top.niunaijun.blackbox.utils.CrashMonitor;
import top.niunaijun.blackbox.utils.StoragePermissionHelper;
import top.niunaijun.blackbox.utils.LogSender;
@@ -1463,11 +1464,9 @@ public class BlackBoxCore extends ClientConfiguration {
}
public static boolean is64Bit() {
if (BuildCompat.isM()) {
return Process.is64Bit();
} else {
return Build.CPU_ABI.equals("arm64-v8a");
}
String processAbi = ProcessAbi.detect(getContext());
return processAbi != null ? ProcessAbi.is64Bit(processAbi)
: BuildCompat.isM() && Process.is64Bit();
}
private void initNotificationManager() {
@@ -420,7 +420,7 @@ public class BActivityThread extends IBActivityThread.Stub {
Slog.w(TAG, "Unable to set guest context ClassLoader: " + error.getMessage());
}
}
FridaGadgetLoader.loadIfEnabled();
FridaGadgetLoader.loadAtProcessBind();
AppBindData bindData = new AppBindData();
bindData.appInfo = applicationInfo;
@@ -686,7 +686,7 @@ public class BPackageManagerService extends IBPackageManagerService.Stub impleme
if (!support) {
String msg = packageArchiveInfo.applicationInfo.loadLabel(BlackBoxCore.getPackageManager()) + "[" + packageArchiveInfo.packageName + "]";
return result.installError(packageArchiveInfo.packageName,
msg + "\n" + (BlackBoxCore.is64Bit() ? "The box does not support 32-bit Application" : "The box does not support 64-bit Application"));
msg + "\nThe application has no native libraries for the FridaBox process ABI");
}
PackageParser.Package aPackage = parserApk(apkFile.getAbsolutePath());
if (aPackage == null) {
@@ -808,4 +808,4 @@ public class BPackageManagerService extends IBPackageManagerService.Stub impleme
mComponentResolver.addAllComponents(value.pkg);
}
}
}
}
@@ -15,7 +15,6 @@ import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import java.util.HashSet;
import java.util.Set;
@@ -31,6 +30,7 @@ import top.niunaijun.blackbox.core.env.BEnvironment;
import top.niunaijun.blackbox.entity.pm.InstallOption;
import top.niunaijun.blackbox.utils.ArrayUtils;
import top.niunaijun.blackbox.utils.FileUtils;
import top.niunaijun.blackbox.utils.ProcessAbi;
import top.niunaijun.blackbox.utils.compat.BuildCompat;
@@ -308,7 +308,7 @@ public class PackageManagerCompat {
if (BuildCompat.isL()) {
BRApplicationInfoL.get(ai)._set_primaryCpuAbi(Build.CPU_ABI);
BRApplicationInfoL.get(ai)._set_primaryCpuAbi(ProcessAbi.detect(BlackBoxCore.getContext()));
BRApplicationInfoL.get(ai)._set_scanPublicSourceDir(BRApplicationInfoL.get(baseApplication).scanPublicSourceDir());
BRApplicationInfoL.get(ai)._set_scanSourceDir(BRApplicationInfoL.get(baseApplication).scanSourceDir());
}
@@ -0,0 +1,165 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import java.io.ByteArrayOutputStream;
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;
import top.niunaijun.blackbox.utils.ProcessAbi;
/** Resolves a selected, downloaded Gadget and prepares per-guest runtime copies. */
final class DownloadedGadgetRuntime {
private static final String DOWNLOAD_ROOT = "fridabox-gadgets";
private static final String RUNTIME_ROOT = "fridabox-gadget-runtimes";
private static final String RUNTIME_NAME = "libpayload.so";
private static final String CONFIG_NAME = "libpayload.config.so";
private static final String SOURCE_NAME = "payload.source";
private DownloadedGadgetRuntime() {
}
static File prepareListener(String packageName) throws IOException {
Context context = BlackBoxCore.getContext();
File source = requireSelected(context);
File directory = new File(context.getFilesDir(),
RUNTIME_ROOT + File.separator + safeName(packageName));
if (!directory.isDirectory() && !directory.mkdirs()) {
throw new IOException("Unable to create the Gadget runtime directory");
}
File runtime = new File(directory, RUNTIME_NAME);
syncSelected(source, runtime);
writeUtf8Atomically(new File(directory, CONFIG_NAME), listenerConfig());
return runtime;
}
static File requireSelected(Context context) throws IOException {
String selectedPath = InstrumentationSettings.getSelectedGadgetPath();
String selectedAbi = InstrumentationSettings.getSelectedGadgetAbi();
if (selectedPath == null || selectedPath.trim().isEmpty()) {
throw new IOException("No downloaded Frida Gadget is selected");
}
File root = new File(context.getFilesDir(), DOWNLOAD_ROOT).getCanonicalFile();
File selected = new File(selectedPath).getCanonicalFile();
if (!isInside(root, selected) || !selected.isFile()) {
throw new IOException("The selected Frida Gadget is missing or outside private storage");
}
if (!supportsAbi(context, selectedAbi)) {
throw new IOException("The selected Frida Gadget ABI does not match this process");
}
validateElf(selected, selectedAbi);
return selected;
}
static void syncSelected(File source, File destination) throws IOException {
String identity = source.getCanonicalPath() + "\n"
+ String.valueOf(InstrumentationSettings.getSelectedGadgetSha256()) + "\n";
File marker = new File(destination.getParentFile(), SOURCE_NAME);
if (!destination.isFile() || destination.length() != source.length()
|| !identity.equals(readUtf8(marker))) {
copyAtomically(source, destination);
writeUtf8Atomically(marker, identity);
}
if (!destination.setReadable(true, false)
|| !destination.setExecutable(true, false)
|| !destination.setWritable(false, false)) {
throw new IOException("Unable to secure the private Frida Gadget runtime");
}
}
private static String listenerConfig() {
return "{\n" +
" \"interaction\": {\n" +
" \"type\": \"listen\",\n" +
" \"address\": \"127.0.0.1\",\n" +
" \"port\": " + InstrumentationSettings.getBasePort() + ",\n" +
" \"on_port_conflict\": \"pick-next\",\n" +
" \"on_load\": \"wait\"\n" +
" },\n" +
" \"runtime\": \"qjs\",\n" +
" \"teardown\": \"minimal\"\n" +
"}\n";
}
private static void validateElf(File file, String abi) throws IOException {
byte[] header = new byte[20];
try (FileInputStream input = new FileInputStream(file)) {
if (input.read(header) != header.length) throw new IOException("Downloaded Gadget is truncated");
}
if ((header[0] & 0xff) != 0x7f || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') {
throw new IOException("Downloaded Gadget is not an ELF library");
}
int machine = (header[18] & 0xff) | ((header[19] & 0xff) << 8);
int expected = expectedMachine(abi);
if (expected < 0 || machine != expected) {
throw new IOException("Downloaded Gadget ELF architecture does not match " + abi);
}
}
private static int expectedMachine(String abi) {
if ("arm64-v8a".equals(abi)) return 183;
if ("armeabi-v7a".equals(abi)) return 40;
if ("x86".equals(abi)) return 3;
if ("x86_64".equals(abi)) return 62;
return -1;
}
private static boolean supportsAbi(Context context, String abi) {
return abi != null && abi.equals(ProcessAbi.detect(context));
}
private static boolean isInside(File root, File child) {
return child.getAbsolutePath().startsWith(root.getAbsolutePath() + File.separator);
}
private static String safeName(String value) {
return value == null ? "unknown" : value.replaceAll("[^A-Za-z0-9._-]", "_");
}
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 String readUtf8(File file) {
if (!file.isFile() || file.length() > 4096) return null;
try (FileInputStream input = new FileInputStream(file);
ByteArrayOutputStream output = new ByteArrayOutputStream((int) file.length())) {
byte[] buffer = new byte[512];
int count;
while ((count = input.read(buffer)) >= 0) output.write(buffer, 0, count);
return output.toString(StandardCharsets.UTF_8.name());
} catch (IOException ignored) {
return null;
}
}
static void writeUtf8Atomically(File destination, String value) throws IOException {
byte[] expected = value.getBytes(StandardCharsets.UTF_8);
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());
}
}
@@ -15,19 +15,36 @@ public final class FridaGadgetLoader {
private FridaGadgetLoader() {
}
/** Listener mode starts immediately; autonomous scripts wait for the guest lifecycle. */
public static boolean loadAtProcessBind() {
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
if (InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)) {
Log.i(TAG, "Deferring on-device agent until the guest application is ready");
return false;
}
return loadIfEnabled();
}
public static boolean loadIfEnabled() {
if (!GuestRuntimeRegistry.isInstrumentationEnabled()) {
Log.i(TAG, "Instrumentation disabled for this guest process");
return false;
}
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
if (InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)
&& !GuestRuntimeRegistry.isPrimaryProcess()) {
Log.i(TAG, "Skipping on-device agent in secondary process "
+ GuestRuntimeRegistry.getGuestProcessName());
return false;
}
if (loaded) return true;
synchronized (LOAD_LOCK) {
if (loaded) return true;
if (!ATTEMPTED.compareAndSet(false, true)) return false;
try {
InstrumentationStatusStore.recordBinding();
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);
@@ -35,7 +52,8 @@ public final class FridaGadgetLoader {
System.load(runtime.getAbsolutePath());
} else {
Log.i(TAG, "Loading Frida Gadget listener for " + GuestRuntimeRegistry.getGuestProcessName());
System.loadLibrary("frida-gadget");
File runtime = DownloadedGadgetRuntime.prepareListener(packageName);
System.load(runtime.getAbsolutePath());
}
loaded = true;
InstrumentationStatusStore.recordLoaded();
@@ -56,6 +56,11 @@ public final class GuestRuntimeRegistry {
public static ClassLoader getGuestClassLoader() { return guestClassLoader; }
public static String getGuestSourceDir() { return guestSourceDir; }
public static boolean isInstrumentationEnabled() { return instrumentationEnabled; }
public static boolean isPrimaryProcess() {
String primaryProcess = guestApplicationInfo == null ? null : guestApplicationInfo.processName;
if (primaryProcess == null || primaryProcess.isEmpty()) primaryProcess = guestPackageName;
return primaryProcess != null && primaryProcess.equals(guestProcessName);
}
public static String getLastError() { return lastError; }
public static long getInitializationTimestamp() { return initializationTimestamp; }
@@ -81,6 +86,7 @@ public final class GuestRuntimeRegistry {
"\"virtualProcessId\":" + virtualProcessId + ',' +
"\"sourceDir\":" + quote(guestSourceDir) + ',' +
"\"instrumentationEnabled\":" + instrumentationEnabled + ',' +
"\"primaryProcess\":" + isPrimaryProcess() + ',' +
"\"lastError\":" + quote(lastError) + ',' +
"\"initializedAt\":" + initializationTimestamp +
'}';
@@ -12,6 +12,15 @@ public final class InstrumentationSettings {
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";
public static final String KEY_GADGET_PATH = "selected_gadget_path";
public static final String KEY_GADGET_SOURCE = "selected_gadget_source";
public static final String KEY_GADGET_VERSION = "selected_gadget_version";
public static final String KEY_GADGET_ABI = "selected_gadget_abi";
public static final String KEY_GADGET_SHA256 = "selected_gadget_sha256";
public static final String KEY_JAVA_BRIDGE_ENABLED = "runtime_bridge_java_enabled";
public static final String KEY_JAVA_BRIDGE_VERSION = "runtime_bridge_java_version";
public static final String KEY_IL2CPP_BRIDGE_ENABLED = "runtime_bridge_il2cpp_enabled";
public static final String KEY_IL2CPP_BRIDGE_VERSION = "runtime_bridge_il2cpp_version";
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_";
@@ -97,6 +106,89 @@ public final class InstrumentationSettings {
return preferences().getBoolean(KEY_ADVANCED_LOGS, false);
}
public static String getSelectedGadgetPath() {
return preferences().getString(KEY_GADGET_PATH, null);
}
public static String getSelectedGadgetSource() {
return preferences().getString(KEY_GADGET_SOURCE, null);
}
public static String getSelectedGadgetVersion() {
return preferences().getString(KEY_GADGET_VERSION, null);
}
public static String getSelectedGadgetAbi() {
return preferences().getString(KEY_GADGET_ABI, null);
}
public static String getSelectedGadgetSha256() {
return preferences().getString(KEY_GADGET_SHA256, null);
}
public static boolean setSelectedGadget(
String path, String source, String version, String abi, String sha256) {
if (path == null || path.trim().isEmpty()
|| source == null || source.trim().isEmpty()
|| version == null || version.trim().isEmpty()
|| abi == null || abi.trim().isEmpty()
|| sha256 == null || sha256.trim().isEmpty()) {
return false;
}
return preferences().edit()
.putString(KEY_GADGET_PATH, path)
.putString(KEY_GADGET_SOURCE, source)
.putString(KEY_GADGET_VERSION, version)
.putString(KEY_GADGET_ABI, abi)
.putString(KEY_GADGET_SHA256, sha256)
.commit();
}
public static boolean clearSelectedGadget() {
return preferences().edit()
.remove(KEY_GADGET_PATH)
.remove(KEY_GADGET_SOURCE)
.remove(KEY_GADGET_VERSION)
.remove(KEY_GADGET_ABI)
.remove(KEY_GADGET_SHA256)
.commit();
}
public static boolean isRuntimeBridgeEnabled(String bridgeId) {
String key = runtimeBridgeEnabledKey(bridgeId);
return key != null && preferences().getBoolean(key, false);
}
public static String getRuntimeBridgeVersion(String bridgeId, String fallback) {
String key = runtimeBridgeVersionKey(bridgeId);
return key == null ? fallback : preferences().getString(key, fallback);
}
public static boolean setRuntimeBridge(String bridgeId, boolean enabled, String version) {
RuntimeBridgeCatalog.BridgeSpec spec = RuntimeBridgeCatalog.find(bridgeId);
String enabledKey = runtimeBridgeEnabledKey(bridgeId);
String versionKey = runtimeBridgeVersionKey(bridgeId);
if (spec == null || !spec.supportsVersion(version) || enabledKey == null || versionKey == null) {
return false;
}
return preferences().edit()
.putBoolean(enabledKey, enabled)
.putString(versionKey, version)
.commit();
}
private static String runtimeBridgeEnabledKey(String bridgeId) {
if (RuntimeBridgeCatalog.JAVA_ID.equals(bridgeId)) return KEY_JAVA_BRIDGE_ENABLED;
if (RuntimeBridgeCatalog.IL2CPP_ID.equals(bridgeId)) return KEY_IL2CPP_BRIDGE_ENABLED;
return null;
}
private static String runtimeBridgeVersionKey(String bridgeId) {
if (RuntimeBridgeCatalog.JAVA_ID.equals(bridgeId)) return KEY_JAVA_BRIDGE_VERSION;
if (RuntimeBridgeCatalog.IL2CPP_ID.equals(bridgeId)) return KEY_IL2CPP_BRIDGE_VERSION;
return null;
}
static int clamp(int value, int minimum, int maximum, int fallback) {
return value < minimum || value > maximum ? fallback : value;
}
@@ -16,6 +16,7 @@ public final class InstrumentationStatusStore {
}
public static void recordBinding() {
if (!GuestRuntimeRegistry.isPrimaryProcess()) return;
String packageName = GuestRuntimeRegistry.getGuestPackageName();
String mode = InstrumentationSettings.getModeForPackage(packageName);
String state;
@@ -49,6 +50,7 @@ public final class InstrumentationStatusStore {
}
public static void recordLoaded() {
if (!GuestRuntimeRegistry.isPrimaryProcess()) return;
String mode = preferences().getString("runtime_mode", InstrumentationSettings.MODE_COMPUTER);
String state = InstrumentationSettings.MODE_LOCAL_SCRIPT.equals(mode)
? "local_script_active" : "computer_attached";
@@ -56,6 +58,7 @@ public final class InstrumentationStatusStore {
}
public static void recordError(String error) {
if (!GuestRuntimeRegistry.isPrimaryProcess()) return;
preferences().edit().putString("runtime_state", "failed").putString("runtime_error", error).commit();
}
}
@@ -3,10 +3,13 @@ package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import top.niunaijun.blackbox.BlackBoxCore;
@@ -14,6 +17,8 @@ import top.niunaijun.blackbox.BlackBoxCore;
final class LocalScriptGadgetRuntime {
private static final String AGENT_ROOT = "fridabox-agents";
private static final String AGENT_NAME = "agent.js";
private static final String INSTRUMENTED_AGENT_NAME = "fridabox-runtime.js";
private static final String LOG_NAME = "runtime.jsonl";
private static final String RUNTIME_NAME = "libfridabox-agent.so";
private static final String CONFIG_NAME = "libfridabox-agent.config.so";
@@ -35,37 +40,83 @@ final class LocalScriptGadgetRuntime {
}
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 log = new File(directory, LOG_NAME);
writeUtf8Atomically(log, "");
File instrumentedAgent = new File(directory, INSTRUMENTED_AGENT_NAME);
List<byte[]> runtimeBridges = RuntimeBridgeCatalog.loadEnabledBundles(
context, InstrumentationSettings.getSelectedGadgetVersion());
writeInstrumentedAgentAtomically(
instrumentedAgent, script, buildLogBridge(log), runtimeBridges);
if (!instrumentedAgent.setReadable(true, true) || !instrumentedAgent.setWritable(false, false)) {
throw new IOException("Unable to secure the instrumented JavaScript agent");
}
File source = DownloadedGadgetRuntime.requireSelected(context);
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");
}
DownloadedGadgetRuntime.syncSelected(source, runtime);
File config = new File(directory, CONFIG_NAME);
writeUtf8Atomically(config, buildConfig(packageName));
writeUtf8Atomically(config, buildConfig(packageName, instrumentedAgent.getAbsolutePath()));
return runtime;
}
static String buildConfig(String packageName) {
static String buildConfig(String packageName, String agentPath) {
return "{\n" +
" \"interaction\": {\n" +
" \"type\": \"script\",\n" +
" \"path\": \"" + AGENT_NAME + "\",\n" +
" \"on_change\": \"reload\",\n" +
" \"path\": \"" + json(agentPath) + "\",\n" +
" \"on_change\": \"ignore\",\n" +
" \"parameters\": { \"package\": \"" + json(packageName) + "\" }\n" +
" },\n" +
" \"runtime\": \"qjs\",\n" +
" \"runtime\": \"v8\",\n" +
" \"teardown\": \"minimal\"\n" +
"}\n";
}
static String buildLogBridge(File log) {
return "(function () {\n" +
" try {\n" +
" const logPath = \"" + json(log.getAbsolutePath()) + "\";\n" +
" const maxBytes = 512 * 1024;\n" +
" const original = { log: console.log, warn: console.warn, error: console.error, send: globalThis.send };\n" +
" function render(value) {\n" +
" if (typeof value === 'string') return value;\n" +
" try { return JSON.stringify(value); } catch (_) { return String(value); }\n" +
" }\n" +
" function append(level, values) {\n" +
" try {\n" +
" const message = Array.prototype.map.call(values, render).join(' ');\n" +
" let line = JSON.stringify({ time: Date.now(), level: level, message: message }) + '\\n';\n" +
" let existing = '';\n" +
" try { existing = File.readAllText(logPath); } catch (_) {}\n" +
" if (existing.length + line.length > maxBytes) {\n" +
" const notice = JSON.stringify({ time: Date.now(), level: 'system', message: 'Earlier logs were truncated' }) + '\\n';\n" +
" const keep = Math.max(0, maxBytes - notice.length - line.length);\n" +
" existing = existing.slice(Math.max(0, existing.length - keep));\n" +
" line = notice + line;\n" +
" File.writeAllText(logPath, existing + line);\n" +
" return;\n" +
" }\n" +
" const stream = new File(logPath, 'a');\n" +
" try { stream.write(line); stream.flush(); } finally { stream.close(); }\n" +
" } catch (error) { try { original.error.call(console, '[FridaBox log bridge]', error.stack || error); } catch (_) {} }\n" +
" }\n" +
" console.log = function () { append('log', arguments); return original.log.apply(console, arguments); };\n" +
" console.warn = function () { append('warn', arguments); return original.warn.apply(console, arguments); };\n" +
" console.error = function () { append('error', arguments); return original.error.apply(console, arguments); };\n" +
" const wrappedSend = function (payload, data) { append('send', [payload]); return original.send(payload, data); };\n" +
" Object.defineProperty(globalThis, '__fridaboxSend', { value: wrappedSend, configurable: true });\n" +
" try {\n" +
" const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'send');\n" +
" if (!descriptor || descriptor.writable) globalThis.send = wrappedSend;\n" +
" else if (descriptor.configurable) Object.defineProperty(globalThis, 'send', Object.assign({}, descriptor, { value: wrappedSend }));\n" +
" } catch (error) { append('system', ['send() capture unavailable: ' + error]); }\n" +
" append('system', ['On-device agent started']);\n" +
" } catch (error) { try { console.error('[FridaBox log bridge]', error.stack || error); } catch (_) {} }\n" +
"})();\n";
}
static boolean isInside(File root, File child) {
String rootPath = root.getAbsolutePath();
String childPath = child.getAbsolutePath();
@@ -107,6 +158,138 @@ final class LocalScriptGadgetRuntime {
replace(temporary, destination);
}
private static void writeInstrumentedAgentAtomically(
File destination, File source, String bridge, List<byte[]> runtimeBridges) throws IOException {
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
if (temporary.exists() && !temporary.delete()) {
throw new IOException("Unable to replace temporary JavaScript agent");
}
try (FileInputStream input = new FileInputStream(source);
ByteArrayOutputStream sourceBytes = new ByteArrayOutputStream((int) source.length())) {
byte[] buffer = new byte[64 * 1024];
int count;
while ((count = input.read(buffer)) >= 0) sourceBytes.write(buffer, 0, count);
byte[] instrumented = instrumentAgent(sourceBytes.toByteArray(), bridge, runtimeBridges);
try (FileOutputStream output = new FileOutputStream(temporary)) {
output.write(instrumented);
output.getFD().sync();
}
}
replace(temporary, destination);
}
static byte[] instrumentAgent(byte[] source, String bridge) throws IOException {
return instrumentAgent(source, bridge, Collections.<byte[]>emptyList());
}
static byte[] instrumentAgent(
byte[] source, String bridge, List<byte[]> runtimeBridgeBundles) throws IOException {
int offset = injectionOffset(source);
if (offset < 0) throw new IOException("Invalid Frida bundle header");
ByteArrayOutputStream prefixOutput = new ByteArrayOutputStream();
prefixOutput.write(bridge.getBytes(StandardCharsets.UTF_8));
for (byte[] bundle : runtimeBridgeBundles) {
byte[] body = extractEntryModule(bundle);
prefixOutput.write(body, 0, body.length);
prefixOutput.write('\n');
}
prefixOutput.write("(function (send) {\n".getBytes(StandardCharsets.UTF_8));
byte[] prefix = prefixOutput.toByteArray();
byte[] suffix = "\n})(globalThis.__fridaboxSend);\n".getBytes(StandardCharsets.UTF_8);
if (offset > 0) return instrumentBundle(source, prefix, suffix, offset);
ByteArrayOutputStream output = new ByteArrayOutputStream(source.length + prefix.length + suffix.length);
output.write(prefix, 0, prefix.length);
output.write(source, 0, source.length);
output.write(suffix, 0, suffix.length);
return output.toByteArray();
}
static byte[] extractEntryModule(byte[] bundle) throws IOException {
int bodyOffset = injectionOffset(bundle);
if (bodyOffset <= 0) throw new IOException("Runtime bridge is not a Frida bundle");
int lengthStart = moduleLengthStart(bundle, bodyOffset);
int lengthEnd = lengthStart;
long declaredLength = 0;
while (lengthEnd < bodyOffset && bundle[lengthEnd] >= '0' && bundle[lengthEnd] <= '9') {
declaredLength = declaredLength * 10 + (bundle[lengthEnd] - '0');
if (declaredLength > Integer.MAX_VALUE) throw new IOException("Invalid Frida bundle module length");
lengthEnd++;
}
if (lengthEnd == lengthStart || lengthEnd >= bodyOffset || bundle[lengthEnd] != ' ') {
throw new IOException("Invalid Frida bundle module length");
}
long bodyEndLong = bodyOffset + declaredLength;
if (bodyEndLong > bundle.length) throw new IOException("Invalid Frida bundle module length");
return java.util.Arrays.copyOfRange(bundle, bodyOffset, (int) bodyEndLong);
}
private static byte[] instrumentBundle(
byte[] source, byte[] prefix, byte[] suffix, int bodyOffset) throws IOException {
int lengthStart = moduleLengthStart(source, bodyOffset);
int lengthEnd = lengthStart;
long declaredLength = 0;
while (lengthEnd < bodyOffset && source[lengthEnd] >= '0' && source[lengthEnd] <= '9') {
declaredLength = declaredLength * 10 + (source[lengthEnd] - '0');
lengthEnd++;
}
if (lengthStart >= bodyOffset || lengthEnd == lengthStart
|| lengthEnd >= bodyOffset || source[lengthEnd] != ' ') {
throw new IOException("Invalid Frida bundle module length");
}
long bodyEndLong = bodyOffset + declaredLength;
if (bodyEndLong > source.length) throw new IOException("Invalid Frida bundle module length");
int bodyEnd = (int) bodyEndLong;
byte[] updatedLength = Long.toString(declaredLength + prefix.length + suffix.length)
.getBytes(StandardCharsets.US_ASCII);
ByteArrayOutputStream output = new ByteArrayOutputStream(
source.length + prefix.length + suffix.length
+ updatedLength.length - (lengthEnd - lengthStart));
output.write(source, 0, lengthStart);
output.write(updatedLength, 0, updatedLength.length);
output.write(source, lengthEnd, bodyOffset - lengthEnd);
output.write(prefix, 0, prefix.length);
output.write(source, bodyOffset, bodyEnd - bodyOffset);
output.write(suffix, 0, suffix.length);
output.write(source, bodyEnd, source.length - bodyEnd);
return output.toByteArray();
}
private static int moduleLengthStart(byte[] source, int bodyOffset) throws IOException {
int lengthStart = 0;
while (lengthStart < bodyOffset && source[lengthStart] != '\n') lengthStart++;
if (lengthStart >= bodyOffset) throw new IOException("Invalid Frida bundle header");
return lengthStart + 1;
}
static int injectionOffset(byte[] source) {
byte[] magic = "📦".getBytes(StandardCharsets.UTF_8);
if (!startsWith(source, magic)) return 0;
byte[] unixMarker = "\n✄\n".getBytes(StandardCharsets.UTF_8);
int unix = indexOf(source, unixMarker);
if (unix >= 0) return unix + unixMarker.length;
byte[] windowsMarker = "\r\n✄\r\n".getBytes(StandardCharsets.UTF_8);
int windows = indexOf(source, windowsMarker);
return windows < 0 ? -1 : windows + windowsMarker.length;
}
private static boolean startsWith(byte[] value, byte[] prefix) {
if (value.length < prefix.length) return false;
for (int i = 0; i < prefix.length; i++) {
if (value[i] != prefix[i]) return false;
}
return true;
}
private static int indexOf(byte[] value, byte[] needle) {
for (int i = 0; i <= value.length - needle.length; i++) {
int j = 0;
while (j < needle.length && value[i + j] == needle[j]) j++;
if (j == needle.length) return i;
}
return -1;
}
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,131 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** Offline, build-pinned runtime bridges available to autonomous Frida agents. */
public final class RuntimeBridgeCatalog {
public static final String JAVA_ID = "java";
public static final String IL2CPP_ID = "il2cpp";
public static final String MINIMUM_GADGET_VERSION = "17.0.0";
private static final int MAX_BUNDLE_BYTES = 2 * 1024 * 1024;
private static final List<BridgeSpec> SPECS = Collections.unmodifiableList(Arrays.asList(
new BridgeSpec(
JAVA_ID,
"Java",
"Official Frida runtime bridge",
"7.0.13",
new String[]{"7.0.13", "7.0.12", "7.0.11"}),
new BridgeSpec(
IL2CPP_ID,
"IL2CPP",
"Community bridge for Unity IL2CPP",
"0.13.1",
new String[]{"0.13.1", "0.13.0", "0.12.2"})
));
private RuntimeBridgeCatalog() {
}
public static List<BridgeSpec> specs() {
return SPECS;
}
public static BridgeSpec find(String id) {
for (BridgeSpec spec : SPECS) {
if (spec.id.equals(id)) return spec;
}
return null;
}
public static boolean supportsRuntimeBridges(String gadgetVersion) {
int[] parsed = parseVersion(gadgetVersion);
return parsed != null && parsed[0] >= 17;
}
public static String selectedVersion(BridgeSpec spec) {
String selected = InstrumentationSettings.getRuntimeBridgeVersion(spec.id, spec.defaultVersion);
return spec.supportsVersion(selected) ? selected : spec.defaultVersion;
}
static List<byte[]> loadEnabledBundles(Context context, String gadgetVersion) throws IOException {
if (!supportsRuntimeBridges(gadgetVersion)) return Collections.emptyList();
List<byte[]> bundles = new ArrayList<>();
for (BridgeSpec spec : SPECS) {
if (!InstrumentationSettings.isRuntimeBridgeEnabled(spec.id)) continue;
String version = selectedVersion(spec);
String asset = "fridabox/bridges/" + spec.id + "-" + version + ".js";
try (InputStream input = context.getAssets().open(asset)) {
bundles.add(readLimited(input));
} catch (IOException error) {
throw new IOException("Runtime bridge asset is unavailable: " + spec.id + " " + version, error);
}
}
return bundles;
}
private static byte[] readLimited(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[32 * 1024];
int total = 0;
int count;
while ((count = input.read(buffer)) >= 0) {
total += count;
if (total > MAX_BUNDLE_BYTES) throw new IOException("Runtime bridge bundle is unexpectedly large");
output.write(buffer, 0, count);
}
if (total == 0) throw new IOException("Runtime bridge bundle is empty");
return output.toByteArray();
}
static int[] parseVersion(String value) {
if (value == null) return null;
String normalized = value.trim();
if (normalized.startsWith("v") || normalized.startsWith("V")) normalized = normalized.substring(1);
String[] parts = normalized.split("[.-]", 4);
if (parts.length < 1 || parts.length > 4) return null;
int[] result = new int[]{0, 0, 0};
try {
for (int index = 0; index < Math.min(3, parts.length); index++) {
if (parts[index].isEmpty()) return null;
result[index] = Integer.parseInt(parts[index]);
if (result[index] < 0) return null;
}
return result;
} catch (NumberFormatException ignored) {
return null;
}
}
public static final class BridgeSpec {
public final String id;
public final String title;
public final String description;
public final String defaultVersion;
private final List<String> versions;
BridgeSpec(String id, String title, String description, String defaultVersion, String[] versions) {
this.id = id;
this.title = title;
this.description = description;
this.defaultVersion = defaultVersion;
this.versions = Collections.unmodifiableList(Arrays.asList(versions.clone()));
}
public List<String> versions() {
return versions;
}
public boolean supportsVersion(String version) {
return version != null && versions.contains(version);
}
}
}
@@ -26,11 +26,7 @@ public class AbiUtils {
return true;
}
if (BlackBoxCore.is64Bit()) {
return abiUtils.is64Bit();
} else {
return abiUtils.is32Bit();
}
return abiUtils.supports(ProcessAbi.detect(BlackBoxCore.getContext()));
}
public AbiUtils(File apkFile) {
@@ -43,10 +39,14 @@ public class AbiUtils {
String name = zipEntry.getName();
if (name.startsWith("lib/arm64-v8a")) {
mLibs.add("arm64-v8a");
} else if (name.startsWith("lib/armeabi")) {
mLibs.add("armeabi");
} else if (name.startsWith("lib/armeabi-v7a")) {
mLibs.add("armeabi-v7a");
} else if (name.startsWith("lib/armeabi")) {
mLibs.add("armeabi");
} else if (name.startsWith("lib/x86_64")) {
mLibs.add("x86_64");
} else if (name.startsWith("lib/x86")) {
mLibs.add("x86");
}
}
} catch (Exception e) {
@@ -57,14 +57,21 @@ public class AbiUtils {
}
public boolean is64Bit() {
return mLibs.contains("arm64-v8a");
return mLibs.contains("arm64-v8a") || mLibs.contains("x86_64");
}
public boolean is32Bit() {
return mLibs.contains("armeabi") || mLibs.contains("armeabi-v7a");
return mLibs.contains("armeabi") || mLibs.contains("armeabi-v7a") || mLibs.contains("x86");
}
public boolean isEmptyAib() {
return mLibs.isEmpty();
}
public boolean supports(String processAbi) {
if ("armeabi-v7a".equals(processAbi)) {
return mLibs.contains("armeabi-v7a") || mLibs.contains("armeabi");
}
return processAbi != null && mLibs.contains(processAbi);
}
}
@@ -1,6 +1,5 @@
package top.niunaijun.blackbox.utils;
import android.os.Build;
import android.util.Log;
import java.io.BufferedInputStream;
@@ -14,6 +13,7 @@ import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import top.niunaijun.blackbox.BlackBoxCore;
public class NativeUtils {
@@ -25,11 +25,13 @@ public class NativeUtils {
nativeLibDir.mkdirs();
}
try (ZipFile zipfile = new ZipFile(apk.getAbsolutePath())) {
if (findAndCopyNativeLib(zipfile, Build.CPU_ABI, nativeLibDir)) {
String processAbi = ProcessAbi.detect(BlackBoxCore.getContext());
if (processAbi != null && findAndCopyNativeLib(zipfile, processAbi, nativeLibDir)) {
return;
}
findAndCopyNativeLib(zipfile, "armeabi", nativeLibDir);
if ("armeabi-v7a".equals(processAbi)) {
findAndCopyNativeLib(zipfile, "armeabi", nativeLibDir);
}
} finally {
Log.d(TAG, "Done! +" + (System.currentTimeMillis() - startTime) + "ms");
}
@@ -0,0 +1,71 @@
package top.niunaijun.blackbox.utils;
import android.content.Context;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Locale;
/** Detects the ABI of the current host process instead of the device's preferred ABI. */
public final class ProcessAbi {
private static final String HOST_LIBRARY = "libblackbox.so";
private ProcessAbi() {
}
public static String detect(Context context) {
if (context != null && context.getApplicationInfo() != null) {
String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir;
if (nativeLibraryDir != null && !nativeLibraryDir.trim().isEmpty()) {
String abi = fromElf(new File(nativeLibraryDir, HOST_LIBRARY));
if (abi != null) return abi;
}
}
return fromOsArch(System.getProperty("os.arch"));
}
public static String fromElf(File file) {
if (file == null || !file.isFile()) return null;
byte[] header = new byte[20];
try (FileInputStream input = new FileInputStream(file)) {
if (input.read(header) != header.length) return null;
return fromElfHeader(header);
} catch (IOException ignored) {
return null;
}
}
static String fromElfHeader(byte[] header) {
if (header == null || header.length < 20
|| (header[0] & 0xff) != 0x7f || header[1] != 'E'
|| header[2] != 'L' || header[3] != 'F' || header[5] != 1) {
return null;
}
int elfClass = header[4] & 0xff;
int machine = (header[18] & 0xff) | ((header[19] & 0xff) << 8);
if (elfClass == 2 && machine == 183) return "arm64-v8a";
if (elfClass == 1 && machine == 40) return "armeabi-v7a";
if (elfClass == 2 && machine == 62) return "x86_64";
if (elfClass == 1 && machine == 3) return "x86";
return null;
}
public static String fromOsArch(String value) {
if (value == null) return null;
String arch = value.trim().toLowerCase(Locale.ROOT);
if ("aarch64".equals(arch) || "arm64".equals(arch) || "arm64-v8a".equals(arch)) {
return "arm64-v8a";
}
if ("arm".equals(arch) || "armeabi-v7a".equals(arch) || arch.startsWith("armv7")) {
return "armeabi-v7a";
}
if ("x86_64".equals(arch) || "amd64".equals(arch)) return "x86_64";
if ("x86".equals(arch) || arch.matches("i[3-6]86")) return "x86";
return null;
}
public static boolean is64Bit(String abi) {
return "arm64-v8a".equals(abi) || "x86_64".equals(abi);
}
}
@@ -20,6 +20,7 @@ public class GuestRuntimeRegistryTest {
public void initializeReplacesProcessLocalSnapshot() {
ApplicationInfo info = new ApplicationInfo();
info.sourceDir = "/private/original.apk";
info.processName = "sample.one";
ClassLoader loader = getClass().getClassLoader();
GuestRuntimeRegistry.initialize("sample.one", "sample.one:remote", 3, 7, info, loader, true);
@@ -30,9 +31,19 @@ public class GuestRuntimeRegistryTest {
assertSame(loader, GuestRuntimeRegistry.getGuestClassLoader());
assertEquals("/private/original.apk", GuestRuntimeRegistry.getGuestSourceDir());
assertTrue(GuestRuntimeRegistry.isInstrumentationEnabled());
assertFalse(GuestRuntimeRegistry.isPrimaryProcess());
assertTrue(GuestRuntimeRegistry.describe().contains("\"package\":\"sample.one\""));
}
@Test
public void primaryProcessUsesApplicationProcessName() {
ApplicationInfo info = new ApplicationInfo();
info.processName = "sample.custom";
GuestRuntimeRegistry.initialize("sample", "sample.custom", 0, 1, info, null, true);
assertTrue(GuestRuntimeRegistry.isPrimaryProcess());
}
@Test
public void clearRemovesPriorGuest() {
GuestRuntimeRegistry.initialize("sample", "sample", 0, 1, null, null, true);
@@ -6,20 +6,94 @@ import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
public class LocalScriptGadgetRuntimeTest {
@Test
public void configUsesAutonomousScriptInteraction() {
String config = LocalScriptGadgetRuntime.buildConfig("sample.\"guest");
String config = LocalScriptGadgetRuntime.buildConfig(
"sample.\"guest", "/private/agents/fridabox-runtime.js");
assertTrue(config.contains("\"type\": \"script\""));
assertTrue(config.contains("\"path\": \"agent.js\""));
assertTrue(config.contains("\"path\": \"/private/agents/fridabox-runtime.js\""));
assertTrue(config.contains("\"on_change\": \"ignore\""));
assertTrue(config.contains("\"runtime\": \"v8\""));
assertTrue(config.contains("sample.\\\"guest"));
}
@Test
public void logBridgeCapturesConsoleAndSendWithoutChangingTheSourceAgent() {
String bridge = LocalScriptGadgetRuntime.buildLogBridge(new File("runtime.jsonl"));
assertTrue(bridge.contains("console.log = function"));
assertTrue(bridge.contains("console.error = function"));
assertTrue(bridge.contains("Object.getOwnPropertyDescriptor(globalThis, 'send')"));
assertTrue(bridge.contains("__fridaboxSend"));
assertTrue(bridge.contains("runtime.jsonl"));
assertTrue(bridge.contains("new File(logPath, 'a')"));
}
@Test
public void compiledBundleKeepsItsHeaderBeforeTheInjectedBridge() throws Exception {
String source = "📦\n16 /scripts/sample-hook.js\n✄\nvar hook = true;";
String bridge = "console.log('bridge');\n";
String result = new String(LocalScriptGadgetRuntime.instrumentAgent(
source.getBytes(StandardCharsets.UTF_8), bridge), StandardCharsets.UTF_8);
assertTrue(result.contains("✄\n" + bridge + "(function (send) {\nvar hook = true;"));
assertTrue(result.contains("})(globalThis.__fridaboxSend);"));
}
@Test
public void plainScriptReceivesBridgeAtTheBeginning() throws Exception {
String source = "console.log('agent');";
String bridge = "console.log('bridge');\n";
String result = new String(LocalScriptGadgetRuntime.instrumentAgent(
source.getBytes(StandardCharsets.UTF_8), bridge), StandardCharsets.UTF_8);
assertTrue(result.startsWith(bridge));
assertTrue(result.contains("(function (send) {\n" + source));
assertTrue(result.endsWith("})(globalThis.__fridaboxSend);\n"));
}
@Test
public void selectedRuntimeBridgeRunsBeforeTheUserAgent() throws Exception {
String bridgeBody = "globalThis.Java = { available: true };";
byte[] bridgeBundle = bundle("/bridges/java.js", bridgeBody);
String source = "console.log(Java.available);";
String result = new String(LocalScriptGadgetRuntime.instrumentAgent(
source.getBytes(StandardCharsets.UTF_8),
"console.log('log bridge');\n",
Collections.singletonList(bridgeBundle)), StandardCharsets.UTF_8);
assertTrue(result.indexOf(bridgeBody) < result.indexOf(source));
assertTrue(result.contains("(function (send) {\n" + source));
}
@Test
public void entryModuleExtractionRejectsPlainJavaScript() {
boolean rejected = false;
try {
LocalScriptGadgetRuntime.extractEntryModule("plain".getBytes(StandardCharsets.UTF_8));
} catch (Exception expected) {
rejected = true;
}
assertTrue(rejected);
}
@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")));
}
private static byte[] bundle(String name, String body) {
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
String header = "\uD83D\uDCE6\n" + bodyBytes.length + " " + name + "\n\u2704\n";
byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8);
byte[] result = new byte[headerBytes.length + bodyBytes.length];
System.arraycopy(headerBytes, 0, result, 0, headerBytes.length);
System.arraycopy(bodyBytes, 0, result, headerBytes.length, bodyBytes.length);
return result;
}
}
@@ -0,0 +1,27 @@
package top.niunaijun.blackbox.instrumentation;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class RuntimeBridgeCatalogTest {
@Test
public void bridgeControlsStartAtFridaSeventeen() {
assertFalse(RuntimeBridgeCatalog.supportsRuntimeBridges("16.7.19"));
assertTrue(RuntimeBridgeCatalog.supportsRuntimeBridges("17.0.0"));
assertTrue(RuntimeBridgeCatalog.supportsRuntimeBridges("17.16.0"));
assertTrue(RuntimeBridgeCatalog.supportsRuntimeBridges("v18.1.2"));
assertFalse(RuntimeBridgeCatalog.supportsRuntimeBridges("not-a-version"));
}
@Test
public void everyBridgeHasAValidDefaultAndPinnedAlternatives() {
for (RuntimeBridgeCatalog.BridgeSpec spec : RuntimeBridgeCatalog.specs()) {
assertNotNull(RuntimeBridgeCatalog.find(spec.id));
assertTrue(spec.supportsVersion(spec.defaultVersion));
assertTrue(spec.versions().size() >= 2);
}
}
}
@@ -0,0 +1,46 @@
package top.niunaijun.blackbox.utils;
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 AbiUtilsTest {
@Rule
public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void x86ProcessRejectsArmOnlyGuest() throws Exception {
AbiUtils abi = new AbiUtils(apkWith("lib/armeabi-v7a/libsample.so"));
assertFalse(abi.supports("x86"));
}
@Test
public void armProcessAcceptsLegacyArmeabiFallback() throws Exception {
AbiUtils abi = new AbiUtils(apkWith("lib/armeabi/libsample.so"));
assertTrue(abi.supports("armeabi-v7a"));
}
@Test
public void sixtyFourBitProcessRejectsThirtyTwoBitGuest() throws Exception {
AbiUtils abi = new AbiUtils(apkWith("lib/x86/libsample.so"));
assertFalse(abi.supports("x86_64"));
}
private File apkWith(String entry) throws Exception {
File file = temporary.newFile("guest-" + System.nanoTime() + ".apk");
try (ZipOutputStream output = new ZipOutputStream(new FileOutputStream(file))) {
output.putNextEntry(new ZipEntry(entry));
output.write(1);
output.closeEntry();
}
return file;
}
}
@@ -0,0 +1,45 @@
package top.niunaijun.blackbox.utils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import org.junit.Test;
public class ProcessAbiTest {
@Test
public void processArchitectureKeepsThirtyTwoBitArmOnArm64Device() {
assertEquals("armeabi-v7a", ProcessAbi.fromOsArch("armv7l"));
}
@Test
public void processArchitectureKeepsThirtyTwoBitX86OnX8664Device() {
assertEquals("x86", ProcessAbi.fromOsArch("i686"));
}
@Test
public void elfHeaderDistinguishesAllReleaseArchitectures() {
assertEquals("arm64-v8a", ProcessAbi.fromElfHeader(elfHeader(2, 183)));
assertEquals("armeabi-v7a", ProcessAbi.fromElfHeader(elfHeader(1, 40)));
assertEquals("x86_64", ProcessAbi.fromElfHeader(elfHeader(2, 62)));
assertEquals("x86", ProcessAbi.fromElfHeader(elfHeader(1, 3)));
}
@Test
public void elfHeaderRejectsMachineAndClassMismatch() {
assertNull(ProcessAbi.fromElfHeader(elfHeader(2, 40)));
assertNull(ProcessAbi.fromElfHeader(elfHeader(1, 62)));
}
private byte[] elfHeader(int elfClass, int machine) {
byte[] header = new byte[20];
header[0] = 0x7f;
header[1] = 'E';
header[2] = 'L';
header[3] = 'F';
header[4] = (byte) elfClass;
header[5] = 1;
header[18] = (byte) (machine & 0xff);
header[19] = (byte) ((machine >> 8) & 0xff);
return header;
}
}
+54
View File
@@ -5,6 +5,60 @@ versioning for its public releases.
## [Unreleased]
## [4.2.0] - 2026-07-28
### Added
- Optional, version-selectable Java and Unity IL2CPP runtime bridges for Frida
Gadget 17 and newer, embedded privately into the on-device agent at launch.
- Reproducible bridge build inputs and packaged Java 7.0.11-7.0.13 and IL2CPP
0.12.2, 0.13.0, and 0.13.1 bridge versions.
### Changed
- Simplified the application navigation to Workspace, Gadgets, and Settings by
removing the redundant Runtime destination.
- Refined the liquid-glass bottom navigation with icon-only active indicators,
heavier active icons, tactile button motion, and animated screen transitions.
- Centered the Gadget version browser and moved in-app notifications above the
bottom navigation.
- Simplified Settings by removing obsolete global instrumentation and advanced
runtime-detail controls.
### Fixed
- Preserve the Gadget screen scroll position when selecting Gadget or bridge
versions instead of rebuilding at the top of the page.
- Keep bridge enablement and version choices device-local and apply them to the
next private on-device agent without requiring imports in user scripts.
- Restore clear spacing between the Settings controls and security warning.
## [4.1.1] - 2026-07-28
### Fixed
- Select downloaded Frida Gadgets by the actual host process ABI instead of the
device's preferred ABI, preventing 64-bit Gadgets from being selected by
32-bit ARM or x86 release APKs.
- Require guest native libraries to match the exact FridaBox process ABI and
extract only that ABI, preventing cross-architecture guest import failures.
- Report the correct x86_64 process bitness on Android 5.0 and 5.1.
## [4.1.0] - 2026-07-28
### Added
- Separate release APKs for `armeabi-v7a`, `arm64-v8a`, `x86`, and `x86_64`.
- In-app APK inspection now accepts supported 32-bit ARM and x86 guest ABIs.
- Downloadable official Frida Gadget management, including ABI validation and
on-device or computer-controlled runtime selection.
- A liquid-glass launcher, downloadable Gadget screen, and guest log overlay.
### Changed
- Updated application iconography, navigation, runtime visibility, and release
dependency set.
### Documentation
- Added the public research roadmap for Frida visibility, `/proc`/maps
+15 -10
View File
@@ -1,5 +1,7 @@
<div align="center">
<img src="app/src/main/res/drawable-nodpi/logo.png" alt="FridaBox logo" width="180">
# FridaBox
### On-device Android instrumentation, inside a private virtual workspace
@@ -180,12 +182,12 @@ in On-device, Computer, and Clean modes. Evidence and exact commands are in
- JDK 21;
- Android SDK 35 and NDK 29.0.14206865;
- Python 3.10+;
- Node.js 20+ and npm when rebuilding the bundled Frida 17 agents.
- Node.js 20+ and npm for the pinned Frida agent toolchain and official Tabler
Icons source package.
### Build
```text
python tools/fetch_frida_gadget.py
npm ci
python tools/build_frida_agents.py
./gradlew :app:assembleDebug :Bcore:testDebugUnitTest :app:testDebugUnitTest :app:check
@@ -198,10 +200,12 @@ documented in [docs/BUILDING.md](docs/BUILDING.md). The output is written under
### Use
1. Install and open FridaBox.
2. Tap **Import APK** and select an ARM64 base APK.
3. Choose **On-device**, **Computer**, or **Clean** on that guest's card.
4. Select an agent for On-device mode, or attach normally in Computer mode.
5. Inspect live state in the **Runtime** tab.
2. Open **Gadgets** and download a compatible Official Frida version.
3. Select one downloaded Gadget as active.
4. Tap the import icon and choose an installed app or an ARM64 base APK file.
5. Tap the guest icon and choose **On-device**, **Computer**, or **Clean** in the launch panel.
6. Select an agent for On-device mode, or attach normally in Computer mode.
7. Inspect live state in the **Runtime** tab.
See [docs/USAGE.md](docs/USAGE.md) and
[docs/FRIDA_CONNECTION.md](docs/FRIDA_CONNECTION.md) for the complete workflow.
@@ -244,8 +248,9 @@ docs/ Build, usage, limitations and device evidence
## Scope and limitations
FridaBox currently targets single-file ARM64 APKs. Split APK sets, privileged or
system applications, full Google Play Services, Play Integrity, hardware-backed
FridaBox targets ARM64 APKs and can clone the active base/split set of a
launchable installed app. Standalone split archives, privileged system
applications, full Google Play Services, Play Integrity, hardware-backed
attestation, and perfect anti-instrumentation resistance are outside the current
scope. Vendor behavior and hidden Android APIs can still affect compatibility.
@@ -264,8 +269,8 @@ internally consistent and reducing avoidable instrumentation fingerprints:
- research safe mediation of `/proc`, including coherent views of process maps;
- improve filesystem, package, process, Binder, identity, ClassLoader, and
service behavior so guests observe a more faithful sandbox;
- replace the workable Codex-assisted UI baseline with a human-led product
design pass, an original FridaBox icon system, and a stronger visual identity;
- continue refining the FridaBox product identity and visual system around the
project logo;
- expand the Android/vendor test matrix and add regression probes for every
detection surface addressed.
+35 -3
View File
@@ -42,15 +42,47 @@ FridaBox gratefully acknowledges the Frida maintainers and contributors for the
instrumentation engine, GumJS runtime, Java bridge, Gadget deployment model, and
controller APIs that make this research possible.
## Frida Java bridge and agent compiler
## Tabler Icons
FridaBox UI control icons are generated from the official `@tabler/icons`
package version 3.45.0, sourced from `https://github.com/tabler/tabler-icons`.
MIT License
Copyright (c) 2020-2026 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Frida Java and IL2CPP runtime bridges 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.
FridaBox also packages offline runtime-bridge bundles for Java 7.0.11 through
7.0.13 and `frida-il2cpp-bridge` 0.12.2 through 0.13.1. These optional bundles
are only composed into a private on-device agent when the user enables them.
- Java bridge origin: `https://github.com/frida/frida-java-bridge`
- IL2CPP bridge origin: `https://github.com/vfsfitvnm/frida-il2cpp-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.
- Java bridge and compiler license: LGPL-2.0 with the wxWindows Library Licence
3.1 exception, as declared by the respective official packages.
- IL2CPP bridge license: MIT, as declared by its package and repository.
The authoritative licence text and exception are the same wxWindows Library
Licence described above. Exact package versions and dependency integrity hashes
+48 -36
View File
@@ -32,7 +32,7 @@ android {
enable true
reset()
include "arm64-v8a"
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk false
}
}
@@ -77,7 +77,6 @@ android {
jniLibs {
excludes.add("**/libandroidx.graphics.path.so")
useLegacyPackaging true
keepDebugSymbols.add("**/libfrida-gadget.config.so")
}
resources {
excludes.add("**/libandroidx.graphics.path.so")
@@ -104,40 +103,20 @@ tasks.matching { it.name == "mergeDebugAssets" }.configureEach { dependsOn("copy
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") {
tasks.register("verifyDebugApkHasNoGadget") {
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")
def apks = fileTree("$buildDir/outputs/apk/debug").matching { include("*.apk") }.files
if (apks.isEmpty()) {
throw new GradleException("No debug APKs were produced")
}
apks.each { apk ->
def entries = zipTree(apk).files.findAll {
it.name.toLowerCase().contains("gadget") && it.name.toLowerCase().endsWith(".so")
}
if (!entries.isEmpty()) {
throw new GradleException("${apk.name} must not bundle Frida Gadget: ${entries*.name}")
}
}
}
}
@@ -145,7 +124,7 @@ tasks.register("verifyDebugApkFridaPackaging") {
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 loader = source.indexOf("FridaGadgetLoader.loadAtProcessBind()")
def makeApplication = source.indexOf("makeApplication(", loader)
if (loader < 0 || makeApplication < 0 || loader >= makeApplication) {
throw new GradleException("Frida Gadget loader must execute before guest makeApplication")
@@ -153,6 +132,37 @@ tasks.register("verifyInstrumentationOrdering") {
}
}
tasks.register("verifyRuntimeBridgeAssets") {
doLast {
def bridgeAssets = [
"java-7.0.11.js", "java-7.0.12.js", "java-7.0.13.js",
"il2cpp-0.12.2.js", "il2cpp-0.13.0.js", "il2cpp-0.13.1.js"
]
def bridgeRoot = file("src/main/assets/fridabox/bridges")
bridgeAssets.each { name ->
def asset = new File(bridgeRoot, name)
if (!asset.isFile() || asset.length() == 0 || asset.length() > 2 * 1024 * 1024) {
throw new GradleException("Missing or invalid runtime bridge asset: ${name}")
}
def source = new String(asset.bytes, java.nio.charset.StandardCharsets.UTF_8)
if (!source.startsWith("📦\n") || !source.contains("\n✄\n")) {
throw new GradleException("Runtime bridge is not a valid frida-compile bundle: ${name}")
}
}
}
}
tasks.register("verifyTablerAndroidIcons", Exec) {
workingDir(rootProject.projectDir)
def nodeExecutable = System.getProperty("os.name").toLowerCase().contains("windows") ? "node.exe" : "node"
commandLine(nodeExecutable, "tools/generate_tabler_android_icons.mjs", "--check")
doFirst {
if (!rootProject.file("node_modules/@tabler/icons/package.json").isFile()) {
throw new GradleException("Missing @tabler/icons. Run npm ci before building FridaBox.")
}
}
}
tasks.register("verifyDemoGuestUnmodified") {
dependsOn("copyDemoGuestDebug")
doLast {
@@ -166,7 +176,8 @@ tasks.register("verifyDemoGuestUnmodified") {
}
tasks.named("check").configure {
dependsOn("verifyDebugApkFridaPackaging", "verifyInstrumentationOrdering", "verifyDemoGuestUnmodified")
dependsOn("verifyDebugApkHasNoGadget", "verifyInstrumentationOrdering",
"verifyDemoGuestUnmodified", "verifyTablerAndroidIcons", "verifyRuntimeBridgeAssets")
}
dependencies {
@@ -176,6 +187,7 @@ dependencies {
implementation libs.material
implementation libs.constraintlayout
implementation libs.core.ktx
implementation "org.tukaani:xz:1.12"
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
+3 -2
View File
@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -17,9 +18,9 @@
android:allowBackup="false"
android:extractNativeLibs="true"
android:fullBackupContent="false"
android:icon="@drawable/ic_fridabox_app"
android:icon="@mipmap/ic_launcher"
android:label="@string/fb_brand"
android:roundIcon="@drawable/ic_fridabox_app"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.FridaBox"
android:enableOnBackInvokedCallback="true"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ package com.qm4rs.fridabox;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -13,41 +14,76 @@ public final class ApkInspector {
}
public static Result inspect(File apk) throws IOException {
return inspect(Collections.singletonList(apk), null);
}
public static Result inspect(File... apks) throws IOException {
return inspect(Arrays.asList(apks), null);
}
public static Result inspect(Iterable<File> apks) throws IOException {
return inspect(apks, null);
}
public static Result inspect(Iterable<File> apks, String processAbi) 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 inspectedAny = false;
for (File apk : apks) {
inspectedAny = true;
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);
if (!inspectedAny) throw new IOException("No APK files were provided");
boolean supported = !hasNativeLibraries || supportsProcessAbi(abis, processAbi);
return new Result(hasNativeLibraries, supported, abis, processAbi);
}
public static final class Result {
public final boolean hasNativeLibraries;
public final boolean supported;
public final Set<String> abis;
public final String processAbi;
Result(boolean hasNativeLibraries, boolean supported, Set<String> abis) {
Result(boolean hasNativeLibraries, boolean supported, Set<String> abis, String processAbi) {
this.hasNativeLibraries = hasNativeLibraries;
this.supported = supported;
this.abis = Collections.unmodifiableSet(new LinkedHashSet<>(abis));
this.processAbi = processAbi;
}
public String description() {
if (!hasNativeLibraries) return "Pure Java/Kotlin (accepted)";
return supported ? "ARM64 supported: " + abis : "Rejected; no arm64-v8a: " + abis;
if (processAbi == null) {
return !Collections.disjoint(abis, SUPPORTED_ABIS)
? "Recognized ABI: " + abis : "Rejected; unsupported ABI: " + abis;
}
return supported ? "Compatible with " + processAbi + ": " + abis
: "Rejected; requires " + processAbi + ", found: " + abis;
}
}
private static boolean supportsProcessAbi(Set<String> abis, String processAbi) {
if ("armeabi-v7a".equals(processAbi)) {
return abis.contains("armeabi-v7a") || abis.contains("armeabi");
}
return processAbi != null && abis.contains(processAbi);
}
private static final Set<String> SUPPORTED_ABIS = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
"armeabi-v7a", "arm64-v8a", "x86", "x86_64"
)));
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,284 @@
package com.qm4rs.fridabox
import android.app.Activity
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import org.json.JSONObject
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
/** Package-scoped floating log console shown inside a virtual guest Activity. */
object FridaBoxGuestLogOverlay {
private const val OVERLAY_TAG = "fridabox.guest.log.overlay"
private const val LOG_NAME = "runtime.jsonl"
private const val REFRESH_INTERVAL_MS = 750L
private val backgroundColor = Color.rgb(8, 13, 18)
private val surfaceColor = Color.rgb(18, 26, 34)
private val surfaceHighColor = Color.rgb(29, 40, 51)
private val primaryColor = Color.rgb(110, 231, 216)
private val textColor = Color.rgb(231, 238, 244)
private val secondaryTextColor = Color.rgb(148, 164, 178)
private val outlineColor = Color.rgb(49, 65, 80)
fun attach(activity: Activity, packageName: String) {
val decor = activity.window?.decorView as? ViewGroup ?: return
val existing = decor.findViewWithTag<View>(OVERLAY_TAG)
val localMode = InstrumentationSettings.MODE_LOCAL_SCRIPT ==
InstrumentationSettings.getModeForPackage(packageName)
val logFile = logFileFor(packageName)
if (!localMode || logFile == null) {
if (existing != null) decor.removeView(existing)
return
}
if (existing != null) {
existing.bringToFront()
return
}
val bubble = TextView(activity).apply {
tag = OVERLAY_TAG
text = ">_"
contentDescription = "Open Frida logs for $packageName"
gravity = Gravity.CENTER
setTextColor(Color.BLACK)
textSize = 15f
setTypeface(Typeface.MONOSPACE, Typeface.BOLD)
background = rounded(primaryColor, dp(activity, 18))
elevation = dp(activity, 10).toFloat()
setOnClickListener { showConsole(activity, packageName, logFile) }
}
val margin = dp(activity, 18)
decor.addView(bubble, FrameLayout.LayoutParams(dp(activity, 54), dp(activity, 54)).apply {
gravity = Gravity.END or Gravity.BOTTOM
marginEnd = margin
bottomMargin = dp(activity, 30)
})
makeDraggable(bubble, decor)
bubble.bringToFront()
}
fun show(activity: Activity, packageName: String): Boolean {
val logFile = logFileFor(packageName) ?: return false
showConsole(activity, packageName, logFile)
return true
}
private fun makeDraggable(view: View, parent: ViewGroup) {
val touchSlop = dp(view.context, 8).toFloat()
var downX = 0f
var downY = 0f
var startTranslationX = 0f
var startTranslationY = 0f
var dragged = false
view.setOnTouchListener { target, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
downX = event.rawX
downY = event.rawY
startTranslationX = target.translationX
startTranslationY = target.translationY
dragged = false
true
}
MotionEvent.ACTION_MOVE -> {
val dx = event.rawX - downX
val dy = event.rawY - downY
dragged = dragged || kotlin.math.abs(dx) > touchSlop || kotlin.math.abs(dy) > touchSlop
val minX = -target.left.toFloat()
val maxX = (parent.width - target.right).toFloat()
val minY = -target.top.toFloat()
val maxY = (parent.height - target.bottom).toFloat()
target.translationX = (startTranslationX + dx).coerceIn(minX, maxX)
target.translationY = (startTranslationY + dy).coerceIn(minY, maxY)
true
}
MotionEvent.ACTION_UP -> {
if (!dragged) target.performClick()
true
}
MotionEvent.ACTION_CANCEL -> true
else -> false
}
}
}
private fun showConsole(activity: Activity, packageName: String, logFile: File) {
if (activity.isFinishing || activity.isDestroyed) return
val handler = Handler(Looper.getMainLooper())
val root = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(activity, 20), dp(activity, 20), dp(activity, 20), dp(activity, 16))
background = rounded(surfaceColor, dp(activity, 22))
}
root.addView(TextView(activity).apply {
text = "On-device agent logs"
textSize = 20f
setTypeface(typeface, Typeface.BOLD)
setTextColor(textColor)
})
val status = TextView(activity).apply {
text = packageName
textSize = 11.5f
setTextColor(secondaryTextColor)
setPadding(0, dp(activity, 4), 0, 0)
}
root.addView(status)
val output = TextView(activity).apply {
text = "No logs yet."
typeface = Typeface.MONOSPACE
textSize = 12f
setTextColor(textColor)
setTextIsSelectable(true)
setPadding(dp(activity, 14), dp(activity, 14), dp(activity, 14), dp(activity, 14))
background = rounded(backgroundColor, dp(activity, 12))
}
val scroll = ScrollView(activity).apply {
isFillViewport = true
addView(output, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
))
}
root.addView(scroll, LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f
).apply {
topMargin = dp(activity, 14)
bottomMargin = dp(activity, 14)
})
val actions = LinearLayout(activity).apply {
orientation = LinearLayout.HORIZONTAL
}
var hiddenBefore = 0L
var fileSignature = Long.MIN_VALUE
val clear = button(activity, "Clear view", false) {
hiddenBefore = System.currentTimeMillis()
fileSignature = Long.MIN_VALUE
output.text = "No logs yet."
}
val copy = button(activity, "Copy", false) {
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("$packageName Frida logs", output.text))
Toast.makeText(activity, "Agent logs copied", Toast.LENGTH_SHORT).show()
}
lateinit var dialog: Dialog
val close = button(activity, "Close", true) { dialog.dismiss() }
actions.addView(clear, LinearLayout.LayoutParams(0, dp(activity, 44), 1f))
actions.addView(copy, LinearLayout.LayoutParams(0, dp(activity, 44), 1f).apply {
marginStart = dp(activity, 8)
})
actions.addView(close, LinearLayout.LayoutParams(0, dp(activity, 44), 1f).apply {
marginStart = dp(activity, 8)
})
root.addView(actions)
dialog = Dialog(activity).apply {
setContentView(root)
setCanceledOnTouchOutside(true)
window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes = attributes.apply { dimAmount = 0.58f }
}
}
var snapshot = ""
lateinit var refresh: Runnable
refresh = Runnable {
if (!dialog.isShowing) return@Runnable
val length = logFile.length()
val nextSignature = length xor logFile.lastModified()
if (nextSignature != fileSignature) {
fileSignature = nextSignature
val next = formatLogs(logFile, hiddenBefore)
if (next != snapshot) {
val stayAtBottom = scroll.scrollY + scroll.height >= output.height - dp(activity, 24)
snapshot = next
output.text = next.ifBlank { "No logs yet." }
if (stayAtBottom) scroll.post { scroll.fullScroll(View.FOCUS_DOWN) }
}
val size = if (length in 1..1023) "<1 KiB" else "${length / 1024} KiB"
status.text = "$packageName · $size"
}
handler.postDelayed(refresh, REFRESH_INTERVAL_MS)
}
dialog.setOnShowListener {
val metrics = activity.resources.displayMetrics
dialog.window?.setLayout((metrics.widthPixels * 0.92f).toInt(), (metrics.heightPixels * 0.76f).toInt())
handler.post(refresh)
}
dialog.setOnDismissListener { handler.removeCallbacks(refresh) }
dialog.show()
}
private fun logFileFor(packageName: String): File? {
val scriptPath = InstrumentationSettings.getScriptPathForPackage(packageName) ?: return null
val directory = File(scriptPath).parentFile ?: return null
return File(directory, LOG_NAME)
}
private fun formatLogs(file: File, hiddenBefore: Long): String {
if (!file.isFile || file.length() == 0L) return ""
val formatter = SimpleDateFormat("HH:mm:ss.SSS", Locale.ROOT)
return runCatching {
file.readLines(Charsets.UTF_8).mapNotNull { line ->
runCatching<String?> {
val item = JSONObject(line)
val time = item.optLong("time")
if (time <= hiddenBefore) return@runCatching null
val timestamp = formatter.format(java.util.Date(time))
val level = item.optString("level", "log").uppercase(Locale.ROOT)
"[$timestamp] ${level.padEnd(6)} ${item.optString("message")}"
}.getOrElse { if (hiddenBefore == 0L) line else null }
}.joinToString("\n")
}.getOrElse { "Unable to read agent logs: ${it.message}" }
}
private fun button(context: Context, label: String, primary: Boolean, action: () -> Unit): TextView {
return TextView(context).apply {
text = label
gravity = Gravity.CENTER
textSize = 12f
setTypeface(typeface, Typeface.BOLD)
setTextColor(if (primary) Color.BLACK else textColor)
background = rounded(if (primary) primaryColor else surfaceHighColor, dp(context, 12),
if (primary) primaryColor else outlineColor)
setOnClickListener { action() }
}
}
private fun rounded(fill: Int, radius: Int, stroke: Int? = null): GradientDrawable {
return GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
setColor(fill)
cornerRadius = radius.toFloat()
if (stroke != null) setStroke(1, stroke)
}
}
private fun dp(context: Context, value: Int): Int =
(value * context.resources.displayMetrics.density).toInt()
}
@@ -1,16 +1,22 @@
package com.qm4rs.fridabox
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.io.File
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.app.BActivityThread
import top.niunaijun.blackbox.app.configuration.AppLifecycleCallback
import top.niunaijun.blackbox.app.configuration.ClientConfiguration
import top.niunaijun.blackbox.instrumentation.FridaGadgetLoader
import top.niunaijun.blackbox.instrumentation.GuestRuntimeRegistry
object FridaBoxRuntime {
private const val TAG = "FridaBox.Runtime"
private const val AGENT_READY_FALLBACK_MS = 5_000L
fun attach(context: Context) {
BlackBoxCore.get().doAttachBaseContext(context, object : ClientConfiguration() {
@@ -48,6 +54,19 @@ object FridaBoxRuntime {
userId: Int
) {
Log.d(TAG, "afterApplicationOnCreate: package=$packageName process=$processName")
if (GuestRuntimeRegistry.isPrimaryProcess()) {
Handler(Looper.getMainLooper()).postDelayed({
FridaGadgetLoader.loadIfEnabled()
}, AGENT_READY_FALLBACK_MS)
}
}
override fun onActivityResumed(activity: Activity) {
val packageName = runCatching { BActivityThread.getAppPackageName() }.getOrNull()
if (!packageName.isNullOrBlank()) {
FridaGadgetLoader.loadIfEnabled()
FridaBoxGuestLogOverlay.attach(activity, packageName)
}
}
override fun onStoragePermissionNeeded(packageName: String?, userId: Int): Boolean {
@@ -0,0 +1,384 @@
package com.qm4rs.fridabox
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import org.tukaani.xz.XZInputStream
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
import top.niunaijun.blackbox.utils.ProcessAbi
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.security.MessageDigest
import java.util.Locale
import java.util.zip.GZIPInputStream
enum class GadgetSource(
val id: String,
val title: String,
val repository: String
) {
OFFICIAL("official", "Official Frida", "frida/frida");
fun expectedAsset(version: String, architecture: String): String =
"frida-gadget-$version-android-$architecture.so.xz"
companion object {
fun fromId(value: String?): GadgetSource? = entries.firstOrNull { it.id == value }
}
}
data class GadgetAbi(val androidName: String, val releaseName: String) {
companion object {
private val supported = listOf(
GadgetAbi("arm64-v8a", "arm64"),
GadgetAbi("armeabi-v7a", "arm"),
GadgetAbi("x86_64", "x86_64"),
GadgetAbi("x86", "x86")
)
fun detect(context: Context): GadgetAbi? {
val processAbi = ProcessAbi.detect(context) ?: return null
return supported.firstOrNull { it.androidName == processAbi }
}
}
}
data class GadgetRelease(
val source: GadgetSource,
val version: String,
val abi: GadgetAbi,
val assetName: String,
val downloadUrl: String,
val compressedSize: Long,
val publishedAt: String
)
data class InstalledGadget(
val source: GadgetSource,
val version: String,
val abi: GadgetAbi,
val file: File,
val sha256: String,
val size: Long
) {
val identity: String get() = "${source.id}:$version:${abi.androidName}"
}
data class GadgetCatalog(
val releases: List<GadgetRelease>,
val fromCache: Boolean,
val hasMore: Boolean
)
class GadgetManager(private val context: Context) {
private val root = File(context.filesDir, DOWNLOAD_ROOT)
private val cacheRoot = File(context.cacheDir, "gadget-catalogs")
init {
if (InstrumentationSettings.getSelectedGadgetSource() != null
&& InstrumentationSettings.getSelectedGadgetSource() != GadgetSource.OFFICIAL.id) {
InstrumentationSettings.clearSelectedGadget()
}
}
fun detectedAbi(): GadgetAbi? = GadgetAbi.detect(context)
fun loadCatalog(source: GadgetSource, abi: GadgetAbi, page: Int): GadgetCatalog {
val cache = File(cacheRoot, "${source.id}-${abi.androidName}-$page.json")
return try {
val json = requestText(releaseCatalogUrl(source, page))
val releases = parseCatalog(json, source, abi)
writeAtomically(cache, json.toByteArray(Charsets.UTF_8))
GadgetCatalog(releases, false, JSONArray(json).length() == CATALOG_PAGE_SIZE)
} catch (networkError: Exception) {
if (!cache.isFile) throw networkError
val cached = cache.readText(Charsets.UTF_8)
val releases = parseCatalog(cached, source, abi)
GadgetCatalog(releases, true, JSONArray(cached).length() == CATALOG_PAGE_SIZE)
}
}
fun download(release: GadgetRelease): InstalledGadget {
val currentAbi = detectedAbi() ?: throw IOException("This device ABI is not supported")
if (release.abi != currentAbi) throw IOException("The selected release does not match this device ABI")
val directory = installDirectory(release.source, release.version, release.abi).apply {
if (!isDirectory && !mkdirs()) throw IOException("Unable to create Gadget storage")
}
val temporary = File(directory, "$PAYLOAD_NAME.partial")
val destination = File(directory, PAYLOAD_NAME)
val metadata = File(directory, METADATA_NAME)
if (temporary.exists() && !temporary.delete()) throw IOException("Unable to replace partial download")
val connection = openConnection(release.downloadUrl, "application/octet-stream")
try {
if (connection.responseCode !in 200..299) {
throw IOException("GitHub download failed (HTTP ${connection.responseCode})")
}
val compressedLength = connection.contentLengthLong
if (compressedLength > MAX_COMPRESSED_BYTES) throw IOException("Gadget download is unexpectedly large")
val digest = MessageDigest.getInstance("SHA-256")
var total = 0L
decompressed(connection.inputStream, release.assetName).use { input ->
FileOutputStream(temporary).use { output ->
val buffer = ByteArray(128 * 1024)
while (true) {
val count = input.read(buffer)
if (count < 0) break
total += count
if (total > MAX_DECOMPRESSED_BYTES) throw IOException("Expanded Gadget is unexpectedly large")
digest.update(buffer, 0, count)
output.write(buffer, 0, count)
}
output.fd.sync()
}
}
if (total == 0L) throw IOException("Downloaded Gadget is empty")
validateElf(temporary, release.abi)
replace(temporary, destination)
if (!destination.setReadable(true, true) || !destination.setWritable(false, false)) {
throw IOException("Unable to secure downloaded Gadget")
}
val sha256 = digest.digest().joinToString("") { "%02x".format(it) }
val record = InstalledGadget(
release.source, release.version, release.abi, destination, sha256, total
)
writeMetadata(metadata, record, release.assetName)
return record
} catch (error: Exception) {
temporary.delete()
throw error
} finally {
connection.disconnect()
}
}
fun installed(): List<InstalledGadget> {
val abi = detectedAbi() ?: return emptyList()
if (!root.isDirectory) return emptyList()
return root.walkTopDown()
.filter { it.isFile && it.name == METADATA_NAME }
.mapNotNull { metadata -> runCatching { readMetadata(metadata) }.getOrNull() }
.filter { it.abi == abi && it.file.isFile }
.filter { runCatching { validateElf(it.file, it.abi) }.isSuccess }
.sortedWith(compareByDescending<InstalledGadget> { versionParts(it.version) }
.thenBy { it.source.ordinal })
.toList()
}
fun selected(): InstalledGadget? {
val path = InstrumentationSettings.getSelectedGadgetPath() ?: return null
val selected = runCatching { File(path).canonicalFile }.getOrNull() ?: return null
return installed().firstOrNull { runCatching { it.file.canonicalFile == selected }.getOrDefault(false) }
}
fun select(gadget: InstalledGadget) {
val currentAbi = detectedAbi() ?: throw IOException("This device ABI is not supported")
if (gadget.abi != currentAbi || !gadget.file.isFile) {
throw IOException("The selected Gadget is not available for this device")
}
validateElf(gadget.file, gadget.abi)
if (!InstrumentationSettings.setSelectedGadget(
gadget.file.canonicalPath,
gadget.source.id,
gadget.version,
gadget.abi.androidName,
gadget.sha256
)) {
throw IOException("Unable to save the selected Gadget")
}
}
fun findInstalled(release: GadgetRelease): InstalledGadget? {
val expected = runCatching {
File(installDirectory(release.source, release.version, release.abi), METADATA_NAME)
}.getOrNull() ?: return null
return runCatching { readMetadata(expected) }.getOrNull()
?.takeIf { it.file.isFile && runCatching { validateElf(it.file, it.abi) }.isSuccess }
}
private fun parseCatalog(json: String, source: GadgetSource, abi: GadgetAbi): List<GadgetRelease> {
val releases = JSONArray(json)
val result = ArrayList<GadgetRelease>()
for (index in 0 until releases.length()) {
val release = releases.optJSONObject(index) ?: continue
if (release.optBoolean("draft") || release.optBoolean("prerelease")) continue
val version = release.optString("tag_name").trim()
if (!isSafeSegment(version)) continue
val expected = source.expectedAsset(version, abi.releaseName)
val assets = release.optJSONArray("assets") ?: continue
for (assetIndex in 0 until assets.length()) {
val asset = assets.optJSONObject(assetIndex) ?: continue
val name = asset.optString("name")
if (name != expected) continue
val url = asset.optString("browser_download_url")
if (!isTrustedUrl(url)) continue
result += GadgetRelease(
source = source,
version = version,
abi = abi,
assetName = name,
downloadUrl = url,
compressedSize = asset.optLong("size", -1L),
publishedAt = release.optString("published_at")
)
break
}
}
return result
}
private fun installDirectory(source: GadgetSource, version: String, abi: GadgetAbi): File {
if (!isSafeSegment(version)) throw IOException("Invalid Gadget version")
return File(root, "${source.id}${File.separator}$version${File.separator}${abi.androidName}")
}
private fun writeMetadata(file: File, gadget: InstalledGadget, assetName: String) {
val json = JSONObject()
.put("source", gadget.source.id)
.put("version", gadget.version)
.put("abi", gadget.abi.androidName)
.put("releaseAbi", gadget.abi.releaseName)
.put("sha256", gadget.sha256)
.put("size", gadget.size)
.put("asset", assetName)
.toString()
writeAtomically(file, json.toByteArray(Charsets.UTF_8))
}
private fun readMetadata(file: File): InstalledGadget {
if (!file.isFile || file.length() > MAX_METADATA_BYTES) throw IOException("Invalid Gadget metadata")
val json = JSONObject(file.readText(Charsets.UTF_8))
val source = GadgetSource.fromId(json.getString("source")) ?: throw IOException("Unknown Gadget source")
val version = json.getString("version")
if (!isSafeSegment(version)) throw IOException("Invalid Gadget version")
val androidAbi = json.getString("abi")
val releaseAbi = json.getString("releaseAbi")
val abi = GadgetAbi(androidAbi, releaseAbi)
val payload = File(file.parentFile, PAYLOAD_NAME).canonicalFile
val canonicalRoot = root.canonicalFile
if (!payload.path.startsWith(canonicalRoot.path + File.separator)) throw IOException("Invalid Gadget path")
return InstalledGadget(
source, version, abi, payload, json.getString("sha256"), json.getLong("size")
)
}
private fun requestText(url: String): String {
val connection = openConnection(url, "application/vnd.github+json")
try {
if (connection.responseCode !in 200..299) {
throw IOException("GitHub release lookup failed (HTTP ${connection.responseCode})")
}
val bytes = readLimited(connection.inputStream, MAX_CATALOG_BYTES)
return bytes.toString(Charsets.UTF_8)
} finally {
connection.disconnect()
}
}
private fun openConnection(value: String, accept: String): HttpURLConnection {
if (!isTrustedUrl(value)) throw IOException("Untrusted Gadget download URL")
return (URL(value).openConnection() as HttpURLConnection).apply {
connectTimeout = 15_000
readTimeout = 45_000
instanceFollowRedirects = true
setRequestProperty("Accept", accept)
setRequestProperty("User-Agent", "FridaBox/${BuildConfig.VERSION_NAME}")
setRequestProperty("X-GitHub-Api-Version", "2022-11-28")
}
}
private fun decompressed(input: InputStream, assetName: String): InputStream = when {
assetName.endsWith(".xz") -> XZInputStream(input)
assetName.endsWith(".gz") -> GZIPInputStream(input, 128 * 1024)
else -> throw IOException("Unsupported Gadget archive format")
}
companion object {
private const val DOWNLOAD_ROOT = "fridabox-gadgets"
internal const val CATALOG_PAGE_SIZE = 10
private const val PAYLOAD_NAME = "payload.so"
private const val METADATA_NAME = "metadata.json"
private const val MAX_METADATA_BYTES = 16L * 1024L
private const val MAX_CATALOG_BYTES = 24L * 1024L * 1024L
private const val MAX_COMPRESSED_BYTES = 96L * 1024L * 1024L
private const val MAX_DECOMPRESSED_BYTES = 192L * 1024L * 1024L
internal fun releaseCatalogUrl(source: GadgetSource, page: Int): String {
require(page > 0) { "Catalog page must be positive" }
return "https://api.github.com/repos/${source.repository}/releases" +
"?per_page=$CATALOG_PAGE_SIZE&page=$page"
}
internal fun validateElf(file: File, abi: GadgetAbi) {
val header = ByteArray(20)
FileInputStream(file).use { input ->
if (input.read(header) != header.size) throw IOException("Downloaded Gadget is truncated")
}
if (header[0].toInt() and 0xff != 0x7f || header[1] != 'E'.code.toByte()
|| header[2] != 'L'.code.toByte() || header[3] != 'F'.code.toByte()) {
throw IOException("Downloaded Gadget is not an ELF library")
}
if (header[5].toInt() != 1) throw IOException("Only little-endian Android ELF libraries are supported")
val machine = (header[18].toInt() and 0xff) or ((header[19].toInt() and 0xff) shl 8)
val expected = when (abi.androidName) {
"arm64-v8a" -> 183
"armeabi-v7a" -> 40
"x86" -> 3
"x86_64" -> 62
else -> -1
}
if (machine != expected) throw IOException("Gadget ELF architecture does not match ${abi.androidName}")
}
private fun readLimited(input: InputStream, maximum: Long): ByteArray {
input.use {
val output = ByteArrayOutputStream()
val buffer = ByteArray(64 * 1024)
var total = 0L
while (true) {
val count = it.read(buffer)
if (count < 0) break
total += count
if (total > maximum) throw IOException("GitHub response is unexpectedly large")
output.write(buffer, 0, count)
}
return output.toByteArray()
}
}
private fun writeAtomically(destination: File, bytes: ByteArray) {
destination.parentFile?.let { if (!it.isDirectory && !it.mkdirs()) throw IOException("Unable to create storage") }
val temporary = File(destination.parentFile, destination.name + ".partial")
FileOutputStream(temporary).use { output ->
output.write(bytes)
output.fd.sync()
}
replace(temporary, destination)
}
private fun replace(temporary: File, destination: File) {
if (destination.exists() && !destination.delete()) throw IOException("Unable to replace ${destination.name}")
if (!temporary.renameTo(destination)) throw IOException("Unable to install ${destination.name}")
}
private fun isSafeSegment(value: String): Boolean =
value.isNotBlank() && value.length <= 64 && value.matches(Regex("[A-Za-z0-9._-]+"))
private fun isTrustedUrl(value: String): Boolean = runCatching {
val url = URL(value)
val host = url.host.lowercase(Locale.ROOT)
url.protocol == "https" && (host == "api.github.com" || host == "github.com"
|| host.endsWith(".githubusercontent.com"))
}.getOrDefault(false)
private fun versionParts(value: String): String = value.split('.')
.joinToString(".") { it.toIntOrNull()?.toString()?.padStart(8, '0') ?: it }
}
}
@@ -1,11 +0,0 @@
{
"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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

@@ -1,6 +0,0 @@
<?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>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="28dp" />
<gradient
android:angle="90"
android:startColor="#E0253850"
android:centerColor="#C91A2B42"
android:endColor="#B3122136" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="22dp" />
<gradient
android:angle="135"
android:startColor="#F02A3E58"
android:centerColor="#E01C2D45"
android:endColor="#D916263C" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="24dp" />
<gradient
android:angle="135"
android:startColor="#D9253850"
android:centerColor="#B51B2B42"
android:endColor="#9915253B" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="22dp" />
<gradient android:angle="135" android:startColor="#D564E8E8" android:endColor="#A329415D" />
<stroke android:width="1dp" android:color="#D9FFFFFF" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="22dp" />
<gradient android:angle="135" android:startColor="#A638516D" android:endColor="#7315263B" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline" />
</shape>
</item>
</selector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="22dp" />
<gradient android:angle="135" android:startColor="#59364E68" android:endColor="#33152235" />
<stroke android:width="1dp" android:color="@color/fb_glass_outline_soft" />
</shape>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<gradient
android:angle="135"
android:startColor="#07111D"
android:centerColor="#11162D"
android:endColor="#080C18" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0.88"
android:centerY="0.08"
android:gradientRadius="420dp"
android:startColor="#5964E8E8"
android:centerColor="#2464E8E8"
android:endColor="#0064E8E8" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<gradient
android:type="radial"
android:centerX="0.08"
android:centerY="0.92"
android:gradientRadius="460dp"
android:startColor="#4D9B87F5"
android:centerColor="#1F9B87F5"
android:endColor="#009B87F5" />
</shape>
</item>
</layer-list>
-4
View File
@@ -1,4 +0,0 @@
<?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>
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/apps.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 7l6 0" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M17 4l0 6" />
</vector>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/database-off.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M3 3l18 18" />
</vector>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/x.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M18 6l-12 12" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M6 6l12 12" />
</vector>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/info-circle.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 9h.01" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M11 12h1v4h1" />
</vector>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/file.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 3v4a1 1 0 0 0 1 1h4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2" />
</vector>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/package.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l0 9" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l-8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M16 5.25l-8 4.5" />
</vector>
@@ -0,0 +1,42 @@
<?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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l0 9" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M12 12l-8 -4.5" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M16 5.25l-8 4.5" />
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:drawable="@drawable/ic_fb_gadget_active" />
<item android:drawable="@drawable/ic_fb_gadget" />
</selector>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/file-import.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 3v4a1 1 0 0 0 1 1h4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M5 13v-8a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-5.5m-9.5 -2h7m-3 -3l3 3l-3 3" />
</vector>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated from @tabler/icons 3.45.0: icons/outline/trash.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 7l16 0" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M10 11l0 6" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 11l0 6" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</vector>
+13 -2
View File
@@ -1,4 +1,15 @@
<?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" />
<!-- Generated from @tabler/icons 3.45.0: icons/outline/activity.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M3 12h4l3 8l4 -16l3 8h4" />
</vector>
+20 -2
View File
@@ -1,4 +1,22 @@
<?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" />
<!-- Generated from @tabler/icons 3.45.0: icons/outline/settings.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</vector>
@@ -0,0 +1,21 @@
<?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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:drawable="@drawable/ic_fb_settings_active" />
<item android:drawable="@drawable/ic_fb_settings" />
</selector>
+34 -2
View File
@@ -1,4 +1,36 @@
<?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" />
<!-- Generated from @tabler/icons 3.45.0: icons/outline/layout-grid.svg -->
<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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
</vector>
@@ -0,0 +1,35 @@
<?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="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="2.7"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:drawable="@drawable/ic_fb_workspace_active" />
<item android:drawable="@drawable/ic_fb_workspace" />
</selector>
+75 -27
View File
@@ -4,15 +4,18 @@
android:id="@+id/fridabox_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/fb_background"
android:background="@drawable/bg_liquid_glass_app"
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"
android:layout_marginStart="12dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
android:background="@android:color/transparent"
android:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@@ -20,16 +23,56 @@
<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" />
android:layout_height="64dp"
android:background="@drawable/bg_glass_toolbar"
android:elevation="10dp"
android:contentInsetStart="0dp"
android:contentInsetEnd="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layoutDirection="ltr"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="14dp">
<ImageView
android:layout_width="44dp"
android:layout_height="44dp"
android:contentDescription="@string/fb_brand"
android:scaleType="centerInside"
android:src="@drawable/logo" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@string/fb_brand"
android:textColor="@color/fb_text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/fb_brand_tagline"
android:textColor="@color/fb_text_secondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
@@ -79,29 +122,34 @@
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"
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/import_fab_icon"
android:layout_width="56dp"
android:layout_height="56dp"
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"
android:layout_marginBottom="14dp"
android:contentDescription="@string/fb_import"
app:backgroundTint="@color/fb_primary"
app:icon="@drawable/ic_fb_add"
app:iconTint="@color/fb_black"
app:elevation="12dp"
app:srcCompat="@drawable/ic_fb_import"
app:tint="@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"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_glass_navigation"
android:elevation="14dp"
app:itemActiveIndicatorStyle="@style/Widget.FridaBox.Navigation.ActiveIndicator"
app:itemIconTint="@color/fb_text_primary"
app:itemRippleColor="@color/fb_glass_highlight"
app:itemTextAppearanceActive="@style/TextAppearance.FridaBox.Navigation.Label"
app:itemTextAppearanceInactive="@style/TextAppearance.FridaBox.Navigation.Label"
app:itemTextColor="@color/fb_text_primary"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
@@ -1,6 +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" />
<item android:id="@+id/nav_workspace" android:icon="@drawable/ic_fb_workspace_nav" android:title="@string/fb_nav_workspace" />
<item android:id="@+id/nav_gadgets" android:icon="@drawable/ic_fb_gadget_nav" android:title="@string/fb_nav_gadgets" />
<item android:id="@+id/nav_settings" android:icon="@drawable/ic_fb_settings_nav" android:title="@string/fb_nav_settings" />
</menu>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+12 -5
View File
@@ -1,15 +1,22 @@
<?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_background">#07111D</color>
<color name="fb_surface">#18263A</color>
<color name="fb_surface_high">#22344D</color>
<color name="fb_surface_tint">#29415D</color>
<color name="fb_glass_surface">#B31A2940</color>
<color name="fb_glass_surface_strong">#E0203047</color>
<color name="fb_glass_surface_soft">#731C3049</color>
<color name="fb_glass_outline">#66FFFFFF</color>
<color name="fb_glass_outline_soft">#2EFFFFFF</color>
<color name="fb_glass_highlight">#38FFFFFF</color>
<color name="fb_nav_indicator">#3864E8E8</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_outline">#52708F</color>
<color name="fb_success">#42D39A</color>
<color name="fb_warning">#F5BF5B</color>
<color name="fb_error">#FB7185</color>
+38 -16
View File
@@ -3,16 +3,16 @@
<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_gadgets">Gadgets</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_import">Import</string>
<string name="fb_import_from_apps">Import from apps</string>
<string name="fb_import_from_file">Import from file</string>
<string name="fb_installed_apps">Installed apps</string>
<string name="fb_import_app">Import</string>
<string name="fb_app_imported">Imported</string>
<string name="fb_loading_apps">Loading apps…</string>
<string name="fb_no_importable_apps">No importable apps found</string>
<string name="fb_mode_local">On-device</string>
<string name="fb_mode_computer">Computer</string>
<string name="fb_mode_clean">Clean</string>
@@ -28,21 +28,15 @@
<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>
@@ -50,10 +44,38 @@
<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>
<string name="fb_gadgets_title">Gadget manager</string>
<string name="fb_active_gadget">Active Gadget</string>
<string name="fb_no_active_gadget_short">Not selected</string>
<string name="fb_installed_gadgets">Installed</string>
<string name="fb_browse_gadgets">Browse versions</string>
<string name="fb_close">Close</string>
<string name="fb_loading_versions">Loading versions…</string>
<string name="fb_all_versions_loaded">All available versions loaded</string>
<string name="fb_cached_versions">Showing cached versions</string>
<string name="fb_use_gadget">Use this Gadget</string>
<string name="fb_gadget_active">Active</string>
<string name="fb_download">Download</string>
<string name="fb_gadget_required">Download and select a Gadget before using an instrumented launch mode.</string>
<string name="fb_gadget_selected">Gadget %1$s selected</string>
<string name="fb_gadget_select_failed">Unable to select Gadget</string>
<string name="fb_gadget_downloading">Downloading Gadget %1$s…</string>
<string name="fb_gadget_downloaded">Gadget %1$s downloaded. Choose it from Installed when you want to use it.</string>
<string name="fb_gadget_downloaded_selected">Gadget %1$s downloaded and selected.</string>
<string name="fb_gadget_download_failed">Gadget download failed: %1$s</string>
<string name="fb_catalog_failed_body">Unable to load compatible Gadget releases from GitHub.</string>
<string name="fb_gadget_unsupported_title">Unsupported device ABI</string>
<string name="fb_runtime_bridges">Runtime bridges</string>
<string name="fb_runtime_bridges_body">Available for Gadget %1$s. Enabled bridges are embedded into the private on-device agent at the next launch.</string>
<string name="fb_runtime_bridge_toggle">Enable %1$s bridge</string>
<string name="fb_runtime_bridge_version">Version %1$s</string>
<string name="fb_runtime_bridge_choose_version">Choose %1$s version</string>
<string name="fb_runtime_bridge_enabled">%1$s bridge enabled for the next on-device launch</string>
<string name="fb_runtime_bridge_disabled">%1$s bridge disabled</string>
<string name="fb_runtime_bridge_save_failed">Unable to save runtime bridge settings</string>
</resources>
+22 -3
View File
@@ -11,8 +11,8 @@
<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:windowBackground">@drawable/bg_liquid_glass_app</item>
<item name="android:navigationBarColor">@color/fb_background</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>
@@ -22,8 +22,27 @@
</style>
<style name="ThemeOverlay.FridaBox.Dialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
<item name="colorSurface">@color/fb_surface_high</item>
<item name="colorSurface">@color/fb_glass_surface_strong</item>
<item name="colorOnSurface">@color/fb_text_primary</item>
<item name="colorPrimary">@color/fb_primary</item>
</style>
<style name="Widget.FridaBox.Navigation.ActiveIndicator" parent="">
<item name="android:width">44dp</item>
<item name="android:height">32dp</item>
<item name="marginHorizontal">0dp</item>
<item name="shapeAppearance">@style/ShapeAppearance.FridaBox.NavigationPill</item>
<item name="android:color">@color/fb_nav_indicator</item>
</style>
<style name="ShapeAppearance.FridaBox.NavigationPill" parent="ShapeAppearance.MaterialComponents.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
<style name="TextAppearance.FridaBox.Navigation.Label" parent="TextAppearance.MaterialComponents.Caption">
<item name="android:fontFamily">sans</item>
<item name="android:textSize">12sp</item>
<item name="android:textStyle">normal</item>
</style>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0A0A0A</color>
</resources>
@@ -18,21 +18,56 @@ public class ApkInspectorTest {
@Test
public void pureJavaApkIsAccepted() throws Exception {
File apk = apkWith("classes.dex");
ApkInspector.Result result = ApkInspector.inspect(apk);
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(apk), "x86");
assertFalse(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void arm64ApkIsAccepted() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(apkWith("lib/arm64-v8a/libsample.so"));
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/arm64-v8a/libsample.so")), "arm64-v8a");
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void thirtyTwoBitOnlyApkIsRejected() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(apkWith("lib/armeabi-v7a/libsample.so"));
public void thirtyTwoBitOnlyApkIsAccepted() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/armeabi-v7a/libsample.so")), "armeabi-v7a");
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void x86ApkIsAccepted() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/x86/libsample.so")), "x86");
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void splitNativeLibrariesAreInspectedTogether() throws Exception {
File base = apkWith("classes.dex");
File arm64Split = apkWith("lib/arm64-v8a/libsample.so");
ApkInspector.Result result = ApkInspector.inspect(java.util.Arrays.asList(base, arm64Split), "arm64-v8a");
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void armGuestIsRejectedByX86Process() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/armeabi-v7a/libsample.so")), "x86");
assertTrue(result.hasNativeLibraries);
assertFalse(result.supported);
}
@Test
public void thirtyTwoBitGuestIsRejectedBySixtyFourBitProcessOfSameFamily() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/armeabi-v7a/libsample.so")), "arm64-v8a");
assertTrue(result.hasNativeLibraries);
assertFalse(result.supported);
}
@@ -0,0 +1,56 @@
package com.qm4rs.fridabox
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.IOException
class GadgetManagerTest {
@get:Rule
val temporary = TemporaryFolder()
@Test
fun releaseAssetNameMatchesOfficialRepository() {
assertEquals(
"frida-gadget-17.9.11-android-arm64.so.xz",
GadgetSource.OFFICIAL.expectedAsset("17.9.11", "arm64")
)
}
@Test
fun releaseCatalogUsesTenItemPages() {
assertEquals(
"https://api.github.com/repos/frida/frida/releases?per_page=10&page=3",
GadgetManager.releaseCatalogUrl(GadgetSource.OFFICIAL, 3)
)
}
@Test
fun elfValidationAcceptsMatchingArchitecture() {
val file = temporary.newFile("payload.so")
file.writeBytes(elfHeader(183))
GadgetManager.validateElf(file, GadgetAbi("arm64-v8a", "arm64"))
}
@Test
fun elfValidationRejectsWrongArchitecture() {
val file = temporary.newFile("payload.so")
file.writeBytes(elfHeader(40))
assertThrows(IOException::class.java) {
GadgetManager.validateElf(file, GadgetAbi("arm64-v8a", "arm64"))
}
}
private fun elfHeader(machine: Int): ByteArray = ByteArray(20).also {
it[0] = 0x7f
it[1] = 'E'.code.toByte()
it[2] = 'L'.code.toByte()
it[3] = 'F'.code.toByte()
it[4] = 2
it[5] = 1
it[18] = (machine and 0xff).toByte()
it[19] = (machine shr 8).toByte()
}
}
+2 -2
View File
@@ -10,8 +10,8 @@ ext {
targetSdkVersion = 28
minSdk = 21
versionCode = 400
versionName = "4.0.0"
versionCode = 403
versionName = "4.2.0"
xVersion = '1.1.0'
hiddenApiBypass = '4.3'
}
+21 -14
View File
@@ -7,23 +7,19 @@
- 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.
- Node.js 20 or newer and npm for reproducibly building Frida 17 Java agents
and generating Android vectors from the official `@tabler/icons` package.
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.
Create `local.properties` with the SDK path. Frida Gadget is intentionally not a
build input and no Gadget binary is packaged in the APK. After installation, the
user downloads and selects an ABI-compatible Official Frida build from the
in-app **Gadgets** screen.
Install the pinned Java bridge/compiler and build the controller agents:
```powershell
npm ci
npm run verify:android-icons
python tools\build_frida_agents.py
```
@@ -31,6 +27,14 @@ 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`.
The same build command generates the optional Java and IL2CPP bridge assets
used by On-device mode. Their version catalog and composition model are
documented in [RUNTIME_BRIDGES.md](RUNTIME_BRIDGES.md).
UI control icons are generated from pinned `@tabler/icons` sources with
`npm run generate:android-icons`. The app `check` task runs the matching
verification script and fails if a committed Android vector is out of sync.
On Windows, if the repository path contains spaces, create a no-space junction
and set the Bcore-only native path override:
@@ -51,7 +55,10 @@ 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.
The final host outputs are under `app/build/outputs/apk/debug/` for `armeabi-v7a`,
`arm64-v8a`, `x86`, and `x86_64`.
`verifyDebugApkHasNoGadget` inspects the assembled APK and fails if a Gadget
library or configuration is accidentally bundled again.
## Production release
@@ -80,5 +87,5 @@ 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`.
Release outputs are written to `app/build/outputs/apk/release/`, one APK each for
`armeabi-v7a`, `arm64-v8a`, `x86`, and `x86_64`.
+70
View File
@@ -0,0 +1,70 @@
# Runtime bridges
## Why this exists
Frida 17.0.0 removed the Java, Objective-C, and Swift runtime bridges from the
GumJS runtime. An autonomous Gadget Script interaction therefore no longer gets
the `Java` global implicitly. Frida's REPL and `frida-trace` remain compatible
because `frida-tools` injects those bridges itself.
Primary references:
- `https://frida.re/news/2025/05/17/frida-17-0-0-released/`
- `https://frida.re/docs/bridges/`
- `https://frida.re/docs/gadget/#using-the-language-bridges`
`frida-il2cpp-bridge` is different: it is a community module and was never a
built-in GumJS bridge. Its current compatibility statement covers Unity 5.3.0
through 6000.3.x, with Android and Linux as the tested platforms:
- `https://github.com/vfsfitvnm/frida-il2cpp-bridge`
## FridaBox architecture
Frida scripts have isolated globals. Loading a bridge in a second Script does
not make `Java` or `Il2Cpp` visible to the user's Script. FridaBox instead builds
each supported package version with `frida-compile`, stores the resulting bundle
as an APK asset, and extracts its entry module while preparing the private
on-device agent. The final execution order is:
1. FridaBox log capture;
2. enabled runtime-bridge entry modules;
3. the user's unchanged imported agent copy.
Only the generated runtime copy is composed. The originally imported agent and
its recorded digest are not modified.
Bridge settings appear only when the active Gadget is 17.0.0 or newer. They
apply to On-device mode on the next process launch. Computer mode uses Gadget's
Listen interaction; bridge availability there belongs to the attaching client
(`frida`, `frida-trace`, or the API-created agent), because remotely created
Scripts have separate isolates.
## Pinned versions
The APK contains these reproducible, offline choices:
| Bridge | Versions | Default | Source |
| --- | --- | --- | --- |
| Java | 7.0.11, 7.0.12, 7.0.13 | 7.0.13 | official `frida-java-bridge` npm package |
| IL2CPP | 0.12.2, 0.13.0, 0.13.1 | 0.13.1 | community `frida-il2cpp-bridge` npm package |
The npm registry contained 92 Java bridge versions from 3.1.1 through 7.0.13
and 77 published IL2CPP versions through 0.13.1 when this catalog was researched
on 2026-07-28. FridaBox deliberately ships a recent curated set instead of every
historical package to keep the APK small and the test surface bounded.
## Rebuilding
Run:
```powershell
npm ci
python tools/build_frida_agents.py
```
The package aliases in `package.json` allow multiple versions to coexist. The
generated bridge bundles are written to
`app/src/main/assets/fridabox/bridges/`. Gradle's
`verifyRuntimeBridgeAssets` task validates that every catalog entry exists and
has a Frida bundle header.
+35 -11
View File
@@ -1,6 +1,30 @@
# Test results
Validation date: 2026-07-19
Validation date: 2026-07-28
## FridaBox 4.2.0 release validation
The signed 4.2.0 release was built after the complete app check and app/Bcore
unit-test suites passed. Each split contains exactly one native ABI, reports
`versionCode 403` and `versionName 4.2.0`, is 16 KiB ZIP-aligned, and verifies
with APK Signature Schemes v1 and v2. All four APKs use the same production
certificate as 4.1.0 and 4.1.1 (SHA-256
`f8db4dcf0f9272bf75ac170e382ee31ca17fea2606a18aaed0cc4040423cb444`).
| ABI | Size | SHA-256 |
| --- | ---: | --- |
| `arm64-v8a` | 3,263,922 bytes | `e37af8801a3bb12c04db2140c09dfb843f8e105c04aa0c0f88f5731fa3e0cabb` |
| `armeabi-v7a` | 3,246,369 bytes | `427b2a4328624cda5881267c4bde00088c2d7df2296f49c5d66cbf4c929b64fc` |
| `x86` | 3,288,071 bytes | `8883788d694d42339332f495cbdb8b043198c52648ac168557f0d66dca0beab8` |
| `x86_64` | 3,293,258 bytes | `17695849ea9ed7884d2e1a777e9cfbf47529d84ade7a8c6e9746269e9d96b249` |
The updated ARM64 debug build was also installed on the Samsung SM-S928B
running Android 16/API 36. Runtime-bridge controls, version selection without a
scroll jump, the centered Gadget browser, Settings cleanup, raised Snackbar,
and icon-only animated bottom-navigation selection were exercised without a
fatal exception. The other three ABI variants were build-, package-, ELF-,
signature-, alignment-, and regression-test-validated but were not exercised
on physical hardware in this release cycle.
## Host validation
@@ -12,10 +36,10 @@ The following commands completed successfully on the build host:
.\gradlew.bat :app:assembleDebug
.\gradlew.bat test
.\gradlew.bat :app:check
.\gradlew.bat :app:verifyDebugApkFridaPackaging :app:verifyInstrumentationOrdering :app:verifyDemoGuestUnmodified
.\gradlew.bat :app:verifyDebugApkHasNoGadget :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
python -m py_compile tools\attach_guest.py tools\forward_frida_ports.py tools\build_frida_agents.py
```
For this workspace path, native builds used:
@@ -24,8 +48,12 @@ For this workspace path, native builds used:
$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:
Unit tests passed with zero failures, including Gadget asset naming, ten-item
release pagination, ELF ABI validation, and combined base/split APK inspection.
The original suite also covers:
The app `check` task also verifies that all committed UI control vectors are
byte-for-byte reproducible from pinned `@tabler/icons` 3.45.0 sources.
- `ApkInspectorTest`: 3 per variant;
- `ApkIntegrityTest`: 1 per variant;
@@ -35,13 +63,9 @@ This is eight test methods run for both debug and release variants:
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`;
- packaged native files do not include a Frida Gadget binary or
Gadget configuration;
- 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.
+37 -7
View File
@@ -5,12 +5,42 @@ Importing an APK does not install that package into Android's real PackageManage
## Import an application
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.
1. Open **Workspace** and tap the import icon.
2. Choose **Import from apps** to clone a launchable app already installed on
the device, or **Import from file** to select one base `.apk` through
Android's document picker.
3. FridaBox validates the full installed base/split set or selected file,
records its SHA-256 fingerprint, and creates a private guest.
Split-only packages (`.apks`, `.xapk`, and `.apkm`) and unsupported native ABIs
are rejected.
Split-only files (`.apks`, `.xapk`, and `.apkm`) and unsupported native ABIs are
rejected. Installed split applications are imported through their system package
so their active base and split APKs remain available to the virtual runtime.
## Install a Gadget
1. Open **Gadgets**. FridaBox detects the device ABI and only offers matching
release assets.
2. Tap **Browse versions**. The minimal release browser loads Official builds
from `frida/frida` ten at a time as the list is scrolled.
3. Download any listed version. Downloads are decompressed in private storage,
checked as ELF libraries, and rejected when their architecture does not match.
4. Select any installed version as active. Downloading another version does not
replace the active selection unless no Gadget had been selected yet.
Instrumented launches require an active Gadget. Clean mode remains available
without one. Gadget binaries are never bundled in the FridaBox APK.
## Select runtime bridges
When the active Gadget is 17.0.0 or newer, the **Gadgets** screen shows Java and
IL2CPP runtime-bridge controls. Enable either bridge and select one of its
offline pinned versions. The choice is composed into the generated private
agent on the next **On-device** launch, so a plain script may use the `Java` or
`Il2Cpp` global without importing that package itself.
The switch does not mutate the imported script and cannot change a process that
is already running. It does not apply to **Computer** mode, where each script is
created by the attaching Frida client in its own JavaScript isolate.
## Choose a launch mode
@@ -34,7 +64,7 @@ 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
Tap an app icon, 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.
@@ -46,7 +76,7 @@ interceptors, and in-app overlays continue to run in the guest process.
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
Use **Manage** in an app's launch panel 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
+5 -6
View File
@@ -1,12 +1,12 @@
[versions]
agp = "8.13.2"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.12.0"
constraintlayout = "2.2.0"
kotlin = "1.9.23"
constraintlayout = "2.2.1"
kotlin = "2.4.10"
coreKtx = "1.15.0"
[libraries]
@@ -22,4 +22,3 @@ core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx"
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
+70 -1
View File
@@ -6,12 +6,30 @@
"": {
"name": "fridabox-agents",
"dependencies": {
"frida-java-bridge": "7.0.13"
"frida-il2cpp-bridge-0-12-2": "npm:frida-il2cpp-bridge@0.12.2",
"frida-il2cpp-bridge-0-13-0": "npm:frida-il2cpp-bridge@0.13.0",
"frida-il2cpp-bridge-0-13-1": "npm:frida-il2cpp-bridge@0.13.1",
"frida-java-bridge": "7.0.13",
"frida-java-bridge-7-0-11": "npm:frida-java-bridge@7.0.11",
"frida-java-bridge-7-0-12": "npm:frida-java-bridge@7.0.12",
"frida-java-bridge-7-0-13": "npm:frida-java-bridge@7.0.13"
},
"devDependencies": {
"@tabler/icons": "3.45.0",
"frida-compile": "19.0.5"
}
},
"node_modules/@tabler/icons": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.45.0.tgz",
"integrity": "sha512-jiATwV8+zGYLTZ7gMLGivCic+KtsMZXcDmufIG8umlLxoHhI6902hGYIEt0Oa9Y9SXblNzUlrisHm5jOFMxOQA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -226,12 +244,63 @@
"frida-compile": "dist/cli.js"
}
},
"node_modules/frida-il2cpp-bridge-0-12-2": {
"name": "frida-il2cpp-bridge",
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/frida-il2cpp-bridge/-/frida-il2cpp-bridge-0.12.2.tgz",
"integrity": "sha512-j9W44it4aw/6f7LBkT2bNSsH1E9OMHdWEPqWbc2zKzQEhzSfA6Ge8b9pwiBvTdmu50+09L9hzgmGN6ZpQaGJkA==",
"license": "MIT",
"bin": {
"frida-il2cpp-bridge": "cli/main.py"
}
},
"node_modules/frida-il2cpp-bridge-0-13-0": {
"name": "frida-il2cpp-bridge",
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/frida-il2cpp-bridge/-/frida-il2cpp-bridge-0.13.0.tgz",
"integrity": "sha512-y337LVmjQPFER2vRYX2klTNkWCauuj2G1WVct4QESb7ME4HC9nSVtOEfYCf77mE3zt3y5YQ0zjXR6xpwudC92A==",
"license": "MIT",
"bin": {
"frida-il2cpp-bridge": "cli/main.py"
}
},
"node_modules/frida-il2cpp-bridge-0-13-1": {
"name": "frida-il2cpp-bridge",
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/frida-il2cpp-bridge/-/frida-il2cpp-bridge-0.13.1.tgz",
"integrity": "sha512-qXjdcdI+KM5wnjCwqZTe/Tas/2pClXpDhcVWjiaQFxzSun+1vaoHEWlnMe4F09Vdz/vGkkjFOPKfvDem2A8UOQ==",
"license": "MIT",
"bin": {
"frida-il2cpp-bridge": "cli/main.py"
}
},
"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/frida-java-bridge-7-0-11": {
"name": "frida-java-bridge",
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/frida-java-bridge/-/frida-java-bridge-7.0.11.tgz",
"integrity": "sha512-px3YKGVcyBx3r2ksqnguEb1p6xlm8jqp7LX3ybaaET4fvS/8e6Ojzs7RKjaiglW/CishI4waZIuGKoSEqbrdcA==",
"license": "LGPL-2.0 WITH WxWindows-exception-3.1"
},
"node_modules/frida-java-bridge-7-0-12": {
"name": "frida-java-bridge",
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/frida-java-bridge/-/frida-java-bridge-7.0.12.tgz",
"integrity": "sha512-xpoTFPQkUjNMeRnyZ6rUj/APlYGWpKEv5iekOk4lQeDcv9fv8J/s3ETjTRdSfyZ8/aPv9r8+oBmsxN2gCHRTqg==",
"license": "LGPL-2.0 WITH WxWindows-exception-3.1"
},
"node_modules/frida-java-bridge-7-0-13": {
"name": "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",
+13 -1
View File
@@ -1,10 +1,22 @@
{
"name": "fridabox-agents",
"private": true,
"scripts": {
"build:frida-agents": "python tools/build_frida_agents.py",
"generate:android-icons": "node tools/generate_tabler_android_icons.mjs",
"verify:android-icons": "node tools/generate_tabler_android_icons.mjs --check"
},
"dependencies": {
"frida-java-bridge": "7.0.13"
"frida-java-bridge": "7.0.13",
"frida-java-bridge-7-0-11": "npm:frida-java-bridge@7.0.11",
"frida-java-bridge-7-0-12": "npm:frida-java-bridge@7.0.12",
"frida-java-bridge-7-0-13": "npm:frida-java-bridge@7.0.13",
"frida-il2cpp-bridge-0-12-2": "npm:frida-il2cpp-bridge@0.12.2",
"frida-il2cpp-bridge-0-13-0": "npm:frida-il2cpp-bridge@0.13.0",
"frida-il2cpp-bridge-0-13-1": "npm:frida-il2cpp-bridge@0.13.1"
},
"devDependencies": {
"@tabler/icons": "3.45.0",
"frida-compile": "19.0.5"
}
}
+2 -2
View File
@@ -10,8 +10,8 @@ android {
applicationId "com.qm4rs.fridabox.sample"
minSdk rootProject.ext.minSdk
targetSdk rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.1"
}
compileOptions {
@@ -3,6 +3,7 @@ package com.qm4rs.fridabox.sample;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -16,7 +17,12 @@ public final class MainActivity extends Activity {
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)));
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
output.setText("Result: " + Target.add(2, 3));
}
});
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setGravity(Gravity.CENTER);
+3
View File
@@ -0,0 +1,3 @@
'use strict';
import 'frida-il2cpp-bridge-0-12-2';
+3
View File
@@ -0,0 +1,3 @@
'use strict';
import 'frida-il2cpp-bridge-0-13-0';
+3
View File
@@ -0,0 +1,3 @@
'use strict';
import 'frida-il2cpp-bridge-0-13-1';
+5
View File
@@ -0,0 +1,5 @@
'use strict';
import JavaBridge from 'frida-java-bridge-7-0-11';
if (typeof globalThis.Java === 'undefined') globalThis.Java = JavaBridge;
+5
View File
@@ -0,0 +1,5 @@
'use strict';
import JavaBridge from 'frida-java-bridge-7-0-12';
if (typeof globalThis.Java === 'undefined') globalThis.Java = JavaBridge;

Some files were not shown because too many files have changed in this diff Show More