Fix runtime ABI selection across release variants

This commit is contained in:
Mahdi Karzari
2026-07-28 13:12:44 +03:30
parent 3b771faa21
commit 4bfc6b5a42
15 changed files with 266 additions and 49 deletions
@@ -68,6 +68,7 @@ import top.niunaijun.blackbox.utils.StackTraceFilter;
import top.niunaijun.blackbox.utils.SocialMediaAppCrashPrevention;
import top.niunaijun.blackbox.utils.DexCrashPrevention;
import top.niunaijun.blackbox.utils.NativeCrashPrevention;
import top.niunaijun.blackbox.utils.ProcessAbi;
import top.niunaijun.blackbox.utils.CrashMonitor;
import top.niunaijun.blackbox.utils.StoragePermissionHelper;
import top.niunaijun.blackbox.utils.LogSender;
@@ -1463,11 +1464,9 @@ public class BlackBoxCore extends ClientConfiguration {
}
public static boolean is64Bit() {
if (BuildCompat.isM()) {
return Process.is64Bit();
} else {
return Build.CPU_ABI.equals("arm64-v8a");
}
String processAbi = ProcessAbi.detect(getContext());
return processAbi != null ? ProcessAbi.is64Bit(processAbi)
: BuildCompat.isM() && Process.is64Bit();
}
private void initNotificationManager() {
@@ -686,7 +686,7 @@ public class BPackageManagerService extends IBPackageManagerService.Stub impleme
if (!support) {
String msg = packageArchiveInfo.applicationInfo.loadLabel(BlackBoxCore.getPackageManager()) + "[" + packageArchiveInfo.packageName + "]";
return result.installError(packageArchiveInfo.packageName,
msg + "\n" + (BlackBoxCore.is64Bit() ? "The box does not support 32-bit Application" : "The box does not support 64-bit Application"));
msg + "\nThe application has no native libraries for the FridaBox process ABI");
}
PackageParser.Package aPackage = parserApk(apkFile.getAbsolutePath());
if (aPackage == null) {
@@ -808,4 +808,4 @@ public class BPackageManagerService extends IBPackageManagerService.Stub impleme
mComponentResolver.addAllComponents(value.pkg);
}
}
}
}
@@ -15,7 +15,6 @@ import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import java.util.HashSet;
import java.util.Set;
@@ -31,6 +30,7 @@ import top.niunaijun.blackbox.core.env.BEnvironment;
import top.niunaijun.blackbox.entity.pm.InstallOption;
import top.niunaijun.blackbox.utils.ArrayUtils;
import top.niunaijun.blackbox.utils.FileUtils;
import top.niunaijun.blackbox.utils.ProcessAbi;
import top.niunaijun.blackbox.utils.compat.BuildCompat;
@@ -308,7 +308,7 @@ public class PackageManagerCompat {
if (BuildCompat.isL()) {
BRApplicationInfoL.get(ai)._set_primaryCpuAbi(Build.CPU_ABI);
BRApplicationInfoL.get(ai)._set_primaryCpuAbi(ProcessAbi.detect(BlackBoxCore.getContext()));
BRApplicationInfoL.get(ai)._set_scanPublicSourceDir(BRApplicationInfoL.get(baseApplication).scanPublicSourceDir());
BRApplicationInfoL.get(ai)._set_scanSourceDir(BRApplicationInfoL.get(baseApplication).scanSourceDir());
}
@@ -1,7 +1,6 @@
package top.niunaijun.blackbox.instrumentation;
import android.content.Context;
import android.os.Build;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -11,6 +10,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import top.niunaijun.blackbox.BlackBoxCore;
import top.niunaijun.blackbox.utils.ProcessAbi;
/** Resolves a selected, downloaded Gadget and prepares per-guest runtime copies. */
final class DownloadedGadgetRuntime {
@@ -48,8 +48,8 @@ final class DownloadedGadgetRuntime {
if (!isInside(root, selected) || !selected.isFile()) {
throw new IOException("The selected Frida Gadget is missing or outside private storage");
}
if (!supportsAbi(selectedAbi)) {
throw new IOException("The selected Frida Gadget ABI is not supported by this device");
if (!supportsAbi(context, selectedAbi)) {
throw new IOException("The selected Frida Gadget ABI does not match this process");
}
validateElf(selected, selectedAbi);
return selected;
@@ -108,12 +108,8 @@ final class DownloadedGadgetRuntime {
return -1;
}
private static boolean supportsAbi(String abi) {
if (abi == null) return false;
for (String supported : Build.SUPPORTED_ABIS) {
if (abi.equals(supported)) return true;
}
return false;
private static boolean supportsAbi(Context context, String abi) {
return abi != null && abi.equals(ProcessAbi.detect(context));
}
private static boolean isInside(File root, File child) {
@@ -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) {
@@ -71,4 +67,11 @@ public class AbiUtils {
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,13 +25,13 @@ public class NativeUtils {
nativeLibDir.mkdirs();
}
try (ZipFile zipfile = new ZipFile(apk.getAbsolutePath())) {
for (String abi : Build.SUPPORTED_ABIS) {
if (findAndCopyNativeLib(zipfile, abi, nativeLibDir)) {
return;
}
String processAbi = ProcessAbi.detect(BlackBoxCore.getContext());
if (processAbi != null && findAndCopyNativeLib(zipfile, processAbi, nativeLibDir)) {
return;
}
if ("armeabi-v7a".equals(processAbi)) {
findAndCopyNativeLib(zipfile, "armeabi", nativeLibDir);
}
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);
}
}
@@ -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;
}
}
+11
View File
@@ -5,6 +5,17 @@ versioning for its public releases.
## [Unreleased]
## [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
@@ -14,14 +14,18 @@ public final class ApkInspector {
}
public static Result inspect(File apk) throws IOException {
return inspect(Collections.singletonList(apk));
return inspect(Collections.singletonList(apk), null);
}
public static Result inspect(File... apks) throws IOException {
return inspect(Arrays.asList(apks));
return inspect(Arrays.asList(apks), null);
}
public static Result inspect(Iterable<File> apks) throws IOException {
return inspect(apks, null);
}
public static Result inspect(Iterable<File> apks, String processAbi) throws IOException {
Set<String> abis = new LinkedHashSet<>();
boolean hasNativeLibraries = false;
boolean inspectedAny = false;
@@ -44,27 +48,41 @@ public final class ApkInspector {
}
}
if (!inspectedAny) throw new IOException("No APK files were provided");
boolean supported = !hasNativeLibraries || !Collections.disjoint(abis, SUPPORTED_ABIS);
return new Result(hasNativeLibraries, supported, abis);
boolean supported = !hasNativeLibraries || supportsProcessAbi(abis, processAbi);
return new Result(hasNativeLibraries, supported, abis, processAbi);
}
public static final class Result {
public final boolean hasNativeLibraries;
public final boolean supported;
public final Set<String> abis;
public final String processAbi;
Result(boolean hasNativeLibraries, boolean supported, Set<String> abis) {
Result(boolean hasNativeLibraries, boolean supported, Set<String> abis, String processAbi) {
this.hasNativeLibraries = hasNativeLibraries;
this.supported = supported;
this.abis = Collections.unmodifiableSet(new LinkedHashSet<>(abis));
this.processAbi = processAbi;
}
public String description() {
if (!hasNativeLibraries) return "Pure Java/Kotlin (accepted)";
return supported ? "Supported ABI: " + abis : "Rejected; unsupported ABI: " + 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"
)));
@@ -54,6 +54,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.utils.ProcessAbi
import com.qm4rs.fridabox.databinding.ActivityFridaboxBinding
import java.io.File
import java.io.FileOutputStream
@@ -1350,7 +1351,10 @@ class FridaBoxActivity : AppCompatActivity() {
require(apkFiles.isNotEmpty() && apkFiles.all { it.isFile }) {
"Installed APK files are unavailable"
}
val abi = ApkInspector.inspect(apkFiles)
val processAbi = requireNotNull(ProcessAbi.detect(this)) {
"Unable to determine the FridaBox process ABI"
}
val abi = ApkInspector.inspect(apkFiles, processAbi)
if (!abi.supported) error("Unsupported native ABI: ${abi.description()}")
val originalHash = installedPackageSha256(apkFiles)
val install = BlackBoxCore.get().installPackageAsUser(packageName, 0)
@@ -1411,7 +1415,10 @@ class FridaBoxActivity : AppCompatActivity() {
val archiveInfo = packageManager.getPackageArchiveInfo(temporary.absolutePath, PackageManager.GET_META_DATA)
?: error("Android could not parse this APK")
if (!archiveInfo.splitNames.isNullOrEmpty()) error("Split-only APKs are not supported")
val abi = ApkInspector.inspect(temporary)
val processAbi = requireNotNull(ProcessAbi.detect(this)) {
"Unable to determine the FridaBox process ABI"
}
val abi = ApkInspector.inspect(listOf(temporary), processAbi)
if (!abi.supported) error("Unsupported native ABI: ${abi.description()}")
val safePackage = safePackageName(archiveInfo.packageName)
val stored = File(directory, "$safePackage-${originalHash.take(12)}.apk")
@@ -1,11 +1,11 @@
package com.qm4rs.fridabox
import android.content.Context
import android.os.Build
import org.json.JSONArray
import org.json.JSONObject
import org.tukaani.xz.XZInputStream
import top.niunaijun.blackbox.instrumentation.InstrumentationSettings
import top.niunaijun.blackbox.utils.ProcessAbi
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
@@ -42,8 +42,10 @@ data class GadgetAbi(val androidName: String, val releaseName: String) {
GadgetAbi("x86", "x86")
)
fun detect(): GadgetAbi? = Build.SUPPORTED_ABIS
.firstNotNullOfOrNull { deviceAbi -> supported.firstOrNull { it.androidName == deviceAbi } }
fun detect(context: Context): GadgetAbi? {
val processAbi = ProcessAbi.detect(context) ?: return null
return supported.firstOrNull { it.androidName == processAbi }
}
}
}
@@ -85,7 +87,7 @@ class GadgetManager(private val context: Context) {
}
}
fun detectedAbi(): GadgetAbi? = GadgetAbi.detect()
fun detectedAbi(): GadgetAbi? = GadgetAbi.detect(context)
fun loadCatalog(source: GadgetSource, abi: GadgetAbi, page: Int): GadgetCatalog {
val cache = File(cacheRoot, "${source.id}-${abi.androidName}-$page.json")
@@ -18,28 +18,31 @@ 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 thirtyTwoBitOnlyApkIsAccepted() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(apkWith("lib/armeabi-v7a/libsample.so"));
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(apkWith("lib/x86/libsample.so"));
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/x86/libsample.so")), "x86");
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@@ -48,11 +51,27 @@ public class ApkInspectorTest {
public void splitNativeLibrariesAreInspectedTogether() throws Exception {
File base = apkWith("classes.dex");
File arm64Split = apkWith("lib/arm64-v8a/libsample.so");
ApkInspector.Result result = ApkInspector.inspect(base, arm64Split);
ApkInspector.Result result = ApkInspector.inspect(java.util.Arrays.asList(base, arm64Split), "arm64-v8a");
assertTrue(result.hasNativeLibraries);
assertTrue(result.supported);
}
@Test
public void armGuestIsRejectedByX86Process() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/armeabi-v7a/libsample.so")), "x86");
assertTrue(result.hasNativeLibraries);
assertFalse(result.supported);
}
@Test
public void thirtyTwoBitGuestIsRejectedBySixtyFourBitProcessOfSameFamily() throws Exception {
ApkInspector.Result result = ApkInspector.inspect(java.util.Collections.singletonList(
apkWith("lib/armeabi-v7a/libsample.so")), "arm64-v8a");
assertTrue(result.hasNativeLibraries);
assertFalse(result.supported);
}
private File apkWith(String entry) throws Exception {
File file = temporary.newFile("test-" + System.nanoTime() + ".apk");
try (ZipOutputStream output = new ZipOutputStream(new FileOutputStream(file))) {
+2 -2
View File
@@ -10,8 +10,8 @@ ext {
targetSdkVersion = 28
minSdk = 21
versionCode = 401
versionName = "4.1.0"
versionCode = 402
versionName = "4.1.1"
xVersion = '1.1.0'
hiddenApiBypass = '4.3'
}