mirror of
https://github.com/QM4RS/FridaBox.git
synced 2026-10-31 19:22:14 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
330c634715 | ||
|
|
4f4e04aafa | ||
|
|
16f51cf1d5 | ||
|
|
c89c6c4f81 |
+1
-1
@@ -106,7 +106,7 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation(project(":black-reflection"))
|
||||
annotationProcessor(project(":compiler"))
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
|
||||
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||
|
||||
|
||||
+39
@@ -17,6 +17,10 @@ public final class InstrumentationSettings {
|
||||
public static final String KEY_GADGET_VERSION = "selected_gadget_version";
|
||||
public static final String KEY_GADGET_ABI = "selected_gadget_abi";
|
||||
public static final String KEY_GADGET_SHA256 = "selected_gadget_sha256";
|
||||
public static final String KEY_JAVA_BRIDGE_ENABLED = "runtime_bridge_java_enabled";
|
||||
public static final String KEY_JAVA_BRIDGE_VERSION = "runtime_bridge_java_version";
|
||||
public static final String KEY_IL2CPP_BRIDGE_ENABLED = "runtime_bridge_il2cpp_enabled";
|
||||
public static final String KEY_IL2CPP_BRIDGE_VERSION = "runtime_bridge_il2cpp_version";
|
||||
private static final String PACKAGE_PREFIX = "package_enabled_";
|
||||
private static final String PACKAGE_MODE_PREFIX = "package_mode_";
|
||||
private static final String PACKAGE_SCRIPT_PREFIX = "package_script_";
|
||||
@@ -150,6 +154,41 @@ public final class InstrumentationSettings {
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static boolean isRuntimeBridgeEnabled(String bridgeId) {
|
||||
String key = runtimeBridgeEnabledKey(bridgeId);
|
||||
return key != null && preferences().getBoolean(key, false);
|
||||
}
|
||||
|
||||
public static String getRuntimeBridgeVersion(String bridgeId, String fallback) {
|
||||
String key = runtimeBridgeVersionKey(bridgeId);
|
||||
return key == null ? fallback : preferences().getString(key, fallback);
|
||||
}
|
||||
|
||||
public static boolean setRuntimeBridge(String bridgeId, boolean enabled, String version) {
|
||||
RuntimeBridgeCatalog.BridgeSpec spec = RuntimeBridgeCatalog.find(bridgeId);
|
||||
String enabledKey = runtimeBridgeEnabledKey(bridgeId);
|
||||
String versionKey = runtimeBridgeVersionKey(bridgeId);
|
||||
if (spec == null || !spec.supportsVersion(version) || enabledKey == null || versionKey == null) {
|
||||
return false;
|
||||
}
|
||||
return preferences().edit()
|
||||
.putBoolean(enabledKey, enabled)
|
||||
.putString(versionKey, version)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private static String runtimeBridgeEnabledKey(String bridgeId) {
|
||||
if (RuntimeBridgeCatalog.JAVA_ID.equals(bridgeId)) return KEY_JAVA_BRIDGE_ENABLED;
|
||||
if (RuntimeBridgeCatalog.IL2CPP_ID.equals(bridgeId)) return KEY_IL2CPP_BRIDGE_ENABLED;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String runtimeBridgeVersionKey(String bridgeId) {
|
||||
if (RuntimeBridgeCatalog.JAVA_ID.equals(bridgeId)) return KEY_JAVA_BRIDGE_VERSION;
|
||||
if (RuntimeBridgeCatalog.IL2CPP_ID.equals(bridgeId)) return KEY_IL2CPP_BRIDGE_VERSION;
|
||||
return null;
|
||||
}
|
||||
|
||||
static int clamp(int value, int minimum, int maximum, int fallback) {
|
||||
return value < minimum || value > maximum ? fallback : value;
|
||||
}
|
||||
|
||||
+49
-7
@@ -8,6 +8,8 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import top.niunaijun.blackbox.BlackBoxCore;
|
||||
|
||||
@@ -41,7 +43,10 @@ final class LocalScriptGadgetRuntime {
|
||||
File log = new File(directory, LOG_NAME);
|
||||
writeUtf8Atomically(log, "");
|
||||
File instrumentedAgent = new File(directory, INSTRUMENTED_AGENT_NAME);
|
||||
writeInstrumentedAgentAtomically(instrumentedAgent, script, buildLogBridge(log));
|
||||
List<byte[]> runtimeBridges = RuntimeBridgeCatalog.loadEnabledBundles(
|
||||
context, InstrumentationSettings.getSelectedGadgetVersion());
|
||||
writeInstrumentedAgentAtomically(
|
||||
instrumentedAgent, script, buildLogBridge(log), runtimeBridges);
|
||||
if (!instrumentedAgent.setReadable(true, true) || !instrumentedAgent.setWritable(false, false)) {
|
||||
throw new IOException("Unable to secure the instrumented JavaScript agent");
|
||||
}
|
||||
@@ -154,7 +159,7 @@ final class LocalScriptGadgetRuntime {
|
||||
}
|
||||
|
||||
private static void writeInstrumentedAgentAtomically(
|
||||
File destination, File source, String bridge) throws IOException {
|
||||
File destination, File source, String bridge, List<byte[]> runtimeBridges) throws IOException {
|
||||
File temporary = new File(destination.getParentFile(), destination.getName() + ".partial");
|
||||
if (temporary.exists() && !temporary.delete()) {
|
||||
throw new IOException("Unable to replace temporary JavaScript agent");
|
||||
@@ -164,7 +169,7 @@ final class LocalScriptGadgetRuntime {
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
int count;
|
||||
while ((count = input.read(buffer)) >= 0) sourceBytes.write(buffer, 0, count);
|
||||
byte[] instrumented = instrumentAgent(sourceBytes.toByteArray(), bridge);
|
||||
byte[] instrumented = instrumentAgent(sourceBytes.toByteArray(), bridge, runtimeBridges);
|
||||
try (FileOutputStream output = new FileOutputStream(temporary)) {
|
||||
output.write(instrumented);
|
||||
output.getFD().sync();
|
||||
@@ -174,9 +179,22 @@ final class LocalScriptGadgetRuntime {
|
||||
}
|
||||
|
||||
static byte[] instrumentAgent(byte[] source, String bridge) throws IOException {
|
||||
return instrumentAgent(source, bridge, Collections.<byte[]>emptyList());
|
||||
}
|
||||
|
||||
static byte[] instrumentAgent(
|
||||
byte[] source, String bridge, List<byte[]> runtimeBridgeBundles) throws IOException {
|
||||
int offset = injectionOffset(source);
|
||||
if (offset < 0) throw new IOException("Invalid Frida bundle header");
|
||||
byte[] prefix = (bridge + "(function (send) {\n").getBytes(StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream prefixOutput = new ByteArrayOutputStream();
|
||||
prefixOutput.write(bridge.getBytes(StandardCharsets.UTF_8));
|
||||
for (byte[] bundle : runtimeBridgeBundles) {
|
||||
byte[] body = extractEntryModule(bundle);
|
||||
prefixOutput.write(body, 0, body.length);
|
||||
prefixOutput.write('\n');
|
||||
}
|
||||
prefixOutput.write("(function (send) {\n".getBytes(StandardCharsets.UTF_8));
|
||||
byte[] prefix = prefixOutput.toByteArray();
|
||||
byte[] suffix = "\n})(globalThis.__fridaboxSend);\n".getBytes(StandardCharsets.UTF_8);
|
||||
if (offset > 0) return instrumentBundle(source, prefix, suffix, offset);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream(source.length + prefix.length + suffix.length);
|
||||
@@ -186,11 +204,28 @@ final class LocalScriptGadgetRuntime {
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
static byte[] extractEntryModule(byte[] bundle) throws IOException {
|
||||
int bodyOffset = injectionOffset(bundle);
|
||||
if (bodyOffset <= 0) throw new IOException("Runtime bridge is not a Frida bundle");
|
||||
int lengthStart = moduleLengthStart(bundle, bodyOffset);
|
||||
int lengthEnd = lengthStart;
|
||||
long declaredLength = 0;
|
||||
while (lengthEnd < bodyOffset && bundle[lengthEnd] >= '0' && bundle[lengthEnd] <= '9') {
|
||||
declaredLength = declaredLength * 10 + (bundle[lengthEnd] - '0');
|
||||
if (declaredLength > Integer.MAX_VALUE) throw new IOException("Invalid Frida bundle module length");
|
||||
lengthEnd++;
|
||||
}
|
||||
if (lengthEnd == lengthStart || lengthEnd >= bodyOffset || bundle[lengthEnd] != ' ') {
|
||||
throw new IOException("Invalid Frida bundle module length");
|
||||
}
|
||||
long bodyEndLong = bodyOffset + declaredLength;
|
||||
if (bodyEndLong > bundle.length) throw new IOException("Invalid Frida bundle module length");
|
||||
return java.util.Arrays.copyOfRange(bundle, bodyOffset, (int) bodyEndLong);
|
||||
}
|
||||
|
||||
private static byte[] instrumentBundle(
|
||||
byte[] source, byte[] prefix, byte[] suffix, int bodyOffset) throws IOException {
|
||||
int lengthStart = 0;
|
||||
while (lengthStart < bodyOffset && source[lengthStart] != '\n') lengthStart++;
|
||||
lengthStart++;
|
||||
int lengthStart = moduleLengthStart(source, bodyOffset);
|
||||
int lengthEnd = lengthStart;
|
||||
long declaredLength = 0;
|
||||
while (lengthEnd < bodyOffset && source[lengthEnd] >= '0' && source[lengthEnd] <= '9') {
|
||||
@@ -220,6 +255,13 @@ final class LocalScriptGadgetRuntime {
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private static int moduleLengthStart(byte[] source, int bodyOffset) throws IOException {
|
||||
int lengthStart = 0;
|
||||
while (lengthStart < bodyOffset && source[lengthStart] != '\n') lengthStart++;
|
||||
if (lengthStart >= bodyOffset) throw new IOException("Invalid Frida bundle header");
|
||||
return lengthStart + 1;
|
||||
}
|
||||
|
||||
static int injectionOffset(byte[] source) {
|
||||
byte[] magic = "📦".getBytes(StandardCharsets.UTF_8);
|
||||
if (!startsWith(source, magic)) return 0;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package top.niunaijun.blackbox.instrumentation;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Offline, build-pinned runtime bridges available to autonomous Frida agents. */
|
||||
public final class RuntimeBridgeCatalog {
|
||||
public static final String JAVA_ID = "java";
|
||||
public static final String IL2CPP_ID = "il2cpp";
|
||||
public static final String MINIMUM_GADGET_VERSION = "17.0.0";
|
||||
|
||||
private static final int MAX_BUNDLE_BYTES = 2 * 1024 * 1024;
|
||||
private static final List<BridgeSpec> SPECS = Collections.unmodifiableList(Arrays.asList(
|
||||
new BridgeSpec(
|
||||
JAVA_ID,
|
||||
"Java",
|
||||
"Official Frida runtime bridge",
|
||||
"7.0.13",
|
||||
new String[]{"7.0.13", "7.0.12", "7.0.11"}),
|
||||
new BridgeSpec(
|
||||
IL2CPP_ID,
|
||||
"IL2CPP",
|
||||
"Community bridge for Unity IL2CPP",
|
||||
"0.13.1",
|
||||
new String[]{"0.13.1", "0.13.0", "0.12.2"})
|
||||
));
|
||||
|
||||
private RuntimeBridgeCatalog() {
|
||||
}
|
||||
|
||||
public static List<BridgeSpec> specs() {
|
||||
return SPECS;
|
||||
}
|
||||
|
||||
public static BridgeSpec find(String id) {
|
||||
for (BridgeSpec spec : SPECS) {
|
||||
if (spec.id.equals(id)) return spec;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean supportsRuntimeBridges(String gadgetVersion) {
|
||||
int[] parsed = parseVersion(gadgetVersion);
|
||||
return parsed != null && parsed[0] >= 17;
|
||||
}
|
||||
|
||||
public static String selectedVersion(BridgeSpec spec) {
|
||||
String selected = InstrumentationSettings.getRuntimeBridgeVersion(spec.id, spec.defaultVersion);
|
||||
return spec.supportsVersion(selected) ? selected : spec.defaultVersion;
|
||||
}
|
||||
|
||||
static List<byte[]> loadEnabledBundles(Context context, String gadgetVersion) throws IOException {
|
||||
if (!supportsRuntimeBridges(gadgetVersion)) return Collections.emptyList();
|
||||
List<byte[]> bundles = new ArrayList<>();
|
||||
for (BridgeSpec spec : SPECS) {
|
||||
if (!InstrumentationSettings.isRuntimeBridgeEnabled(spec.id)) continue;
|
||||
String version = selectedVersion(spec);
|
||||
String asset = "fridabox/bridges/" + spec.id + "-" + version + ".js";
|
||||
try (InputStream input = context.getAssets().open(asset)) {
|
||||
bundles.add(readLimited(input));
|
||||
} catch (IOException error) {
|
||||
throw new IOException("Runtime bridge asset is unavailable: " + spec.id + " " + version, error);
|
||||
}
|
||||
}
|
||||
return bundles;
|
||||
}
|
||||
|
||||
private static byte[] readLimited(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
int total = 0;
|
||||
int count;
|
||||
while ((count = input.read(buffer)) >= 0) {
|
||||
total += count;
|
||||
if (total > MAX_BUNDLE_BYTES) throw new IOException("Runtime bridge bundle is unexpectedly large");
|
||||
output.write(buffer, 0, count);
|
||||
}
|
||||
if (total == 0) throw new IOException("Runtime bridge bundle is empty");
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
static int[] parseVersion(String value) {
|
||||
if (value == null) return null;
|
||||
String normalized = value.trim();
|
||||
if (normalized.startsWith("v") || normalized.startsWith("V")) normalized = normalized.substring(1);
|
||||
String[] parts = normalized.split("[.-]", 4);
|
||||
if (parts.length < 1 || parts.length > 4) return null;
|
||||
int[] result = new int[]{0, 0, 0};
|
||||
try {
|
||||
for (int index = 0; index < Math.min(3, parts.length); index++) {
|
||||
if (parts[index].isEmpty()) return null;
|
||||
result[index] = Integer.parseInt(parts[index]);
|
||||
if (result[index] < 0) return null;
|
||||
}
|
||||
return result;
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class BridgeSpec {
|
||||
public final String id;
|
||||
public final String title;
|
||||
public final String description;
|
||||
public final String defaultVersion;
|
||||
private final List<String> versions;
|
||||
|
||||
BridgeSpec(String id, String title, String description, String defaultVersion, String[] versions) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.defaultVersion = defaultVersion;
|
||||
this.versions = Collections.unmodifiableList(Arrays.asList(versions.clone()));
|
||||
}
|
||||
|
||||
public List<String> versions() {
|
||||
return versions;
|
||||
}
|
||||
|
||||
public boolean supportsVersion(String version) {
|
||||
return version != null && versions.contains(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -7,6 +7,7 @@ import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
public class LocalScriptGadgetRuntimeTest {
|
||||
@Test
|
||||
@@ -54,10 +55,45 @@ public class LocalScriptGadgetRuntimeTest {
|
||||
assertTrue(result.endsWith("})(globalThis.__fridaboxSend);\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectedRuntimeBridgeRunsBeforeTheUserAgent() throws Exception {
|
||||
String bridgeBody = "globalThis.Java = { available: true };";
|
||||
byte[] bridgeBundle = bundle("/bridges/java.js", bridgeBody);
|
||||
String source = "console.log(Java.available);";
|
||||
String result = new String(LocalScriptGadgetRuntime.instrumentAgent(
|
||||
source.getBytes(StandardCharsets.UTF_8),
|
||||
"console.log('log bridge');\n",
|
||||
Collections.singletonList(bridgeBundle)), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(result.indexOf(bridgeBody) < result.indexOf(source));
|
||||
assertTrue(result.contains("(function (send) {\n" + source));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entryModuleExtractionRejectsPlainJavaScript() {
|
||||
boolean rejected = false;
|
||||
try {
|
||||
LocalScriptGadgetRuntime.extractEntryModule("plain".getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception expected) {
|
||||
rejected = true;
|
||||
}
|
||||
assertTrue(rejected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void privatePathCheckRejectsSiblingPrefix() {
|
||||
File root = new File("/data/user/0/host/files/fridabox-agents");
|
||||
assertTrue(LocalScriptGadgetRuntime.isInside(root, new File(root, "guest/agent.js")));
|
||||
assertFalse(LocalScriptGadgetRuntime.isInside(root, new File(root.getPath() + "-other/agent.js")));
|
||||
}
|
||||
|
||||
private static byte[] bundle(String name, String body) {
|
||||
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
String header = "\uD83D\uDCE6\n" + bodyBytes.length + " " + name + "\n\u2704\n";
|
||||
byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] result = new byte[headerBytes.length + bodyBytes.length];
|
||||
System.arraycopy(headerBytes, 0, result, 0, headerBytes.length);
|
||||
System.arraycopy(bodyBytes, 0, result, headerBytes.length, bodyBytes.length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,34 @@ 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
|
||||
|
||||
@@ -69,15 +69,20 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Frida Java bridge and agent compiler
|
||||
## Frida Java and IL2CPP runtime bridges and agent compiler
|
||||
|
||||
The compiled JavaScript agents under `scripts/dist/` include
|
||||
`frida-java-bridge` 7.0.13. They are built with `frida-compile` 19.0.5.
|
||||
FridaBox also packages offline runtime-bridge bundles for Java 7.0.11 through
|
||||
7.0.13 and `frida-il2cpp-bridge` 0.12.2 through 0.13.1. These optional bundles
|
||||
are only composed into a private on-device agent when the user enables them.
|
||||
|
||||
- Java bridge origin: `https://github.com/frida/frida-java-bridge`
|
||||
- IL2CPP bridge origin: `https://github.com/vfsfitvnm/frida-il2cpp-bridge`
|
||||
- Compiler origin: `https://github.com/frida/frida-compile`
|
||||
- License: LGPL-2.0 with the wxWindows Library Licence 3.1 exception, as
|
||||
declared by the respective official packages.
|
||||
- Java bridge and compiler license: LGPL-2.0 with the wxWindows Library Licence
|
||||
3.1 exception, as declared by the respective official packages.
|
||||
- IL2CPP bridge license: MIT, as declared by its package and repository.
|
||||
|
||||
The authoritative licence text and exception are the same wxWindows Library
|
||||
Licence described above. Exact package versions and dependency integrity hashes
|
||||
|
||||
+22
-2
@@ -132,6 +132,26 @@ tasks.register("verifyInstrumentationOrdering") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyRuntimeBridgeAssets") {
|
||||
doLast {
|
||||
def bridgeAssets = [
|
||||
"java-7.0.11.js", "java-7.0.12.js", "java-7.0.13.js",
|
||||
"il2cpp-0.12.2.js", "il2cpp-0.13.0.js", "il2cpp-0.13.1.js"
|
||||
]
|
||||
def bridgeRoot = file("src/main/assets/fridabox/bridges")
|
||||
bridgeAssets.each { name ->
|
||||
def asset = new File(bridgeRoot, name)
|
||||
if (!asset.isFile() || asset.length() == 0 || asset.length() > 2 * 1024 * 1024) {
|
||||
throw new GradleException("Missing or invalid runtime bridge asset: ${name}")
|
||||
}
|
||||
def source = new String(asset.bytes, java.nio.charset.StandardCharsets.UTF_8)
|
||||
if (!source.startsWith("📦\n") || !source.contains("\n✄\n")) {
|
||||
throw new GradleException("Runtime bridge is not a valid frida-compile bundle: ${name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyTablerAndroidIcons", Exec) {
|
||||
workingDir(rootProject.projectDir)
|
||||
def nodeExecutable = System.getProperty("os.name").toLowerCase().contains("windows") ? "node.exe" : "node"
|
||||
@@ -157,7 +177,7 @@ tasks.register("verifyDemoGuestUnmodified") {
|
||||
|
||||
tasks.named("check").configure {
|
||||
dependsOn("verifyDebugApkHasNoGadget", "verifyInstrumentationOrdering",
|
||||
"verifyDemoGuestUnmodified", "verifyTablerAndroidIcons")
|
||||
"verifyDemoGuestUnmodified", "verifyTablerAndroidIcons", "verifyRuntimeBridgeAssets")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -167,7 +187,7 @@ dependencies {
|
||||
implementation libs.material
|
||||
implementation libs.constraintlayout
|
||||
implementation libs.core.ktx
|
||||
implementation "org.tukaani:xz:1.10"
|
||||
implementation "org.tukaani:xz:1.12"
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation libs.ext.junit
|
||||
androidTestImplementation libs.espresso.core
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,8 +6,6 @@ import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageInfo
|
||||
@@ -23,11 +21,15 @@ import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.InputType
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.GridLayout
|
||||
@@ -36,7 +38,6 @@ import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.Space
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
@@ -54,6 +55,7 @@ import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import top.niunaijun.blackbox.BlackBoxCore
|
||||
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
|
||||
import top.niunaijun.blackbox.instrumentation.RuntimeBridgeCatalog
|
||||
import top.niunaijun.blackbox.utils.ProcessAbi
|
||||
import com.qm4rs.fridabox.databinding.ActivityFridaboxBinding
|
||||
import java.io.File
|
||||
@@ -64,7 +66,7 @@ import java.util.concurrent.Executors
|
||||
|
||||
/** Product workspace for importing, configuring, and launching FridaBox guests. */
|
||||
class FridaBoxActivity : AppCompatActivity() {
|
||||
private enum class Screen { WORKSPACE, GADGETS, RUNTIME, SETTINGS }
|
||||
private enum class Screen { WORKSPACE, GADGETS, SETTINGS }
|
||||
|
||||
private data class ImportBubbleViews(
|
||||
val scrim: View,
|
||||
@@ -107,6 +109,10 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
binding = ActivityFridaboxBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
pendingScriptPackage = savedInstanceState?.getString("pending_script_package")
|
||||
settings.edit()
|
||||
.remove(InstrumentationSettings.KEY_ENABLED)
|
||||
.remove(InstrumentationSettings.KEY_ADVANCED_LOGS)
|
||||
.apply()
|
||||
|
||||
binding.importFabIcon.setOnClickListener { showImportActions() }
|
||||
binding.bottomNavigation.setOnItemSelectedListener { item ->
|
||||
@@ -114,12 +120,16 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
when (item.itemId) {
|
||||
R.id.nav_workspace -> showWorkspace()
|
||||
R.id.nav_gadgets -> showGadgets()
|
||||
R.id.nav_runtime -> showRuntime()
|
||||
R.id.nav_settings -> showSettings()
|
||||
else -> return@setOnItemSelectedListener false
|
||||
}
|
||||
animateNavigationSelection(item.itemId)
|
||||
animateContentIn()
|
||||
true
|
||||
}
|
||||
binding.bottomNavigation.setOnItemReselectedListener { item ->
|
||||
animateNavigationSelection(item.itemId)
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (importMenuOverlay != null) {
|
||||
@@ -704,7 +714,6 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
setLoading(false)
|
||||
result.onSuccess { launched ->
|
||||
if (!launched) toast("This guest has no launchable activity")
|
||||
else if (mode == InstrumentationSettings.MODE_COMPUTER) showRuntime(packageName)
|
||||
}.onFailure { toast("Launch failed: ${it.message}") }
|
||||
}
|
||||
}
|
||||
@@ -803,10 +812,6 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
popup.dismiss()
|
||||
showAppDetails(info, appLabel)
|
||||
})
|
||||
panel.addView(menuActionButton(getString(R.string.fb_runtime_details), R.drawable.ic_fb_runtime) {
|
||||
popup.dismiss()
|
||||
showRuntime(info.packageName)
|
||||
})
|
||||
panel.addView(menuActionButton(getString(R.string.fb_clear_data), R.drawable.ic_fb_clear) {
|
||||
popup.dismiss()
|
||||
clearApp(info.packageName)
|
||||
@@ -1451,9 +1456,10 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGadgets() {
|
||||
private fun showGadgets(preserveScroll: Boolean = false) {
|
||||
val previousScrollY = binding.contentScroll.scrollY
|
||||
screen = Screen.GADGETS
|
||||
resetScreen(R.id.nav_gadgets, showImport = false)
|
||||
resetScreen(R.id.nav_gadgets, showImport = false, resetScroll = !preserveScroll)
|
||||
binding.toolbar.title = getString(R.string.fb_gadgets_title)
|
||||
|
||||
val abi = gadgetManager.detectedAbi()
|
||||
@@ -1497,6 +1503,10 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
}
|
||||
})
|
||||
|
||||
if (selected != null && RuntimeBridgeCatalog.supportsRuntimeBridges(selected.version)) {
|
||||
binding.content.addView(runtimeBridgeControls(selected.version))
|
||||
}
|
||||
|
||||
val installed = gadgetManager.installed()
|
||||
if (installed.isNotEmpty()) {
|
||||
binding.content.addView(labelText(
|
||||
@@ -1506,6 +1516,106 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
binding.content.addView(installedGadgetRow(gadget, selected?.identity == gadget.identity))
|
||||
}
|
||||
}
|
||||
if (preserveScroll) restoreScrollBeforeNextDraw(previousScrollY)
|
||||
}
|
||||
|
||||
private fun runtimeBridgeControls(gadgetVersion: String): View {
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(labelText(
|
||||
getString(R.string.fb_runtime_bridges), R.color.fb_text_secondary, 12f, false
|
||||
).apply { setPadding(0, 0, 0, dp(6)) })
|
||||
addView(labelText(
|
||||
getString(R.string.fb_runtime_bridges_body, gadgetVersion),
|
||||
R.color.fb_text_secondary,
|
||||
12.5f,
|
||||
false
|
||||
).apply { setPadding(0, 0, 0, dp(10)) })
|
||||
RuntimeBridgeCatalog.specs().forEach { spec ->
|
||||
addView(runtimeBridgeRow(spec))
|
||||
}
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = dp(20) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun runtimeBridgeRow(spec: RuntimeBridgeCatalog.BridgeSpec): View {
|
||||
val version = RuntimeBridgeCatalog.selectedVersion(spec)
|
||||
val enabled = InstrumentationSettings.isRuntimeBridgeEnabled(spec.id)
|
||||
return surfaceCard().apply {
|
||||
addView(LinearLayout(this@FridaBoxActivity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(dp(16), dp(14), dp(16), dp(14))
|
||||
addView(LinearLayout(this@FridaBoxActivity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
addView(verticalText(spec.title, spec.description), LinearLayout.LayoutParams(
|
||||
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f
|
||||
))
|
||||
addView(SwitchMaterial(this@FridaBoxActivity).apply {
|
||||
isChecked = enabled
|
||||
contentDescription = getString(R.string.fb_runtime_bridge_toggle, spec.title)
|
||||
var rollingBack = false
|
||||
setOnCheckedChangeListener { button, checked ->
|
||||
if (rollingBack) return@setOnCheckedChangeListener
|
||||
if (!InstrumentationSettings.setRuntimeBridge(spec.id, checked, version)) {
|
||||
rollingBack = true
|
||||
button.isChecked = !checked
|
||||
rollingBack = false
|
||||
notify(getString(R.string.fb_runtime_bridge_save_failed))
|
||||
} else {
|
||||
notify(getString(
|
||||
if (checked) R.string.fb_runtime_bridge_enabled else R.string.fb_runtime_bridge_disabled,
|
||||
spec.title
|
||||
))
|
||||
}
|
||||
}
|
||||
}, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { marginStart = dp(8) })
|
||||
})
|
||||
addView(outlineButton(getString(R.string.fb_runtime_bridge_version, version)) { button ->
|
||||
showRuntimeBridgeVersions(
|
||||
spec,
|
||||
RuntimeBridgeCatalog.selectedVersion(spec),
|
||||
button as MaterialButton
|
||||
)
|
||||
}.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
dp(42)
|
||||
).apply { topMargin = dp(10) }
|
||||
})
|
||||
})
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = dp(10) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRuntimeBridgeVersions(
|
||||
spec: RuntimeBridgeCatalog.BridgeSpec,
|
||||
current: String,
|
||||
versionButton: MaterialButton
|
||||
) {
|
||||
val versions = spec.versions().toTypedArray()
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.fb_runtime_bridge_choose_version, spec.title))
|
||||
.setSingleChoiceItems(versions, versions.indexOf(current)) { dialog, which ->
|
||||
val enabled = InstrumentationSettings.isRuntimeBridgeEnabled(spec.id)
|
||||
if (InstrumentationSettings.setRuntimeBridge(spec.id, enabled, versions[which])) {
|
||||
dialog.dismiss()
|
||||
versionButton.text = getString(R.string.fb_runtime_bridge_version, versions[which])
|
||||
} else {
|
||||
notify(getString(R.string.fb_runtime_bridge_save_failed))
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(R.string.fb_cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun installedGadgetRow(gadget: InstalledGadget, active: Boolean): View {
|
||||
@@ -1518,7 +1628,7 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
runCatching { gadgetManager.select(gadget) }
|
||||
.onSuccess {
|
||||
notify(getString(R.string.fb_gadget_selected, gadget.version))
|
||||
showGadgets()
|
||||
showGadgets(preserveScroll = true)
|
||||
}
|
||||
.onFailure { notify(it.message ?: getString(R.string.fb_gadget_select_failed)) }
|
||||
}
|
||||
@@ -1548,7 +1658,7 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
installed != null -> runCatching { gadgetManager.select(installed) }
|
||||
.onSuccess {
|
||||
dialog.dismiss()
|
||||
showGadgets()
|
||||
showGadgets(preserveScroll = true)
|
||||
}
|
||||
.onFailure { notify(it.message ?: getString(R.string.fb_gadget_select_failed)) }
|
||||
else -> {
|
||||
@@ -1586,7 +1696,7 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
private fun showGadgetBrowser(abi: GadgetAbi) {
|
||||
val dialog = Dialog(this)
|
||||
val root = FrameLayout(this).apply {
|
||||
setPadding(dp(12), dp(32), dp(12), dp(12))
|
||||
setPadding(dp(12), dp(20), dp(12), dp(20))
|
||||
setOnClickListener { dialog.dismiss() }
|
||||
}
|
||||
val panel = surfaceCard().apply {
|
||||
@@ -1637,8 +1747,8 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
|
||||
root.addView(panel, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
(resources.displayMetrics.heightPixels * 0.82f).toInt(),
|
||||
Gravity.BOTTOM
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
Gravity.CENTER
|
||||
))
|
||||
dialog.setContentView(root)
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
@@ -1653,6 +1763,20 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
}
|
||||
dialog.show()
|
||||
dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
panel.apply {
|
||||
alpha = 0f
|
||||
scaleX = 0.96f
|
||||
scaleY = 0.96f
|
||||
translationY = dp(10).toFloat()
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(280L)
|
||||
.setInterpolator(DecelerateInterpolator(1.6f))
|
||||
.start()
|
||||
}
|
||||
|
||||
val request = ++catalogRequest
|
||||
val identities = HashSet<String>()
|
||||
@@ -1722,88 +1846,12 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
if (hadSelection) R.string.fb_gadget_downloaded else R.string.fb_gadget_downloaded_selected,
|
||||
release.version
|
||||
))
|
||||
showGadgets()
|
||||
showGadgets(preserveScroll = true)
|
||||
}.onFailure { notify(getString(R.string.fb_gadget_download_failed, it.message ?: "Unknown error")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRuntime(packageHint: String? = null) {
|
||||
screen = Screen.RUNTIME
|
||||
resetScreen(R.id.nav_runtime, showImport = false)
|
||||
binding.toolbar.title = getString(R.string.fb_runtime_title)
|
||||
binding.toolbar.subtitle = getString(R.string.fb_runtime_subtitle)
|
||||
|
||||
val packageName = packageHint ?: settings.getString("runtime_package", null)
|
||||
val state = settings.getString("runtime_state", "idle") ?: "idle"
|
||||
val mode = settings.getString("runtime_mode", packageName?.let {
|
||||
InstrumentationSettings.getModeForPackage(it)
|
||||
} ?: InstrumentationSettings.MODE_CLEAN) ?: InstrumentationSettings.MODE_CLEAN
|
||||
val stateColor = when (state) {
|
||||
"local_script_active", "computer_attached" -> R.color.fb_success
|
||||
"failed" -> R.color.fb_error
|
||||
"waiting_for_attach", "loading_local_script" -> R.color.fb_warning
|
||||
else -> R.color.fb_text_secondary
|
||||
}
|
||||
|
||||
binding.content.addView(pageHeading(
|
||||
getString(R.string.fb_runtime_title),
|
||||
getString(R.string.fb_runtime_subtitle)
|
||||
))
|
||||
binding.content.addView(messageCard(
|
||||
runtimeStateLabel(state),
|
||||
packageName ?: "No guest process has reported runtime state yet.",
|
||||
stateColor
|
||||
))
|
||||
|
||||
if (mode == InstrumentationSettings.MODE_COMPUTER) {
|
||||
val command = "frida -U gadget -l path/to/agent.js"
|
||||
binding.content.addView(infoCard("Computer attach", buildString {
|
||||
append("The guest waits at Gadget until a controller attaches.\n\n")
|
||||
append(command)
|
||||
}, command))
|
||||
} else if (mode == InstrumentationSettings.MODE_LOCAL_SCRIPT) {
|
||||
val scriptName = packageName?.let { metadata.getString("$it.scriptName", null) }
|
||||
val scriptHash = packageName?.let { metadata.getString("$it.scriptSha", null) }
|
||||
binding.content.addView(infoCard(
|
||||
"On-device agent",
|
||||
buildString {
|
||||
append(scriptName ?: "No agent selected")
|
||||
if (scriptHash != null) append("\nSHA-256 ").append(scriptHash)
|
||||
append("\n\nThe private agent is loaded autonomously by Frida Gadget.")
|
||||
}
|
||||
))
|
||||
if (packageName != null) {
|
||||
binding.content.addView(outlineButton("Open agent logs") {
|
||||
if (!FridaBoxGuestLogOverlay.show(this, packageName)) {
|
||||
toast("No agent log is available for $packageName")
|
||||
}
|
||||
}.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(50)).apply {
|
||||
topMargin = dp(14)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
binding.content.addView(infoCard("Clean launch", getString(R.string.fb_mode_clean_body)))
|
||||
}
|
||||
|
||||
val details = listOf(
|
||||
"Guest package" to (packageName ?: "Not reported"),
|
||||
"Guest process" to (settings.getString("runtime_process", "—") ?: "—"),
|
||||
"Virtual process slot" to settings.getInt("runtime_vpid", -1).toString(),
|
||||
"Guest source" to (settings.getString("runtime_source", "—") ?: "—"),
|
||||
"Virtual user ID" to settings.getInt("runtime_user_id", -1).toString(),
|
||||
"ClassLoader" to (settings.getString("runtime_class_loader", "Not reported") ?: "Not reported"),
|
||||
"Mode" to modeLongLabel(mode),
|
||||
"Latest error" to (settings.getString("runtime_error", "None") ?: "None")
|
||||
)
|
||||
binding.content.addView(detailsCard("Process registry", details))
|
||||
binding.content.addView(outlineButton(getString(R.string.fb_refresh)) { showRuntime(packageHint) }.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(50)).apply { topMargin = dp(14) }
|
||||
})
|
||||
}
|
||||
|
||||
private fun showSettings() {
|
||||
screen = Screen.SETTINGS
|
||||
resetScreen(R.id.nav_settings, showImport = false)
|
||||
@@ -1820,35 +1868,25 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
setPadding(dp(18), dp(18), dp(18), dp(18))
|
||||
}
|
||||
controls.addView(body)
|
||||
val enabled = SwitchMaterial(this).apply {
|
||||
text = getString(R.string.fb_global_instrumentation)
|
||||
setTextColor(color(R.color.fb_text_primary))
|
||||
isChecked = settings.getBoolean(InstrumentationSettings.KEY_ENABLED, true)
|
||||
}
|
||||
val logs = SwitchMaterial(this).apply {
|
||||
text = getString(R.string.fb_advanced_logs)
|
||||
setTextColor(color(R.color.fb_text_primary))
|
||||
isChecked = settings.getBoolean(InstrumentationSettings.KEY_ADVANCED_LOGS, false)
|
||||
}
|
||||
val port = numberInput(getString(R.string.fb_base_port), settings.getInt(InstrumentationSettings.KEY_BASE_PORT, 27042))
|
||||
val count = numberInput(getString(R.string.fb_scan_count), settings.getInt(InstrumentationSettings.KEY_SCAN_COUNT, 32))
|
||||
body.addView(enabled)
|
||||
body.addView(logs)
|
||||
body.addView(port.first)
|
||||
body.addView(count.first)
|
||||
body.addView(primaryButton(getString(R.string.fb_save_settings)) {
|
||||
settings.edit()
|
||||
.putBoolean(InstrumentationSettings.KEY_ENABLED, enabled.isChecked)
|
||||
.putInt(InstrumentationSettings.KEY_BASE_PORT,
|
||||
InstrumentationPreferenceParser.parsePort(port.second.text?.toString().orEmpty(), 27042))
|
||||
.putInt(InstrumentationSettings.KEY_SCAN_COUNT,
|
||||
InstrumentationPreferenceParser.parseScanCount(count.second.text?.toString().orEmpty(), 32))
|
||||
.putBoolean(InstrumentationSettings.KEY_ADVANCED_LOGS, logs.isChecked)
|
||||
.apply()
|
||||
notify("Settings saved")
|
||||
}.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(50)).apply { topMargin = dp(12) }
|
||||
})
|
||||
controls.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = dp(14) }
|
||||
binding.content.addView(controls)
|
||||
binding.content.addView(messageCard(
|
||||
getString(R.string.fb_security_title),
|
||||
@@ -1902,50 +1940,6 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun infoCard(title: String, bodyText: String, copyValue: String? = null): View {
|
||||
val card = surfaceCard()
|
||||
val body = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(dp(18), dp(18), dp(18), dp(18))
|
||||
addView(labelText(title, R.color.fb_text_primary, 16f, true))
|
||||
addView(labelText(bodyText, R.color.fb_text_secondary, 13f, false).apply {
|
||||
setPadding(0, dp(8), 0, 0)
|
||||
setTextIsSelectable(true)
|
||||
})
|
||||
if (copyValue != null) addView(outlineButton(getString(R.string.fb_copy_command)) {
|
||||
(getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
|
||||
.setPrimaryClip(ClipData.newPlainText("FridaBox command", copyValue))
|
||||
notify("Command copied")
|
||||
}.apply {
|
||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(46)).apply { topMargin = dp(12) }
|
||||
})
|
||||
}
|
||||
card.addView(body)
|
||||
card.layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = dp(14) }
|
||||
return card
|
||||
}
|
||||
|
||||
private fun detailsCard(title: String, rows: List<Pair<String, String>>): View {
|
||||
val card = surfaceCard()
|
||||
val body = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(dp(18), dp(18), dp(18), dp(18))
|
||||
addView(labelText(title, R.color.fb_text_primary, 16f, true))
|
||||
}
|
||||
rows.forEach { (label, value) ->
|
||||
body.addView(labelText(label.uppercase(Locale.ROOT), R.color.fb_text_secondary, 10.5f, true).apply {
|
||||
setPadding(0, dp(13), 0, 0)
|
||||
})
|
||||
body.addView(labelText(value, R.color.fb_text_primary, 13f, false).apply { setTextIsSelectable(true) })
|
||||
}
|
||||
card.addView(body)
|
||||
card.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
return card
|
||||
}
|
||||
|
||||
private fun verticalText(title: String, subtitle: String): LinearLayout {
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
@@ -1997,7 +1991,7 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
strokeColor = checkedColors(color(R.color.fb_primary), color(R.color.fb_glass_outline))
|
||||
backgroundTintList = checkedColors(color(R.color.fb_primary), color(R.color.fb_glass_surface_soft))
|
||||
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||
}
|
||||
}.also(::installPressMotion)
|
||||
}
|
||||
|
||||
private fun primaryButton(label: String, action: (View) -> Unit): MaterialButton {
|
||||
@@ -2014,7 +2008,7 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
strokeWidth = dp(1)
|
||||
elevation = dp(3).toFloat()
|
||||
setOnClickListener(action)
|
||||
}
|
||||
}.also(::installPressMotion)
|
||||
}
|
||||
|
||||
private fun outlineButton(label: String, action: (View) -> Unit): MaterialButton {
|
||||
@@ -2029,7 +2023,7 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
strokeColor = ColorStateList.valueOf(color(R.color.fb_glass_outline))
|
||||
backgroundTintList = ColorStateList.valueOf(color(R.color.fb_glass_surface_soft))
|
||||
setOnClickListener(action)
|
||||
}
|
||||
}.also(::installPressMotion)
|
||||
}
|
||||
|
||||
private fun iconButton(iconResource: Int, description: String, action: () -> Unit): MaterialButton {
|
||||
@@ -2044,15 +2038,120 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetScreen(navId: Int, showImport: Boolean): Int {
|
||||
private fun installPressMotion(view: View) {
|
||||
view.isHapticFeedbackEnabled = true
|
||||
view.setOnTouchListener { target, event ->
|
||||
if (!target.isEnabled) return@setOnTouchListener false
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
target.animate().cancel()
|
||||
target.performHapticFeedback(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
HapticFeedbackConstants.CONTEXT_CLICK
|
||||
} else {
|
||||
HapticFeedbackConstants.VIRTUAL_KEY
|
||||
}
|
||||
)
|
||||
target.animate()
|
||||
.scaleX(0.965f)
|
||||
.scaleY(0.965f)
|
||||
.alpha(0.9f)
|
||||
.translationY(dp(1).toFloat())
|
||||
.setDuration(85L)
|
||||
.setInterpolator(AccelerateInterpolator(1.4f))
|
||||
.start()
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
target.animate().cancel()
|
||||
target.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(240L)
|
||||
.setInterpolator(OvershootInterpolator(1.45f))
|
||||
.start()
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateNavigationSelection(itemId: Int) {
|
||||
updateNavigationIconEmphasis(itemId)
|
||||
val item = binding.bottomNavigation.findViewById<View>(itemId) ?: return
|
||||
val target = firstImageDescendant(item) ?: item
|
||||
target.animate().cancel()
|
||||
target.scaleX = 0.9f
|
||||
target.scaleY = 0.9f
|
||||
target.translationY = dp(2).toFloat()
|
||||
target.animate()
|
||||
.scaleX(1.18f)
|
||||
.scaleY(1.18f)
|
||||
.translationY(0f)
|
||||
.setDuration(360L)
|
||||
.setInterpolator(OvershootInterpolator(1.8f))
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun updateNavigationIconEmphasis(selectedItemId: Int) {
|
||||
intArrayOf(R.id.nav_workspace, R.id.nav_gadgets, R.id.nav_settings).forEach { itemId ->
|
||||
val item = binding.bottomNavigation.findViewById<View>(itemId) ?: return@forEach
|
||||
val icon = firstImageDescendant(item) ?: return@forEach
|
||||
icon.animate().cancel()
|
||||
val scale = if (itemId == selectedItemId) 1.18f else 1f
|
||||
icon.scaleX = scale
|
||||
icon.scaleY = scale
|
||||
icon.translationY = 0f
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstImageDescendant(view: View): ImageView? {
|
||||
if (view is ImageView) return view
|
||||
if (view !is ViewGroup) return null
|
||||
for (index in 0 until view.childCount) {
|
||||
firstImageDescendant(view.getChildAt(index))?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun animateContentIn() {
|
||||
binding.content.animate().cancel()
|
||||
binding.content.alpha = 0.62f
|
||||
binding.content.translationY = dp(8).toFloat()
|
||||
binding.content.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(260L)
|
||||
.setInterpolator(DecelerateInterpolator(1.5f))
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun restoreScrollBeforeNextDraw(scrollY: Int) {
|
||||
if (scrollY <= 0) return
|
||||
val scroll = binding.contentScroll
|
||||
scroll.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
if (scroll.viewTreeObserver.isAlive) {
|
||||
scroll.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
}
|
||||
val maximum = (scroll.getChildAt(0)?.height ?: 0) - scroll.height
|
||||
scroll.scrollTo(0, scrollY.coerceIn(0, maximum.coerceAtLeast(0)))
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun resetScreen(navId: Int, showImport: Boolean, resetScroll: Boolean = true): Int {
|
||||
dismissImportActions(immediate = true)
|
||||
screenGeneration += 1
|
||||
binding.content.removeAllViews()
|
||||
binding.contentScroll.scrollTo(0, 0)
|
||||
if (resetScroll) binding.contentScroll.scrollTo(0, 0)
|
||||
binding.importFabIcon.isVisible = showImport
|
||||
changingNavigation = true
|
||||
binding.bottomNavigation.selectedItemId = navId
|
||||
changingNavigation = false
|
||||
updateNavigationIconEmphasis(navId)
|
||||
setLoading(false)
|
||||
return screenGeneration
|
||||
}
|
||||
@@ -2068,28 +2167,12 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun runtimeStateLabel(state: String): String = when (state) {
|
||||
"local_script_active" -> "On-device agent active"
|
||||
"computer_attached" -> "Computer attached"
|
||||
"waiting_for_attach" -> "Waiting for computer"
|
||||
"loading_local_script" -> "Loading on-device agent"
|
||||
"failed" -> "Instrumentation failed"
|
||||
"disabled" -> "Clean runtime"
|
||||
else -> "Runtime idle"
|
||||
}
|
||||
|
||||
private fun modeShortLabel(mode: String): String = when (mode) {
|
||||
InstrumentationSettings.MODE_LOCAL_SCRIPT -> getString(R.string.fb_mode_local)
|
||||
InstrumentationSettings.MODE_CLEAN -> getString(R.string.fb_mode_clean)
|
||||
else -> getString(R.string.fb_mode_computer)
|
||||
}
|
||||
|
||||
private fun modeLongLabel(mode: String): String = when (mode) {
|
||||
InstrumentationSettings.MODE_LOCAL_SCRIPT -> getString(R.string.fb_mode_local_title)
|
||||
InstrumentationSettings.MODE_CLEAN -> getString(R.string.fb_mode_clean_title)
|
||||
else -> getString(R.string.fb_mode_computer_title)
|
||||
}
|
||||
|
||||
private fun modeColor(mode: String, background: Boolean): Int {
|
||||
val resource = when (mode) {
|
||||
InstrumentationSettings.MODE_LOCAL_SCRIPT -> if (background) R.color.fb_glass_surface_soft else R.color.fb_success
|
||||
@@ -2132,13 +2215,14 @@ class FridaBoxActivity : AppCompatActivity() {
|
||||
}
|
||||
private fun notify(message: String) {
|
||||
Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).apply {
|
||||
setAnchorView(binding.bottomNavigation)
|
||||
setTextColor(color(R.color.fb_text_primary))
|
||||
view.background = ContextCompat.getDrawable(this@FridaBoxActivity, R.drawable.bg_glass_popup)
|
||||
view.elevation = dp(12).toFloat()
|
||||
show()
|
||||
}
|
||||
}
|
||||
private fun toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
private fun toast(message: String) = notify(message)
|
||||
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
|
||||
|
||||
private fun humanSize(bytes: Long): String {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 12l8 -4.5" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 12l0 9" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M12 12l-8 -4.5" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M16 5.25l-8 4.5" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:drawable="@drawable/ic_fb_gadget_active" />
|
||||
<item android:drawable="@drawable/ic_fb_gadget" />
|
||||
</selector>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:drawable="@drawable/ic_fb_settings_active" />
|
||||
<item android:drawable="@drawable/ic_fb_settings" />
|
||||
</selector>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M4 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2.7"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M14 15a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1l0 -4" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:drawable="@drawable/ic_fb_workspace_active" />
|
||||
<item android:drawable="@drawable/ic_fb_workspace" />
|
||||
</selector>
|
||||
@@ -145,9 +145,11 @@
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/bg_glass_navigation"
|
||||
android:elevation="14dp"
|
||||
app:itemActiveIndicatorStyle="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
|
||||
app:itemActiveIndicatorStyle="@style/Widget.FridaBox.Navigation.ActiveIndicator"
|
||||
app:itemIconTint="@color/fb_text_primary"
|
||||
app:itemRippleColor="@color/fb_glass_highlight"
|
||||
app:itemTextAppearanceActive="@style/TextAppearance.FridaBox.Navigation.Label"
|
||||
app:itemTextAppearanceInactive="@style/TextAppearance.FridaBox.Navigation.Label"
|
||||
app:itemTextColor="@color/fb_text_primary"
|
||||
app:labelVisibilityMode="labeled"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/nav_workspace" android:icon="@drawable/ic_fb_workspace" android:title="@string/fb_nav_workspace" />
|
||||
<item android:id="@+id/nav_gadgets" android:icon="@drawable/ic_fb_gadget" android:title="@string/fb_nav_gadgets" />
|
||||
<item android:id="@+id/nav_runtime" android:icon="@drawable/ic_fb_runtime" android:title="@string/fb_nav_runtime" />
|
||||
<item android:id="@+id/nav_settings" android:icon="@drawable/ic_fb_settings" android:title="@string/fb_nav_settings" />
|
||||
<item android:id="@+id/nav_workspace" android:icon="@drawable/ic_fb_workspace_nav" android:title="@string/fb_nav_workspace" />
|
||||
<item android:id="@+id/nav_gadgets" android:icon="@drawable/ic_fb_gadget_nav" android:title="@string/fb_nav_gadgets" />
|
||||
<item android:id="@+id/nav_settings" android:icon="@drawable/ic_fb_settings_nav" android:title="@string/fb_nav_settings" />
|
||||
</menu>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<color name="fb_glass_outline">#66FFFFFF</color>
|
||||
<color name="fb_glass_outline_soft">#2EFFFFFF</color>
|
||||
<color name="fb_glass_highlight">#38FFFFFF</color>
|
||||
<color name="fb_nav_indicator">#3864E8E8</color>
|
||||
<color name="fb_primary">#64E8E8</color>
|
||||
<color name="fb_primary_dark">#1AAFB5</color>
|
||||
<color name="fb_secondary">#9B87F5</color>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<string name="fb_brand_tagline">Mobile instrumentation workspace</string>
|
||||
<string name="fb_nav_workspace">Workspace</string>
|
||||
<string name="fb_nav_gadgets">Gadgets</string>
|
||||
<string name="fb_nav_runtime">Runtime</string>
|
||||
<string name="fb_nav_settings">Settings</string>
|
||||
<string name="fb_import">Import</string>
|
||||
<string name="fb_import_from_apps">Import from apps</string>
|
||||
@@ -29,21 +28,15 @@
|
||||
<string name="fb_more">Manage</string>
|
||||
<string name="fb_no_script">No JavaScript agent selected</string>
|
||||
<string name="fb_script_ready">Agent ready</string>
|
||||
<string name="fb_runtime_title">Runtime</string>
|
||||
<string name="fb_runtime_subtitle">Latest guest process and instrumentation state</string>
|
||||
<string name="fb_settings_title">Settings</string>
|
||||
<string name="fb_settings_subtitle">Connection defaults and product information</string>
|
||||
<string name="fb_save_settings">Save settings</string>
|
||||
<string name="fb_global_instrumentation">Allow instrumentation</string>
|
||||
<string name="fb_advanced_logs">Show advanced runtime details</string>
|
||||
<string name="fb_base_port">Gadget base port</string>
|
||||
<string name="fb_scan_count">Port discovery range</string>
|
||||
<string name="fb_security_title">Run only agents you trust</string>
|
||||
<string name="fb_security_body">An on-device agent executes inside the virtual guest process and has the same access as that process. FridaBox stores a private byte-for-byte copy of the selected file.</string>
|
||||
<string name="fb_about_title">Independent by design</string>
|
||||
<string name="fb_about_body">FridaBox combines a private Android virtualization layer with Frida Gadget. It does not modify imported APKs and it does not require root.</string>
|
||||
<string name="fb_copy_command">Copy command</string>
|
||||
<string name="fb_refresh">Refresh</string>
|
||||
<string name="fb_choose_trusted_title">Choose a trusted JavaScript agent?</string>
|
||||
<string name="fb_choose_trusted_body">The file will be copied into FridaBox private storage and executed automatically whenever this guest starts in On-device mode.</string>
|
||||
<string name="fb_choose">Choose file</string>
|
||||
@@ -51,7 +44,6 @@
|
||||
<string name="fb_computer_launch_title">Ready to attach from a computer?</string>
|
||||
<string name="fb_computer_launch_body">The guest will pause before its Application starts. Connect with Frida to resume it.</string>
|
||||
<string name="fb_clear_data">Clear guest data</string>
|
||||
<string name="fb_runtime_details">Runtime details</string>
|
||||
<string name="fb_remove_guest">Remove guest</string>
|
||||
<string name="fb_remove_title">Remove this guest?</string>
|
||||
<string name="fb_remove_body">The virtual app, its virtual data, and its saved on-device agent will be removed from FridaBox. The Android PackageManager installation is not changed.</string>
|
||||
@@ -78,4 +70,12 @@
|
||||
<string name="fb_gadget_download_failed">Gadget download failed: %1$s</string>
|
||||
<string name="fb_catalog_failed_body">Unable to load compatible Gadget releases from GitHub.</string>
|
||||
<string name="fb_gadget_unsupported_title">Unsupported device ABI</string>
|
||||
<string name="fb_runtime_bridges">Runtime bridges</string>
|
||||
<string name="fb_runtime_bridges_body">Available for Gadget %1$s. Enabled bridges are embedded into the private on-device agent at the next launch.</string>
|
||||
<string name="fb_runtime_bridge_toggle">Enable %1$s bridge</string>
|
||||
<string name="fb_runtime_bridge_version">Version %1$s</string>
|
||||
<string name="fb_runtime_bridge_choose_version">Choose %1$s version</string>
|
||||
<string name="fb_runtime_bridge_enabled">%1$s bridge enabled for the next on-device launch</string>
|
||||
<string name="fb_runtime_bridge_disabled">%1$s bridge disabled</string>
|
||||
<string name="fb_runtime_bridge_save_failed">Unable to save runtime bridge settings</string>
|
||||
</resources>
|
||||
|
||||
@@ -26,4 +26,23 @@
|
||||
<item name="colorOnSurface">@color/fb_text_primary</item>
|
||||
<item name="colorPrimary">@color/fb_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.FridaBox.Navigation.ActiveIndicator" parent="">
|
||||
<item name="android:width">44dp</item>
|
||||
<item name="android:height">32dp</item>
|
||||
<item name="marginHorizontal">0dp</item>
|
||||
<item name="shapeAppearance">@style/ShapeAppearance.FridaBox.NavigationPill</item>
|
||||
<item name="android:color">@color/fb_nav_indicator</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearance.FridaBox.NavigationPill" parent="ShapeAppearance.MaterialComponents.LargeComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">50%</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.FridaBox.Navigation.Label" parent="TextAppearance.MaterialComponents.Caption">
|
||||
<item name="android:fontFamily">sans</item>
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:textStyle">normal</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ ext {
|
||||
targetSdkVersion = 28
|
||||
|
||||
minSdk = 21
|
||||
versionCode = 402
|
||||
versionName = "4.1.1"
|
||||
versionCode = 403
|
||||
versionName = "4.2.0"
|
||||
xVersion = '1.1.0'
|
||||
hiddenApiBypass = '4.3'
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ Frida 17 no longer bundles runtime bridges into API-loaded GumJS agents. The
|
||||
compiled files under `scripts/dist/` are reproducible from the source scripts,
|
||||
`package.json`, and `package-lock.json`.
|
||||
|
||||
The same build command generates the optional Java and IL2CPP bridge assets
|
||||
used by On-device mode. Their version catalog and composition model are
|
||||
documented in [RUNTIME_BRIDGES.md](RUNTIME_BRIDGES.md).
|
||||
|
||||
UI control icons are generated from pinned `@tabler/icons` sources with
|
||||
`npm run generate:android-icons`. The app `check` task runs the matching
|
||||
verification script and fails if a committed Android vector is out of sync.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Runtime bridges
|
||||
|
||||
## Why this exists
|
||||
|
||||
Frida 17.0.0 removed the Java, Objective-C, and Swift runtime bridges from the
|
||||
GumJS runtime. An autonomous Gadget Script interaction therefore no longer gets
|
||||
the `Java` global implicitly. Frida's REPL and `frida-trace` remain compatible
|
||||
because `frida-tools` injects those bridges itself.
|
||||
|
||||
Primary references:
|
||||
|
||||
- `https://frida.re/news/2025/05/17/frida-17-0-0-released/`
|
||||
- `https://frida.re/docs/bridges/`
|
||||
- `https://frida.re/docs/gadget/#using-the-language-bridges`
|
||||
|
||||
`frida-il2cpp-bridge` is different: it is a community module and was never a
|
||||
built-in GumJS bridge. Its current compatibility statement covers Unity 5.3.0
|
||||
through 6000.3.x, with Android and Linux as the tested platforms:
|
||||
|
||||
- `https://github.com/vfsfitvnm/frida-il2cpp-bridge`
|
||||
|
||||
## FridaBox architecture
|
||||
|
||||
Frida scripts have isolated globals. Loading a bridge in a second Script does
|
||||
not make `Java` or `Il2Cpp` visible to the user's Script. FridaBox instead builds
|
||||
each supported package version with `frida-compile`, stores the resulting bundle
|
||||
as an APK asset, and extracts its entry module while preparing the private
|
||||
on-device agent. The final execution order is:
|
||||
|
||||
1. FridaBox log capture;
|
||||
2. enabled runtime-bridge entry modules;
|
||||
3. the user's unchanged imported agent copy.
|
||||
|
||||
Only the generated runtime copy is composed. The originally imported agent and
|
||||
its recorded digest are not modified.
|
||||
|
||||
Bridge settings appear only when the active Gadget is 17.0.0 or newer. They
|
||||
apply to On-device mode on the next process launch. Computer mode uses Gadget's
|
||||
Listen interaction; bridge availability there belongs to the attaching client
|
||||
(`frida`, `frida-trace`, or the API-created agent), because remotely created
|
||||
Scripts have separate isolates.
|
||||
|
||||
## Pinned versions
|
||||
|
||||
The APK contains these reproducible, offline choices:
|
||||
|
||||
| Bridge | Versions | Default | Source |
|
||||
| --- | --- | --- | --- |
|
||||
| Java | 7.0.11, 7.0.12, 7.0.13 | 7.0.13 | official `frida-java-bridge` npm package |
|
||||
| IL2CPP | 0.12.2, 0.13.0, 0.13.1 | 0.13.1 | community `frida-il2cpp-bridge` npm package |
|
||||
|
||||
The npm registry contained 92 Java bridge versions from 3.1.1 through 7.0.13
|
||||
and 77 published IL2CPP versions through 0.13.1 when this catalog was researched
|
||||
on 2026-07-28. FridaBox deliberately ships a recent curated set instead of every
|
||||
historical package to keep the APK small and the test surface bounded.
|
||||
|
||||
## Rebuilding
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
npm ci
|
||||
python tools/build_frida_agents.py
|
||||
```
|
||||
|
||||
The package aliases in `package.json` allow multiple versions to coexist. The
|
||||
generated bridge bundles are written to
|
||||
`app/src/main/assets/fridabox/bridges/`. Gradle's
|
||||
`verifyRuntimeBridgeAssets` task validates that every catalog entry exists and
|
||||
has a Frida bundle header.
|
||||
+25
-1
@@ -1,6 +1,30 @@
|
||||
# Test results
|
||||
|
||||
Validation date: 2026-07-27
|
||||
Validation date: 2026-07-28
|
||||
|
||||
## FridaBox 4.2.0 release validation
|
||||
|
||||
The signed 4.2.0 release was built after the complete app check and app/Bcore
|
||||
unit-test suites passed. Each split contains exactly one native ABI, reports
|
||||
`versionCode 403` and `versionName 4.2.0`, is 16 KiB ZIP-aligned, and verifies
|
||||
with APK Signature Schemes v1 and v2. All four APKs use the same production
|
||||
certificate as 4.1.0 and 4.1.1 (SHA-256
|
||||
`f8db4dcf0f9272bf75ac170e382ee31ca17fea2606a18aaed0cc4040423cb444`).
|
||||
|
||||
| ABI | Size | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `arm64-v8a` | 3,263,922 bytes | `e37af8801a3bb12c04db2140c09dfb843f8e105c04aa0c0f88f5731fa3e0cabb` |
|
||||
| `armeabi-v7a` | 3,246,369 bytes | `427b2a4328624cda5881267c4bde00088c2d7df2296f49c5d66cbf4c929b64fc` |
|
||||
| `x86` | 3,288,071 bytes | `8883788d694d42339332f495cbdb8b043198c52648ac168557f0d66dca0beab8` |
|
||||
| `x86_64` | 3,293,258 bytes | `17695849ea9ed7884d2e1a777e9cfbf47529d84ade7a8c6e9746269e9d96b249` |
|
||||
|
||||
The updated ARM64 debug build was also installed on the Samsung SM-S928B
|
||||
running Android 16/API 36. Runtime-bridge controls, version selection without a
|
||||
scroll jump, the centered Gadget browser, Settings cleanup, raised Snackbar,
|
||||
and icon-only animated bottom-navigation selection were exercised without a
|
||||
fatal exception. The other three ABI variants were build-, package-, ELF-,
|
||||
signature-, alignment-, and regression-test-validated but were not exercised
|
||||
on physical hardware in this release cycle.
|
||||
|
||||
## Host validation
|
||||
|
||||
|
||||
@@ -30,6 +30,18 @@ so their active base and split APKs remain available to the virtual runtime.
|
||||
Instrumented launches require an active Gadget. Clean mode remains available
|
||||
without one. Gadget binaries are never bundled in the FridaBox APK.
|
||||
|
||||
## Select runtime bridges
|
||||
|
||||
When the active Gadget is 17.0.0 or newer, the **Gadgets** screen shows Java and
|
||||
IL2CPP runtime-bridge controls. Enable either bridge and select one of its
|
||||
offline pinned versions. The choice is composed into the generated private
|
||||
agent on the next **On-device** launch, so a plain script may use the `Java` or
|
||||
`Il2Cpp` global without importing that package itself.
|
||||
|
||||
The switch does not mutate the imported script and cannot change a process that
|
||||
is already running. It does not apply to **Computer** mode, where each script is
|
||||
created by the attaching Frida client in its own JavaScript isolate.
|
||||
|
||||
## Choose a launch mode
|
||||
|
||||
Every guest remembers one of three independent modes:
|
||||
|
||||
@@ -3,9 +3,9 @@ agp = "8.13.2"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.12.0"
|
||||
constraintlayout = "2.2.0"
|
||||
constraintlayout = "2.2.1"
|
||||
kotlin = "2.4.10"
|
||||
coreKtx = "1.15.0"
|
||||
|
||||
|
||||
Generated
+58
-1
@@ -6,7 +6,13 @@
|
||||
"": {
|
||||
"name": "fridabox-agents",
|
||||
"dependencies": {
|
||||
"frida-java-bridge": "7.0.13"
|
||||
"frida-il2cpp-bridge-0-12-2": "npm:frida-il2cpp-bridge@0.12.2",
|
||||
"frida-il2cpp-bridge-0-13-0": "npm:frida-il2cpp-bridge@0.13.0",
|
||||
"frida-il2cpp-bridge-0-13-1": "npm:frida-il2cpp-bridge@0.13.1",
|
||||
"frida-java-bridge": "7.0.13",
|
||||
"frida-java-bridge-7-0-11": "npm:frida-java-bridge@7.0.11",
|
||||
"frida-java-bridge-7-0-12": "npm:frida-java-bridge@7.0.12",
|
||||
"frida-java-bridge-7-0-13": "npm:frida-java-bridge@7.0.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tabler/icons": "3.45.0",
|
||||
@@ -238,12 +244,63 @@
|
||||
"frida-compile": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/frida-il2cpp-bridge-0-12-2": {
|
||||
"name": "frida-il2cpp-bridge",
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/frida-il2cpp-bridge/-/frida-il2cpp-bridge-0.12.2.tgz",
|
||||
"integrity": "sha512-j9W44it4aw/6f7LBkT2bNSsH1E9OMHdWEPqWbc2zKzQEhzSfA6Ge8b9pwiBvTdmu50+09L9hzgmGN6ZpQaGJkA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"frida-il2cpp-bridge": "cli/main.py"
|
||||
}
|
||||
},
|
||||
"node_modules/frida-il2cpp-bridge-0-13-0": {
|
||||
"name": "frida-il2cpp-bridge",
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/frida-il2cpp-bridge/-/frida-il2cpp-bridge-0.13.0.tgz",
|
||||
"integrity": "sha512-y337LVmjQPFER2vRYX2klTNkWCauuj2G1WVct4QESb7ME4HC9nSVtOEfYCf77mE3zt3y5YQ0zjXR6xpwudC92A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"frida-il2cpp-bridge": "cli/main.py"
|
||||
}
|
||||
},
|
||||
"node_modules/frida-il2cpp-bridge-0-13-1": {
|
||||
"name": "frida-il2cpp-bridge",
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/frida-il2cpp-bridge/-/frida-il2cpp-bridge-0.13.1.tgz",
|
||||
"integrity": "sha512-qXjdcdI+KM5wnjCwqZTe/Tas/2pClXpDhcVWjiaQFxzSun+1vaoHEWlnMe4F09Vdz/vGkkjFOPKfvDem2A8UOQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"frida-il2cpp-bridge": "cli/main.py"
|
||||
}
|
||||
},
|
||||
"node_modules/frida-java-bridge": {
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/frida-java-bridge/-/frida-java-bridge-7.0.13.tgz",
|
||||
"integrity": "sha512-YSyKjxbxKnSi3KSUy9vciOvTOuq0RRh9dkxzkQVEdfIIZlw20zE8D3Cq9eL2FDqUVj4YKas6Wf09kCjL5zbffg==",
|
||||
"license": "LGPL-2.0 WITH WxWindows-exception-3.1"
|
||||
},
|
||||
"node_modules/frida-java-bridge-7-0-11": {
|
||||
"name": "frida-java-bridge",
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/frida-java-bridge/-/frida-java-bridge-7.0.11.tgz",
|
||||
"integrity": "sha512-px3YKGVcyBx3r2ksqnguEb1p6xlm8jqp7LX3ybaaET4fvS/8e6Ojzs7RKjaiglW/CishI4waZIuGKoSEqbrdcA==",
|
||||
"license": "LGPL-2.0 WITH WxWindows-exception-3.1"
|
||||
},
|
||||
"node_modules/frida-java-bridge-7-0-12": {
|
||||
"name": "frida-java-bridge",
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/frida-java-bridge/-/frida-java-bridge-7.0.12.tgz",
|
||||
"integrity": "sha512-xpoTFPQkUjNMeRnyZ6rUj/APlYGWpKEv5iekOk4lQeDcv9fv8J/s3ETjTRdSfyZ8/aPv9r8+oBmsxN2gCHRTqg==",
|
||||
"license": "LGPL-2.0 WITH WxWindows-exception-3.1"
|
||||
},
|
||||
"node_modules/frida-java-bridge-7-0-13": {
|
||||
"name": "frida-java-bridge",
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/frida-java-bridge/-/frida-java-bridge-7.0.13.tgz",
|
||||
"integrity": "sha512-YSyKjxbxKnSi3KSUy9vciOvTOuq0RRh9dkxzkQVEdfIIZlw20zE8D3Cq9eL2FDqUVj4YKas6Wf09kCjL5zbffg==",
|
||||
"license": "LGPL-2.0 WITH WxWindows-exception-3.1"
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
|
||||
+8
-1
@@ -2,11 +2,18 @@
|
||||
"name": "fridabox-agents",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:frida-agents": "python tools/build_frida_agents.py",
|
||||
"generate:android-icons": "node tools/generate_tabler_android_icons.mjs",
|
||||
"verify:android-icons": "node tools/generate_tabler_android_icons.mjs --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"frida-java-bridge": "7.0.13"
|
||||
"frida-java-bridge": "7.0.13",
|
||||
"frida-java-bridge-7-0-11": "npm:frida-java-bridge@7.0.11",
|
||||
"frida-java-bridge-7-0-12": "npm:frida-java-bridge@7.0.12",
|
||||
"frida-java-bridge-7-0-13": "npm:frida-java-bridge@7.0.13",
|
||||
"frida-il2cpp-bridge-0-12-2": "npm:frida-il2cpp-bridge@0.12.2",
|
||||
"frida-il2cpp-bridge-0-13-0": "npm:frida-il2cpp-bridge@0.13.0",
|
||||
"frida-il2cpp-bridge-0-13-1": "npm:frida-il2cpp-bridge@0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tabler/icons": "3.45.0",
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
import JavaBridge from 'frida-java-bridge-7-0-13';
|
||||
|
||||
if (typeof globalThis.Java === 'undefined') globalThis.Java = JavaBridge;
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Frida 17 Java agents with the explicitly pinned Java bridge."""
|
||||
"""Build Frida agents and the version-pinned offline runtime bridges."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,6 +12,16 @@ ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
OUTPUT = SCRIPTS / "dist"
|
||||
ENTRIES = ("registry-probe.js", "guest-bootstrap.js", "sample-hook.js")
|
||||
BRIDGE_SCRIPTS = SCRIPTS / "bridges"
|
||||
BRIDGE_OUTPUT = ROOT / "app" / "src" / "main" / "assets" / "fridabox" / "bridges"
|
||||
BRIDGES = (
|
||||
("java", "7.0.11"),
|
||||
("java", "7.0.12"),
|
||||
("java", "7.0.13"),
|
||||
("il2cpp", "0.12.2"),
|
||||
("il2cpp", "0.13.0"),
|
||||
("il2cpp", "0.13.1"),
|
||||
)
|
||||
|
||||
|
||||
def compiler_path() -> pathlib.Path:
|
||||
@@ -31,7 +41,18 @@ def main() -> int:
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
)
|
||||
BRIDGE_OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
for bridge_id, version in BRIDGES:
|
||||
name = f"{bridge_id}-{version}.js"
|
||||
subprocess.run(
|
||||
[str(compiler), str(BRIDGE_SCRIPTS / name), "-o", str(BRIDGE_OUTPUT / name), "-S", "-c"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
)
|
||||
print("Built Frida agents: " + ", ".join(str(OUTPUT / name) for name in ENTRIES))
|
||||
print("Built runtime bridges: " + ", ".join(
|
||||
str(BRIDGE_OUTPUT / f"{bridge_id}-{version}.js") for bridge_id, version in BRIDGES
|
||||
))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user