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

334 lines
9.5 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
# Ensure user-local bin is in PATH (install-dep.sh installs tools there)
if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
export PATH="$HOME/.local/bin:$PATH"
fi
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 and decodes it. Split APKs and
XAPK metadata are preserved in .xapk-origin/ inside the decoded directory
so that rebuild-apk.sh can reassemble a complete XAPK.
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>
XAPK_ORIGIN:<path> (only for XAPK input)
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, preserve structure for rebuild
# =====================================================================
APK_TO_DECODE="$INPUT_FILE_ABS"
XAPK_TMPDIR=""
IS_XAPK=false
cleanup_xapk() {
if [[ -n "$XAPK_TMPDIR" ]] && [[ -d "$XAPK_TMPDIR" ]]; then
rm -rf "$XAPK_TMPDIR"
fi
}
if [[ "$ext_lower" == "xapk" ]]; then
IS_XAPK=true
echo "=== Extracting XAPK archive ==="
XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX")
trap cleanup_xapk EXIT
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
rm -rf "$XAPK_TMPDIR"
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
rm -rf "$XAPK_TMPDIR"
exit 1
fi
BASE_APK_NAME=$(basename "$base_apk")
echo
echo "Selected base APK: $BASE_APK_NAME"
# List split APKs
split_apks=()
for f in "${all_apks[@]}"; do
if [[ "$f" != "$base_apk" ]]; then
split_apks+=("$(basename "$f")")
echo " [split] $(basename "$f")"
fi
done
if (( ${#split_apks[@]} > 0 )); then
echo "${#split_apks[@]} split APK(s) preserved in .xapk-origin/splits/ for rebuild."
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
# =====================================================================
# Preserve XAPK structure for rebuild
# =====================================================================
if [[ "$IS_XAPK" == true ]] && [[ -n "$XAPK_TMPDIR" ]]; then
echo
echo "=== Preserving XAPK structure ==="
XAPK_ORIGIN_DIR="$OUTPUT_DIR/.xapk-origin"
mkdir -p "$XAPK_ORIGIN_DIR/splits"
# Copy manifest.json from XAPK
if [[ -f "$XAPK_TMPDIR/manifest.json" ]]; then
cp "$XAPK_TMPDIR/manifest.json" "$XAPK_ORIGIN_DIR/manifest.json"
echo " Copied manifest.json"
fi
# Copy icon if present
for icon_file in "$XAPK_TMPDIR"/icon.png "$XAPK_TMPDIR"/icon.jpg; do
if [[ -f "$icon_file" ]]; then
cp "$icon_file" "$XAPK_ORIGIN_DIR/"
echo " Copied $(basename "$icon_file")"
break
fi
done
# Copy split APKs
for f in "${all_apks[@]}"; do
if [[ "$f" != "$base_apk" ]]; then
cp "$f" "$XAPK_ORIGIN_DIR/splits/"
echo " Copied split: $(basename "$f")"
fi
done
# Extract metadata from XAPK manifest.json using sed (no jq dependency)
xapk_package_name=""
xapk_version_code=""
xapk_version_name=""
if [[ -f "$XAPK_ORIGIN_DIR/manifest.json" ]]; then
xapk_package_name=$(sed -n 's/.*"package_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$XAPK_ORIGIN_DIR/manifest.json" | head -1)
xapk_version_code=$(sed -n 's/.*"version_code"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9]*\)"\{0,1\}.*/\1/p' "$XAPK_ORIGIN_DIR/manifest.json" | head -1)
xapk_version_name=$(sed -n 's/.*"version_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$XAPK_ORIGIN_DIR/manifest.json" | head -1)
fi
# Detect OBB files (registered but NOT copied — can be gigabytes)
obb_json="[]"
obb_entries=()
while IFS= read -r -d '' obb_file; do
obb_name=$(basename "$obb_file")
obb_size=$(stat -c%s "$obb_file" 2>/dev/null || stat -f%z "$obb_file" 2>/dev/null || echo 0)
obb_entries+=("{\"name\": \"$obb_name\", \"size_bytes\": $obb_size}")
done < <(find "$XAPK_TMPDIR" -name "*.obb" -print0 2>/dev/null)
if (( ${#obb_entries[@]} > 0 )); then
obb_json="["
for i in "${!obb_entries[@]}"; do
if (( i > 0 )); then obb_json+=", "; fi
obb_json+="${obb_entries[$i]}"
done
obb_json+="]"
echo " OBB files detected (not copied — registered in metadata only):"
for entry in "${obb_entries[@]}"; do echo " $entry"; done
fi
# Build split_apks JSON array
splits_json="["
for i in "${!split_apks[@]}"; do
if (( i > 0 )); then splits_json+=", "; fi
splits_json+="\"${split_apks[$i]}\""
done
splits_json+="]"
# Write metadata.json
decoded_ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
printf '{\n "format": "xapk",\n "original_file": "%s",\n "package_name": "%s",\n "version_code": "%s",\n "version_name": "%s",\n "base_apk": "%s",\n "split_apks": %s,\n "obb_files": %s,\n "decoded_timestamp": "%s"\n}\n' \
"$INPUT_FILE_ABS" \
"$xapk_package_name" \
"$xapk_version_code" \
"$xapk_version_name" \
"$BASE_APK_NAME" \
"$splits_json" \
"$obb_json" \
"$decoded_ts" \
> "$XAPK_ORIGIN_DIR/metadata.json"
echo " Wrote metadata.json"
echo
echo "XAPK structure preserved in: $XAPK_ORIGIN_DIR"
echo "XAPK_ORIGIN:$XAPK_ORIGIN_DIR"
fi
# Clean up XAPK tmpdir now that everything is copied
cleanup_xapk
# Set trap to no-op since we already cleaned up
trap - EXIT
echo
echo "Decoded successfully: $OUTPUT_DIR"
echo "DECODED_DIR:$OUTPUT_DIR"
exit 0