Files
2026-07-09 07:40:42 +02:00

71 lines
3.8 KiB
Bash

#!/usr/bin/env bash
# verify_apk.sh - structural validation of a built APK.
# Checks: zip integrity, dex present, manifest present, per-ABI native libs are the RIGHT ELF class,
# and (for arm64) that ALL required libs (libfmod, libg, libcr) exist as ELF64 AArch64.
# Usage: verify_apk.sh <app.apk> [expected-abi]
set -euo pipefail
APK="${1:?usage: verify_apk.sh <app.apk> [armeabi-v7a|arm64-v8a|universal]}"
EXPECT="${2:-}"
[ -f "$APK" ] || { echo "APK not found: $APK"; exit 1; }
REQUIRED_LIBS=(libfmod.so libg.so) # libcr is OPTIONAL (owned mod; loadLibrary("cr") is caught)
fail=0
echo "[*] Zip integrity"; unzip -tq "$APK" >/dev/null && echo " ok" || { echo " CORRUPT"; fail=1; }
# Cache the entry listing once (piping a large `unzip -l` into `grep -q` trips SIGPIPE under pipefail).
ENTRIES="$(unzip -Z1 "$APK" 2>/dev/null || true)"
echo "[*] classes.dex present"; printf '%s\n' "$ENTRIES" | grep -q '^classes.*\.dex$' && echo " ok" || { echo " MISSING dex"; fail=1; }
echo "[*] AndroidManifest present"; printf '%s\n' "$ENTRIES" | grep -q '^AndroidManifest.xml$' && echo " ok" || { echo " MISSING manifest"; fail=1; }
echo "[*] Native libs / ABI check"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
unzip -qo "$APK" 'lib/*' -d "$TMP" 2>/dev/null || true
mapfile -t ABIS < <(ls "$TMP/lib" 2>/dev/null || true)
if [ "${#ABIS[@]}" -eq 0 ]; then echo " NO native libs in APK"; fail=1; fi
for abi in "${ABIS[@]}"; do
echo " ABI $abi:"
case "$abi" in
armeabi-v7a) want="ELF 32-bit.*ARM";;
arm64-v8a) want="ELF 64-bit.*aarch64";; # grep -i also matches "AArch64"
*) want=".";;
esac
for f in "$TMP/lib/$abi"/*.so; do
[ -e "$f" ] || continue
desc="$(file -b "$f")"
if echo "$desc" | grep -qiE "$want"; then echo " ok $(basename "$f") [$desc]"
else echo " BAD $(basename "$f") expected /$want/ got: $desc"; fail=1; fi
done
# arm64 completeness gate
if [ "$abi" = "arm64-v8a" ]; then
for req in "${REQUIRED_LIBS[@]}"; do
[ -e "$TMP/lib/$abi/$req" ] || { echo " MISSING required arm64 lib: $req (arm64 INCOMPLETE)"; fail=1; }
done
fi
# Dependency-resolution gate: every NEEDED lib of every bundled .so must be either bundled in the
# APK or a public Android system library. Catches the classic "NEEDED libc++_shared.so but not
# bundled" load failure (libc++_shared is NOT a system lib) before it reaches a device.
READELF="$(command -v llvm-readelf || command -v readelf || true)"
[ -n "$READELF" ] || READELF="$(ls "$PWD"/tools/android-sdk/ndk/*/toolchains/llvm/prebuilt/*/bin/llvm-readelf 2>/dev/null | head -1)"
if [ -n "$READELF" ]; then
# Public system libs available to apps on modern Android (NDK-accessible + legacy libstdc++).
SYSLIBS=" libc.so libm.so libdl.so liblog.so libandroid.so libGLESv1_CM.so libGLESv2.so libGLESv3.so libEGL.so libz.so libjnigraphics.so libOpenSLES.so libOpenMAXAL.so libvulkan.so libstdc++.so libmediandk.so libnativewindow.so libcamera2ndk.so "
BUNDLED=" $(ls "$TMP/lib/$abi"/*.so 2>/dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ') "
for f in "$TMP/lib/$abi"/*.so; do
[ -e "$f" ] || continue
while read -r dep; do
[ -n "$dep" ] || continue
if [[ "$BUNDLED" == *" $dep "* ]] || [[ "$SYSLIBS" == *" $dep "* ]]; then :; \
else echo " UNRESOLVED dep: $(basename "$f") needs [$dep] (not bundled, not a system lib)"; fail=1; fi
done < <("$READELF" -d "$f" 2>/dev/null | grep NEEDED | grep -oE '\[[^]]+\]' | tr -d '[]')
done
echo " dep-resolution ok for $abi"
else
echo " (readelf unavailable - skipped dependency-resolution gate)"
fi
done
[ -z "$EXPECT" ] || { [ -d "$TMP/lib/$EXPECT" ] || [ "$EXPECT" = universal ] || { echo "[!] expected ABI '$EXPECT' not in APK"; fail=1; }; }
if [ "$fail" -eq 0 ]; then echo "[+] VERIFY OK: $APK"; else echo "[x] VERIFY FAILED: $APK"; fi
exit "$fail"