android-reverse-engineering.../plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh

357 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# rebuild-apk.sh — Rebuild and sign a decoded APK directory
#
# Pipeline: apktool b → zipalign (optional) → sign → verify
#
# Exit codes:
# 0 — success
# 1 — error (build failed, signing failed)
# 2 — manual action needed (missing tools)
set -euo pipefail
usage() {
cat <<EOF
Usage: rebuild-apk.sh <decoded-dir> [OPTIONS]
Rebuild an apktool-decoded APK directory back into a signed APK.
Arguments:
<decoded-dir> Path to the apktool-decoded APK directory
Options:
-o, --output <file> Output APK path (default: <decoded-dir>-neutralized.apk)
--debug-key Sign with an auto-generated debug keystore (default)
--keystore <file> Path to a custom keystore file
--key-alias <alias> Key alias within the keystore (default: key0)
--key-pass <password> Key password (default: android)
--store-pass <password> Keystore password (default: android)
--no-sign Skip signing (output unsigned APK)
--no-res Skip resource recompilation (apktool b --no-res)
--zipalign Run zipalign before signing (recommended, default if available)
--no-zipalign Skip zipalign
-h, --help Show this help message
Output:
BUILD_OK:<output-apk>
BUILD_WARNING:Resources were not recompiled (--no-res fallback)
SIGN_OK:<output-apk>
VERIFY_OK:<output-apk>
EOF
exit 0
}
# =====================================================================
# Argument parsing
# =====================================================================
DECODED_DIR=""
OUTPUT=""
USE_DEBUG_KEY=true
KEYSTORE=""
KEY_ALIAS="key0"
KEY_PASS="android"
STORE_PASS="android"
DO_SIGN=true
DO_ZIPALIGN=true
NO_ZIPALIGN=false
FORCE_NO_RES=false
BUILD_USED_NO_RES=false
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
shift
if [[ $# -eq 0 ]]; then echo "Error: --output requires a file argument" >&2; exit 1; fi
OUTPUT="$1"; shift ;;
--debug-key) USE_DEBUG_KEY=true; shift ;;
--keystore)
shift
if [[ $# -eq 0 ]]; then echo "Error: --keystore requires a file argument" >&2; exit 1; fi
KEYSTORE="$1"; USE_DEBUG_KEY=false; shift ;;
--key-alias)
shift
if [[ $# -eq 0 ]]; then echo "Error: --key-alias requires an argument" >&2; exit 1; fi
KEY_ALIAS="$1"; shift ;;
--key-pass)
shift
if [[ $# -eq 0 ]]; then echo "Error: --key-pass requires an argument" >&2; exit 1; fi
KEY_PASS="$1"; shift ;;
--store-pass)
shift
if [[ $# -eq 0 ]]; then echo "Error: --store-pass requires an argument" >&2; exit 1; fi
STORE_PASS="$1"; shift ;;
--no-sign) DO_SIGN=false; shift ;;
--no-res) FORCE_NO_RES=true; shift ;;
--zipalign) DO_ZIPALIGN=true; shift ;;
--no-zipalign) NO_ZIPALIGN=true; DO_ZIPALIGN=false; shift ;;
-h|--help) usage ;;
-*) echo "Error: Unknown option $1" >&2; usage ;;
*) DECODED_DIR="$1"; shift ;;
esac
done
if [[ -z "$DECODED_DIR" ]]; then
echo "Error: No decoded directory specified." >&2
usage
fi
if [[ ! -d "$DECODED_DIR" ]]; then
echo "Error: Directory not found: $DECODED_DIR" >&2
exit 1
fi
# Default output name
if [[ -z "$OUTPUT" ]]; then
# Strip trailing slash
local_dir="${DECODED_DIR%/}"
OUTPUT="${local_dir}-neutralized.apk"
fi
# =====================================================================
# Tool checks
# =====================================================================
info() { echo "[INFO] $*"; }
ok() { echo "[OK] $*"; }
fail() { echo "[FAIL] $*" >&2; }
if ! command -v apktool &>/dev/null; then
fail "apktool is not installed. Run: install-dep.sh apktool"
exit 1
fi
if ! command -v java &>/dev/null; then
fail "java is not installed. Run: install-dep.sh java"
exit 1
fi
# Determine signing tool
SIGNER=""
if [[ "$DO_SIGN" == true ]]; then
if command -v apksigner &>/dev/null; then
SIGNER="apksigner"
elif command -v jarsigner &>/dev/null; then
SIGNER="jarsigner"
else
fail "Neither apksigner nor jarsigner found. Install apksigner or use --no-sign."
exit 1
fi
fi
# Check zipalign availability
ZIPALIGN_CMD=""
if [[ "$DO_ZIPALIGN" == true ]] && [[ "$NO_ZIPALIGN" == false ]]; then
if command -v zipalign &>/dev/null; then
ZIPALIGN_CMD="zipalign"
else
# Check Android SDK
for sdk_dir in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}"; do
if [[ -n "$sdk_dir" ]] && [[ -d "$sdk_dir/build-tools" ]]; then
latest_bt=$(ls -1 "$sdk_dir/build-tools" 2>/dev/null | sort -V | tail -1)
if [[ -n "$latest_bt" ]] && [[ -f "$sdk_dir/build-tools/$latest_bt/zipalign" ]]; then
ZIPALIGN_CMD="$sdk_dir/build-tools/$latest_bt/zipalign"
break
fi
fi
done
if [[ -z "$ZIPALIGN_CMD" ]]; then
info "zipalign not found — skipping alignment (APK will still work)"
DO_ZIPALIGN=false
fi
fi
fi
# =====================================================================
# Step 1: Build APK with apktool
# =====================================================================
echo "=== Rebuilding APK ==="
echo "Source: $DECODED_DIR"
echo "Output: $OUTPUT"
echo
info "Running apktool build..."
BUILT_APK="$DECODED_DIR/dist/$(basename "$DECODED_DIR").apk"
build_apk() {
local no_res_flag="${1:-}"
# Clean previous build artifacts
rm -rf "$DECODED_DIR/dist/" 2>/dev/null || true
if apktool b $no_res_flag "$DECODED_DIR" 2>&1; then
return 0
fi
return 1
}
if [[ "$FORCE_NO_RES" == true ]]; then
# User explicitly requested --no-res
if ! build_apk "--no-res"; then
fail "apktool build failed (with --no-res)."
echo "Tip: If this is a framework error, try: rm -f ~/.local/share/apktool/framework/1.apk"
exit 1
fi
BUILD_USED_NO_RES=true
else
# First attempt: normal build
if ! build_apk ""; then
info "Build failed — retrying with --no-res (skipping resource recompilation)..."
if ! build_apk "--no-res"; then
fail "apktool build failed (both normal and --no-res)."
echo "Tip: If this is a framework error, try: rm -f ~/.local/share/apktool/framework/1.apk"
exit 1
fi
BUILD_USED_NO_RES=true
fi
fi
# apktool outputs to dist/ inside the decoded dir
if [[ ! -f "$BUILT_APK" ]]; then
# Try finding any APK in dist/
BUILT_APK=$(find "$DECODED_DIR/dist/" -name "*.apk" -type f | head -1)
if [[ -z "$BUILT_APK" ]] || [[ ! -f "$BUILT_APK" ]]; then
fail "Built APK not found in $DECODED_DIR/dist/"
exit 1
fi
fi
ok "APK built: $BUILT_APK"
echo "BUILD_OK:$BUILT_APK"
if [[ "$BUILD_USED_NO_RES" == true ]]; then
echo "BUILD_WARNING:Resources were not recompiled (--no-res fallback)"
fi
# =====================================================================
# Step 2: Zipalign (optional)
# =====================================================================
ALIGNED_APK="$BUILT_APK"
if [[ "$DO_ZIPALIGN" == true ]] && [[ -n "$ZIPALIGN_CMD" ]]; then
info "Running zipalign..."
ALIGNED_APK="${BUILT_APK%.apk}-aligned.apk"
if "$ZIPALIGN_CMD" -f 4 "$BUILT_APK" "$ALIGNED_APK"; then
ok "Zipaligned: $ALIGNED_APK"
# Remove unaligned APK
rm -f "$BUILT_APK"
else
info "Zipalign failed — continuing with unaligned APK"
ALIGNED_APK="$BUILT_APK"
fi
fi
# =====================================================================
# Step 3: Sign APK
# =====================================================================
if [[ "$DO_SIGN" == false ]]; then
cp "$ALIGNED_APK" "$OUTPUT"
ok "Unsigned APK saved to: $OUTPUT"
echo "BUILD_OK:$OUTPUT"
echo
echo "WARNING: APK is unsigned and cannot be installed without signing."
exit 0
fi
# Generate debug keystore if needed
if [[ "$USE_DEBUG_KEY" == true ]]; then
KEYSTORE="$DECODED_DIR/.neutralizer-debug.keystore"
if [[ ! -f "$KEYSTORE" ]]; then
info "Generating debug keystore..."
keytool -genkeypair \
-keystore "$KEYSTORE" \
-alias "$KEY_ALIAS" \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass "$STORE_PASS" \
-keypass "$KEY_PASS" \
-dname "CN=SDK Neutralizer Debug Key, OU=Debug, O=Debug, L=Unknown, ST=Unknown, C=US" \
2>/dev/null
ok "Debug keystore generated: $KEYSTORE"
fi
fi
if [[ ! -f "$KEYSTORE" ]]; then
fail "Keystore not found: $KEYSTORE"
exit 1
fi
info "Signing APK with $SIGNER..."
if [[ "$SIGNER" == "apksigner" ]]; then
apksigner sign \
--ks "$KEYSTORE" \
--ks-key-alias "$KEY_ALIAS" \
--ks-pass "pass:$STORE_PASS" \
--key-pass "pass:$KEY_PASS" \
--out "$OUTPUT" \
"$ALIGNED_APK"
elif [[ "$SIGNER" == "jarsigner" ]]; then
# jarsigner signs in-place, so copy first
cp "$ALIGNED_APK" "$OUTPUT"
jarsigner \
-keystore "$KEYSTORE" \
-storepass "$STORE_PASS" \
-keypass "$KEY_PASS" \
-signedjar "$OUTPUT" \
"$ALIGNED_APK" \
"$KEY_ALIAS"
fi
if [[ ! -f "$OUTPUT" ]]; then
fail "Signed APK not found at: $OUTPUT"
exit 1
fi
ok "APK signed: $OUTPUT"
echo "SIGN_OK:$OUTPUT"
# Clean up intermediate files
rm -f "$ALIGNED_APK" 2>/dev/null || true
# =====================================================================
# Step 4: Verify signature
# =====================================================================
info "Verifying signature..."
if [[ "$SIGNER" == "apksigner" ]]; then
if apksigner verify "$OUTPUT" 2>/dev/null; then
ok "Signature verified (apksigner)"
echo "VERIFY_OK:$OUTPUT"
else
info "Signature verification returned warnings (may still be installable)"
fi
elif [[ "$SIGNER" == "jarsigner" ]]; then
if jarsigner -verify "$OUTPUT" 2>/dev/null; then
ok "Signature verified (jarsigner)"
echo "VERIFY_OK:$OUTPUT"
else
info "Signature verification returned warnings (may still be installable)"
fi
fi
# =====================================================================
# Summary
# =====================================================================
echo
echo "=== Rebuild Complete ==="
echo "Output APK: $OUTPUT"
echo "Signed with: $SIGNER ($( [[ "$USE_DEBUG_KEY" == true ]] && echo "debug key" || echo "custom keystore" ))"
APK_SIZE=$(stat -f%z "$OUTPUT" 2>/dev/null || stat -c%s "$OUTPUT" 2>/dev/null || echo "unknown")
echo "APK size: $APK_SIZE bytes"
if [[ "$BUILD_USED_NO_RES" == true ]]; then
echo
echo "NOTE: Resources were NOT recompiled (--no-res was used)."
echo " The APK uses original resources. Manifest XML changes (e.g., android:enabled)"
echo " may not be reflected unless they were applied before decode."
fi
echo
echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading."
echo "Install via: adb install $OUTPUT"
exit 0