diff --git a/README.md b/README.md index e5934ba..bc7b751 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,47 @@ # Android Reverse Engineering & API Extraction — Claude Code skill +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GitHub stars](https://img.shields.io/github/stars/SimoneAvogadro/android-reverse-engineering-skill?style=social)](https://github.com/SimoneAvogadro/android-reverse-engineering-skill/stargazers) [![GitHub last commit](https://img.shields.io/github/last-commit/SimoneAvogadro/android-reverse-engineering-skill)](https://github.com/SimoneAvogadro/android-reverse-engineering-skill/commits/master) + A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts HTTP APIs**, **audits privacy** by detecting tracker/analytics and advertising SDKs, and **neutralizes SDK telemetry** at the smali bytecode level for enterprise deployment — so you can document endpoints, understand data collection, assess ad monetization, and produce sanitized APKs without the original source code. +> **Windows / PowerShell support (experimental)**: The `*.ps1` scripts alongside the bash ones are a recent community contribution, still being stabilised. For any issues please open an issue on **this** repository (not on the contributors' upstream forks): the PowerShell scripts are maintained here by [@SimoneAvogadro](https://github.com/SimoneAvogadro). + +## Table of Contents + +- [What it does](#what-it-does) +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) +- [Repository Structure](#repository-structure) +- [References](#references) +- [Acknowledgments](#acknowledgments) +- [Disclaimer](#disclaimer) +- [License](#license) + ## What it does -- **Decompiles** APK, XAPK, JAR, and AAR files using jadx and Fernflower/Vineflower (single engine or side-by-side comparison) -- **Extracts and documents APIs**: Retrofit endpoints, OkHttp calls, hardcoded URLs, auth headers and tokens -- **Traces call flows** from Activities/Fragments through ViewModels and repositories down to HTTP calls -- **Detects tracker/analytics SDKs**: Firebase Analytics, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry — with deep analysis of init, events, user identification, consent, and data exfiltration endpoints -- **Detects advertising SDKs**: AdMob, Unity Ads, IronSource/LevelPlay, AppLovin/MAX, Meta Audience Network, Vungle, InMobi, Chartboost, Pangle, Mintegral — with ad format mapping, mediation analysis, and consent framework detection -- **Neutralizes SDK entry points**: replaces tracker/ad SDK method bodies with no-op stubs at the smali level, disables manifest components, and rebuilds a signed APK for enterprise sideloading -- **Analyzes** app structure: manifest, packages, architecture patterns -- **Handles obfuscated code**: strategies for navigating ProGuard/R8 output +| Capability | Description | +|------------|-------------| +| **Decompile** | APK, XAPK, JAR, and AAR files using jadx and Fernflower/Vineflower (single engine or side-by-side comparison) | +| **Extract APIs** | Retrofit endpoints, OkHttp calls, hardcoded URLs, auth headers and tokens | +| **Trace call flows** | From Activities/Fragments through ViewModels and repositories down to HTTP calls | +| **Detect tracker/analytics SDKs** | Firebase Analytics, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry — with deep analysis of init, events, user identification, consent, and data exfiltration endpoints | +| **Detect advertising SDKs** | AdMob, Unity Ads, IronSource/LevelPlay, AppLovin/MAX, Meta Audience Network, Vungle, InMobi, Chartboost, Pangle, Mintegral — with ad format mapping, mediation analysis, and consent framework detection | +| **Neutralize SDK entry points** | Replace tracker/ad SDK method bodies with no-op stubs at the smali level, disable manifest components, and rebuild a signed APK for enterprise sideloading | +| **Analyze structure** | Manifest, packages, architecture patterns | +| **Handle obfuscation** | Strategies for navigating ProGuard/R8 output | ## Requirements **Required:** + - Java JDK 17+ - [jadx](https://github.com/skylot/jadx) (CLI) **Optional (recommended):** + - [Vineflower](https://github.com/Vineflower/vineflower) or [Fernflower](https://github.com/JetBrains/fernflower) — better output on complex Java code -- [dex2jar](https://github.com/pxb1988/dex2jar) — needed to use Fernflower on APK/DEX files +- [dex2jar](https://github.com/ThexXTURBOXx/dex2jar) — needed to use Fernflower on APK/DEX files **For SDK neutralization (`/neutralize`):** - [apktool](https://apktool.org/) (required) — APK decode/rebuild @@ -36,7 +56,7 @@ See `plugins/android-reverse-engineering/skills/android-reverse-engineering/refe Inside Claude Code, run: -``` +```text /plugin marketplace add SimoneAvogadro/android-reverse-engineering-skill /plugin install android-reverse-engineering@android-reverse-engineering-skill ``` @@ -74,7 +94,7 @@ git clone https://github.com/SimoneAvogadro/android-reverse-engineering-skill.gi Then in Claude Code: -``` +```text /plugin marketplace add /path/to/android-reverse-engineering-skill /plugin install android-reverse-engineering@android-reverse-engineering-skill ``` @@ -83,7 +103,7 @@ Then in Claude Code: ### Slash commands -``` +```text /decompile path/to/app.apk ``` Runs the full workflow: dependency check, decompilation, and initial structure analysis. @@ -184,7 +204,7 @@ bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutrali ## Repository Structure -``` +```text android-reverse-engineering-skill/ ├── .claude-plugin/ │ └── marketplace.json # Marketplace catalog @@ -202,10 +222,14 @@ android-reverse-engineering-skill/ │ │ │ │ ├── api-extraction-patterns.md │ │ │ │ └── call-flow-analysis.md │ │ │ └── scripts/ -│ │ │ ├── check-deps.sh +│ │ │ ├── check-deps.sh # Bash +│ │ │ ├── check-deps.ps1 # PowerShell │ │ │ ├── install-dep.sh +│ │ │ ├── install-dep.ps1 │ │ │ ├── decompile.sh -│ │ │ └── find-api-calls.sh +│ │ │ ├── decompile.ps1 +│ │ │ ├── find-api-calls.sh +│ │ │ └── find-api-calls.ps1 │ │ ├── tracker-analysis/ # Tracker/analytics SDK detection │ │ │ ├── SKILL.md # 4-phase workflow │ │ │ ├── references/ @@ -230,8 +254,10 @@ android-reverse-engineering-skill/ │ │ └── scripts/ │ │ ├── check-neutralize-deps.sh │ │ ├── decode-apk.sh +│ │ ├── merge-splits.sh │ │ ├── neutralize.sh -│ │ └── rebuild-apk.sh +│ │ ├── rebuild-apk.sh +│ │ └── registry-scan.py │ └── commands/ │ ├── decompile.md # /decompile slash command │ ├── find-trackers.md # /find-trackers slash command @@ -246,9 +272,18 @@ android-reverse-engineering-skill/ - [jadx — Dex to Java decompiler](https://github.com/skylot/jadx) - [Fernflower — JetBrains analytical decompiler](https://github.com/JetBrains/fernflower) - [Vineflower — Fernflower community fork](https://github.com/Vineflower/vineflower) -- [dex2jar — DEX to JAR converter](https://github.com/pxb1988/dex2jar) +- [dex2jar — DEX to JAR converter](https://github.com/ThexXTURBOXx/dex2jar) - [apktool — Android resource decoder](https://apktool.org/) +## Acknowledgments + +Thanks to the contributors who have shaped this skill: + +- [@philjn](https://github.com/philjn) — Native Windows / PowerShell support (`check-deps.ps1`, `install-dep.ps1`, `decompile.ps1`, `find-api-calls.ps1`) and split/bundled APK detection in `decompile.sh` (#8) +- [@txhno](https://github.com/txhno) — Migration to the maintained [`ThexXTURBOXx/dex2jar`](https://github.com/ThexXTURBOXx/dex2jar) fork (#12) +- [@muqiao215](https://github.com/muqiao215) — Decompile partial-success handling, Fernflower timeout safeguard, intermediate-artifact directory (#10) +- [@kevinaimonster](https://github.com/kevinaimonster) — Chinese localization (`SKILL.md` discovery keywords) (#4) + ## Disclaimer This plugin is provided strictly for **lawful purposes**, including but not limited to: diff --git a/plugins/android-reverse-engineering/commands/decompile.md b/plugins/android-reverse-engineering/commands/decompile.md index 2489b82..34c888e 100644 --- a/plugins/android-reverse-engineering/commands/decompile.md +++ b/plugins/android-reverse-engineering/commands/decompile.md @@ -46,16 +46,19 @@ After any installations, re-run `check-deps.sh` to verify. Do not proceed until Run the decompile script on the target file. Choose the engine based on the input: - **APK or XAPK** → use jadx first (handles resources natively; XAPK is auto-extracted): + ```bash bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh ``` - **JAR/AAR** and Fernflower is available → prefer fernflower for better Java output: + ```bash bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh --engine fernflower ``` - **If jadx output has warnings** or the user wants the best quality → run both and compare: + ```bash bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh --engine both ``` @@ -79,6 +82,7 @@ After decompilation completes: ### Step 5: Offer next steps Tell the user what they can do next: + - **Trace call flows**: "I can follow the execution flow from any Activity to its API calls" - **Extract APIs**: "I can search for all HTTP endpoints and document them" - **Analyze specific classes**: "Point me to a specific class or feature to analyze" diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md b/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md index 0979ffb..6b31074 100644 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md @@ -1,6 +1,6 @@ --- -description: Decompile Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineer Android apps, extract HTTP API endpoints (Retrofit, OkHttp, Volley), and trace call flows from UI to network layer. Use when the user wants to decompile, analyze, or reverse engineer Android packages, find API endpoints, or follow call flows. -trigger: decompile APK|decompile XAPK|reverse engineer Android|extract API|analyze Android|jadx|fernflower|vineflower|follow call flow|decompile JAR|decompile AAR|Android reverse engineering|find API endpoints +description: Decompile Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineer Android apps, extract HTTP API endpoints (Retrofit, OkHttp, Volley), and trace call flows from UI to network layer. Use when the user wants to decompile, analyze, or reverse engineer Android packages, find API endpoints, or follow call flows. 中文触发词:反编译APK、安卓逆向、提取API、分析安卓应用、反编译安卓、逆向工程、追踪调用链、提取接口 +trigger: decompile APK|decompile XAPK|reverse engineer Android|extract API|analyze Android|jadx|fernflower|vineflower|follow call flow|decompile JAR|decompile AAR|Android reverse engineering|find API endpoints|反编译APK|安卓逆向|提取API|分析安卓应用 --- # Android Reverse Engineering @@ -15,6 +15,11 @@ This skill requires **Java JDK 17+** and **jadx** to be installed. **Fernflower/ bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.ps1" +``` + If anything is missing, follow the installation instructions in `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/setup-guide.md`. ## Workflow @@ -29,6 +34,11 @@ Before decompiling, confirm that the required tools are available — and instal bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.ps1" +``` + The output contains machine-readable lines: - `INSTALL_REQUIRED:` — must be installed before proceeding - `INSTALL_OPTIONAL:` — recommended but not blocking @@ -39,11 +49,18 @@ The output contains machine-readable lines: bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.ps1" +``` + The install script detects the OS and package manager, then: - Installs without sudo when possible (downloads to `~/.local/share/`, symlinks in `~/.local/bin/`) - Uses sudo and the system package manager when necessary (apt, dnf, pacman) - If sudo is needed but unavailable or the user declines, it prints the exact manual command and exits with code 2 — show these instructions to the user +**Windows notes**: The PowerShell install script uses `winget`, `scoop`, or `choco` (in that order). If none are available, it downloads directly to `%USERPROFILE%\.local\share\` and adds the directory to the user's PATH. After running `install-dep.ps1`, the PATH is persisted but the current terminal session may not see it. The `check-deps.ps1` and `decompile.ps1` scripts automatically refresh PATH from the user environment, so re-running them will find newly installed tools without restarting the terminal. + **For optional dependencies**, ask the user if they want to install them. Vineflower and dex2jar are recommended for best results. After installation, re-run `check-deps.sh` to confirm everything is in place. Do not proceed to Phase 2 until all required dependencies are OK. @@ -58,8 +75,15 @@ Use the decompile wrapper script to process the target file. The script supports bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh [OPTIONS] ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.ps1" [OPTIONS] +``` + For **XAPK** files (ZIP bundles containing multiple APKs, used by APKPure and similar stores): the script automatically extracts the archive, identifies all APK files inside (base + split APKs), and decompiles each one into a separate subdirectory. The XAPK manifest is copied to the output for reference. +**Split/bundled APK detection**: Some APKs are actually bundle wrappers — the outer APK contains `base.apk` plus `split_config.*.apk` files inside its resources directory. When this happens, jadx will decompile the thin wrapper and produce very few Java files. The decompile scripts automatically detect this (≤10 Java files + inner APKs present) and re-decompile `base.apk` into an `/base/` subdirectory. Config-only splits (ABI, language, density) are skipped. The main decompiled source will be in `/base/sources/`. + Options: - `-o ` — Custom output directory (default: `-decompiled`) - `--deobf` — Enable deobfuscation (recommended for obfuscated apps) @@ -137,6 +161,11 @@ Find all API endpoints and produce structured documentation. bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh /sources/ ``` +On Windows (PowerShell): +```powershell +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ +``` + Targeted searches: ```bash # Only Retrofit @@ -149,6 +178,18 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-c bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh /sources/ --auth ``` +On Windows (PowerShell): +```powershell +# Only Retrofit +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ -Retrofit + +# Only hardcoded URLs +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ -Urls + +# Only auth patterns +& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" /sources/ -Auth +``` + Then, for each discovered endpoint, read the surrounding source code to extract: - HTTP method and path - Base URL diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md b/plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md index 75e5303..68f09bf 100644 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md @@ -133,7 +133,7 @@ Converts Android DEX bytecode to standard Java JAR files. ### GitHub Releases -1. Go to +1. Go to 2. Download and extract: ```bash diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1 new file mode 100644 index 0000000..3ced181 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1 @@ -0,0 +1,165 @@ +# check-deps.ps1 — Verify dependencies and report what's missing +# Output includes machine-readable INSTALL_REQUIRED/INSTALL_OPTIONAL lines. +$ErrorActionPreference = 'Stop' + +# Refresh PATH from user environment so we pick up tools installed in the same session +$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') +if ($userPath) { + foreach ($dir in $userPath -split ';') { + if ($dir -and $env:PATH -notlike "*$dir*") { + $env:PATH = "$dir;$env:PATH" + } + } +} + +$REQUIRED_JAVA_MAJOR = 17 +$errors = 0 +$missingRequired = @() +$missingOptional = @() + +Write-Host "=== Android Reverse Engineering: Dependency Check ===" +Write-Host "" + +# --- Java --- +$javaOk = $false +$javaBin = Get-Command java -ErrorAction SilentlyContinue +if ($javaBin) { + $javaVersionOutput = & java -version 2>&1 | Select-Object -First 1 + $javaVersionStr = "$javaVersionOutput" + if ($javaVersionStr -match '"(\d+)') { + $javaVersion = [int]$Matches[1] + if ($javaVersion -eq 1 -and $javaVersionStr -match '"1\.(\d+)') { + $javaVersion = [int]$Matches[1] + } + if ($javaVersion -ge $REQUIRED_JAVA_MAJOR) { + Write-Host "[OK] Java $javaVersion detected" + $javaOk = $true + } else { + Write-Host "[WARN] Java detected but version $javaVersion is below $REQUIRED_JAVA_MAJOR" + $errors++ + $missingRequired += "java" + } + } else { + Write-Host "[WARN] Java detected but could not parse version from: $javaVersionStr" + $errors++ + $missingRequired += "java" + } +} else { + Write-Host "[MISSING] Java is not installed or not in PATH" + $errors++ + $missingRequired += "java" +} + +# --- jadx --- +$jadxBin = Get-Command jadx -ErrorAction SilentlyContinue +if (-not $jadxBin) { + # Check common Windows install locations + $jadxCandidates = @( + "$env:USERPROFILE\.local\share\jadx\bin\jadx.bat", + "$env:USERPROFILE\jadx\bin\jadx.bat", + "$env:LOCALAPPDATA\jadx\bin\jadx.bat" + ) + foreach ($c in $jadxCandidates) { + if (Test-Path $c) { + $jadxBin = $c + break + } + } +} +if ($jadxBin) { + try { + $jadxCmd = if ($jadxBin -is [string]) { $jadxBin } else { 'jadx' } + $jadxVersion = & $jadxCmd --version 2>$null + Write-Host "[OK] jadx $jadxVersion detected" + } catch { + Write-Host "[OK] jadx detected" + } +} else { + Write-Host "[MISSING] jadx is not installed or not in PATH" + $errors++ + $missingRequired += "jadx" +} + +# --- Fernflower / Vineflower --- +$ffFound = $false +$vineflowerBin = Get-Command vineflower -ErrorAction SilentlyContinue +$fernflowerBin = Get-Command fernflower -ErrorAction SilentlyContinue +if ($vineflowerBin) { + Write-Host "[OK] vineflower CLI detected" + $ffFound = $true +} elseif ($fernflowerBin) { + Write-Host "[OK] fernflower CLI detected" + $ffFound = $true +} else { + $ffCandidates = @( + $env:FERNFLOWER_JAR_PATH, + "$env:USERPROFILE\.local\share\vineflower\vineflower.jar", + "$env:USERPROFILE\fernflower\build\libs\fernflower.jar", + "$env:USERPROFILE\vineflower\build\libs\vineflower.jar", + "$env:USERPROFILE\fernflower\fernflower.jar", + "$env:USERPROFILE\vineflower\vineflower.jar" + ) + foreach ($candidate in $ffCandidates) { + if ($candidate -and (Test-Path $candidate -ErrorAction SilentlyContinue)) { + Write-Host "[OK] Fernflower/Vineflower JAR found: $candidate" + $ffFound = $true + break + } + } +} +if (-not $ffFound) { + Write-Host "[MISSING] Fernflower/Vineflower not found (optional - better output on complex Java code)" + $missingOptional += "vineflower" +} + +# --- dex2jar --- +$d2jBin = Get-Command d2j-dex2jar -ErrorAction SilentlyContinue +if (-not $d2jBin) { + $d2jBin = Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue +} +if ($d2jBin) { + Write-Host "[OK] dex2jar detected" +} else { + Write-Host "[MISSING] dex2jar not found (optional - needed to use Fernflower on APK/DEX files)" + $missingOptional += "dex2jar" +} + +# --- Optional: apktool --- +if (Get-Command apktool -ErrorAction SilentlyContinue) { + Write-Host "[OK] apktool detected (optional)" +} else { + Write-Host "[MISSING] apktool not found (optional - useful for resource decoding)" + $missingOptional += "apktool" +} + +# --- Optional: adb --- +if (Get-Command adb -ErrorAction SilentlyContinue) { + Write-Host "[OK] adb detected (optional)" +} else { + Write-Host "[MISSING] adb not found (optional - useful for pulling APKs from devices)" + $missingOptional += "adb" +} + +# --- Machine-readable summary --- +Write-Host "" +foreach ($dep in $missingRequired) { + Write-Host "INSTALL_REQUIRED:$dep" +} +foreach ($dep in $missingOptional) { + Write-Host "INSTALL_OPTIONAL:$dep" +} + +Write-Host "" +if ($errors -gt 0) { + Write-Host "*** $($missingRequired.Count) required dependency/ies missing. ***" + Write-Host "Run install-dep.ps1 to install, or see references/setup-guide.md." + exit 1 +} else { + if ($missingOptional.Count -gt 0) { + Write-Host "Required dependencies OK. $($missingOptional.Count) optional dependency/ies missing." + Write-Host "Run install-dep.ps1 to install optional tools." + } else { + Write-Host "All dependencies are installed. Ready to decompile." + } + exit 0 +} diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 new file mode 100644 index 0000000..684c419 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 @@ -0,0 +1,400 @@ +# decompile.ps1 — Decompile APK/XAPK/JAR/AAR using jadx, fernflower, or both +param( + [Alias('o')] + [string]$Output, + [switch]$Deobf, + [switch]$NoRes, + [string]$Engine = 'jadx', + [Parameter(Position=0)] + [string]$InputFile, + [Alias('h')] + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +# Refresh PATH from user environment so we pick up tools installed in the same session +$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User') +if ($userPath) { + foreach ($dir in $userPath -split ';') { + if ($dir -and $env:PATH -notlike "*$dir*") { + $env:PATH = "$dir;$env:PATH" + } + } +} + +function Show-Usage { + Write-Host @" +Usage: decompile.ps1 [OPTIONS] + +Decompile an Android APK, XAPK, JAR, or AAR file. + +Arguments: + Path to the .apk, .xapk, .jar, or .aar file + +Options: + -Output DIR Output directory (default: -decompiled) + -Deobf Enable deobfuscation of names + -NoRes Skip resource decoding (faster, code-only) + -Engine ENGINE Decompiler engine: jadx, fernflower, or both (default: jadx) + -Help Show this help message + +Engines: + jadx Use jadx (default). Handles APK/JAR/AAR natively, decodes resources. + fernflower Use Fernflower/Vineflower. Better on complex Java, lambdas, generics. + For APK files, requires dex2jar as intermediate step. + both Run both decompilers side by side for comparison. + jadx output -> /jadx/ + fernflower -> /fernflower/ + +Environment: + FERNFLOWER_JAR_PATH Path to fernflower.jar or vineflower.jar + +Examples: + .\decompile.ps1 app-release.apk + .\decompile.ps1 app-bundle.xapk + .\decompile.ps1 -Engine both -Deobf app-release.apk + .\decompile.ps1 -Engine fernflower library.jar +"@ + exit 0 +} + +if ($Help) { Show-Usage } + +# --- Validate input --- +if (-not $InputFile) { + Write-Host "Error: No input file specified." -ForegroundColor Red + Show-Usage +} + +if (-not (Test-Path $InputFile)) { + Write-Host "Error: File not found: $InputFile" -ForegroundColor Red + exit 1 +} + +$extLower = [IO.Path]::GetExtension($InputFile).TrimStart('.').ToLower() +if ($extLower -notin @('apk', 'xapk', 'jar', 'aar')) { + Write-Host "Error: Unsupported file type '.$extLower'. Expected .apk, .xapk, .jar, or .aar" -ForegroundColor Red + exit 1 +} + +if ($Engine -notin @('jadx', 'fernflower', 'both')) { + Write-Host "Error: Unknown engine '$Engine'. Use jadx, fernflower, or both." -ForegroundColor Red + exit 1 +} + +$baseName = [IO.Path]::GetFileNameWithoutExtension($InputFile) +$inputFileAbs = (Resolve-Path $InputFile).Path + +if (-not $Output) { + $Output = "$baseName-decompiled" +} + +# --- XAPK handling --- +$xapkExtractedDir = $null +$xapkApkFiles = @() + +if ($extLower -eq 'xapk') { + $xapkExtractedDir = Join-Path $env:TEMP "xapk-extract-$(Get-Random)" + Write-Host "=== Extracting XAPK archive ===" + New-Item -ItemType Directory -Path $xapkExtractedDir -Force | Out-Null + Expand-Archive -Path $inputFileAbs -DestinationPath $xapkExtractedDir -Force + + # Show manifest.json if present + $manifestPath = Join-Path $xapkExtractedDir 'manifest.json' + if (Test-Path $manifestPath) { + Write-Host "XAPK manifest found:" + Get-Content $manifestPath + Write-Host "" + } + + # Find all APK files inside + $xapkApkFiles = Get-ChildItem -Path $xapkExtractedDir -Recurse -Filter '*.apk' | Sort-Object Name + + if ($xapkApkFiles.Count -eq 0) { + Write-Host "Error: No APK files found inside XAPK archive." -ForegroundColor Red + Remove-Item $xapkExtractedDir -Recurse -Force + exit 1 + } + + Write-Host "Found $($xapkApkFiles.Count) APK(s) inside XAPK:" + foreach ($f in $xapkApkFiles) { + Write-Host " - $($f.Name)" + } + Write-Host "" +} + +# --- Locate fernflower JAR --- +function Find-FernflowerJar { + if ($env:FERNFLOWER_JAR_PATH -and (Test-Path $env:FERNFLOWER_JAR_PATH -ErrorAction SilentlyContinue)) { + return $env:FERNFLOWER_JAR_PATH + } + $candidates = @( + "$env:USERPROFILE\.local\share\vineflower\vineflower.jar", + "$env:USERPROFILE\fernflower\build\libs\fernflower.jar", + "$env:USERPROFILE\vineflower\build\libs\vineflower.jar", + "$env:USERPROFILE\fernflower\fernflower.jar", + "$env:USERPROFILE\vineflower\vineflower.jar" + ) + foreach ($c in $candidates) { + if (Test-Path $c -ErrorAction SilentlyContinue) { return $c } + } + return $null +} + +# --- Locate dex2jar --- +function Find-Dex2Jar { + $cmd = Get-Command d2j-dex2jar -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + $cmd = Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + return $null +} + +# --- jadx decompilation --- +function Invoke-Jadx { + param([string]$OutDir, [string]$FileAbs, [string]$FileExt) + + if (-not (Get-Command jadx -ErrorAction SilentlyContinue)) { + Write-Host "Error: jadx is not installed or not in PATH." -ForegroundColor Red + return $false + } + + $jadxArgs = @('-d', $OutDir) + if ($Deobf) { $jadxArgs += '--deobf' } + if ($NoRes) { $jadxArgs += '--no-res' } + $jadxArgs += '--show-bad-code' + $jadxArgs += $FileAbs + + Write-Host "Running: jadx $($jadxArgs -join ' ')" + & jadx @jadxArgs + + $sourcesDir = Join-Path $OutDir 'sources' + if (Test-Path $sourcesDir) { + $count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java').Count + Write-Host "jadx output: $sourcesDir\" + Write-Host "Java files decompiled by jadx: $count" + } + return $true +} + +# --- Fernflower decompilation --- +function Invoke-Fernflower { + param([string]$OutDir, [string]$FileAbs, [string]$FileExt) + + $ffJar = Find-FernflowerJar + if (-not $ffJar) { + Write-Host "Error: Fernflower/Vineflower JAR not found." -ForegroundColor Red + Write-Host "Set FERNFLOWER_JAR_PATH or see references/setup-guide.md" + return $false + } + + New-Item -ItemType Directory -Path $OutDir -Force | Out-Null + + $jarToDecompile = $FileAbs + $convertedJar = $null + + # For APK/AAR, we need dex2jar first + if ($FileExt -in @('apk', 'aar')) { + $d2j = Find-Dex2Jar + if (-not $d2j) { + Write-Host "Error: dex2jar is required to use Fernflower on .$FileExt files." -ForegroundColor Red + Write-Host "Install dex2jar - see references/setup-guide.md" + return $false + } + + Write-Host "Converting $FileExt to JAR with dex2jar..." + $convertedJar = Join-Path $OutDir "$baseName-dex2jar.jar" + & $d2j -f -o $convertedJar $FileAbs 2>&1 | Write-Host + if (-not (Test-Path $convertedJar)) { + Write-Host "Error: dex2jar conversion failed." -ForegroundColor Red + return $false + } + $jarToDecompile = $convertedJar + } + + # Build fernflower args + $ffArgs = @('-dgs=1', '-mpm=60') + if ($Deobf) { $ffArgs += '-ren=1' } + $ffArgs += $jarToDecompile + $ffArgs += $OutDir + + Write-Host "Running: java -jar $ffJar $($ffArgs -join ' ')" + & java -jar $ffJar @ffArgs + + # Fernflower outputs a JAR containing .java files — extract it + $resultJar = Join-Path $OutDir ([IO.Path]::GetFileName($jarToDecompile)) + if (Test-Path $resultJar) { + $sourcesDir = Join-Path $OutDir 'sources' + New-Item -ItemType Directory -Path $sourcesDir -Force | Out-Null + Expand-Archive -Path $resultJar -DestinationPath $sourcesDir -Force + Remove-Item $resultJar -Force + $count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java').Count + Write-Host "Fernflower output: $sourcesDir\" + Write-Host "Java files decompiled by Fernflower: $count" + } + + # Clean up intermediate dex2jar output + if ($convertedJar -and (Test-Path $convertedJar -ErrorAction SilentlyContinue)) { + Remove-Item $convertedJar -Force + } + return $true +} + +# --- Summary helper --- +function Show-Structure { + param([string]$SrcDir, [string]$Label) + if (Test-Path $SrcDir) { + Write-Host "" + Write-Host "Top-level packages ($Label):" + Get-ChildItem -Path $SrcDir -Directory -Recurse -Depth 2 | + Select-Object -First 20 | + ForEach-Object { $_.FullName.Replace("$SrcDir\", '') } | + Sort-Object + } +} + +# --- Decompile a single file --- +function Invoke-DecompileSingle { + param([string]$FileAbs, [string]$OutDir, [string]$Label) + + $fileExt = [IO.Path]::GetExtension($FileAbs).TrimStart('.').ToLower() + + if ($Label) { + Write-Host "=== Decompiling $Label (engine: $Engine) ===" + } + + switch ($Engine) { + 'jadx' { + Invoke-Jadx -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt + Show-Structure (Join-Path $OutDir 'sources') 'jadx' + } + 'fernflower' { + Invoke-Fernflower -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt + Show-Structure (Join-Path $OutDir 'sources') 'fernflower' + } + 'both' { + Write-Host "--- Pass 1: jadx ---" + Invoke-Jadx -OutDir (Join-Path $OutDir 'jadx') -FileAbs $FileAbs -FileExt $fileExt + Write-Host "" + Write-Host "--- Pass 2: Fernflower ---" + Invoke-Fernflower -OutDir (Join-Path $OutDir 'fernflower') -FileAbs $FileAbs -FileExt $fileExt + + Show-Structure (Join-Path $OutDir 'jadx\sources') 'jadx' + Show-Structure (Join-Path $OutDir 'fernflower\sources') 'fernflower' + + Write-Host "" + Write-Host "=== Comparison ===" + $jadxCount = 0; $ffCount = 0 + $jadxSources = Join-Path $OutDir 'jadx\sources' + $ffSources = Join-Path $OutDir 'fernflower\sources' + if (Test-Path $jadxSources) { + $jadxCount = (Get-ChildItem -Path $jadxSources -Recurse -Filter '*.java').Count + } + if (Test-Path $ffSources) { + $ffCount = (Get-ChildItem -Path $ffSources -Recurse -Filter '*.java').Count + } + Write-Host "jadx: $jadxCount Java files" + Write-Host "Fernflower: $ffCount Java files" + + if (Test-Path $jadxSources) { + $jadxErrors = (Get-ChildItem -Path $jadxSources -Recurse -Filter '*.java' -File | + Select-String -Pattern 'JADX WARNING|JADX WARN|JADX ERROR|Code decompiled incorrectly' -SimpleMatch -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty Path -Unique).Count + Write-Host "jadx files with warnings/errors: $jadxErrors" + } + Write-Host "" + Write-Host "Tip: compare specific classes between jadx/ and fernflower/ to pick the better output." + } + } +} + +# --- Run --- +Write-Host "=== Decompiling $InputFile (engine: $Engine) ===" +Write-Host "Output directory: $Output" +Write-Host "" + +if ($extLower -eq 'xapk') { + New-Item -ItemType Directory -Path $Output -Force | Out-Null + + # Copy XAPK manifest for reference + $manifestSrc = Join-Path $xapkExtractedDir 'manifest.json' + if (Test-Path $manifestSrc) { + Copy-Item $manifestSrc (Join-Path $Output 'xapk-manifest.json') + } + + # List OBB files + $obbFiles = Get-ChildItem -Path $xapkExtractedDir -Recurse -Filter '*.obb' -ErrorAction SilentlyContinue + if ($obbFiles) { + Write-Host "OBB files found (not decompiled, data-only):" + foreach ($obb in $obbFiles) { + $size = '{0:N1} MB' -f ($obb.Length / 1MB) + Write-Host " - $($obb.Name) ($size)" + } + Write-Host "" + } + + foreach ($apkFile in $xapkApkFiles) { + $apkName = [IO.Path]::GetFileNameWithoutExtension($apkFile.Name) + Write-Host "" + Write-Host "======================================================" + Invoke-DecompileSingle -FileAbs $apkFile.FullName -OutDir (Join-Path $Output $apkName) -Label "$($apkFile.Name)" + } + + # Cleanup extracted XAPK + Remove-Item $xapkExtractedDir -Recurse -Force + + Write-Host "" + Write-Host "=== XAPK decompilation complete ===" + Write-Host "Subdirectories in ${Output}\" + Get-ChildItem -Path $Output -Directory | ForEach-Object { Write-Host $_.Name } +} else { + Invoke-DecompileSingle -FileAbs $inputFileAbs -OutDir $Output -Label '' + + # --- Split/bundled APK detection --- + # Some APKs are bundles: the outer APK contains + # base.apk + split_config.*.apk inside the resources directory. jadx will + # decompile the thin outer wrapper and produce very few Java files. + # Detect this and automatically decompile the inner base.apk. + $sourcesDir = Join-Path $Output 'sources' + $resourcesDir = Join-Path $Output 'resources' + if ((Test-Path $sourcesDir) -and (Test-Path $resourcesDir)) { + $javaCount = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java' -File -ErrorAction SilentlyContinue).Count + $innerApks = Get-ChildItem -Path $resourcesDir -Filter '*.apk' -File -ErrorAction SilentlyContinue + $baseApk = $innerApks | Where-Object { $_.Name -eq 'base.apk' } + + if ($javaCount -le 10 -and $baseApk) { + Write-Host "" + Write-Host "=== Split/bundled APK detected ===" + Write-Host "Outer APK produced only $javaCount Java file(s) but contains $($innerApks.Count) inner APK(s):" + foreach ($inner in $innerApks) { + Write-Host " - $($inner.Name)" + } + Write-Host "" + Write-Host "Decompiling base.apk (contains the actual app code)..." + $baseOutput = Join-Path $Output 'base' + Invoke-DecompileSingle -FileAbs $baseApk.FullName -OutDir $baseOutput -Label 'base.apk' + + # Decompile any split APKs that aren't just config splits + $splitApks = $innerApks | Where-Object { $_.Name -ne 'base.apk' -and $_.Name -notmatch 'split_config\.' } + foreach ($split in $splitApks) { + $splitName = [IO.Path]::GetFileNameWithoutExtension($split.Name) + Write-Host "" + Write-Host "Decompiling $($split.Name)..." + Invoke-DecompileSingle -FileAbs $split.FullName -OutDir (Join-Path $Output $splitName) -Label $split.Name + } + + if ($innerApks | Where-Object { $_.Name -match 'split_config\.' }) { + Write-Host "" + Write-Host "Skipped config splits (resource/ABI only):" + $innerApks | Where-Object { $_.Name -match 'split_config\.' } | ForEach-Object { Write-Host " - $($_.Name)" } + } + + Write-Host "" + Write-Host "NOTE: The main decompiled source is in: $(Join-Path $Output 'base\sources')" + } + } + + Write-Host "" + Write-Host "=== Decompilation complete ===" +} diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh index f626018..4ff56f7 100755 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh @@ -197,6 +197,8 @@ find_dex2jar() { # --- jadx decompilation --- run_jadx() { local out_dir="$1" + local jadx_status=0 + local count=0 if ! command -v jadx &>/dev/null; then echo "Error: jadx is not installed or not in PATH." >&2 @@ -213,30 +215,46 @@ run_jadx() { echo "Running: jadx ${args[*]}" if [[ "$TIMEOUT" -gt 0 ]] 2>/dev/null; then - if ! timeout "${TIMEOUT}s" jadx "${args[@]}"; then - local exit_code=$? - if [[ $exit_code -eq 124 ]]; then - echo "WARNING: jadx timed out after ${TIMEOUT}s. Partial output may be available." >&2 - else - return $exit_code - fi + if timeout "${TIMEOUT}s" jadx "${args[@]}"; then + jadx_status=0 + else + jadx_status=$? + [[ $jadx_status -eq 124 ]] && echo "WARNING: jadx timed out after ${TIMEOUT}s. Partial output may be available." >&2 fi + elif jadx "${args[@]}"; then + jadx_status=0 else - jadx "${args[@]}" + jadx_status=$? fi echo "jadx output: $out_dir/sources/" if [[ -d "$out_dir/sources" ]]; then - local count count=$(find "$out_dir/sources" -name "*.java" | wc -l) echo "Java files decompiled by jadx: $count" fi + + if [[ $jadx_status -eq 0 ]]; then + return 0 + fi + + if [[ $count -gt 0 ]]; then + echo "Warning: jadx exited with status $jadx_status after writing $count Java files; treating this as partial success." >&2 + return 2 + fi + + echo "Error: jadx failed with status $jadx_status and produced no Java output." >&2 + return 1 } # --- Fernflower decompilation --- run_fernflower() { local out_dir="$1" local jar_to_decompile="" + local converted_jar="" + local intermediate_dir="$out_dir/intermediate" + local ff_status=0 + local d2j_status=0 + local count=0 local ff_jar if ! ff_jar=$(find_fernflower_jar); then @@ -257,12 +275,20 @@ run_fernflower() { fi echo "Converting $ext_lower to JAR with dex2jar..." - local converted_jar="$out_dir/${BASENAME}-dex2jar.jar" - "$d2j" -f -o "$converted_jar" "$INPUT_FILE_ABS" 2>&1 || true + mkdir -p "$intermediate_dir" + converted_jar="$intermediate_dir/${BASENAME}-dex2jar.jar" + if "$d2j" -f -o "$converted_jar" "$INPUT_FILE_ABS" 2>&1; then + d2j_status=0 + else + d2j_status=$? + fi if [[ ! -f "$converted_jar" ]]; then - echo "Error: dex2jar conversion failed." >&2 + echo "Error: dex2jar conversion failed with status $d2j_status." >&2 return 1 fi + if [[ $d2j_status -ne 0 ]]; then + echo "Warning: dex2jar exited with status $d2j_status but produced $converted_jar; continuing." >&2 + fi jar_to_decompile="$converted_jar" else jar_to_decompile="$INPUT_FILE_ABS" @@ -279,17 +305,18 @@ run_fernflower() { ff_args+=("$out_dir") echo "Running: java -Xmx${HEAP_SIZE} -jar $ff_jar ${ff_args[*]}" - if [[ "$TIMEOUT" -gt 0 ]] 2>/dev/null; then - if ! timeout "${TIMEOUT}s" java "-Xmx${HEAP_SIZE}" -jar "$ff_jar" "${ff_args[@]}"; then - local exit_code=$? - if [[ $exit_code -eq 124 ]]; then - echo "WARNING: Fernflower timed out after ${TIMEOUT}s. Partial output may be available." >&2 - else - return $exit_code - fi + if [[ "$TIMEOUT" -gt 0 ]] 2>/dev/null && command -v timeout &>/dev/null; then + echo "Fernflower timeout: ${TIMEOUT}s (override with --timeout)" + if timeout "${TIMEOUT}s" java "-Xmx${HEAP_SIZE}" -jar "$ff_jar" "${ff_args[@]}"; then + ff_status=0 + else + ff_status=$? + [[ $ff_status -eq 124 ]] && echo "WARNING: Fernflower timed out after ${TIMEOUT}s. Partial output may be available." >&2 fi + elif java "-Xmx${HEAP_SIZE}" -jar "$ff_jar" "${ff_args[@]}"; then + ff_status=0 else - java "-Xmx${HEAP_SIZE}" -jar "$ff_jar" "${ff_args[@]}" + ff_status=$? fi # Fernflower outputs a JAR containing .java files — extract it @@ -297,18 +324,65 @@ run_fernflower() { if [[ -f "$result_jar" ]]; then local sources_dir="$out_dir/sources" mkdir -p "$sources_dir" - unzip -qo "$result_jar" -d "$sources_dir" - rm -f "$result_jar" - echo "Fernflower output: $sources_dir/" - local count - count=$(find "$sources_dir" -name "*.java" | wc -l) - echo "Java files decompiled by Fernflower: $count" + if unzip -qo "$result_jar" -d "$sources_dir"; then + rm -f "$result_jar" + else + echo "Warning: Fernflower result jar $result_jar could not be extracted; checking for direct folder output." >&2 + fi + fi + + local sources_dir="$out_dir/sources" + mkdir -p "$sources_dir" + count=$(find "$sources_dir" -name "*.java" | wc -l) + + # Vineflower may write sources directly into the destination folder tree instead of a result jar. + if [[ $count -eq 0 ]]; then + local direct_count=0 + direct_count=$(find "$out_dir" \ + -path "$sources_dir" -prune -o \ + -path "$intermediate_dir" -prune -o \ + -name "*.java" -type f -print | wc -l) + if [[ $direct_count -gt 0 ]]; then + while IFS= read -r -d '' entry; do + mv "$entry" "$sources_dir"/ + done < <(find "$out_dir" -mindepth 1 -maxdepth 1 \ + ! -name "sources" \ + ! -name "intermediate" \ + -print0) + count=$(find "$sources_dir" -name "*.java" | wc -l) + fi fi # Clean up intermediate dex2jar output - if [[ -n "${converted_jar:-}" ]] && [[ -f "${converted_jar:-}" ]]; then - rm -f "$converted_jar" + if [[ $count -gt 0 ]]; then + echo "Fernflower output: $sources_dir/" + echo "Java files decompiled by Fernflower: $count" + if [[ -n "${converted_jar:-}" ]] && [[ -f "${converted_jar:-}" ]]; then + rm -f "$converted_jar" + fi + if [[ -d "$intermediate_dir" ]]; then + rmdir "$intermediate_dir" 2>/dev/null || true + fi + if [[ $ff_status -ne 0 ]]; then + echo "Warning: Fernflower/Vineflower exited with status $ff_status after writing $count Java files; treating this as partial success." >&2 + return 2 + fi + return 0 fi + + if [[ -n "${converted_jar:-}" ]] && [[ -f "${converted_jar:-}" ]]; then + echo "Error: Fernflower/Vineflower produced no Java output. Intermediate dex2jar artifact kept at $converted_jar" >&2 + else + echo "Error: Fernflower/Vineflower produced no Java output." >&2 + fi + + if [[ $ff_status -ne 0 ]]; then + if [[ $ff_status -eq 124 ]]; then + echo "Error: Fernflower/Vineflower exceeded timeout (${TIMEOUT}s)." >&2 + fi + echo "Error: Fernflower/Vineflower exited with status $ff_status." >&2 + fi + return 1 } # --- Summary helper --- @@ -316,9 +390,28 @@ print_structure() { local src_dir="$1" local label="$2" if [[ -d "$src_dir" ]]; then + local packages=() echo echo "Top-level packages ($label):" - find "$src_dir" -mindepth 1 -maxdepth 3 -type d | head -20 | sed "s|$src_dir/||" | grep -v '^$' | sort + while IFS= read -r pkg; do + [[ -n "$pkg" ]] && packages+=("$pkg") + done < <(find "$src_dir" -mindepth 1 -maxdepth 3 -type d -printf '%P\n' | sort) + + local limit=${#packages[@]} + if (( limit > 20 )); then + limit=20 + fi + + if (( limit == 0 )); then + echo "(none)" + return + fi + + local i=0 + while (( i < limit )); do + echo "${packages[$i]}" + ((i += 1)) + done fi } @@ -341,19 +434,63 @@ decompile_single() { case "$ENGINE" in jadx) - run_jadx "$out_dir" + local jadx_status=0 + if run_jadx "$out_dir"; then + jadx_status=0 + else + jadx_status=$? + fi print_structure "$out_dir/sources" "jadx" + if [[ $jadx_status -eq 1 ]]; then + return 1 + fi + if [[ $jadx_status -eq 2 ]]; then + echo "jadx completed with warnings but produced usable output." + fi ;; fernflower) - run_fernflower "$out_dir" + local ff_status=0 + if run_fernflower "$out_dir"; then + ff_status=0 + else + ff_status=$? + fi print_structure "$out_dir/sources" "fernflower" + if [[ $ff_status -eq 1 ]]; then + return 1 + fi + if [[ $ff_status -eq 2 ]]; then + echo "Fernflower completed with warnings but produced usable output." + fi ;; both) + local jadx_status=0 + local ff_status=0 echo "--- Pass 1: jadx ---" - run_jadx "$out_dir/jadx" + if run_jadx "$out_dir/jadx"; then + jadx_status=0 + else + jadx_status=$? + fi + if [[ $jadx_status -eq 1 ]]; then + return 1 + fi + if [[ $jadx_status -eq 2 ]]; then + echo "Continuing to Fernflower because jadx produced usable output despite warnings." + fi echo echo "--- Pass 2: Fernflower ---" - run_fernflower "$out_dir/fernflower" + if run_fernflower "$out_dir/fernflower"; then + ff_status=0 + else + ff_status=$? + fi + if [[ $ff_status -eq 1 ]]; then + return 1 + fi + if [[ $ff_status -eq 2 ]]; then + echo "Continuing with Fernflower output because it produced usable sources despite warnings." + fi print_structure "$out_dir/jadx/sources" "jadx" print_structure "$out_dir/fernflower/sources" "fernflower" @@ -371,8 +508,14 @@ decompile_single() { echo "Fernflower: $ff_count Java files" if [[ -d "$out_dir/jadx/sources" ]]; then + local jadx_error_files local jadx_errors - jadx_errors=$(grep -rl 'JADX WARNING\|JADX WARN\|JADX ERROR\|Code decompiled incorrectly' "$out_dir/jadx/sources" 2>/dev/null | wc -l || echo 0) + jadx_error_files=$(grep -rl 'JADX WARNING\|JADX WARN\|JADX ERROR\|Code decompiled incorrectly' "$out_dir/jadx/sources" 2>/dev/null || true) + if [[ -n "$jadx_error_files" ]]; then + jadx_errors=$(printf '%s\n' "$jadx_error_files" | wc -l) + else + jadx_errors=0 + fi echo "jadx files with warnings/errors: $jadx_errors" fi echo @@ -430,6 +573,51 @@ if [[ "$ext_lower" == "xapk" ]]; then ls -1 "$OUTPUT_DIR/" else decompile_single "$INPUT_FILE_ABS" "$OUTPUT_DIR" "" + + # --- Split/bundled APK detection --- + # Some APKs are bundles: the outer APK contains base.apk + split_config.*.apk + # inside the resources directory. jadx will decompile the thin outer wrapper + # and produce very few Java files. Detect this and re-decompile base.apk. + sources_dir="$OUTPUT_DIR/sources" + resources_dir="$OUTPUT_DIR/resources" + if [[ -d "$sources_dir" && -d "$resources_dir" ]]; then + java_count=$(find "$sources_dir" -name "*.java" -type f 2>/dev/null | wc -l) + base_apk=$(find "$resources_dir" -maxdepth 1 -name "base.apk" -type f 2>/dev/null | head -1) + inner_apk_count=$(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f 2>/dev/null | wc -l) + + if [[ "$java_count" -le 10 && -n "$base_apk" ]]; then + echo + echo "=== Split/bundled APK detected ===" + echo "Outer APK produced only $java_count Java file(s) but contains $inner_apk_count inner APK(s):" + find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -exec basename {} \; | while read -r f; do echo " - $f"; done + echo + echo "Decompiling base.apk (contains the actual app code)..." + decompile_single "$base_apk" "$OUTPUT_DIR/base" "base.apk" + + # Decompile non-config split APKs + while IFS= read -r -d '' split_apk; do + split_name=$(basename "$split_apk" .apk) + case "$split_name" in + base|split_config.*) continue ;; + esac + echo + echo "Decompiling $split_name.apk..." + decompile_single "$split_apk" "$OUTPUT_DIR/$split_name" "$split_name.apk" + done < <(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -print0 2>/dev/null) + + # Report skipped config splits + config_splits=$(find "$resources_dir" -maxdepth 1 -name "split_config.*.apk" -type f 2>/dev/null) + if [[ -n "$config_splits" ]]; then + echo + echo "Skipped config splits (resource/ABI only):" + echo "$config_splits" | while read -r f; do echo " - $(basename "$f")"; done + fi + + echo + echo "Main decompiled source is in: $OUTPUT_DIR/base/sources/" + fi + fi + echo echo "=== Decompilation complete ===" fi diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 new file mode 100644 index 0000000..9084795 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 @@ -0,0 +1,121 @@ +# find-api-calls.ps1 — Search decompiled source for API calls and HTTP endpoints +param( + [Parameter(Position=0)] + [string]$SourceDir, + [switch]$Retrofit, + [switch]$OkHttp, + [switch]$Volley, + [switch]$Urls, + [switch]$Auth, + [switch]$All, + [Alias('h')] + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +function Show-Usage { + Write-Host @" +Usage: find-api-calls.ps1 [OPTIONS] + +Search decompiled Java/Kotlin source for HTTP API calls and endpoints. + +Arguments: + Path to the decompiled sources directory + +Options: + -Retrofit Search only for Retrofit annotations + -OkHttp Search only for OkHttp patterns + -Volley Search only for Volley patterns + -Urls Search only for hardcoded URLs + -Auth Search only for auth-related patterns + -All Search all patterns (default) + -Help Show this help message + +Output: + Results are printed as file:line:match for easy navigation. +"@ + exit 0 +} + +if ($Help) { Show-Usage } + +if (-not $SourceDir) { + Write-Host "Error: No source directory specified." -ForegroundColor Red + Show-Usage +} + +if (-not (Test-Path $SourceDir)) { + Write-Host "Error: Directory not found: $SourceDir" -ForegroundColor Red + exit 1 +} + +# Default to all if no specific flag set +$searchAll = (-not $Retrofit -and -not $OkHttp -and -not $Volley -and -not $Urls -and -not $Auth) -or $All + +function Write-Section { + param([string]$Title) + Write-Host "" + Write-Host "==== $Title ====" + Write-Host "" +} + +function Search-Sources { + param([string]$Pattern) + Get-ChildItem -Path $SourceDir -Recurse -Include '*.java','*.kt' -File | + Select-String -Pattern $Pattern -ErrorAction SilentlyContinue | + ForEach-Object { + "$($_.Path):$($_.LineNumber):$($_.Line.Trim())" + } +} + +# --- Retrofit --- +if ($searchAll -or $Retrofit) { + Write-Section "Retrofit Annotations" + Search-Sources '@(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|HTTP)\s*\(' + + Write-Section "Retrofit Headers & Parameters" + Search-Sources '@(Headers|Header|Query|QueryMap|Path|Body|Field|FieldMap|Part|PartMap|Url)\s*\(' + + Write-Section "Retrofit Base URL" + Search-Sources '(baseUrl|base_url)\s*\(' +} + +# --- OkHttp --- +if ($searchAll -or $OkHttp) { + Write-Section "OkHttp Request Building" + Search-Sources '(Request\.Builder|HttpUrl|\.newCall|\.enqueue|addInterceptor|addNetworkInterceptor)' + + Write-Section "OkHttp URL Construction" + Search-Sources '(\.url\s*\(|\.addQueryParameter|\.addPathSegment|\.scheme\s*\(|\.host\s*\()' +} + +# --- Volley --- +if ($searchAll -or $Volley) { + Write-Section "Volley Requests" + Search-Sources '(StringRequest|JsonObjectRequest|JsonArrayRequest|ImageRequest|RequestQueue|Volley\.newRequestQueue)' +} + +# --- Hardcoded URLs --- +if ($searchAll -or $Urls) { + Write-Section "Hardcoded URLs (http:// and https://)" + Search-Sources '"https?://[^"]+' + + Write-Section "HttpURLConnection" + Search-Sources '(openConnection|setRequestMethod|HttpURLConnection|HttpsURLConnection)' + + Write-Section "WebView URLs" + Search-Sources '(loadUrl|loadData|evaluateJavascript|addJavascriptInterface|WebViewClient|WebChromeClient)' +} + +# --- Auth patterns --- +if ($searchAll -or $Auth) { + Write-Section "Authentication & API Keys" + Search-Sources '(?i)(api[_\-]?key|auth[_\-]?token|bearer|authorization|x-api-key|client[_\-]?secret|access[_\-]?token)' + + Write-Section "Base URLs and Constants" + Search-Sources '(?i)(BASE_URL|API_URL|SERVER_URL|ENDPOINT|API_BASE|HOST_NAME)' +} + +Write-Host "" +Write-Host "=== Search complete ===" diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 new file mode 100644 index 0000000..81fab3c --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 @@ -0,0 +1,345 @@ +# install-dep.ps1 — Install a single dependency for Android reverse engineering +# Usage: install-dep.ps1 +# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb +# +# Exit codes: +# 0 — installed successfully +# 1 — installation failed +# 2 — requires manual action +param( + [Parameter(Position=0)] + [string]$Dep +) + +$ErrorActionPreference = 'Stop' + +function Show-Usage { + Write-Host @" +Usage: install-dep.ps1 + +Install a dependency required for Android reverse engineering. + +Available dependencies: + java Java JDK 17+ + jadx jadx decompiler + vineflower Vineflower (Fernflower fork) decompiler + dex2jar DEX to JAR converter + apktool Android resource decoder + adb Android Debug Bridge + +The script detects available package managers (winget, scoop, choco), then: + - Installs using the first available manager + - Falls back to direct download to %USERPROFILE%\.local\share\ + - Prints manual instructions if no option works +"@ + exit 0 +} + +if (-not $Dep -or $Dep -eq '-h' -or $Dep -eq '--help') { Show-Usage } + +# --- Detect environment --- +$hasWinget = [bool](Get-Command winget -ErrorAction SilentlyContinue) +$hasScoop = [bool](Get-Command scoop -ErrorAction SilentlyContinue) +$hasChoco = [bool](Get-Command choco -ErrorAction SilentlyContinue) + +function Write-Info { param($msg) Write-Host "[INFO] $msg" } +function Write-Ok { param($msg) Write-Host "[OK] $msg" } +function Write-Fail { param($msg) Write-Host "[FAIL] $msg" -ForegroundColor Red } +function Write-Manual { + param($msg) + Write-Host "[MANUAL] $msg" -ForegroundColor Yellow + Write-Host " Cannot install automatically. Please install manually and retry." -ForegroundColor Yellow + exit 2 +} + +# --- Helper: download a file --- +function Invoke-Download { + param([string]$Url, [string]$Dest) + Write-Info "Downloading $Url..." + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $Url -OutFile $Dest -UseBasicParsing +} + +# --- Helper: get latest GitHub release tag --- +function Get-GHLatestTag { + param([string]$Repo) + $url = "https://api.github.com/repos/$Repo/releases/latest" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $response = Invoke-RestMethod -Uri $url -UseBasicParsing + return $response.tag_name +} + +# --- Helper: ensure directory on PATH --- +function Add-ToUserPath { + param([string]$Dir) + $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User') + if ($currentPath -notlike "*$Dir*") { + [Environment]::SetEnvironmentVariable('PATH', "$Dir;$currentPath", 'User') + Write-Info "Added $Dir to user PATH. Restart your terminal to apply." + } + if ($env:PATH -notlike "*$Dir*") { + $env:PATH = "$Dir;$env:PATH" + } +} + +$localBin = Join-Path $env:USERPROFILE '.local\bin' +$localShare = Join-Path $env:USERPROFILE '.local\share' + +# ===================================================================== +# Dependency installers +# ===================================================================== + +function Install-Java { + $javaBin = Get-Command java -ErrorAction SilentlyContinue + if ($javaBin) { + $verOutput = & java -version 2>&1 | Select-Object -First 1 + if ("$verOutput" -match '"(\d+)') { + $ver = [int]$Matches[1] + if ($ver -ge 17) { + Write-Ok "Java $ver already installed" + return + } + } + } + + Write-Info "Installing Java JDK 17+..." + if ($hasWinget) { + Write-Info "Installing via winget..." + winget install --id Microsoft.OpenJDK.17 --accept-source-agreements --accept-package-agreements + } elseif ($hasScoop) { + Write-Info "Installing via scoop..." + scoop install openjdk17 + } elseif ($hasChoco) { + Write-Info "Installing via choco..." + choco install openjdk17 -y + } else { + Write-Manual "Install Java JDK 17+ from https://adoptium.net/" + } + + # Verify + $javaBin = Get-Command java -ErrorAction SilentlyContinue + if ($javaBin) { + Write-Ok "Java installed: $(& java -version 2>&1 | Select-Object -First 1)" + } else { + Write-Fail "Java installation may require a terminal restart for PATH update." + exit 1 + } +} + +function Install-Jadx { + if (Get-Command jadx -ErrorAction SilentlyContinue) { + Write-Ok "jadx already installed" + return + } + + # Try scoop first (cleanest on Windows) + if ($hasScoop) { + Write-Info "Installing jadx via scoop..." + scoop install jadx + if (Get-Command jadx -ErrorAction SilentlyContinue) { + Write-Ok "jadx installed via scoop" + return + } + } + + # Direct download from GitHub releases + Write-Info "Installing jadx from GitHub releases..." + $tag = Get-GHLatestTag "skylot/jadx" + if (-not $tag) { + Write-Fail "Could not determine latest jadx version." + Write-Manual "Download from https://github.com/skylot/jadx/releases/latest" + } + + $version = $tag -replace '^v', '' + $url = "https://github.com/skylot/jadx/releases/download/$tag/jadx-$version.zip" + $tmpZip = Join-Path $env:TEMP "jadx-$version.zip" + + Invoke-Download -Url $url -Dest $tmpZip + + $installDir = Join-Path $localShare 'jadx' + if (Test-Path $installDir) { Remove-Item $installDir -Recurse -Force } + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + Expand-Archive -Path $tmpZip -DestinationPath $installDir -Force + Remove-Item $tmpZip -Force + + # Add jadx\bin to PATH + $jadxBin = Join-Path $installDir 'bin' + Add-ToUserPath $jadxBin + + if (Get-Command jadx -ErrorAction SilentlyContinue) { + Write-Ok "jadx $version installed to $installDir" + } else { + Write-Ok "jadx $version installed to $installDir" + Write-Info "Restart your terminal or run: `$env:PATH = '$jadxBin;' + `$env:PATH" + } +} + +function Install-Vineflower { + if (Get-Command vineflower -ErrorAction SilentlyContinue) { + Write-Ok "Vineflower CLI already installed" + return + } + if (Get-Command fernflower -ErrorAction SilentlyContinue) { + Write-Ok "Fernflower CLI already installed" + return + } + $ffCandidates = @( + $env:FERNFLOWER_JAR_PATH, + "$env:USERPROFILE\.local\share\vineflower\vineflower.jar", + "$env:USERPROFILE\vineflower\vineflower.jar", + "$env:USERPROFILE\fernflower\fernflower.jar" + ) + foreach ($c in $ffCandidates) { + if ($c -and (Test-Path $c -ErrorAction SilentlyContinue)) { + Write-Ok "Vineflower/Fernflower JAR already exists: $c" + return + } + } + + # Download JAR from GitHub releases + Write-Info "Installing Vineflower from GitHub releases..." + $tag = Get-GHLatestTag "Vineflower/vineflower" + if (-not $tag) { + Write-Fail "Could not determine latest Vineflower version." + Write-Manual "Download from https://github.com/Vineflower/vineflower/releases/latest" + } + + $version = $tag -replace '^v', '' + $url = "https://github.com/Vineflower/vineflower/releases/download/$tag/vineflower-$version.jar" + $installDir = Join-Path $localShare 'vineflower' + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + + Invoke-Download -Url $url -Dest (Join-Path $installDir 'vineflower.jar') + + # Create wrapper batch file + New-Item -ItemType Directory -Path $localBin -Force | Out-Null + $wrapperPath = Join-Path $localBin 'vineflower.cmd' + Set-Content -Path $wrapperPath -Value "@echo off`r`njava -jar `"$installDir\vineflower.jar`" %*" + + Add-ToUserPath $localBin + [Environment]::SetEnvironmentVariable('FERNFLOWER_JAR_PATH', "$installDir\vineflower.jar", 'User') + $env:FERNFLOWER_JAR_PATH = "$installDir\vineflower.jar" + + Write-Ok "Vineflower $version installed to $installDir\vineflower.jar" + Write-Info "FERNFLOWER_JAR_PATH set to $installDir\vineflower.jar" +} + +function Install-Dex2Jar { + if ((Get-Command d2j-dex2jar -ErrorAction SilentlyContinue) -or + (Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue)) { + Write-Ok "dex2jar already installed" + return + } + + Write-Info "Installing dex2jar from GitHub releases..." + $tag = try { Get-GHLatestTag "ThexXTURBOXx/dex2jar" } catch { "2.4.35" } + if (-not $tag) { $tag = "2.4.35" } + + $version = $tag -replace '^v', '' + $url = "https://github.com/ThexXTURBOXx/dex2jar/releases/download/$tag/dex-tools-$version.zip" + $tmpZip = Join-Path $env:TEMP "dex2jar-$version.zip" + + try { + Invoke-Download -Url $url -Dest $tmpZip + } catch { + # Try alternate naming (pre-2.4.30 releases) + $url = "https://github.com/ThexXTURBOXx/dex2jar/releases/download/$tag/dex-tools-v$version.zip" + try { + Invoke-Download -Url $url -Dest $tmpZip + } catch { + Write-Fail "Download failed." + Write-Manual "Download from https://github.com/ThexXTURBOXx/dex2jar/releases/latest" + } + } + + $installDir = Join-Path $localShare 'dex2jar' + if (Test-Path $installDir) { Remove-Item $installDir -Recurse -Force } + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + Expand-Archive -Path $tmpZip -DestinationPath $installDir -Force + Remove-Item $tmpZip -Force + + # Find the actual bin directory (may be nested) + $d2jBat = Get-ChildItem -Path $installDir -Recurse -Filter 'd2j-dex2jar.bat' | Select-Object -First 1 + if (-not $d2jBat) { + $d2jBat = Get-ChildItem -Path $installDir -Recurse -Filter 'd2j-dex2jar.sh' | Select-Object -First 1 + } + if (-not $d2jBat) { + Write-Fail "Could not find d2j-dex2jar in extracted archive." + Write-Manual "Download and extract manually from https://github.com/ThexXTURBOXx/dex2jar/releases" + } + + $binDir = $d2jBat.DirectoryName + Add-ToUserPath $binDir + + Write-Ok "dex2jar $version installed to $installDir" +} + +function Install-Apktool { + if (Get-Command apktool -ErrorAction SilentlyContinue) { + Write-Ok "apktool already installed" + return + } + + if ($hasScoop) { + Write-Info "Installing apktool via scoop..." + scoop install apktool + } elseif ($hasChoco) { + Write-Info "Installing apktool via choco..." + choco install apktool -y + } else { + Write-Manual "Install apktool from https://apktool.org/docs/install" + } + + if (Get-Command apktool -ErrorAction SilentlyContinue) { + Write-Ok "apktool installed" + } else { + Write-Fail "apktool installation may have failed." + exit 1 + } +} + +function Install-Adb { + if (Get-Command adb -ErrorAction SilentlyContinue) { + Write-Ok "adb already installed" + return + } + + if ($hasScoop) { + Write-Info "Installing adb via scoop..." + scoop install adb + } elseif ($hasChoco) { + Write-Info "Installing adb via choco..." + choco install adb -y + } elseif ($hasWinget) { + Write-Info "Installing via winget..." + winget install Google.PlatformTools --accept-source-agreements --accept-package-agreements + } else { + Write-Manual "Install Android SDK Platform Tools from https://developer.android.com/tools/releases/platform-tools" + } + + if (Get-Command adb -ErrorAction SilentlyContinue) { + Write-Ok "adb installed" + } else { + Write-Fail "adb installation may have failed." + exit 1 + } +} + +# ===================================================================== +# Dispatch +# ===================================================================== + +switch ($Dep) { + 'java' { Install-Java } + 'jadx' { Install-Jadx } + 'vineflower' { Install-Vineflower } + 'fernflower' { Install-Vineflower } + 'dex2jar' { Install-Dex2Jar } + 'apktool' { Install-Apktool } + 'adb' { Install-Adb } + default { + Write-Host "Error: Unknown dependency '$Dep'" -ForegroundColor Red + Write-Host "Available: java, jadx, vineflower, dex2jar, apktool, adb" + exit 1 + } +} diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh index 9813ffd..e4ffc60 100755 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh @@ -388,24 +388,24 @@ install_dex2jar() { # Download from GitHub (no sudo needed) info "Installing dex2jar from GitHub releases..." local tag - tag=$(gh_latest_tag "pxb1988/dex2jar") + tag=$(gh_latest_tag "ThexXTURBOXx/dex2jar") if [[ -z "$tag" ]]; then - # Fallback: pxb1988 hasn't released in a while, try known version - tag="v2.4" + # Fallback to a known maintained release if GitHub metadata is unavailable. + tag="2.4.35" fi local version="${tag#v}" - local url="https://github.com/pxb1988/dex2jar/releases/download/${tag}/dex-tools-${version}.zip" + local url="https://github.com/ThexXTURBOXx/dex2jar/releases/download/${tag}/dex-tools-${version}.zip" local tmp_zip tmp_zip=$(mktemp /tmp/dex2jar-XXXXXX.zip) info "Downloading dex2jar $version..." if ! download "$url" "$tmp_zip"; then # Try alternate naming - url="https://github.com/pxb1988/dex2jar/releases/download/${tag}/dex-tools-v${version}.zip" + url="https://github.com/ThexXTURBOXx/dex2jar/releases/download/${tag}/dex-tools-v${version}.zip" download "$url" "$tmp_zip" || { fail "Download failed." - manual "Download from https://github.com/pxb1988/dex2jar/releases/latest" + manual "Download from https://github.com/ThexXTURBOXx/dex2jar/releases/latest" } fi @@ -425,7 +425,7 @@ install_dex2jar() { if [[ -z "$bin_dir" ]]; then fail "Could not find d2j-dex2jar.sh in extracted archive." - manual "Download and extract manually from https://github.com/pxb1988/dex2jar/releases" + manual "Download and extract manually from https://github.com/ThexXTURBOXx/dex2jar/releases" fi chmod +x "$bin_dir"/*.sh 2>/dev/null || true