Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c89c6c4f81 | ||
|
|
4bfc6b5a42 | ||
|
|
3b771faa21 | ||
|
|
bb608b4663 | ||
|
|
ca9b32026d | ||
|
|
4d0789ecd2 | ||
|
|
1d9ff51165 | ||
|
|
44cf537d89 | ||
|
|
e4b4f6e350 | ||
|
|
00cc4ebde3 | ||
|
|
ab88654013 |
@@ -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:
|
||||
- "*"
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
@@ -31,22 +31,23 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -27,3 +27,5 @@ __pycache__/
|
||||
*.pyc
|
||||
node_modules/
|
||||
.npm-cache/
|
||||
/release-secrets/
|
||||
/.release-work/
|
||||
|
||||
@@ -26,7 +26,7 @@ android {
|
||||
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.10"
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation libs.ext.junit
|
||||
androidTestImplementation libs.espresso.core
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
After Width: | Height: | Size: 224 KiB |
|
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[versions]
|
||||
agp = "8.13.2"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.0"
|
||||
material = "1.12.0"
|
||||
constraintlayout = "2.2.0"
|
||||
kotlin = "1.9.23"
|
||||
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" }
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
import 'frida-il2cpp-bridge-0-12-2';
|
||||
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
import 'frida-il2cpp-bridge-0-13-0';
|
||||
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
import 'frida-il2cpp-bridge-0-13-1';
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
import JavaBridge from 'frida-java-bridge-7-0-11';
|
||||
|
||||
if (typeof globalThis.Java === 'undefined') globalThis.Java = JavaBridge;
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
import JavaBridge from 'frida-java-bridge-7-0-12';
|
||||
|
||||
if (typeof globalThis.Java === 'undefined') globalThis.Java = JavaBridge;
|
||||