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

228 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# decode-apk.sh — Decode an APK or XAPK into smali using apktool
#
# Handles both .apk (direct decode) and .xapk (extract base APK, then decode).
#
# Exit codes:
# 0 — success
# 1 — error (invalid input, missing tools, decode failed)
set -euo pipefail
usage() {
cat <<EOF
Usage: decode-apk.sh <file> [OPTIONS]
Decode an APK or XAPK file into smali and resources using apktool.
For XAPK files, extracts the base APK from the archive and decodes it.
Split APKs (config.*.apk) are skipped with a warning.
Arguments:
<file> Path to .apk or .xapk file
Options:
-o, --output <dir> Output directory (default: <basename>-decoded)
-f, --force Overwrite output directory if it exists (default)
--no-force Do not overwrite existing output directory
-h, --help Show this help message
Output:
DECODED_DIR:<path>
EOF
exit 0
}
# =====================================================================
# Argument parsing
# =====================================================================
INPUT_FILE=""
OUTPUT_DIR=""
FORCE=true
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
shift
if [[ $# -eq 0 ]]; then echo "Error: --output requires a directory argument" >&2; exit 1; fi
OUTPUT_DIR="$1"; shift ;;
-f|--force) FORCE=true; shift ;;
--no-force) FORCE=false; shift ;;
-h|--help) usage ;;
-*) echo "Error: Unknown option $1" >&2; usage ;;
*) INPUT_FILE="$1"; shift ;;
esac
done
if [[ -z "$INPUT_FILE" ]]; then
echo "Error: No input file specified." >&2
usage
fi
if [[ ! -f "$INPUT_FILE" ]]; then
echo "Error: File not found: $INPUT_FILE" >&2
exit 1
fi
# Check apktool
if ! command -v apktool &>/dev/null; then
echo "Error: apktool is not installed or not in PATH." >&2
echo "Run: install-dep.sh apktool" >&2
exit 1
fi
# Determine file type
ext_lower="${INPUT_FILE##*.}"
ext_lower=$(echo "$ext_lower" | tr '[:upper:]' '[:lower:]')
case "$ext_lower" in
apk|xapk) ;;
*)
echo "Error: Unsupported file type '.$ext_lower'. Expected .apk or .xapk" >&2
exit 1
;;
esac
BASENAME=$(basename "$INPUT_FILE" ".$ext_lower")
INPUT_FILE_ABS=$(realpath "$INPUT_FILE")
if [[ -z "$OUTPUT_DIR" ]]; then
OUTPUT_DIR="${BASENAME}-decoded"
fi
# =====================================================================
# XAPK handling — extract base APK
# =====================================================================
APK_TO_DECODE="$INPUT_FILE_ABS"
XAPK_TMPDIR=""
cleanup_xapk() {
if [[ -n "$XAPK_TMPDIR" ]] && [[ -d "$XAPK_TMPDIR" ]]; then
rm -rf "$XAPK_TMPDIR"
fi
}
trap cleanup_xapk EXIT
if [[ "$ext_lower" == "xapk" ]]; then
echo "=== Extracting XAPK archive ==="
XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX")
unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_TMPDIR"
# Show manifest.json if present
if [[ -f "$XAPK_TMPDIR/manifest.json" ]]; then
echo "XAPK manifest found."
fi
# Collect all APK files
all_apks=()
while IFS= read -r -d '' apk_file; do
all_apks+=("$apk_file")
done < <(find "$XAPK_TMPDIR" -name "*.apk" -print0 | sort -z)
if [[ ${#all_apks[@]} -eq 0 ]]; then
echo "Error: No APK files found inside XAPK archive." >&2
exit 1
fi
echo "Found ${#all_apks[@]} APK(s) inside XAPK:"
for f in "${all_apks[@]}"; do
echo " - $(basename "$f")"
done
# Select base APK: prefer "base.apk" by name
base_apk=""
for f in "${all_apks[@]}"; do
if [[ "$(basename "$f")" == "base.apk" ]]; then
base_apk="$f"
break
fi
done
# Fallback: largest APK excluding config.*.apk
if [[ -z "$base_apk" ]]; then
largest_size=0
for f in "${all_apks[@]}"; do
fname=$(basename "$f")
# Skip config splits
if [[ "$fname" == config.* ]]; then
continue
fi
fsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || echo 0)
if (( fsize > largest_size )); then
largest_size=$fsize
base_apk="$f"
fi
done
fi
if [[ -z "$base_apk" ]]; then
echo "Error: Could not identify a base APK inside the XAPK." >&2
exit 1
fi
echo
echo "Selected base APK: $(basename "$base_apk")"
# Warn about skipped splits
skipped=0
for f in "${all_apks[@]}"; do
if [[ "$f" != "$base_apk" ]]; then
echo " [skipped] $(basename "$f")"
skipped=$((skipped + 1))
fi
done
if (( skipped > 0 )); then
echo "Warning: $skipped split APK(s) skipped. Only the base APK is decoded."
echo " Split APKs contain config-specific resources (density, locale, ABI)."
fi
echo
APK_TO_DECODE="$base_apk"
fi
# =====================================================================
# Decode with apktool
# =====================================================================
echo "=== Decoding APK with apktool ==="
APKTOOL_ARGS=()
if [[ "$FORCE" == true ]]; then
APKTOOL_ARGS+=("-f")
fi
APKTOOL_ARGS+=("-o" "$OUTPUT_DIR")
APKTOOL_ARGS+=("$APK_TO_DECODE")
if ! apktool d "${APKTOOL_ARGS[@]}" 2>&1; then
echo "Error: apktool decode failed." >&2
echo "Tip: If this is a framework error, try: rm -f ~/.local/share/apktool/framework/1.apk" >&2
exit 1
fi
# =====================================================================
# Verify output
# =====================================================================
has_smali=false
for d in "$OUTPUT_DIR"/smali*; do
if [[ -d "$d" ]]; then
has_smali=true
break
fi
done
if [[ "$has_smali" == false ]]; then
echo "Error: No smali/ directory found in decoded output." >&2
exit 1
fi
if [[ ! -f "$OUTPUT_DIR/AndroidManifest.xml" ]]; then
echo "Warning: AndroidManifest.xml not found in decoded output." >&2
fi
echo
echo "Decoded successfully: $OUTPUT_DIR"
echo "DECODED_DIR:$OUTPUT_DIR"
exit 0