From 3d68cf392f9b81954ec7c6aa9cd2f77f7ccdd4d8 Mon Sep 17 00:00:00 2001 From: Simone Avogadro Date: Mon, 9 Mar 2026 15:16:51 +0100 Subject: [PATCH] Add merged single APK support for XAPK and fix plugin version to 1.2.0 New merge-splits.sh script merges XAPK split APK contents (native libs, resources, manifest cleanup) into the decoded base directory, enabling single APK output installable via standard adb install. rebuild-apk.sh gains --single-apk flag with auto-detection via .merged marker. SKILL.md updated with Phase 5a/5b/5c workflow for XAPK rebuild choice. Fixed plugin.json version from 1.0.0 to 1.2.0 to match marketplace.json. Added versioning guidance to CLAUDE.md to prevent future mismatches. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 17 +- .../.claude-plugin/plugin.json | 2 +- .../skills/sdk-neutralizer/SKILL.md | 71 ++- .../sdk-neutralizer/scripts/merge-splits.sh | 448 ++++++++++++++++++ .../sdk-neutralizer/scripts/rebuild-apk.sh | 32 +- 5 files changed, 555 insertions(+), 15 deletions(-) create mode 100755 plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/merge-splits.sh diff --git a/CLAUDE.md b/CLAUDE.md index 1dde649..a4a55cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ A Claude Code Skill (plugin) for Android reverse engineering, API extraction, an - `plugins/android-reverse-engineering/skills/android-reverse-engineering/` — Core RE skill (5-phase workflow, references, scripts) - `plugins/android-reverse-engineering/skills/tracker-analysis/` — Tracker/analytics SDK detection skill (4-phase workflow, references, find-trackers.sh) - `plugins/android-reverse-engineering/skills/ad-analysis/` — Advertising SDK detection skill (3-phase workflow, references, find-ads.sh) -- `plugins/android-reverse-engineering/skills/sdk-neutralizer/` — SDK neutralization skill (6-phase workflow, references, decode-apk.sh, neutralize.sh, rebuild-apk.sh) +- `plugins/android-reverse-engineering/skills/sdk-neutralizer/` — SDK neutralization skill (6-phase workflow, references, decode-apk.sh, neutralize.sh, merge-splits.sh, rebuild-apk.sh) ## Key Scripts @@ -70,8 +70,11 @@ bash decode-apk.sh [-o ] # Neutralize SDK entry points in decoded APK (dry-run first) bash neutralize.sh [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file ] [--replay] [--no-save-manifest] -# Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists) -bash rebuild-apk.sh [--auto-keystore|--debug-key|--keystore ] [-o ] [--no-sign] [--no-res] [--zipalign] +# Merge XAPK splits into decoded base for single APK output (optional, for XAPK input) +bash merge-splits.sh [--abi ] [--all-abis] [--skip-resources] + +# Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists, or single APK if merged) +bash rebuild-apk.sh [--auto-keystore|--debug-key|--keystore ] [-o ] [--no-sign] [--no-res] [--zipalign] [--single-apk] ``` ## Architecture @@ -97,6 +100,14 @@ bash rebuild-apk.sh [--auto-keystore|--debug-key|--keystore This is a documentation-and-scripts plugin with no compiled code, no test suite, and no linter configuration. Changes are validated by reading the markdown/bash and testing scripts manually against APK files. +## Versioning + +The plugin version is declared in **two** files that **must be kept in sync**: +- `.claude-plugin/marketplace.json` → `plugins[0].version` (marketplace catalog) +- `plugins/android-reverse-engineering/.claude-plugin/plugin.json` → `version` (plugin manifest) + +Claude Code reads the version from `plugin.json` with priority — if that file has a stale version, `/plugin` will show the old number even if `marketplace.json` is updated. **Always bump both files together.** + ## Conventions - Line endings are LF (enforced via `.gitattributes` for WSL/Windows compatibility) diff --git a/plugins/android-reverse-engineering/.claude-plugin/plugin.json b/plugins/android-reverse-engineering/.claude-plugin/plugin.json index 720653c..213f415 100644 --- a/plugins/android-reverse-engineering/.claude-plugin/plugin.json +++ b/plugins/android-reverse-engineering/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "android-reverse-engineering", - "version": "1.0.0", + "version": "1.2.0", "description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, analyze tracker/ad SDKs for privacy auditing, and neutralize SDK entry points for enterprise deployment.", "author": { "name": "Simone Avogadro" diff --git a/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md b/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md index 106c5d4..1a93865 100644 --- a/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md +++ b/plugins/android-reverse-engineering/skills/sdk-neutralizer/SKILL.md @@ -94,7 +94,7 @@ For XAPK input, the script also outputs `XAPK_ORIGIN:` and creates: - `.xapk-origin/manifest.json` — original XAPK manifest - `.xapk-origin/splits/` — all split APKs (config.arm64_v8a.apk, config.en.apk, etc.) -If the input is an XAPK, inform the user that it's a split APK bundle and that all splits will be automatically re-signed during rebuild. +If the input is an XAPK, inform the user that it's a split APK bundle. Let them know that in Phase 5 (Rebuild) they will be asked whether to produce a **merged single APK** (easier to install) or keep the **XAPK bundle** (preserves all splits). ### Phase 3: Identify Targets @@ -224,6 +224,42 @@ After a successful (non-dry-run) neutralization, a `neutralize-manifest.json` is Rebuild the decoded directory back into a signed APK (or XAPK if the original was an XAPK). +#### Phase 5a — XAPK Output Format Choice (XAPK input only) + +**If the input was an XAPK**, you **MUST ask the user** how to rebuild: + +> How would you like to rebuild the neutralized app? +> +> 1. **Merged single APK** (recommended for sideloading) — merges split contents into one APK, installable with standard `adb install`. May be missing some locale/density resources. +> 2. **XAPK bundle** (preserves original structure) — requires `adb install-multiple` or SAI app to install. All splits preserved exactly. + +**If the user chooses option 1 (merged single APK):** + +Run `merge-splits.sh` to merge split contents into the decoded base APK directory: + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/merge-splits.sh +``` + +Options: +- `--abi ` — merge only a specific ABI (e.g., `arm64-v8a`) +- `--all-abis` — merge all ABIs (larger but universal APK) +- `--skip-resources` — skip resource split merge (locale/density) +- Default (no flags): picks the most common ABI (`arm64-v8a` > `armeabi-v7a` > `x86_64` > `x86`) + +Parse output for `MERGE_ABI:`, `MERGE_RESOURCES:`, `SKIPPED_RESOURCES:`, `FEATURE_SPLIT_WARNING:`, `MANIFEST_CLEANED:`, and `MERGE_COMPLETE:` lines. + +**Important merge limitations to communicate to the user:** +- Resource splits (locale, density) are merged best-effort — compiled `resources.arsc` cannot be fused without `aapt2`. The merged APK uses default resources from the base APK. +- Feature module splits (containing DEX code) **cannot** be merged — the script warns about these. +- Native library merge changes `android:extractNativeLibs` to `true`, which increases installed size. + +After merge, the rebuild script auto-detects the `.merged` marker and produces a single `.apk`. + +**If the user chooses option 2 (XAPK bundle):** skip `merge-splits.sh` and proceed directly to rebuild — the script will auto-reassemble the XAPK. + +#### Phase 5b — Signing Preference + **Before calling rebuild**, you **MUST ask the user** their signing preference: > How would you like to sign the rebuilt APK? @@ -237,22 +273,31 @@ Map the user's choice to the corresponding flag: - Option 2 → `--keystore --key-alias --store-pass --key-pass ` - Option 3 → `--no-sign` +#### Phase 5c — Run Rebuild + **Action**: Run the rebuild script with the chosen signing option. ```bash -# Example with auto-keystore (recommended default) +# Single merged APK (after merge-splits.sh, auto-detected via .merged marker) +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh --auto-keystore + +# Or explicitly force single APK output +bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh --auto-keystore --single-apk + +# XAPK bundle (default when .xapk-origin/ exists and no .merged marker) bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh --auto-keystore ``` Options: - `-o ` — custom output path +- `--single-apk` — force single APK output (auto-enabled when `.merged` marker exists) - `--auto-keystore` — auto-detect best keystore (recommended) - `--debug-key` — always generate new debug keystore - `--keystore ` — use a custom keystore - `--no-sign` — output unsigned APK - `--zipalign` / `--no-zipalign` — control zipalign step -For XAPK input, the rebuild is automatic: the script detects `.xapk-origin/`, re-signs all split APKs with the same keystore, and produces a `.xapk` output. Parse the output for `KEYSTORE_USED:`, `KEYSTORE_SOURCE:`, `SPLIT_SIGNED:`, and `XAPK_ASSEMBLED:` lines. +For XAPK input without merge, the rebuild is automatic: the script detects `.xapk-origin/`, re-signs all split APKs with the same keystore, and produces a `.xapk` output. Parse the output for `KEYSTORE_USED:`, `KEYSTORE_SOURCE:`, `SPLIT_SIGNED:`, and `XAPK_ASSEMBLED:` lines. ### Phase 6: Verify & Report @@ -308,13 +353,29 @@ Modifying APKs may violate the app's EULA, SDK provider agreements, or intellect property laws. This tool is intended for authorized enterprise use, security research, and privacy compliance only. +## Split Merge Details (if applicable) + +If the original input was an XAPK and the user chose merged single APK output, include this section: + +| Merge Step | Result | +|---|---| +| ABI splits merged | arm64-v8a (3 native libraries) | +| Resource splits | 2 merged (best-effort), 1 skipped | +| Feature splits | 0 (or: 1 warning — could not merge) | +| Manifest cleanup | isSplitRequired, extractNativeLibs→true, com.android.vending.splits.required | + +**Merge limitations:** +- Locale/density resources use defaults from the base APK (compiled `resources.arsc` from splits cannot be fused) +- `android:extractNativeLibs` was set to `true` — native libs are extracted on install (uses more disk space) +- Feature module splits (if any) were NOT merged and their functionality may be missing + ## Output - Sanitized APK/XAPK: `` -- Output format: APK (single) / XAPK (split bundle) +- Output format: APK (single) / APK (merged from XAPK) / XAPK (split bundle) - Signed with: auto-detected debug key / generated debug key / custom keystore - Keystore used: `` (source: `KEYSTORE_SOURCE:` value) -- Install via: `adb install ` (APK) or `adb install-multiple ...` (XAPK) +- Install via: `adb install ` (APK / merged APK) or `adb install-multiple ...` (XAPK) - For XAPK: can also use SAI (Split APKs Installer) or unzip and `adb install-multiple *.apk` ``` diff --git a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/merge-splits.sh b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/merge-splits.sh new file mode 100755 index 0000000..7efad55 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/merge-splits.sh @@ -0,0 +1,448 @@ +#!/usr/bin/env bash +# merge-splits.sh — Merge XAPK split APKs into the decoded base APK directory +# +# Produces a single-APK-ready directory by merging native libraries, +# resources (best-effort), and cleaning the manifest of split-related attributes. +# +# Exit codes: +# 0 — success (splits merged) +# 1 — error (invalid input, missing files) +# 2 — not an XAPK (no .xapk-origin/ found) +set -euo pipefail + +usage() { + cat < [OPTIONS] + +Merge XAPK split APK contents into the decoded base APK directory, +producing a directory ready to rebuild as a single merged APK. + +Arguments: + Path to the apktool-decoded APK directory (must contain .xapk-origin/) + +Options: + --abi Merge only this ABI (e.g., arm64-v8a, armeabi-v7a, x86_64, x86) + --all-abis Merge all ABIs found in splits (larger but universal) + --skip-resources Skip merging resource splits (locale, density) + -h, --help Show this help message + +Output (machine-readable): + MERGE_ABI: ( native libraries) + MERGE_RESOURCES: ( files) + SKIPPED_RESOURCES:: + FEATURE_SPLIT_WARNING: (feature modules cannot be merged) + MANIFEST_CLEANED: + MERGE_COMPLETE: +EOF + exit 0 +} + +# ===================================================================== +# Argument parsing +# ===================================================================== + +DECODED_DIR="" +TARGET_ABI="" +ALL_ABIS=false +SKIP_RESOURCES=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --abi) + shift + if [[ $# -eq 0 ]]; then echo "Error: --abi requires an argument" >&2; exit 1; fi + TARGET_ABI="$1"; shift ;; + --all-abis) ALL_ABIS=true; shift ;; + --skip-resources) SKIP_RESOURCES=true; 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 + +# ===================================================================== +# Validate XAPK origin +# ===================================================================== + +info() { echo "[INFO] $*"; } +ok() { echo "[OK] $*"; } +fail() { echo "[FAIL] $*" >&2; } + +XAPK_ORIGIN_DIR="$DECODED_DIR/.xapk-origin" + +if [[ ! -f "$XAPK_ORIGIN_DIR/metadata.json" ]]; then + info "Not an XAPK — no .xapk-origin/metadata.json found. Nothing to merge." + exit 2 +fi + +if [[ ! -d "$XAPK_ORIGIN_DIR/splits" ]]; then + info "No splits directory found in .xapk-origin/. Nothing to merge." + exit 2 +fi + +# Count split APKs +split_count=0 +for f in "$XAPK_ORIGIN_DIR/splits/"*.apk; do + [[ -f "$f" ]] && (( split_count++ )) || true +done + +if (( split_count == 0 )); then + info "No split APKs found in .xapk-origin/splits/. Nothing to merge." + exit 2 +fi + +echo "=== Merging XAPK Splits ===" +echo "Decoded dir: $DECODED_DIR" +echo "Split APKs found: $split_count" +echo + +# ===================================================================== +# Step 0: Classify splits (ABI, resource, feature) +# ===================================================================== + +declare -a abi_splits=() +declare -a resource_splits=() +declare -a feature_splits=() + +for split_apk in "$XAPK_ORIGIN_DIR/splits/"*.apk; do + [[ -f "$split_apk" ]] || continue + split_name=$(basename "$split_apk") + + # Check if it's an ABI split by looking for lib/ inside + if unzip -l "$split_apk" 2>/dev/null | grep -q "lib/[^/]*/"; then + abi_splits+=("$split_apk") + # Check if it's a feature split (contains classes.dex or AndroidManifest.xml with split name) + elif unzip -l "$split_apk" 2>/dev/null | grep -qE "(classes[0-9]*\.dex|^.*AndroidManifest\.xml)" && \ + ! [[ "$split_name" == config.* ]]; then + feature_splits+=("$split_apk") + else + # Config split (locale, density, etc.) + resource_splits+=("$split_apk") + fi +done + +info "ABI splits: ${#abi_splits[@]}" +info "Resource splits: ${#resource_splits[@]}" +info "Feature splits: ${#feature_splits[@]}" +echo + +# Warn about feature splits — cannot be merged +if (( ${#feature_splits[@]} > 0 )); then + for fs in "${feature_splits[@]}"; do + fname=$(basename "$fs") + echo "FEATURE_SPLIT_WARNING:$fname (feature modules cannot be merged — contains DEX code and own manifest)" + done + echo +fi + +# ===================================================================== +# Step 1: Merge native libraries (ABI splits) +# ===================================================================== + +echo "=== Step 1: Merge Native Libraries ===" + +if (( ${#abi_splits[@]} == 0 )); then + info "No ABI splits found — skipping native library merge." +else + # Detect available ABIs + declare -A abi_to_split=() + for split_apk in "${abi_splits[@]}"; do + # Extract ABI name from the lib/ structure inside the APK + while IFS= read -r abi_name; do + abi_to_split["$abi_name"]="$split_apk" + done < <(unzip -l "$split_apk" 2>/dev/null | grep -oP 'lib/\K[^/]+(?=/)' | sort -u) + done + + info "Available ABIs: ${!abi_to_split[*]}" + + # Determine which ABIs to merge + declare -a abis_to_merge=() + + if [[ -n "$TARGET_ABI" ]]; then + # User specified a specific ABI + if [[ -z "${abi_to_split[$TARGET_ABI]:-}" ]]; then + fail "Requested ABI '$TARGET_ABI' not found in splits. Available: ${!abi_to_split[*]}" + exit 1 + fi + abis_to_merge=("$TARGET_ABI") + elif [[ "$ALL_ABIS" == true ]]; then + # Merge all ABIs + abis_to_merge=("${!abi_to_split[@]}") + else + # Default: pick most common ABI by priority + for preferred in arm64-v8a armeabi-v7a x86_64 x86; do + if [[ -n "${abi_to_split[$preferred]:-}" ]]; then + abis_to_merge=("$preferred") + break + fi + done + # Fallback: pick the first available + if (( ${#abis_to_merge[@]} == 0 )); then + for abi in "${!abi_to_split[@]}"; do + abis_to_merge=("$abi") + break + done + fi + fi + + info "Merging ABIs: ${abis_to_merge[*]}" + + # Create lib/ directory in decoded dir if it doesn't exist + mkdir -p "$DECODED_DIR/lib" + + TMPDIR_NATIVE=$(mktemp -d "${TMPDIR:-/tmp}/merge-native-XXXXXX") + cleanup_native() { rm -rf "$TMPDIR_NATIVE"; } + trap cleanup_native EXIT + + for abi in "${abis_to_merge[@]}"; do + split_apk="${abi_to_split[$abi]}" + split_name=$(basename "$split_apk") + + info "Extracting native libs from $split_name (ABI: $abi)..." + + # Extract lib/ contents + rm -rf "$TMPDIR_NATIVE/"* + unzip -qo "$split_apk" "lib/*" -d "$TMPDIR_NATIVE" 2>/dev/null || true + + if [[ -d "$TMPDIR_NATIVE/lib/$abi" ]]; then + # Copy to decoded dir + mkdir -p "$DECODED_DIR/lib/$abi" + lib_count=0 + for so_file in "$TMPDIR_NATIVE/lib/$abi/"*; do + [[ -f "$so_file" ]] || continue + cp "$so_file" "$DECODED_DIR/lib/$abi/" + (( lib_count++ )) + done + ok "Merged $lib_count native libraries for $abi" + echo "MERGE_ABI:$abi ($lib_count native libraries)" + else + info "No native libs found for ABI $abi in $split_name" + fi + done + + cleanup_native + trap - EXIT +fi + +echo + +# ===================================================================== +# Step 2: Merge resources (locale/density splits) — best-effort +# ===================================================================== + +echo "=== Step 2: Merge Resources (best-effort) ===" + +if [[ "$SKIP_RESOURCES" == true ]]; then + info "Resource merge skipped (--skip-resources)." + for rs in "${resource_splits[@]}"; do + fname=$(basename "$rs") + echo "SKIPPED_RESOURCES:$fname:user-requested" + done +elif (( ${#resource_splits[@]} == 0 )); then + info "No resource splits found — skipping." +else + TMPDIR_RES=$(mktemp -d "${TMPDIR:-/tmp}/merge-res-XXXXXX") + cleanup_res() { rm -rf "$TMPDIR_RES"; } + trap cleanup_res EXIT + + for split_apk in "${resource_splits[@]}"; do + split_name=$(basename "$split_apk") + + # Extract everything except META-INF/ and resources.arsc (cannot merge compiled resources) + rm -rf "$TMPDIR_RES/"* + unzip -qo "$split_apk" -d "$TMPDIR_RES" -x "META-INF/*" "resources.arsc" 2>/dev/null || true + + # Count extracted files (excluding directories) + file_count=0 + while IFS= read -r -d '' f; do + (( file_count++ )) + done < <(find "$TMPDIR_RES" -type f -print0 2>/dev/null) + + if (( file_count == 0 )); then + echo "SKIPPED_RESOURCES:$split_name:no-extractable-files" + continue + fi + + # Copy res/ contents if present + res_copied=0 + if [[ -d "$TMPDIR_RES/res" ]]; then + # Copy resource directories (values-*, drawable-*, etc.) + for res_subdir in "$TMPDIR_RES/res/"*; do + [[ -d "$res_subdir" ]] || continue + subdir_name=$(basename "$res_subdir") + mkdir -p "$DECODED_DIR/res/$subdir_name" + for res_file in "$res_subdir/"*; do + [[ -f "$res_file" ]] || continue + # Don't overwrite existing resources from the base APK + target="$DECODED_DIR/res/$subdir_name/$(basename "$res_file")" + if [[ ! -f "$target" ]]; then + cp "$res_file" "$target" + (( res_copied++ )) + fi + done + done + fi + + # Copy assets/ contents if present + assets_copied=0 + if [[ -d "$TMPDIR_RES/assets" ]]; then + mkdir -p "$DECODED_DIR/assets" + while IFS= read -r -d '' asset_file; do + rel_path="${asset_file#$TMPDIR_RES/assets/}" + target="$DECODED_DIR/assets/$rel_path" + if [[ ! -f "$target" ]]; then + mkdir -p "$(dirname "$target")" + cp "$asset_file" "$target" + (( assets_copied++ )) + fi + done < <(find "$TMPDIR_RES/assets" -type f -print0 2>/dev/null) + fi + + total_copied=$(( res_copied + assets_copied )) + if (( total_copied > 0 )); then + ok "Merged $total_copied files from $split_name" + echo "MERGE_RESOURCES:$split_name ($total_copied files)" + else + echo "SKIPPED_RESOURCES:$split_name:all-files-already-exist-in-base" + fi + done + + cleanup_res + trap - EXIT +fi + +echo + +# ===================================================================== +# Step 3: Patch AndroidManifest.xml +# ===================================================================== + +echo "=== Step 3: Patch AndroidManifest.xml ===" + +MANIFEST="$DECODED_DIR/AndroidManifest.xml" + +if [[ ! -f "$MANIFEST" ]]; then + fail "AndroidManifest.xml not found in $DECODED_DIR" + exit 1 +fi + +# 3a: Remove android:isSplitRequired="true" → set to "false" +if grep -q 'android:isSplitRequired="true"' "$MANIFEST"; then + sed -i 's/android:isSplitRequired="true"/android:isSplitRequired="false"/g' "$MANIFEST" + ok "Patched: isSplitRequired=true → false" + echo "MANIFEST_CLEANED:isSplitRequired" +fi + +# 3b: Remove split-related elements +# Each pattern removes the entire tag (single-line or multi-line) +declare -a meta_names=( + "com.android.vending.splits.required" + "com.android.vending.splits" + "com.android.vending.derived.apk.id" + "com.android.stamp.source" + "com.android.stamp.type" +) + +for meta_name in "${meta_names[@]}"; do + if grep -q "android:name=\"$meta_name\"" "$MANIFEST"; then + # Remove single-line + sed -i "/]*android:name=\"$meta_name\"[^>]*\/>/d" "$MANIFEST" + # Remove multi-line ... (rare but handle it) + # Use a simple approach: remove lines containing the meta-data name within meta-data blocks + sed -i "/]*android:name=\"$meta_name\"/,/<\/meta-data>/d" "$MANIFEST" + ok "Removed: " + echo "MANIFEST_CLEANED:$meta_name" + fi +done + +# 3c: Remove hasFragileUserData if present (split-related cleanup) +if grep -q 'android:hasFragileUserData' "$MANIFEST"; then + sed -i 's/ *android:hasFragileUserData="[^"]*"//g' "$MANIFEST" + info "Removed: android:hasFragileUserData (split-related)" +fi + +# 3d: Fix extractNativeLibs — change false to true when we have merged native libs +if [[ -d "$DECODED_DIR/lib" ]] && [[ -n "$(ls -A "$DECODED_DIR/lib/" 2>/dev/null)" ]]; then + if grep -q 'android:extractNativeLibs="false"' "$MANIFEST"; then + sed -i 's/android:extractNativeLibs="false"/android:extractNativeLibs="true"/g' "$MANIFEST" + ok "Patched: extractNativeLibs=false → true (required for merged native libs)" + echo "MANIFEST_CLEANED:extractNativeLibs→true" + elif ! grep -q 'android:extractNativeLibs' "$MANIFEST"; then + # Add extractNativeLibs="true" to the tag + sed -i 's/" + echo "MANIFEST_CLEANED:extractNativeLibs→true (added)" + fi +fi + +# 3e: Remove android:splitTypes attribute if present +if grep -q 'android:splitTypes=' "$MANIFEST"; then + sed -i 's/ *android:splitTypes="[^"]*"//g' "$MANIFEST" + ok "Removed: android:splitTypes" + echo "MANIFEST_CLEANED:splitTypes" +fi + +# 3f: Remove android:requiredSplitTypes attribute if present +if grep -q 'android:requiredSplitTypes=' "$MANIFEST"; then + sed -i 's/ *android:requiredSplitTypes="[^"]*"//g' "$MANIFEST" + ok "Removed: android:requiredSplitTypes" + echo "MANIFEST_CLEANED:requiredSplitTypes" +fi + +echo + +# ===================================================================== +# Step 4: Create merge marker +# ===================================================================== + +echo "=== Step 4: Finalize ===" + +# Create .merged marker so rebuild-apk.sh knows to produce a single APK +touch "$XAPK_ORIGIN_DIR/.merged" +ok "Created merge marker: $XAPK_ORIGIN_DIR/.merged" + +# Write merge metadata +merged_ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ") +{ + echo "{" + echo " \"merged_timestamp\": \"$merged_ts\"," + echo " \"abi_splits_merged\": ${#abi_splits[@]}," + echo " \"resource_splits_merged\": ${#resource_splits[@]}," + echo " \"feature_splits_skipped\": ${#feature_splits[@]}," + echo " \"skip_resources\": $SKIP_RESOURCES" + echo "}" +} > "$XAPK_ORIGIN_DIR/.merged-metadata.json" + +echo +echo "=== Merge Complete ===" + +# Summary +echo +echo "Merged splits into: $DECODED_DIR" +if (( ${#feature_splits[@]} > 0 )); then + echo + echo "WARNING: ${#feature_splits[@]} feature module split(s) could NOT be merged." + echo " Feature modules contain their own DEX code and manifest entries." + echo " The merged APK may be missing functionality from these modules." + for fs in "${feature_splits[@]}"; do + echo " - $(basename "$fs")" + done +fi + +echo +echo "MERGE_COMPLETE:$DECODED_DIR" +echo +echo "Next: rebuild with --single-apk flag or let rebuild-apk.sh auto-detect the .merged marker." +echo " rebuild-apk.sh $DECODED_DIR --auto-keystore" + +exit 0 diff --git a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh index 5e7546d..e407430 100755 --- a/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh +++ b/plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh @@ -20,13 +20,17 @@ Usage: rebuild-apk.sh [OPTIONS] Rebuild an apktool-decoded APK directory back into a signed APK. If the decoded dir contains .xapk-origin/ (from decode-apk.sh), automatically -reassembles a complete XAPK with all split APKs re-signed. +reassembles a complete XAPK with all split APKs re-signed — unless --single-apk +is used or .xapk-origin/.merged exists (from merge-splits.sh), in which case +a single merged APK is produced instead. Arguments: Path to the apktool-decoded APK directory Options: -o, --output Output path (default: -neutralized.apk/.xapk) + --single-apk Force output as single .apk even if .xapk-origin/ exists + (auto-enabled when .xapk-origin/.merged marker is present) --auto-keystore Auto-detect best keystore: ~/.android/debug.keystore, then previous .neutralizer-debug.keystore, then generate new --debug-key Sign with an auto-generated debug keystore (default) @@ -70,6 +74,7 @@ DO_ZIPALIGN=true NO_ZIPALIGN=false FORCE_NO_RES=false BUILD_USED_NO_RES=false +SINGLE_APK=false while [[ $# -gt 0 ]]; do case "$1" in @@ -95,6 +100,7 @@ while [[ $# -gt 0 ]]; do shift if [[ $# -eq 0 ]]; then echo "Error: --store-pass requires an argument" >&2; exit 1; fi STORE_PASS="$1"; shift ;; + --single-apk) SINGLE_APK=true; shift ;; --no-sign) DO_SIGN=false; shift ;; --no-res) FORCE_NO_RES=true; shift ;; --zipalign) DO_ZIPALIGN=true; shift ;; @@ -122,10 +128,16 @@ if [[ -f "$XAPK_ORIGIN_DIR/metadata.json" ]]; then IS_XAPK=true fi -# Default output name — .xapk if original was XAPK, .apk otherwise +# Auto-detect merged marker from merge-splits.sh +if [[ -f "$XAPK_ORIGIN_DIR/.merged" ]]; then + SINGLE_APK=true + echo "[INFO] Detected .xapk-origin/.merged marker — will produce a single merged APK" +fi + +# Default output name — .xapk if original was XAPK (and not merging), .apk otherwise if [[ -z "$OUTPUT" ]]; then local_dir="${DECODED_DIR%/}" - if [[ "$IS_XAPK" == true ]]; then + if [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == false ]]; then OUTPUT="${local_dir}-neutralized.xapk" else OUTPUT="${local_dir}-neutralized.apk" @@ -435,7 +447,7 @@ fi # Step 5: XAPK assembly (if original was XAPK) # ===================================================================== -if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]]; then +if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]] && [[ "$SINGLE_APK" == false ]]; then echo echo "=== Assembling XAPK ===" @@ -514,8 +526,10 @@ fi echo echo "=== Rebuild Complete ===" -if [[ "$IS_XAPK" == true ]]; then +if [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == false ]]; then echo "Output XAPK: $OUTPUT" +elif [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == true ]]; then + echo "Output APK (merged from XAPK): $OUTPUT" else echo "Output APK: $OUTPUT" fi @@ -543,10 +557,16 @@ fi echo echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading." -if [[ "$IS_XAPK" == true ]]; then +if [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == false ]]; then echo "Install via: adb install-multiple ..." echo " or: use a split APK installer (e.g., SAI — Split APKs Installer)" echo " or: unzip the XAPK and run: adb install-multiple *.apk" +elif [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == true ]]; then + echo "Install via: adb install $OUTPUT" + echo + echo "NOTE: This is a merged single APK from an XAPK split bundle." + echo " Some locale/density-specific resources may use defaults." + echo " Test thoroughly on target devices." else echo "Install via: adb install $OUTPUT" fi