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
59 changed files with 7276 additions and 269 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
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'
+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() {
@@ -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());
}
@@ -1,7 +1,6 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import android.os.Build;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -11,6 +10,7 @@ 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 {
@@ -48,8 +48,8 @@ final class DownloadedGadgetRuntime {
if (!isInside(root, selected) || !selected.isFile()) {
throw new IOException("The selected Frida Gadget is missing or outside private storage");
}
if (!supportsAbi(selectedAbi)) {
throw new IOException("The selected Frida Gadget ABI is not supported by this device");
if (!supportsAbi(context, selectedAbi)) {
throw new IOException("The selected Frida Gadget ABI does not match this process");
}
validateElf(selected, selectedAbi);
return selected;
@@ -108,12 +108,8 @@ final class DownloadedGadgetRuntime {
return -1;
}
private static boolean supportsAbi(String abi) {
if (abi == null) return false;
for (String supported : Build.SUPPORTED_ABIS) {
if (abi.equals(supported)) return true;
}
return false;
private static boolean supportsAbi(Context context, String abi) {
return abi != null && abi.equals(ProcessAbi.detect(context));
}
private static boolean isInside(File root, File child) {
@@ -17,6 +17,10 @@ public final class InstrumentationSettings {
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_";
@@ -150,6 +154,41 @@ public final class InstrumentationSettings {
.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;
}
@@ -8,6 +8,8 @@ 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;
@@ -41,7 +43,10 @@ final class LocalScriptGadgetRuntime {
File log = new File(directory, LOG_NAME);
writeUtf8Atomically(log, "");
File instrumentedAgent = new File(directory, INSTRUMENTED_AGENT_NAME);
writeInstrumentedAgentAtomically(instrumentedAgent, script, buildLogBridge(log));
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");
}
@@ -154,7 +159,7 @@ final class LocalScriptGadgetRuntime {
}
private static void writeInstrumentedAgentAtomically(
File destination, File source, String bridge) throws IOException {
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");
@@ -164,7 +169,7 @@ final class LocalScriptGadgetRuntime {
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);
byte[] instrumented = instrumentAgent(sourceBytes.toByteArray(), bridge, runtimeBridges);
try (FileOutputStream output = new FileOutputStream(temporary)) {
output.write(instrumented);
output.getFD().sync();
@@ -174,9 +179,22 @@ final class LocalScriptGadgetRuntime {
}
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");
byte[] prefix = (bridge + "(function (send) {\n").getBytes(StandardCharsets.UTF_8);
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);
@@ -186,11 +204,28 @@ final class LocalScriptGadgetRuntime {
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 = 0;
while (lengthStart < bodyOffset && source[lengthStart] != '\n') lengthStart++;
lengthStart++;
int lengthStart = moduleLengthStart(source, bodyOffset);
int lengthEnd = lengthStart;
long declaredLength = 0;
while (lengthEnd < bodyOffset && source[lengthEnd] >= '0' && source[lengthEnd] <= '9') {
@@ -220,6 +255,13 @@ final class LocalScriptGadgetRuntime {
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;
@@ -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);
}
}
@@ -7,6 +7,7 @@ import org.junit.Test;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
public class LocalScriptGadgetRuntimeTest {
@Test
@@ -54,10 +55,45 @@ public class LocalScriptGadgetRuntimeTest {
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
+8 -3
View File
@@ -69,15 +69,20 @@ 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 bridge and agent compiler
## 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
+33 -8
View File
@@ -32,7 +32,7 @@ android {
enable true
reset()
include "arm64-v8a"
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
universalApk false
}
}
@@ -106,12 +106,17 @@ tasks.matching { it.name.toLowerCase().contains("debug") && it.name.toLowerCase(
tasks.register("verifyDebugApkHasNoGadget") {
dependsOn("assembleDebug")
doLast {
def apk = fileTree("$buildDir/outputs/apk/debug").matching { include("*.apk") }.singleFile
def entries = zipTree(apk).files.findAll {
it.name.toLowerCase().contains("gadget") && it.name.toLowerCase().endsWith(".so")
def apks = fileTree("$buildDir/outputs/apk/debug").matching { include("*.apk") }.files
if (apks.isEmpty()) {
throw new GradleException("No debug APKs were produced")
}
if (!entries.isEmpty()) {
throw new GradleException("APK must not bundle Frida Gadget: ${entries*.name}")
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}")
}
}
}
}
@@ -127,6 +132,26 @@ 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"
@@ -152,7 +177,7 @@ tasks.register("verifyDemoGuestUnmodified") {
tasks.named("check").configure {
dependsOn("verifyDebugApkHasNoGadget", "verifyInstrumentationOrdering",
"verifyDemoGuestUnmodified", "verifyTablerAndroidIcons")
"verifyDemoGuestUnmodified", "verifyTablerAndroidIcons", "verifyRuntimeBridgeAssets")
}
dependencies {
@@ -162,7 +187,7 @@ dependencies {
implementation libs.material
implementation libs.constraintlayout
implementation libs.core.ktx
implementation "org.tukaani:xz:1.10"
implementation "org.tukaani:xz:1.12"
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
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
@@ -14,14 +14,18 @@ public final class ApkInspector {
}
public static Result inspect(File apk) throws IOException {
return inspect(Collections.singletonList(apk));
return inspect(Collections.singletonList(apk), null);
}
public static Result inspect(File... apks) throws IOException {
return inspect(Arrays.asList(apks));
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;
boolean inspectedAny = false;
@@ -44,24 +48,42 @@ public final class ApkInspector {
}
}
if (!inspectedAny) throw new IOException("No APK files were provided");
boolean supported = !hasNativeLibraries || abis.contains("arm64-v8a");
return new Result(hasNativeLibraries, supported, abis);
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"
)));
}
@@ -6,8 +6,6 @@ import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageInfo
@@ -23,11 +21,15 @@ import android.os.Bundle
import android.provider.OpenableColumns
import android.text.InputType
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.OvershootInterpolator
import android.widget.FrameLayout
import android.widget.GridLayout
@@ -36,7 +38,6 @@ import android.widget.LinearLayout
import android.widget.PopupWindow
import android.widget.Space
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
@@ -54,6 +55,8 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
import top.niunaijun.blackbox.instrumentation.RuntimeBridgeCatalog
import top.niunaijun.blackbox.utils.ProcessAbi
import com.qm4rs.fridabox.databinding.ActivityFridaboxBinding
import java.io.File
import java.io.FileOutputStream
@@ -63,7 +66,7 @@ import java.util.concurrent.Executors
/** Product workspace for importing, configuring, and launching FridaBox guests. */
class FridaBoxActivity : AppCompatActivity() {
private enum class Screen { WORKSPACE, GADGETS, RUNTIME, SETTINGS }
private enum class Screen { WORKSPACE, GADGETS, SETTINGS }
private data class ImportBubbleViews(
val scrim: View,
@@ -106,6 +109,10 @@ class FridaBoxActivity : AppCompatActivity() {
binding = ActivityFridaboxBinding.inflate(layoutInflater)
setContentView(binding.root)
pendingScriptPackage = savedInstanceState?.getString("pending_script_package")
settings.edit()
.remove(InstrumentationSettings.KEY_ENABLED)
.remove(InstrumentationSettings.KEY_ADVANCED_LOGS)
.apply()
binding.importFabIcon.setOnClickListener { showImportActions() }
binding.bottomNavigation.setOnItemSelectedListener { item ->
@@ -113,12 +120,16 @@ class FridaBoxActivity : AppCompatActivity() {
when (item.itemId) {
R.id.nav_workspace -> showWorkspace()
R.id.nav_gadgets -> showGadgets()
R.id.nav_runtime -> showRuntime()
R.id.nav_settings -> showSettings()
else -> return@setOnItemSelectedListener false
}
animateNavigationSelection(item.itemId)
animateContentIn()
true
}
binding.bottomNavigation.setOnItemReselectedListener { item ->
animateNavigationSelection(item.itemId)
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (importMenuOverlay != null) {
@@ -703,7 +714,6 @@ class FridaBoxActivity : AppCompatActivity() {
setLoading(false)
result.onSuccess { launched ->
if (!launched) toast("This guest has no launchable activity")
else if (mode == InstrumentationSettings.MODE_COMPUTER) showRuntime(packageName)
}.onFailure { toast("Launch failed: ${it.message}") }
}
}
@@ -802,10 +812,6 @@ class FridaBoxActivity : AppCompatActivity() {
popup.dismiss()
showAppDetails(info, appLabel)
})
panel.addView(menuActionButton(getString(R.string.fb_runtime_details), R.drawable.ic_fb_runtime) {
popup.dismiss()
showRuntime(info.packageName)
})
panel.addView(menuActionButton(getString(R.string.fb_clear_data), R.drawable.ic_fb_clear) {
popup.dismiss()
clearApp(info.packageName)
@@ -1350,7 +1356,10 @@ class FridaBoxActivity : AppCompatActivity() {
require(apkFiles.isNotEmpty() && apkFiles.all { it.isFile }) {
"Installed APK files are unavailable"
}
val abi = ApkInspector.inspect(apkFiles)
val processAbi = requireNotNull(ProcessAbi.detect(this)) {
"Unable to determine the FridaBox process ABI"
}
val abi = ApkInspector.inspect(apkFiles, processAbi)
if (!abi.supported) error("Unsupported native ABI: ${abi.description()}")
val originalHash = installedPackageSha256(apkFiles)
val install = BlackBoxCore.get().installPackageAsUser(packageName, 0)
@@ -1411,7 +1420,10 @@ class FridaBoxActivity : AppCompatActivity() {
val archiveInfo = packageManager.getPackageArchiveInfo(temporary.absolutePath, PackageManager.GET_META_DATA)
?: error("Android could not parse this APK")
if (!archiveInfo.splitNames.isNullOrEmpty()) error("Split-only APKs are not supported")
val abi = ApkInspector.inspect(temporary)
val processAbi = requireNotNull(ProcessAbi.detect(this)) {
"Unable to determine the FridaBox process ABI"
}
val abi = ApkInspector.inspect(listOf(temporary), processAbi)
if (!abi.supported) error("Unsupported native ABI: ${abi.description()}")
val safePackage = safePackageName(archiveInfo.packageName)
val stored = File(directory, "$safePackage-${originalHash.take(12)}.apk")
@@ -1444,9 +1456,10 @@ class FridaBoxActivity : AppCompatActivity() {
}
}
private fun showGadgets() {
private fun showGadgets(preserveScroll: Boolean = false) {
val previousScrollY = binding.contentScroll.scrollY
screen = Screen.GADGETS
resetScreen(R.id.nav_gadgets, showImport = false)
resetScreen(R.id.nav_gadgets, showImport = false, resetScroll = !preserveScroll)
binding.toolbar.title = getString(R.string.fb_gadgets_title)
val abi = gadgetManager.detectedAbi()
@@ -1490,6 +1503,10 @@ class FridaBoxActivity : AppCompatActivity() {
}
})
if (selected != null && RuntimeBridgeCatalog.supportsRuntimeBridges(selected.version)) {
binding.content.addView(runtimeBridgeControls(selected.version))
}
val installed = gadgetManager.installed()
if (installed.isNotEmpty()) {
binding.content.addView(labelText(
@@ -1499,6 +1516,106 @@ class FridaBoxActivity : AppCompatActivity() {
binding.content.addView(installedGadgetRow(gadget, selected?.identity == gadget.identity))
}
}
if (preserveScroll) restoreScrollBeforeNextDraw(previousScrollY)
}
private fun runtimeBridgeControls(gadgetVersion: String): View {
return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
addView(labelText(
getString(R.string.fb_runtime_bridges), R.color.fb_text_secondary, 12f, false
).apply { setPadding(0, 0, 0, dp(6)) })
addView(labelText(
getString(R.string.fb_runtime_bridges_body, gadgetVersion),
R.color.fb_text_secondary,
12.5f,
false
).apply { setPadding(0, 0, 0, dp(10)) })
RuntimeBridgeCatalog.specs().forEach { spec ->
addView(runtimeBridgeRow(spec))
}
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = dp(20) }
}
}
private fun runtimeBridgeRow(spec: RuntimeBridgeCatalog.BridgeSpec): View {
val version = RuntimeBridgeCatalog.selectedVersion(spec)
val enabled = InstrumentationSettings.isRuntimeBridgeEnabled(spec.id)
return surfaceCard().apply {
addView(LinearLayout(this@FridaBoxActivity).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(16), dp(14), dp(16), dp(14))
addView(LinearLayout(this@FridaBoxActivity).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
addView(verticalText(spec.title, spec.description), LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f
))
addView(SwitchMaterial(this@FridaBoxActivity).apply {
isChecked = enabled
contentDescription = getString(R.string.fb_runtime_bridge_toggle, spec.title)
var rollingBack = false
setOnCheckedChangeListener { button, checked ->
if (rollingBack) return@setOnCheckedChangeListener
if (!InstrumentationSettings.setRuntimeBridge(spec.id, checked, version)) {
rollingBack = true
button.isChecked = !checked
rollingBack = false
notify(getString(R.string.fb_runtime_bridge_save_failed))
} else {
notify(getString(
if (checked) R.string.fb_runtime_bridge_enabled else R.string.fb_runtime_bridge_disabled,
spec.title
))
}
}
}, LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply { marginStart = dp(8) })
})
addView(outlineButton(getString(R.string.fb_runtime_bridge_version, version)) { button ->
showRuntimeBridgeVersions(
spec,
RuntimeBridgeCatalog.selectedVersion(spec),
button as MaterialButton
)
}.apply {
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(42)
).apply { topMargin = dp(10) }
})
})
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = dp(10) }
}
}
private fun showRuntimeBridgeVersions(
spec: RuntimeBridgeCatalog.BridgeSpec,
current: String,
versionButton: MaterialButton
) {
val versions = spec.versions().toTypedArray()
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.fb_runtime_bridge_choose_version, spec.title))
.setSingleChoiceItems(versions, versions.indexOf(current)) { dialog, which ->
val enabled = InstrumentationSettings.isRuntimeBridgeEnabled(spec.id)
if (InstrumentationSettings.setRuntimeBridge(spec.id, enabled, versions[which])) {
dialog.dismiss()
versionButton.text = getString(R.string.fb_runtime_bridge_version, versions[which])
} else {
notify(getString(R.string.fb_runtime_bridge_save_failed))
}
}
.setNegativeButton(getString(R.string.fb_cancel), null)
.show()
}
private fun installedGadgetRow(gadget: InstalledGadget, active: Boolean): View {
@@ -1511,7 +1628,7 @@ class FridaBoxActivity : AppCompatActivity() {
runCatching { gadgetManager.select(gadget) }
.onSuccess {
notify(getString(R.string.fb_gadget_selected, gadget.version))
showGadgets()
showGadgets(preserveScroll = true)
}
.onFailure { notify(it.message ?: getString(R.string.fb_gadget_select_failed)) }
}
@@ -1541,7 +1658,7 @@ class FridaBoxActivity : AppCompatActivity() {
installed != null -> runCatching { gadgetManager.select(installed) }
.onSuccess {
dialog.dismiss()
showGadgets()
showGadgets(preserveScroll = true)
}
.onFailure { notify(it.message ?: getString(R.string.fb_gadget_select_failed)) }
else -> {
@@ -1579,7 +1696,7 @@ class FridaBoxActivity : AppCompatActivity() {
private fun showGadgetBrowser(abi: GadgetAbi) {
val dialog = Dialog(this)
val root = FrameLayout(this).apply {
setPadding(dp(12), dp(32), dp(12), dp(12))
setPadding(dp(12), dp(20), dp(12), dp(20))
setOnClickListener { dialog.dismiss() }
}
val panel = surfaceCard().apply {
@@ -1630,8 +1747,8 @@ class FridaBoxActivity : AppCompatActivity() {
root.addView(panel, FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
(resources.displayMetrics.heightPixels * 0.82f).toInt(),
Gravity.BOTTOM
ViewGroup.LayoutParams.MATCH_PARENT,
Gravity.CENTER
))
dialog.setContentView(root)
dialog.setCanceledOnTouchOutside(true)
@@ -1646,6 +1763,20 @@ class FridaBoxActivity : AppCompatActivity() {
}
dialog.show()
dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
panel.apply {
alpha = 0f
scaleX = 0.96f
scaleY = 0.96f
translationY = dp(10).toFloat()
animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.translationY(0f)
.setDuration(280L)
.setInterpolator(DecelerateInterpolator(1.6f))
.start()
}
val request = ++catalogRequest
val identities = HashSet<String>()
@@ -1715,88 +1846,12 @@ class FridaBoxActivity : AppCompatActivity() {
if (hadSelection) R.string.fb_gadget_downloaded else R.string.fb_gadget_downloaded_selected,
release.version
))
showGadgets()
showGadgets(preserveScroll = true)
}.onFailure { notify(getString(R.string.fb_gadget_download_failed, it.message ?: "Unknown error")) }
}
}
}
private fun showRuntime(packageHint: String? = null) {
screen = Screen.RUNTIME
resetScreen(R.id.nav_runtime, showImport = false)
binding.toolbar.title = getString(R.string.fb_runtime_title)
binding.toolbar.subtitle = getString(R.string.fb_runtime_subtitle)
val packageName = packageHint ?: settings.getString("runtime_package", null)
val state = settings.getString("runtime_state", "idle") ?: "idle"
val mode = settings.getString("runtime_mode", packageName?.let {
InstrumentationSettings.getModeForPackage(it)
} ?: InstrumentationSettings.MODE_CLEAN) ?: InstrumentationSettings.MODE_CLEAN
val stateColor = when (state) {
"local_script_active", "computer_attached" -> R.color.fb_success
"failed" -> R.color.fb_error
"waiting_for_attach", "loading_local_script" -> R.color.fb_warning
else -> R.color.fb_text_secondary
}
binding.content.addView(pageHeading(
getString(R.string.fb_runtime_title),
getString(R.string.fb_runtime_subtitle)
))
binding.content.addView(messageCard(
runtimeStateLabel(state),
packageName ?: "No guest process has reported runtime state yet.",
stateColor
))
if (mode == InstrumentationSettings.MODE_COMPUTER) {
val command = "frida -U gadget -l path/to/agent.js"
binding.content.addView(infoCard("Computer attach", buildString {
append("The guest waits at Gadget until a controller attaches.\n\n")
append(command)
}, command))
} else if (mode == InstrumentationSettings.MODE_LOCAL_SCRIPT) {
val scriptName = packageName?.let { metadata.getString("$it.scriptName", null) }
val scriptHash = packageName?.let { metadata.getString("$it.scriptSha", null) }
binding.content.addView(infoCard(
"On-device agent",
buildString {
append(scriptName ?: "No agent selected")
if (scriptHash != null) append("\nSHA-256 ").append(scriptHash)
append("\n\nThe private agent is loaded autonomously by Frida Gadget.")
}
))
if (packageName != null) {
binding.content.addView(outlineButton("Open agent logs") {
if (!FridaBoxGuestLogOverlay.show(this, packageName)) {
toast("No agent log is available for $packageName")
}
}.apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(50)).apply {
topMargin = dp(14)
}
})
}
} else {
binding.content.addView(infoCard("Clean launch", getString(R.string.fb_mode_clean_body)))
}
val details = listOf(
"Guest package" to (packageName ?: "Not reported"),
"Guest process" to (settings.getString("runtime_process", "—") ?: "—"),
"Virtual process slot" to settings.getInt("runtime_vpid", -1).toString(),
"Guest source" to (settings.getString("runtime_source", "—") ?: "—"),
"Virtual user ID" to settings.getInt("runtime_user_id", -1).toString(),
"ClassLoader" to (settings.getString("runtime_class_loader", "Not reported") ?: "Not reported"),
"Mode" to modeLongLabel(mode),
"Latest error" to (settings.getString("runtime_error", "None") ?: "None")
)
binding.content.addView(detailsCard("Process registry", details))
binding.content.addView(outlineButton(getString(R.string.fb_refresh)) { showRuntime(packageHint) }.apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(50)).apply { topMargin = dp(14) }
})
}
private fun showSettings() {
screen = Screen.SETTINGS
resetScreen(R.id.nav_settings, showImport = false)
@@ -1813,35 +1868,25 @@ class FridaBoxActivity : AppCompatActivity() {
setPadding(dp(18), dp(18), dp(18), dp(18))
}
controls.addView(body)
val enabled = SwitchMaterial(this).apply {
text = getString(R.string.fb_global_instrumentation)
setTextColor(color(R.color.fb_text_primary))
isChecked = settings.getBoolean(InstrumentationSettings.KEY_ENABLED, true)
}
val logs = SwitchMaterial(this).apply {
text = getString(R.string.fb_advanced_logs)
setTextColor(color(R.color.fb_text_primary))
isChecked = settings.getBoolean(InstrumentationSettings.KEY_ADVANCED_LOGS, false)
}
val port = numberInput(getString(R.string.fb_base_port), settings.getInt(InstrumentationSettings.KEY_BASE_PORT, 27042))
val count = numberInput(getString(R.string.fb_scan_count), settings.getInt(InstrumentationSettings.KEY_SCAN_COUNT, 32))
body.addView(enabled)
body.addView(logs)
body.addView(port.first)
body.addView(count.first)
body.addView(primaryButton(getString(R.string.fb_save_settings)) {
settings.edit()
.putBoolean(InstrumentationSettings.KEY_ENABLED, enabled.isChecked)
.putInt(InstrumentationSettings.KEY_BASE_PORT,
InstrumentationPreferenceParser.parsePort(port.second.text?.toString().orEmpty(), 27042))
.putInt(InstrumentationSettings.KEY_SCAN_COUNT,
InstrumentationPreferenceParser.parseScanCount(count.second.text?.toString().orEmpty(), 32))
.putBoolean(InstrumentationSettings.KEY_ADVANCED_LOGS, logs.isChecked)
.apply()
notify("Settings saved")
}.apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(50)).apply { topMargin = dp(12) }
})
controls.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = dp(14) }
binding.content.addView(controls)
binding.content.addView(messageCard(
getString(R.string.fb_security_title),
@@ -1895,50 +1940,6 @@ class FridaBoxActivity : AppCompatActivity() {
}
}
private fun infoCard(title: String, bodyText: String, copyValue: String? = null): View {
val card = surfaceCard()
val body = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(18), dp(18), dp(18), dp(18))
addView(labelText(title, R.color.fb_text_primary, 16f, true))
addView(labelText(bodyText, R.color.fb_text_secondary, 13f, false).apply {
setPadding(0, dp(8), 0, 0)
setTextIsSelectable(true)
})
if (copyValue != null) addView(outlineButton(getString(R.string.fb_copy_command)) {
(getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
.setPrimaryClip(ClipData.newPlainText("FridaBox command", copyValue))
notify("Command copied")
}.apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(46)).apply { topMargin = dp(12) }
})
}
card.addView(body)
card.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = dp(14) }
return card
}
private fun detailsCard(title: String, rows: List<Pair<String, String>>): View {
val card = surfaceCard()
val body = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(18), dp(18), dp(18), dp(18))
addView(labelText(title, R.color.fb_text_primary, 16f, true))
}
rows.forEach { (label, value) ->
body.addView(labelText(label.uppercase(Locale.ROOT), R.color.fb_text_secondary, 10.5f, true).apply {
setPadding(0, dp(13), 0, 0)
})
body.addView(labelText(value, R.color.fb_text_primary, 13f, false).apply { setTextIsSelectable(true) })
}
card.addView(body)
card.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
return card
}
private fun verticalText(title: String, subtitle: String): LinearLayout {
return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
@@ -1990,7 +1991,7 @@ class FridaBoxActivity : AppCompatActivity() {
strokeColor = checkedColors(color(R.color.fb_primary), color(R.color.fb_glass_outline))
backgroundTintList = checkedColors(color(R.color.fb_primary), color(R.color.fb_glass_surface_soft))
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
}
}.also(::installPressMotion)
}
private fun primaryButton(label: String, action: (View) -> Unit): MaterialButton {
@@ -2007,7 +2008,7 @@ class FridaBoxActivity : AppCompatActivity() {
strokeWidth = dp(1)
elevation = dp(3).toFloat()
setOnClickListener(action)
}
}.also(::installPressMotion)
}
private fun outlineButton(label: String, action: (View) -> Unit): MaterialButton {
@@ -2022,7 +2023,7 @@ class FridaBoxActivity : AppCompatActivity() {
strokeColor = ColorStateList.valueOf(color(R.color.fb_glass_outline))
backgroundTintList = ColorStateList.valueOf(color(R.color.fb_glass_surface_soft))
setOnClickListener(action)
}
}.also(::installPressMotion)
}
private fun iconButton(iconResource: Int, description: String, action: () -> Unit): MaterialButton {
@@ -2037,15 +2038,120 @@ class FridaBoxActivity : AppCompatActivity() {
}
}
private fun resetScreen(navId: Int, showImport: Boolean): Int {
private fun installPressMotion(view: View) {
view.isHapticFeedbackEnabled = true
view.setOnTouchListener { target, event ->
if (!target.isEnabled) return@setOnTouchListener false
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
target.animate().cancel()
target.performHapticFeedback(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
HapticFeedbackConstants.CONTEXT_CLICK
} else {
HapticFeedbackConstants.VIRTUAL_KEY
}
)
target.animate()
.scaleX(0.965f)
.scaleY(0.965f)
.alpha(0.9f)
.translationY(dp(1).toFloat())
.setDuration(85L)
.setInterpolator(AccelerateInterpolator(1.4f))
.start()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
target.animate().cancel()
target.animate()
.scaleX(1f)
.scaleY(1f)
.alpha(1f)
.translationY(0f)
.setDuration(240L)
.setInterpolator(OvershootInterpolator(1.45f))
.start()
}
}
false
}
}
private fun animateNavigationSelection(itemId: Int) {
updateNavigationIconEmphasis(itemId)
val item = binding.bottomNavigation.findViewById<View>(itemId) ?: return
val target = firstImageDescendant(item) ?: item
target.animate().cancel()
target.scaleX = 0.9f
target.scaleY = 0.9f
target.translationY = dp(2).toFloat()
target.animate()
.scaleX(1.18f)
.scaleY(1.18f)
.translationY(0f)
.setDuration(360L)
.setInterpolator(OvershootInterpolator(1.8f))
.start()
}
private fun updateNavigationIconEmphasis(selectedItemId: Int) {
intArrayOf(R.id.nav_workspace, R.id.nav_gadgets, R.id.nav_settings).forEach { itemId ->
val item = binding.bottomNavigation.findViewById<View>(itemId) ?: return@forEach
val icon = firstImageDescendant(item) ?: return@forEach
icon.animate().cancel()
val scale = if (itemId == selectedItemId) 1.18f else 1f
icon.scaleX = scale
icon.scaleY = scale
icon.translationY = 0f
}
}
private fun firstImageDescendant(view: View): ImageView? {
if (view is ImageView) return view
if (view !is ViewGroup) return null
for (index in 0 until view.childCount) {
firstImageDescendant(view.getChildAt(index))?.let { return it }
}
return null
}
private fun animateContentIn() {
binding.content.animate().cancel()
binding.content.alpha = 0.62f
binding.content.translationY = dp(8).toFloat()
binding.content.animate()
.alpha(1f)
.translationY(0f)
.setDuration(260L)
.setInterpolator(DecelerateInterpolator(1.5f))
.start()
}
private fun restoreScrollBeforeNextDraw(scrollY: Int) {
if (scrollY <= 0) return
val scroll = binding.contentScroll
scroll.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (scroll.viewTreeObserver.isAlive) {
scroll.viewTreeObserver.removeOnPreDrawListener(this)
}
val maximum = (scroll.getChildAt(0)?.height ?: 0) - scroll.height
scroll.scrollTo(0, scrollY.coerceIn(0, maximum.coerceAtLeast(0)))
return true
}
})
}
private fun resetScreen(navId: Int, showImport: Boolean, resetScroll: Boolean = true): Int {
dismissImportActions(immediate = true)
screenGeneration += 1
binding.content.removeAllViews()
binding.contentScroll.scrollTo(0, 0)
if (resetScroll) binding.contentScroll.scrollTo(0, 0)
binding.importFabIcon.isVisible = showImport
changingNavigation = true
binding.bottomNavigation.selectedItemId = navId
changingNavigation = false
updateNavigationIconEmphasis(navId)
setLoading(false)
return screenGeneration
}
@@ -2061,28 +2167,12 @@ class FridaBoxActivity : AppCompatActivity() {
)
}
private fun runtimeStateLabel(state: String): String = when (state) {
"local_script_active" -> "On-device agent active"
"computer_attached" -> "Computer attached"
"waiting_for_attach" -> "Waiting for computer"
"loading_local_script" -> "Loading on-device agent"
"failed" -> "Instrumentation failed"
"disabled" -> "Clean runtime"
else -> "Runtime idle"
}
private fun modeShortLabel(mode: String): String = when (mode) {
InstrumentationSettings.MODE_LOCAL_SCRIPT -> getString(R.string.fb_mode_local)
InstrumentationSettings.MODE_CLEAN -> getString(R.string.fb_mode_clean)
else -> getString(R.string.fb_mode_computer)
}
private fun modeLongLabel(mode: String): String = when (mode) {
InstrumentationSettings.MODE_LOCAL_SCRIPT -> getString(R.string.fb_mode_local_title)
InstrumentationSettings.MODE_CLEAN -> getString(R.string.fb_mode_clean_title)
else -> getString(R.string.fb_mode_computer_title)
}
private fun modeColor(mode: String, background: Boolean): Int {
val resource = when (mode) {
InstrumentationSettings.MODE_LOCAL_SCRIPT -> if (background) R.color.fb_glass_surface_soft else R.color.fb_success
@@ -2125,13 +2215,14 @@ class FridaBoxActivity : AppCompatActivity() {
}
private fun notify(message: String) {
Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).apply {
setAnchorView(binding.bottomNavigation)
setTextColor(color(R.color.fb_text_primary))
view.background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_glass_popup)
view.elevation = dp(12).toFloat()
show()
}
}
private fun toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_LONG).show()
private fun toast(message: String) = notify(message)
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
private fun humanSize(bytes: Long): String {
@@ -1,11 +1,11 @@
package com.qm4rs.fridabox
import android.content.Context
import android.os.Build
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
@@ -42,8 +42,10 @@ data class GadgetAbi(val androidName: String, val releaseName: String) {
GadgetAbi("x86", "x86")
)
fun detect(): GadgetAbi? = Build.SUPPORTED_ABIS
.firstNotNullOfOrNull { deviceAbi -> supported.firstOrNull { it.androidName == deviceAbi } }
fun detect(context: Context): GadgetAbi? {
val processAbi = ProcessAbi.detect(context) ?: return null
return supported.firstOrNull { it.androidName == processAbi }
}
}
}
@@ -85,7 +87,7 @@ class GadgetManager(private val context: Context) {
}
}
fun detectedAbi(): GadgetAbi? = GadgetAbi.detect()
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")
@@ -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,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>
@@ -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>
@@ -145,9 +145,11 @@
android:layout_marginBottom="8dp"
android:background="@drawable/bg_glass_navigation"
android:elevation="14dp"
app:itemActiveIndicatorStyle="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
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,7 +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_gadgets" android:icon="@drawable/ic_fb_gadget" android:title="@string/fb_nav_gadgets" />
<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>
@@ -10,6 +10,7 @@
<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>
+8 -8
View File
@@ -4,7 +4,6 @@
<string name="fb_brand_tagline">Mobile instrumentation workspace</string>
<string name="fb_nav_workspace">Workspace</string>
<string name="fb_nav_gadgets">Gadgets</string>
<string name="fb_nav_runtime">Runtime</string>
<string name="fb_nav_settings">Settings</string>
<string name="fb_import">Import</string>
<string name="fb_import_from_apps">Import from apps</string>
@@ -29,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>
@@ -51,7 +44,6 @@
<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>
@@ -78,4 +70,12 @@
<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>
@@ -26,4 +26,23 @@
<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>
@@ -18,34 +18,60 @@ 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);
assertFalse(result.supported);
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(base, arm64Split);
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);
}
private File apkWith(String entry) throws Exception {
File file = temporary.newFile("test-" + System.nanoTime() + ".apk");
try (ZipOutputStream output = new ZipOutputStream(new FileOutputStream(file))) {
+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'
}
+8 -3
View File
@@ -27,6 +27,10 @@ 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.
@@ -51,7 +55,8 @@ 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.
@@ -82,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.
+25 -1
View File
@@ -1,6 +1,30 @@
# Test results
Validation date: 2026-07-27
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
View File
@@ -30,6 +30,18 @@ so their active base and split APKs remain available to the virtual runtime.
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
Every guest remembers one of three independent modes:
+2 -2
View File
@@ -3,9 +3,9 @@ agp = "8.13.2"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.0"
appcompat = "1.7.1"
material = "1.12.0"
constraintlayout = "2.2.0"
constraintlayout = "2.2.1"
kotlin = "2.4.10"
coreKtx = "1.15.0"
+58 -1
View File
@@ -6,7 +6,13 @@
"": {
"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",
@@ -238,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",
+8 -1
View File
@@ -2,11 +2,18 @@
"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",
+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;
+5
View File
@@ -0,0 +1,5 @@
'use strict';
import JavaBridge from 'frida-java-bridge-7-0-13';
if (typeof globalThis.Java === 'undefined') globalThis.Java = JavaBridge;
+22 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Build Frida 17 Java agents with the explicitly pinned Java bridge."""
"""Build Frida agents and the version-pinned offline runtime bridges."""
from __future__ import annotations
@@ -12,6 +12,16 @@ ROOT = pathlib.Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
OUTPUT = SCRIPTS / "dist"
ENTRIES = ("registry-probe.js", "guest-bootstrap.js", "sample-hook.js")
BRIDGE_SCRIPTS = SCRIPTS / "bridges"
BRIDGE_OUTPUT = ROOT / "app" / "src" / "main" / "assets" / "fridabox" / "bridges"
BRIDGES = (
("java", "7.0.11"),
("java", "7.0.12"),
("java", "7.0.13"),
("il2cpp", "0.12.2"),
("il2cpp", "0.13.0"),
("il2cpp", "0.13.1"),
)
def compiler_path() -> pathlib.Path:
@@ -31,7 +41,18 @@ def main() -> int:
cwd=ROOT,
check=True,
)
BRIDGE_OUTPUT.mkdir(parents=True, exist_ok=True)
for bridge_id, version in BRIDGES:
name = f"{bridge_id}-{version}.js"
subprocess.run(
[str(compiler), str(BRIDGE_SCRIPTS / name), "-o", str(BRIDGE_OUTPUT / name), "-S", "-c"],
cwd=ROOT,
check=True,
)
print("Built Frida agents: " + ", ".join(str(OUTPUT / name) for name in ENTRIES))
print("Built runtime bridges: " + ", ".join(
str(BRIDGE_OUTPUT / f"{bridge_id}-{version}.js") for bridge_id, version in BRIDGES
))
return 0