Feature/windows powershell support (#8)
* feat: add Windows/PowerShell support Add PowerShell equivalents of all bash scripts for Windows users: - check-deps.ps1: Dependency verification with PATH refresh - install-dep.ps1: Install via winget/scoop/choco or direct download - decompile.ps1: APK/XAPK/JAR/AAR decompilation with split APK detection - find-api-calls.ps1: API endpoint extraction (Retrofit, URLs, auth) Update SKILL.md with Windows-specific instructions and notes for each workflow phase. PowerShell scripts support the same options as their bash counterparts and automatically refresh PATH after installations. * fix: check-deps.ps1 jadx fallback path version check, decompile.md lint fixes
This commit is contained in:
parent
5a810d94b3
commit
f8d394a69e
18
README.md
18
README.md
|
|
@ -13,10 +13,12 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files and **extract
|
|||
## 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/ThexXTURBOXx/dex2jar) — needed to use Fernflower on APK/DEX files
|
||||
|
||||
|
|
@ -28,7 +30,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
|
||||
```
|
||||
|
|
@ -43,7 +45,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
|
||||
```
|
||||
|
|
@ -52,7 +54,7 @@ Then in Claude Code:
|
|||
|
||||
### Slash command
|
||||
|
||||
```
|
||||
```text
|
||||
/decompile path/to/app.apk
|
||||
```
|
||||
|
||||
|
|
@ -100,7 +102,7 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri
|
|||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
```text
|
||||
android-reverse-engineering-skill/
|
||||
├── .claude-plugin/
|
||||
│ └── marketplace.json # Marketplace catalog
|
||||
|
|
@ -118,10 +120,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
|
||||
│ └── commands/
|
||||
│ └── decompile.md # /decompile slash command
|
||||
├── LICENSE
|
||||
|
|
|
|||
|
|
@ -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 <file>
|
||||
```
|
||||
|
||||
- **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 <file>
|
||||
```
|
||||
|
||||
- **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 <file>
|
||||
```
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:<dep>` — must be installed before proceeding
|
||||
- `INSTALL_OPTIONAL:<dep>` — 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 <dep>
|
||||
```
|
||||
|
||||
On Windows (PowerShell):
|
||||
```powershell
|
||||
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.ps1" <dep>
|
||||
```
|
||||
|
||||
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] <file>
|
||||
```
|
||||
|
||||
On Windows (PowerShell):
|
||||
```powershell
|
||||
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.ps1" [OPTIONS] <file>
|
||||
```
|
||||
|
||||
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 `<output>/base/` subdirectory. Config-only splits (ABI, language, density) are skipped. The main decompiled source will be in `<output>/base/sources/`.
|
||||
|
||||
Options:
|
||||
- `-o <dir>` — Custom output directory (default: `<filename>-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 <output>/sources/
|
||||
```
|
||||
|
||||
On Windows (PowerShell):
|
||||
```powershell
|
||||
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/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 <output>/sources/ --auth
|
||||
```
|
||||
|
||||
On Windows (PowerShell):
|
||||
```powershell
|
||||
# Only Retrofit
|
||||
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/sources/ -Retrofit
|
||||
|
||||
# Only hardcoded URLs
|
||||
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/sources/ -Urls
|
||||
|
||||
# Only auth patterns
|
||||
& "${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.ps1" <output>/sources/ -Auth
|
||||
```
|
||||
|
||||
Then, for each discovered endpoint, read the surrounding source code to extract:
|
||||
- HTTP method and path
|
||||
- Base URL
|
||||
|
|
|
|||
|
|
@ -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 <name> 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 <name> to install optional tools."
|
||||
} else {
|
||||
Write-Host "All dependencies are installed. Ready to decompile."
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
|
@ -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] <file>
|
||||
|
||||
Decompile an Android APK, XAPK, JAR, or AAR file.
|
||||
|
||||
Arguments:
|
||||
<file> Path to the .apk, .xapk, .jar, or .aar file
|
||||
|
||||
Options:
|
||||
-Output DIR Output directory (default: <filename>-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 -> <output>/jadx/
|
||||
fernflower -> <output>/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 ==="
|
||||
}
|
||||
|
|
@ -528,6 +528,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
|
||||
|
|
|
|||
|
|
@ -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 <source-dir> [OPTIONS]
|
||||
|
||||
Search decompiled Java/Kotlin source for HTTP API calls and endpoints.
|
||||
|
||||
Arguments:
|
||||
<source-dir> 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 ==="
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
# install-dep.ps1 — Install a single dependency for Android reverse engineering
|
||||
# Usage: install-dep.ps1 <dependency>
|
||||
# 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 <dependency>
|
||||
|
||||
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 "pxb1988/dex2jar" } catch { "v2.4" }
|
||||
if (-not $tag) { $tag = "v2.4" }
|
||||
|
||||
$version = $tag -replace '^v', ''
|
||||
$url = "https://github.com/pxb1988/dex2jar/releases/download/$tag/dex-tools-v$version.zip"
|
||||
$tmpZip = Join-Path $env:TEMP "dex2jar-$version.zip"
|
||||
|
||||
try {
|
||||
Invoke-Download -Url $url -Dest $tmpZip
|
||||
} catch {
|
||||
$url = "https://github.com/pxb1988/dex2jar/releases/download/$tag/dex-tools-$version.zip"
|
||||
try {
|
||||
Invoke-Download -Url $url -Dest $tmpZip
|
||||
} catch {
|
||||
Write-Fail "Download failed."
|
||||
Write-Manual "Download from https://github.com/pxb1988/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/pxb1988/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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue