SDK Neutralizer v3: full XAPK round-trip, auto-keystore, neutralize-all installer
- decode-apk.sh preserves XAPK structure (.xapk-origin/) with splits, manifest, and metadata for automatic reassembly - rebuild-apk.sh detects .xapk-origin/, re-signs all split APKs, and assembles final .xapk output - Add --auto-keystore flag: prioritizes ~/.android/debug.keystore → previous debug key → generated key - Add neutralize-all compound target to install-dep.sh (java + apktool + apksigner + zip in one command) - Add apktool version check (>= 2.9.0) with automatic upgrade from GitHub releases - Add install_zip() and zip dependency check for XAPK rebuild - All scripts now prepend ~/.local/bin to PATH for user-local tool installs - Update SKILL.md Phase 3 with built-in catalog detection workflow and custom SDK discovery using Claude Code tools - Update neutralize.md, CLAUDE.md, and README.md with XAPK and auto-keystore documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
291e785c67
commit
e70920cf87
|
|
@ -34,6 +34,9 @@ bash scripts/check-deps.sh
|
|||
# Install a dependency (auto-detects OS/package manager)
|
||||
bash scripts/install-dep.sh <dep> # e.g., jadx, vineflower, dex2jar
|
||||
|
||||
# Install ALL neutralizer dependencies at once (java, apktool, apksigner, zip)
|
||||
bash scripts/install-dep.sh neutralize-all
|
||||
|
||||
# Decompile an APK/JAR/AAR/XAPK
|
||||
bash scripts/decompile.sh [--engine jadx|fernflower|both] [--deobf] [--no-res] [-o outdir] <file>
|
||||
|
||||
|
|
@ -61,14 +64,14 @@ SDK neutralizer scripts under `plugins/android-reverse-engineering/skills/sdk-ne
|
|||
# Check neutralization dependencies (including apktool >= 2.9.0)
|
||||
bash check-neutralize-deps.sh
|
||||
|
||||
# Decode APK or XAPK (extracts base APK from XAPK automatically)
|
||||
# Decode APK or XAPK (for XAPK: decodes base APK, preserves splits in .xapk-origin/)
|
||||
bash decode-apk.sh <file.apk|file.xapk> [-o <decoded-dir>]
|
||||
|
||||
# Neutralize SDK entry points in decoded APK (dry-run first)
|
||||
bash neutralize.sh <decoded-dir> [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file <file>] [--replay] [--no-save-manifest]
|
||||
|
||||
# Rebuild and sign neutralized APK
|
||||
bash rebuild-apk.sh <decoded-dir> [--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign]
|
||||
# Rebuild and sign neutralized APK (auto-reassembles XAPK if .xapk-origin/ exists)
|
||||
bash rebuild-apk.sh <decoded-dir> [--auto-keystore|--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign]
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -25,7 +25,8 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts H
|
|||
|
||||
**For SDK neutralization (`/neutralize`):**
|
||||
- [apktool](https://apktool.org/) (required) — APK decode/rebuild
|
||||
- apksigner or jarsigner (required) — APK signing
|
||||
- apksigner or jarsigner (required) — APK signing (apksigner required for XAPK)
|
||||
- zip (required for XAPK rebuild)
|
||||
|
||||
See `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md` for detailed installation instructions.
|
||||
|
||||
|
|
@ -133,6 +134,9 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri
|
|||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh jadx
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh vineflower
|
||||
|
||||
# Install ALL neutralizer dependencies at once (java, apktool, apksigner, zip)
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all
|
||||
|
||||
# Decompile APK with jadx (default)
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh app.apk
|
||||
|
||||
|
|
@ -164,10 +168,15 @@ bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh
|
|||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh app.apk -o app-decoded
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all --dry-run
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh app-decoded --debug-key
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh app-decoded --auto-keystore
|
||||
|
||||
# XAPK support — decode-apk.sh extracts the base APK automatically
|
||||
# XAPK full round-trip — decode preserves splits, rebuild reassembles XAPK
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh app-bundle.xapk -o app-decoded
|
||||
# .xapk-origin/ now contains splits/, manifest.json, metadata.json
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all --dry-run
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh app-decoded --auto-keystore
|
||||
# → produces app-decoded-neutralized.xapk with all splits re-signed
|
||||
|
||||
# Replay previous patches after re-decoding
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --replay
|
||||
|
|
|
|||
|
|
@ -48,15 +48,23 @@ Run the dependency check to ensure all required tools are installed:
|
|||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh
|
||||
```
|
||||
|
||||
If any `INSTALL_REQUIRED:` lines appear, install the missing dependencies:
|
||||
If any `INSTALL_REQUIRED:` lines appear, install all dependencies at once:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh <dep>
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all
|
||||
```
|
||||
|
||||
If the script exits with code 2 (sudo needed but no TTY — common inside Claude Code), tell the user to run this command in their terminal:
|
||||
|
||||
```
|
||||
sudo bash <full-path-to>/install-dep.sh neutralize-all
|
||||
```
|
||||
|
||||
Provide the **full resolved path** (replace `${CLAUDE_PLUGIN_ROOT}` with the actual path) so the user can copy-paste directly.
|
||||
|
||||
### Step 4: Decode APK/XAPK
|
||||
|
||||
Decode the APK or XAPK using decode-apk.sh (handles both formats; for XAPKs extracts the base APK automatically):
|
||||
Decode the APK or XAPK using decode-apk.sh (handles both formats; for XAPKs extracts and decodes the base APK while preserving the full XAPK structure for rebuild):
|
||||
|
||||
```bash
|
||||
# Strip both .apk and .xapk extensions for the output dir name
|
||||
|
|
@ -66,6 +74,8 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/decode-apk.sh "$APK_PA
|
|||
|
||||
Verify the decoded directory contains `smali/` and `AndroidManifest.xml` (the script does this automatically and outputs `DECODED_DIR:<path>`).
|
||||
|
||||
If the output includes `XAPK_ORIGIN:<path>`, inform the user: "This is an XAPK (split APK bundle). The base APK has been decoded for neutralization, and all split APKs are preserved. During rebuild, all APKs (base + splits) will be re-signed with the same key and reassembled into a new XAPK."
|
||||
|
||||
### Step 5: Identify targets
|
||||
|
||||
Run entry point detection to find which SDK calls exist in the app code:
|
||||
|
|
@ -102,18 +112,46 @@ Parse the `PATCHED:` and `MANIFEST_DISABLED:` output lines for the report.
|
|||
|
||||
### Step 8: Rebuild & sign
|
||||
|
||||
Rebuild the APK with a debug signing key:
|
||||
**Before rebuilding**, ask the user their signing preference:
|
||||
|
||||
> How would you like to sign the rebuilt APK?
|
||||
>
|
||||
> 1. **Auto-detect** (recommended) — checks `~/.android/debug.keystore` first, then generates a debug key
|
||||
> 2. **Custom keystore** — provide path, alias, and password
|
||||
> 3. **No signing** — output unsigned APK
|
||||
|
||||
Then rebuild with the appropriate flag:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --debug-key
|
||||
# Auto-detect keystore (recommended)
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --auto-keystore
|
||||
|
||||
# Or with custom keystore
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --keystore /path/to/keystore
|
||||
|
||||
# Or unsigned
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --no-sign
|
||||
```
|
||||
|
||||
Parse the output for:
|
||||
- `KEYSTORE_USED:<path>` — which keystore was used
|
||||
- `KEYSTORE_SOURCE:<source>` — how it was resolved (debug-standard, debug-previous, debug-generated, custom)
|
||||
- `SPLIT_SIGNED:<filename>` — each re-signed split APK (XAPK only)
|
||||
- `XAPK_ASSEMBLED:<path>` — final XAPK output (XAPK only)
|
||||
|
||||
For XAPK output: inform the user that install requires `adb install-multiple` or a split APK installer (SAI).
|
||||
|
||||
### Step 9: Report & next steps
|
||||
|
||||
Generate a neutralization report following the format in `${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/SKILL.md` (Phase 6). **The report must include the "Side Effects & Legal Notice" section.**
|
||||
|
||||
Include in the report:
|
||||
- **Output format**: APK or XAPK (split bundle)
|
||||
- **Keystore used**: path and source (from `KEYSTORE_USED:` / `KEYSTORE_SOURCE:` output)
|
||||
- **Install command**: `adb install <path>` for APK, `adb install-multiple <base.apk> <splits...>` for XAPK
|
||||
|
||||
Tell the user what they can do next:
|
||||
- **Test thoroughly**: "Install via `adb install <apk>` and test for crashes — especially features tied to ads or analytics"
|
||||
- **Test thoroughly**: for APK: "Install via `adb install <apk>`"; for XAPK: "Install via `adb install-multiple` or use SAI (Split APKs Installer)" — test for crashes, especially features tied to ads or analytics
|
||||
- **Verify**: "I can re-run entry point detection on the rebuilt APK to confirm neutralization"
|
||||
- **Custom targets**: "If the app uses obfuscated SDK calls, provide a targets file for additional patching"
|
||||
- **Deep analysis**: "Run `/find-trackers` or `/find-ads` for full SDK analysis"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ errors=0
|
|||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
# 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
|
||||
|
||||
echo "=== Android Reverse Engineering: Dependency Check ==="
|
||||
echo
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
# decompile.sh — Decompile APK/JAR/AAR using jadx, fernflower, or both
|
||||
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: decompile.sh [OPTIONS] <file>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
# install-dep.sh — Install a single dependency for Android reverse engineering
|
||||
# Usage: install-dep.sh <dependency>
|
||||
# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner
|
||||
# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner, zip
|
||||
# Compound: neutralize-all (java + apktool + apksigner + zip)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — installed successfully
|
||||
|
|
@ -9,6 +10,11 @@
|
|||
# 2 — requires manual action (e.g. sudo needed but not available)
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure user-local bin is in PATH (previous installs may have placed tools there)
|
||||
if [[ -d "$HOME/.local/bin" ]] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: install-dep.sh <dependency>
|
||||
|
|
@ -24,6 +30,10 @@ Available dependencies:
|
|||
adb Android Debug Bridge
|
||||
smali Smali/baksmali assembler/disassembler
|
||||
apksigner Android APK signing tool
|
||||
zip zip archiver (needed for XAPK rebuild)
|
||||
|
||||
Compound targets:
|
||||
neutralize-all Install all SDK neutralizer deps (java, apktool, apksigner, zip)
|
||||
|
||||
The script detects your OS and package manager, then:
|
||||
- Installs directly if possible (brew, or user-local install)
|
||||
|
|
@ -434,22 +444,79 @@ install_dex2jar() {
|
|||
}
|
||||
|
||||
install_apktool() {
|
||||
# Minimum version required by sdk-neutralizer (modern APKs, targetSdk 34+)
|
||||
local MIN_MAJOR=2 MIN_MINOR=9 MIN_PATCH=0
|
||||
local need_install=false
|
||||
|
||||
if command -v apktool &>/dev/null; then
|
||||
ok "apktool already installed"
|
||||
return 0
|
||||
local ver_raw
|
||||
ver_raw=$(apktool --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
if [[ -n "$ver_raw" ]]; then
|
||||
local cur_major cur_minor cur_patch
|
||||
IFS='.' read -r cur_major cur_minor cur_patch <<< "$ver_raw"
|
||||
cur_major=${cur_major:-0}; cur_minor=${cur_minor:-0}; cur_patch=${cur_patch:-0}
|
||||
|
||||
if (( cur_major > MIN_MAJOR )) || \
|
||||
(( cur_major == MIN_MAJOR && cur_minor > MIN_MINOR )) || \
|
||||
(( cur_major == MIN_MAJOR && cur_minor == MIN_MINOR && cur_patch >= MIN_PATCH )); then
|
||||
ok "apktool $ver_raw already installed (>= ${MIN_MAJOR}.${MIN_MINOR}.${MIN_PATCH})"
|
||||
return 0
|
||||
else
|
||||
info "apktool $ver_raw found but >= ${MIN_MAJOR}.${MIN_MINOR}.${MIN_PATCH} is required — upgrading..."
|
||||
need_install=true
|
||||
fi
|
||||
else
|
||||
ok "apktool detected (could not parse version — assuming compatible)"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
need_install=true
|
||||
fi
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
brew) info "Installing apktool via Homebrew..."; brew install apktool ;;
|
||||
apt) pkg_install "apktool" ;;
|
||||
*) manual "Install apktool from https://apktool.org/docs/install" ;;
|
||||
esac
|
||||
if [[ "$need_install" == true ]]; then
|
||||
# User-local install from GitHub (no sudo needed, takes PATH precedence)
|
||||
info "Installing apktool from GitHub releases to ~/.local/bin..."
|
||||
local tag
|
||||
tag=$(gh_latest_tag "iBotPeaches/Apktool")
|
||||
if [[ -z "$tag" ]]; then
|
||||
tag="v2.10.0" # fallback known-good version
|
||||
info "Could not fetch latest tag, using fallback: $tag"
|
||||
fi
|
||||
|
||||
if command -v apktool &>/dev/null; then
|
||||
ok "apktool installed"
|
||||
else
|
||||
fail "apktool installation may have failed."
|
||||
exit 1
|
||||
local version="${tag#v}"
|
||||
local install_dir="$HOME/.local/share/apktool"
|
||||
mkdir -p "$install_dir" "$HOME/.local/bin"
|
||||
|
||||
info "Downloading apktool $version..."
|
||||
download "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${version}.jar" \
|
||||
"$install_dir/apktool.jar" 2>/dev/null || \
|
||||
download "https://github.com/iBotPeaches/Apktool/releases/download/${tag}/apktool_${version}.jar" \
|
||||
"$install_dir/apktool.jar"
|
||||
|
||||
if [[ ! -f "$install_dir/apktool.jar" ]]; then
|
||||
fail "Failed to download apktool $version."
|
||||
manual "Download from https://apktool.org/docs/install"
|
||||
fi
|
||||
|
||||
# Create wrapper script
|
||||
cat > "$HOME/.local/bin/apktool" <<'WRAPPER'
|
||||
#!/usr/bin/env bash
|
||||
exec java -jar "$HOME/.local/share/apktool/apktool.jar" "$@"
|
||||
WRAPPER
|
||||
chmod +x "$HOME/.local/bin/apktool"
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
add_to_profile 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
|
||||
# Verify
|
||||
local installed_ver
|
||||
installed_ver=$("$HOME/.local/bin/apktool" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
if [[ -n "$installed_ver" ]]; then
|
||||
ok "apktool $installed_ver installed to $install_dir"
|
||||
else
|
||||
fail "apktool installation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -656,6 +723,49 @@ install_apksigner() {
|
|||
manual "Install Android SDK build-tools or run: sudo apt install apksigner (Debian/Ubuntu)"
|
||||
}
|
||||
|
||||
install_zip() {
|
||||
if command -v zip &>/dev/null; then
|
||||
ok "zip already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Installing zip..."
|
||||
case "$PKG_MANAGER" in
|
||||
brew) brew install zip ;;
|
||||
apt) pkg_install "zip" ;;
|
||||
dnf) pkg_install "zip" ;;
|
||||
pacman) pkg_install "zip" ;;
|
||||
*) manual "Install zip using your system package manager." ;;
|
||||
esac
|
||||
|
||||
if command -v zip &>/dev/null; then
|
||||
ok "zip installed"
|
||||
else
|
||||
fail "zip installation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_neutralize_all() {
|
||||
echo "=== Installing all SDK Neutralizer dependencies ==="
|
||||
echo
|
||||
local failed=()
|
||||
for dep_fn in install_java install_apktool install_apksigner install_zip; do
|
||||
dep_name="${dep_fn#install_}"
|
||||
info "--- $dep_name ---"
|
||||
if ! $dep_fn; then
|
||||
failed+=("$dep_name")
|
||||
fi
|
||||
echo
|
||||
done
|
||||
|
||||
if [[ ${#failed[@]} -gt 0 ]]; then
|
||||
fail "Failed to install: ${failed[*]}"
|
||||
exit 1
|
||||
fi
|
||||
ok "All SDK Neutralizer dependencies installed."
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Dispatch
|
||||
# =====================================================================
|
||||
|
|
@ -669,9 +779,12 @@ case "$DEP" in
|
|||
adb) install_adb ;;
|
||||
smali|baksmali) install_smali ;;
|
||||
apksigner) install_apksigner ;;
|
||||
zip) install_zip ;;
|
||||
neutralize-all) install_neutralize_all ;;
|
||||
*)
|
||||
echo "Error: Unknown dependency '$DEP'" >&2
|
||||
echo "Available: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner" >&2
|
||||
echo "Available: java, jadx, vineflower, dex2jar, apktool, adb, smali, apksigner, zip" >&2
|
||||
echo "Compound: neutralize-all" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -64,15 +64,22 @@ Check that all required tools are installed.
|
|||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh
|
||||
```
|
||||
|
||||
If any `INSTALL_REQUIRED:` lines appear, install the missing dependencies:
|
||||
If any `INSTALL_REQUIRED:` lines appear, ask the user to install all dependencies at once:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh <dep>
|
||||
# Install all neutralizer deps (java, apktool, apksigner, zip) in one command
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all
|
||||
```
|
||||
|
||||
If the script exits with code 2 (sudo needed but no TTY), tell the user to run in their terminal:
|
||||
|
||||
```
|
||||
sudo bash <plugin-root>/skills/android-reverse-engineering/scripts/install-dep.sh neutralize-all
|
||||
```
|
||||
|
||||
### Phase 2: Decode APK
|
||||
|
||||
Decode the APK (or XAPK) into smali and resources using decode-apk.sh. This script handles both `.apk` and `.xapk` files — for XAPKs it automatically extracts the base APK, skipping split/config APKs.
|
||||
Decode the APK (or XAPK) into smali and resources using decode-apk.sh. This script handles both `.apk` and `.xapk` files — for XAPKs it automatically extracts and decodes the base APK, while preserving the full XAPK structure (split APKs, manifest, icon) in a `.xapk-origin/` directory inside the decoded output for automatic reassembly during rebuild.
|
||||
|
||||
**Action**: Run the decode script.
|
||||
|
||||
|
|
@ -82,24 +89,99 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/decode-apk.sh <apk-or-
|
|||
|
||||
The script verifies the output contains `smali/` and `AndroidManifest.xml` and outputs `DECODED_DIR:<path>`.
|
||||
|
||||
For XAPK input, the script also outputs `XAPK_ORIGIN:<path>` and creates:
|
||||
- `.xapk-origin/metadata.json` — XAPK metadata (package, version, split list)
|
||||
- `.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.
|
||||
|
||||
### Phase 3: Identify Targets
|
||||
|
||||
Run the tracker and ad detection scripts on the decoded smali to identify which SDKs are present and which entry points the app uses.
|
||||
Target identification has three sub-phases. The goal is to find all SDK entry points to neutralize while minimizing manual approval prompts.
|
||||
|
||||
**Action**: Run entry point detection.
|
||||
#### Phase 3a — Built-in Catalog Detection
|
||||
|
||||
Use `neutralize.sh --dry-run` to detect known SDK targets. This is more reliable than `find-ads.sh`/`find-trackers.sh` because it searches smali directly (not Java source) and matches the exact patterns that will be patched.
|
||||
|
||||
**Action**: Run dry-run detection.
|
||||
|
||||
```bash
|
||||
# Detect ad SDK entry points called from app code
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <decoded-dir> --entrypoints
|
||||
|
||||
# Detect tracker SDK entry points called from app code
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh <decoded-dir> --entrypoints
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run
|
||||
```
|
||||
|
||||
Present the detected SDKs and entry points to the user. Ask which categories to neutralize:
|
||||
Parse the output for:
|
||||
- `DRY_RUN:WOULD_PATCH:` lines — smali methods that would be stubbed
|
||||
- `DRY_RUN:WOULD_DISABLE:` lines — manifest components that would be disabled
|
||||
|
||||
**NOTE**: Do NOT use `find-ads.sh` or `find-trackers.sh` here — those scripts search Java/Kotlin source (`.java`, `.kt`), not smali. The decoded directory from Phase 2 contains only smali bytecode, so those scripts will find nothing.
|
||||
|
||||
#### Phase 3b — Custom/Proprietary SDK Discovery (if needed)
|
||||
|
||||
Activate this sub-phase when:
|
||||
- The user mentions a specific SDK not in the built-in catalog (e.g., `guru/ads/fusion`, `com/proprietary/analytics`)
|
||||
- The dry-run found few or no targets but the user expects more
|
||||
- The user asks to find "all" trackers/ads, including custom/proprietary ones
|
||||
|
||||
**CRITICAL — Use Claude Code built-in tools, NOT bash commands.** Glob, Grep, and Read are auto-approved and require no manual user approval. Using `bash find`, `bash grep`, or `bash head` forces the user to approve each command individually.
|
||||
|
||||
**Discovery workflow using built-in tools:**
|
||||
|
||||
1. **Find smali files by package pattern** — use Glob:
|
||||
```
|
||||
Glob: **/smali*/com/guru/**/*.smali
|
||||
Glob: **/smali*/com/proprietary/analytics/**/*.smali
|
||||
```
|
||||
|
||||
2. **Find SDK method signatures** — use Grep on the matched files:
|
||||
```
|
||||
Grep: pattern="^\.method.*(init|track|log|show|load|send|report|emit)"
|
||||
Grep: pattern="^\.method.*getInstance"
|
||||
```
|
||||
|
||||
3. **Find SDK invocations from app code** — use Grep on the app's own smali:
|
||||
```
|
||||
Grep: pattern="invoke-(static|virtual|direct).*Lcom/guru/ads/"
|
||||
Grep: pattern="invoke-(static|virtual|direct).*Lcom/proprietary/analytics/"
|
||||
```
|
||||
|
||||
4. **Examine specific files** — use Read to inspect method bodies and confirm they are SDK entry points worth neutralizing.
|
||||
|
||||
**Build the custom targets file** with discovered entry points, one per line, in the format:
|
||||
|
||||
```
|
||||
<smali-class-path>:<method-name>
|
||||
```
|
||||
|
||||
For example:
|
||||
```
|
||||
smali/com/guru/ads/fusion/FusionAd.smali:initialize
|
||||
smali/com/guru/ads/fusion/FusionAd.smali:showAd
|
||||
smali/com/guru/ads/fusion/FusionTracker.smali:trackEvent
|
||||
```
|
||||
|
||||
**Action**: Write the targets file.
|
||||
|
||||
```
|
||||
Write: <decoded-dir>/custom-targets.txt
|
||||
```
|
||||
|
||||
#### Phase 3c — Compile Target List and Confirm
|
||||
|
||||
Present a summary table of all targets to the user:
|
||||
|
||||
| SDK | Category | Source | Entry Points |
|
||||
|---|---|---|---|
|
||||
| AdMob | Ads | Built-in catalog | 5 methods, 2 components |
|
||||
| Firebase Analytics | Trackers | Built-in catalog | 3 methods, 1 component |
|
||||
| guru/ads/fusion | Ads | Custom discovery | 3 methods |
|
||||
| ... | | | |
|
||||
|
||||
Ask which categories/SDKs to neutralize:
|
||||
- `--ads` — only ad SDKs
|
||||
- `--trackers` — only tracker/analytics SDKs
|
||||
- `--all` — both (default)
|
||||
- Optionally exclude specific SDKs
|
||||
|
||||
### Phase 4: Neutralize
|
||||
|
||||
|
|
@ -115,6 +197,16 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded
|
|||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all
|
||||
```
|
||||
|
||||
**If Phase 3b produced custom targets**, add `--targets-file` to both commands:
|
||||
|
||||
```bash
|
||||
# Preview with custom targets
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run --targets-file <decoded-dir>/custom-targets.txt
|
||||
|
||||
# Apply with custom targets
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --targets-file <decoded-dir>/custom-targets.txt
|
||||
```
|
||||
|
||||
Parse the output for `PATCHED:` and `MANIFEST_DISABLED:` lines to build the report.
|
||||
|
||||
Options:
|
||||
|
|
@ -130,21 +222,38 @@ After a successful (non-dry-run) neutralization, a `neutralize-manifest.json` is
|
|||
|
||||
### Phase 5: Rebuild & Sign
|
||||
|
||||
Rebuild the decoded directory back into a signed APK.
|
||||
Rebuild the decoded directory back into a signed APK (or XAPK if the original was an XAPK).
|
||||
|
||||
**Action**: Run the rebuild script.
|
||||
**Before calling rebuild**, you **MUST ask the user** their signing preference:
|
||||
|
||||
> How would you like to sign the rebuilt APK?
|
||||
>
|
||||
> 1. **Auto-detect** (recommended) — checks for `~/.android/debug.keystore` first, then generates a debug key
|
||||
> 2. **Custom keystore** — provide path, alias, and password
|
||||
> 3. **No signing** — output unsigned APK (cannot be installed directly)
|
||||
|
||||
Map the user's choice to the corresponding flag:
|
||||
- Option 1 → `--auto-keystore`
|
||||
- Option 2 → `--keystore <file> --key-alias <alias> --store-pass <pass> --key-pass <pass>`
|
||||
- Option 3 → `--no-sign`
|
||||
|
||||
**Action**: Run the rebuild script with the chosen signing option.
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --debug-key
|
||||
# Example with auto-keystore (recommended default)
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --auto-keystore
|
||||
```
|
||||
|
||||
Options:
|
||||
- `-o <output.apk>` — custom output path
|
||||
- `--debug-key` — auto-generate debug keystore (default)
|
||||
- `-o <output>` — custom output path
|
||||
- `--auto-keystore` — auto-detect best keystore (recommended)
|
||||
- `--debug-key` — always generate new debug keystore
|
||||
- `--keystore <file>` — 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.
|
||||
|
||||
### Phase 6: Verify & Report
|
||||
|
||||
Generate a structured neutralization report and suggest next steps.
|
||||
|
|
@ -201,9 +310,12 @@ and privacy compliance only.
|
|||
|
||||
## Output
|
||||
|
||||
- Sanitized APK: `<path>`
|
||||
- Signed with: debug key / custom keystore
|
||||
- Install via: `adb install <path>`
|
||||
- Sanitized APK/XAPK: `<path>`
|
||||
- Output format: APK (single) / XAPK (split bundle)
|
||||
- Signed with: auto-detected debug key / generated debug key / custom keystore
|
||||
- Keystore used: `<path>` (source: `KEYSTORE_SOURCE:` value)
|
||||
- Install via: `adb install <path>` (APK) or `adb install-multiple <base.apk> <split1.apk> ...` (XAPK)
|
||||
- For XAPK: can also use SAI (Split APKs Installer) or unzip and `adb install-multiple *.apk`
|
||||
```
|
||||
|
||||
**Next steps to suggest:**
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ REQUIRED_JAVA_MAJOR=17
|
|||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
# 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
|
||||
|
||||
echo "=== SDK Neutralizer: Dependency Check ==="
|
||||
echo
|
||||
|
||||
|
|
@ -69,11 +74,15 @@ fi
|
|||
|
||||
# --- apksigner or jarsigner (at least one required) ---
|
||||
signer_found=false
|
||||
has_apksigner=false
|
||||
if command -v apksigner &>/dev/null; then
|
||||
echo "[OK] apksigner detected"
|
||||
signer_found=true
|
||||
has_apksigner=true
|
||||
elif command -v jarsigner &>/dev/null; then
|
||||
echo "[OK] jarsigner detected (fallback signer)"
|
||||
echo " Note: XAPK rebuild requires apksigner (APK Signature Scheme v2/v3)."
|
||||
echo " jarsigner only supports v1 signatures — insufficient for split APKs."
|
||||
signer_found=true
|
||||
fi
|
||||
if [[ "$signer_found" == false ]]; then
|
||||
|
|
@ -111,6 +120,14 @@ else
|
|||
fi
|
||||
fi
|
||||
|
||||
# --- zip (optional, required for XAPK rebuild) ---
|
||||
if command -v zip &>/dev/null; then
|
||||
echo "[OK] zip detected (required for XAPK rebuild)"
|
||||
else
|
||||
echo "[MISSING] zip not found (required for XAPK rebuild — install: apt install zip / brew install zip)"
|
||||
missing_optional+=("zip")
|
||||
fi
|
||||
|
||||
# --- Machine-readable summary ---
|
||||
echo
|
||||
if [[ ${#missing_required[@]} -gt 0 ]]; then
|
||||
|
|
@ -131,7 +148,14 @@ echo
|
|||
|
||||
if [[ ${#missing_required[@]} -gt 0 ]]; then
|
||||
echo "*** ${#missing_required[@]} required dependency/ies missing. ***"
|
||||
echo "Run install-dep.sh <name> to install, or see references/neutralization-guide.md."
|
||||
echo
|
||||
echo "Install all neutralizer dependencies at once:"
|
||||
echo " bash install-dep.sh neutralize-all"
|
||||
echo
|
||||
echo "If sudo is needed (e.g. inside Claude Code where there's no TTY):"
|
||||
echo " sudo bash install-dep.sh neutralize-all"
|
||||
echo
|
||||
echo "Or install individually: install-dep.sh <name>"
|
||||
exit 1
|
||||
else
|
||||
if [[ ${#missing_optional[@]} -gt 0 ]]; then
|
||||
|
|
|
|||
|
|
@ -8,14 +8,20 @@
|
|||
# 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 from the archive and decodes it.
|
||||
Split APKs (config.*.apk) are skipped with a warning.
|
||||
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
|
||||
|
|
@ -28,6 +34,7 @@ Options:
|
|||
|
||||
Output:
|
||||
DECODED_DIR:<path>
|
||||
XAPK_ORIGIN:<path> (only for XAPK input)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
|
@ -91,20 +98,21 @@ if [[ -z "$OUTPUT_DIR" ]]; then
|
|||
fi
|
||||
|
||||
# =====================================================================
|
||||
# XAPK handling — extract base APK
|
||||
# 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
|
||||
}
|
||||
trap cleanup_xapk EXIT
|
||||
|
||||
if [[ "$ext_lower" == "xapk" ]]; then
|
||||
IS_XAPK=true
|
||||
echo "=== Extracting XAPK archive ==="
|
||||
XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX")
|
||||
unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_TMPDIR"
|
||||
|
|
@ -122,6 +130,7 @@ if [[ "$ext_lower" == "xapk" ]]; then
|
|||
|
||||
if [[ ${#all_apks[@]} -eq 0 ]]; then
|
||||
echo "Error: No APK files found inside XAPK archive." >&2
|
||||
rm -rf "$XAPK_TMPDIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -158,23 +167,24 @@ if [[ "$ext_lower" == "xapk" ]]; then
|
|||
|
||||
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: $(basename "$base_apk")"
|
||||
echo "Selected base APK: $BASE_APK_NAME"
|
||||
|
||||
# Warn about skipped splits
|
||||
skipped=0
|
||||
# List split APKs
|
||||
split_apks=()
|
||||
for f in "${all_apks[@]}"; do
|
||||
if [[ "$f" != "$base_apk" ]]; then
|
||||
echo " [skipped] $(basename "$f")"
|
||||
skipped=$((skipped + 1))
|
||||
split_apks+=("$(basename "$f")")
|
||||
echo " [split] $(basename "$f")"
|
||||
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)."
|
||||
if (( ${#split_apks[@]} > 0 )); then
|
||||
echo "${#split_apks[@]} split APK(s) preserved in .xapk-origin/splits/ for rebuild."
|
||||
fi
|
||||
echo
|
||||
|
||||
|
|
@ -221,6 +231,101 @@ 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"
|
||||
|
|
|
|||
|
|
@ -9,17 +9,26 @@
|
|||
# 2 — manual action needed (missing tools)
|
||||
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: rebuild-apk.sh <decoded-dir> [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.
|
||||
|
||||
Arguments:
|
||||
<decoded-dir> Path to the apktool-decoded APK directory
|
||||
|
||||
Options:
|
||||
-o, --output <file> Output APK path (default: <decoded-dir>-neutralized.apk)
|
||||
-o, --output <file> Output path (default: <decoded-dir>-neutralized.apk/.xapk)
|
||||
--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)
|
||||
--keystore <file> Path to a custom keystore file
|
||||
--key-alias <alias> Key alias within the keystore (default: key0)
|
||||
|
|
@ -36,6 +45,10 @@ Output:
|
|||
BUILD_WARNING:Resources were not recompiled (--no-res fallback)
|
||||
SIGN_OK:<output-apk>
|
||||
VERIFY_OK:<output-apk>
|
||||
KEYSTORE_USED:<path>
|
||||
KEYSTORE_SOURCE:debug-standard|debug-previous|debug-generated|custom
|
||||
SPLIT_SIGNED:<filename> (XAPK only)
|
||||
XAPK_ASSEMBLED:<output-xapk> (XAPK only)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
|
@ -47,6 +60,7 @@ EOF
|
|||
DECODED_DIR=""
|
||||
OUTPUT=""
|
||||
USE_DEBUG_KEY=true
|
||||
USE_AUTO_KEYSTORE=false
|
||||
KEYSTORE=""
|
||||
KEY_ALIAS="key0"
|
||||
KEY_PASS="android"
|
||||
|
|
@ -63,7 +77,8 @@ while [[ $# -gt 0 ]]; do
|
|||
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 ;;
|
||||
--auto-keystore) USE_AUTO_KEYSTORE=true; USE_DEBUG_KEY=false; shift ;;
|
||||
--debug-key) USE_DEBUG_KEY=true; USE_AUTO_KEYSTORE=false; shift ;;
|
||||
--keystore)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then echo "Error: --keystore requires a file argument" >&2; exit 1; fi
|
||||
|
|
@ -100,11 +115,21 @@ if [[ ! -d "$DECODED_DIR" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Default output name
|
||||
# Auto-detect XAPK origin
|
||||
IS_XAPK=false
|
||||
XAPK_ORIGIN_DIR="$DECODED_DIR/.xapk-origin"
|
||||
if [[ -f "$XAPK_ORIGIN_DIR/metadata.json" ]]; then
|
||||
IS_XAPK=true
|
||||
fi
|
||||
|
||||
# Default output name — .xapk if original was XAPK, .apk otherwise
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
# Strip trailing slash
|
||||
local_dir="${DECODED_DIR%/}"
|
||||
OUTPUT="${local_dir}-neutralized.apk"
|
||||
if [[ "$IS_XAPK" == true ]]; then
|
||||
OUTPUT="${local_dir}-neutralized.xapk"
|
||||
else
|
||||
OUTPUT="${local_dir}-neutralized.apk"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
|
|
@ -138,6 +163,23 @@ if [[ "$DO_SIGN" == true ]]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# XAPK requires apksigner (v2/v3 signatures needed for split APKs on Android 7+)
|
||||
if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]] && [[ "$SIGNER" != "apksigner" ]]; then
|
||||
fail "XAPK rebuild requires apksigner for APK Signature Scheme v2/v3."
|
||||
echo " jarsigner only supports v1 signatures, which do not work with split APKs on Android 7+." >&2
|
||||
echo " Install apksigner (part of Android SDK build-tools) or use --no-sign." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# XAPK rebuild requires zip
|
||||
if [[ "$IS_XAPK" == true ]]; then
|
||||
if ! command -v zip &>/dev/null; then
|
||||
fail "XAPK rebuild requires 'zip' command to assemble the final XAPK."
|
||||
echo " Install zip: apt install zip / brew install zip" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check zipalign availability
|
||||
ZIPALIGN_CMD=""
|
||||
if [[ "$DO_ZIPALIGN" == true ]] && [[ "$NO_ZIPALIGN" == false ]]; then
|
||||
|
|
@ -243,16 +285,68 @@ fi
|
|||
# =====================================================================
|
||||
|
||||
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."
|
||||
if [[ "$IS_XAPK" == true ]]; then
|
||||
# For unsigned XAPK, just output the base APK — cannot assemble unsigned XAPK
|
||||
cp "$ALIGNED_APK" "$OUTPUT"
|
||||
ok "Unsigned base APK saved to: $OUTPUT"
|
||||
echo "BUILD_OK:$OUTPUT"
|
||||
echo
|
||||
echo "WARNING: APK is unsigned and cannot be installed without signing."
|
||||
echo " XAPK assembly skipped (split APKs require signing for Android 7+)."
|
||||
echo " Split APKs are preserved in: $XAPK_ORIGIN_DIR/splits/"
|
||||
else
|
||||
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."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate debug keystore if needed
|
||||
if [[ "$USE_DEBUG_KEY" == true ]]; then
|
||||
# Resolve keystore
|
||||
KEYSTORE_SOURCE=""
|
||||
|
||||
if [[ "$USE_AUTO_KEYSTORE" == true ]]; then
|
||||
# Priority 1: Android SDK standard debug keystore
|
||||
ANDROID_DEBUG_KS="$HOME/.android/debug.keystore"
|
||||
if [[ -f "$ANDROID_DEBUG_KS" ]]; then
|
||||
KEYSTORE="$ANDROID_DEBUG_KS"
|
||||
KEY_ALIAS="androiddebugkey"
|
||||
KEY_PASS="android"
|
||||
STORE_PASS="android"
|
||||
KEYSTORE_SOURCE="debug-standard"
|
||||
info "Using Android SDK debug keystore: $KEYSTORE"
|
||||
fi
|
||||
|
||||
# Priority 2: Previous neutralizer debug keystore
|
||||
if [[ -z "$KEYSTORE_SOURCE" ]]; then
|
||||
PREV_DEBUG_KS="$DECODED_DIR/.neutralizer-debug.keystore"
|
||||
if [[ -f "$PREV_DEBUG_KS" ]]; then
|
||||
KEYSTORE="$PREV_DEBUG_KS"
|
||||
KEYSTORE_SOURCE="debug-previous"
|
||||
info "Using previous neutralizer debug keystore: $KEYSTORE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Priority 3: Generate new debug keystore (fallback)
|
||||
if [[ -z "$KEYSTORE_SOURCE" ]]; then
|
||||
KEYSTORE="$DECODED_DIR/.neutralizer-debug.keystore"
|
||||
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"
|
||||
KEYSTORE_SOURCE="debug-generated"
|
||||
fi
|
||||
elif [[ "$USE_DEBUG_KEY" == true ]]; then
|
||||
KEYSTORE="$DECODED_DIR/.neutralizer-debug.keystore"
|
||||
if [[ ! -f "$KEYSTORE" ]]; then
|
||||
info "Generating debug keystore..."
|
||||
|
|
@ -268,6 +362,10 @@ if [[ "$USE_DEBUG_KEY" == true ]]; then
|
|||
2>/dev/null
|
||||
ok "Debug keystore generated: $KEYSTORE"
|
||||
fi
|
||||
KEYSTORE_SOURCE="debug-generated"
|
||||
else
|
||||
# Custom keystore provided via --keystore
|
||||
KEYSTORE_SOURCE="custom"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$KEYSTORE" ]]; then
|
||||
|
|
@ -275,6 +373,9 @@ if [[ ! -f "$KEYSTORE" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "KEYSTORE_USED:$KEYSTORE"
|
||||
echo "KEYSTORE_SOURCE:$KEYSTORE_SOURCE"
|
||||
|
||||
info "Signing APK with $SIGNER..."
|
||||
|
||||
if [[ "$SIGNER" == "apksigner" ]]; then
|
||||
|
|
@ -330,17 +431,108 @@ elif [[ "$SIGNER" == "jarsigner" ]]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Step 5: XAPK assembly (if original was XAPK)
|
||||
# =====================================================================
|
||||
|
||||
if [[ "$IS_XAPK" == true ]] && [[ "$DO_SIGN" == true ]]; then
|
||||
echo
|
||||
echo "=== Assembling XAPK ==="
|
||||
|
||||
XAPK_WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-rebuild-XXXXXX")
|
||||
xapk_cleanup() { rm -rf "$XAPK_WORKDIR"; }
|
||||
trap xapk_cleanup EXIT
|
||||
|
||||
# Read base APK name from metadata
|
||||
BASE_APK_NAME=$(sed -n 's/.*"base_apk"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$XAPK_ORIGIN_DIR/metadata.json" | head -1)
|
||||
if [[ -z "$BASE_APK_NAME" ]]; then
|
||||
BASE_APK_NAME="base.apk"
|
||||
fi
|
||||
|
||||
# Copy signed base APK
|
||||
cp "$OUTPUT" "$XAPK_WORKDIR/$BASE_APK_NAME"
|
||||
info "Copied signed base APK as $BASE_APK_NAME"
|
||||
|
||||
# Re-sign and copy each split APK
|
||||
if [[ -d "$XAPK_ORIGIN_DIR/splits" ]]; then
|
||||
for split_apk in "$XAPK_ORIGIN_DIR/splits/"*.apk; do
|
||||
if [[ ! -f "$split_apk" ]]; then continue; fi
|
||||
split_name=$(basename "$split_apk")
|
||||
info "Signing split: $split_name"
|
||||
apksigner sign \
|
||||
--ks "$KEYSTORE" \
|
||||
--ks-key-alias "$KEY_ALIAS" \
|
||||
--ks-pass "pass:$STORE_PASS" \
|
||||
--key-pass "pass:$KEY_PASS" \
|
||||
--out "$XAPK_WORKDIR/$split_name" \
|
||||
"$split_apk"
|
||||
echo "SPLIT_SIGNED:$split_name"
|
||||
done
|
||||
fi
|
||||
|
||||
# Copy manifest.json and icon from .xapk-origin/
|
||||
if [[ -f "$XAPK_ORIGIN_DIR/manifest.json" ]]; then
|
||||
cp "$XAPK_ORIGIN_DIR/manifest.json" "$XAPK_WORKDIR/"
|
||||
fi
|
||||
for icon_file in "$XAPK_ORIGIN_DIR"/icon.png "$XAPK_ORIGIN_DIR"/icon.jpg; do
|
||||
if [[ -f "$icon_file" ]]; then
|
||||
cp "$icon_file" "$XAPK_WORKDIR/"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Assemble XAPK (zip with no compression for APKs)
|
||||
# Make output path absolute for the subshell cd
|
||||
XAPK_OUTPUT=$(realpath -m "$OUTPUT" 2>/dev/null || echo "$(pwd)/$OUTPUT")
|
||||
# Remove the base APK output (it's now inside the XAPK)
|
||||
rm -f "$XAPK_OUTPUT" 2>/dev/null || true
|
||||
|
||||
(cd "$XAPK_WORKDIR" && zip -r -0 "$XAPK_OUTPUT" . 2>&1) || {
|
||||
fail "Failed to assemble XAPK archive"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ ! -f "$XAPK_OUTPUT" ]]; then
|
||||
fail "XAPK output not found at: $XAPK_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
# Update OUTPUT to the absolute path used
|
||||
OUTPUT="$XAPK_OUTPUT"
|
||||
|
||||
ok "XAPK assembled: $XAPK_OUTPUT"
|
||||
echo "XAPK_ASSEMBLED:$XAPK_OUTPUT"
|
||||
|
||||
# Clean up workdir
|
||||
xapk_cleanup
|
||||
trap - EXIT
|
||||
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 [[ "$IS_XAPK" == true ]]; then
|
||||
echo "Output XAPK: $OUTPUT"
|
||||
else
|
||||
echo "Output APK: $OUTPUT"
|
||||
fi
|
||||
|
||||
if [[ "$DO_SIGN" == true ]]; then
|
||||
SIGN_DESC="$KEYSTORE_SOURCE"
|
||||
case "$KEYSTORE_SOURCE" in
|
||||
debug-standard) SIGN_DESC="Android SDK debug key (~/.android/debug.keystore)" ;;
|
||||
debug-previous) SIGN_DESC="previous neutralizer debug key" ;;
|
||||
debug-generated) SIGN_DESC="auto-generated debug key" ;;
|
||||
custom) SIGN_DESC="custom keystore ($KEYSTORE)" ;;
|
||||
esac
|
||||
echo "Signed with: $SIGNER ($SIGN_DESC)"
|
||||
fi
|
||||
|
||||
OUTPUT_SIZE=$(stat -f%z "$OUTPUT" 2>/dev/null || stat -c%s "$OUTPUT" 2>/dev/null || echo "unknown")
|
||||
echo "Output size: $OUTPUT_SIZE bytes"
|
||||
|
||||
if [[ "$BUILD_USED_NO_RES" == true ]]; then
|
||||
echo
|
||||
|
|
@ -351,6 +543,12 @@ fi
|
|||
|
||||
echo
|
||||
echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading."
|
||||
echo "Install via: adb install $OUTPUT"
|
||||
if [[ "$IS_XAPK" == true ]]; then
|
||||
echo "Install via: adb install-multiple <base.apk> <split1.apk> <split2.apk> ..."
|
||||
echo " or: use a split APK installer (e.g., SAI — Split APKs Installer)"
|
||||
echo " or: unzip the XAPK and run: adb install-multiple *.apk"
|
||||
else
|
||||
echo "Install via: adb install $OUTPUT"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
|
|
|||
Loading…
Reference in New Issue