Compare commits

...

12 Commits
v1.0 ... master

Author SHA1 Message Date
Simone Avogadro 6a31ed3fa2 chore: bump plugin version to 1.1.0
Reflects features integrated since 1.0.0:
- PowerShell support for Windows (#8)
- dex2jar fork migration to ThexXTURBOXx (#12)
- Decompile partial-success and Fernflower timeout handling (#10)
- Chinese localization (#4)
- README badges, TOC, Acknowledgments

Updates:
- .claude-plugin/marketplace.json (metadata.version + plugins[0].version)
- plugins/android-reverse-engineering/.claude-plugin/plugin.json (version)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 22:58:48 +02:00
YIPEI WEI cedc1a3368 docs: improve README with badges, TOC, and features table 2026-04-27 10:53:49 +02:00
Simone Avogadro f3fb1e9484 chore(install-dep.ps1): align dex2jar to ThexXTURBOXx fork
Mirrors the bash counterpart updated in #12. Switches the GitHub repo,
the fallback tag (v2.4 -> 2.4.35), and the URL pattern order so that the
canonical ThexXTURBOXx naming (dex-tools-2.4.35.zip, no leading 'v') is
tried first, with the pre-2.4.30 naming as fallback.

Closes drift items 9-11 from post-merge-followup-2026-04. Functional bugs
in decompile.ps1 and PR #10 drift items remain pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:26:42 +02:00
Simone Avogadro 87388d06b3 docs: add PowerShell support disclaimer and Acknowledgments section
Add a top-level note flagging PS1 scripts as experimental and pointing
issues to this repo. Add an Acknowledgments section crediting the four
external contributors of the recent PR wave (#4, #8, #10, #12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:23:31 +02:00
Phil Nachreiner f8d394a69e
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
2026-04-27 10:14:59 +02:00
Roshan Warrier 5a810d94b3
fix: use maintained dex2jar fork (#12)
Co-authored-by: txhno <198242577+txhno@users.noreply.github.com>
2026-04-27 09:59:14 +02:00
muqiao215 c25dfd78d2
fix(decompile): handle partial-success flows (#10)
Allow jadx-only mode to succeed when jadx exits non-zero after writing usable Java output.

Keep both-mode resilient when jadx partially succeeds, normalize Fernflower APK output handling, and make timeout/no-output failures explicit for Vineflower runs.

Co-authored-by: root <root@dbyqhnca.colocrossing.cloud>
2026-04-27 09:59:05 +02:00
kevinaimonster 5bc7cd53e6
feat: add Chinese localization / 添加中文支持 (#4)
Add Chinese trigger words to SKILL.md description and trigger field
for better discoverability by Chinese-speaking users.
2026-04-27 09:58:56 +02:00
Simone Avogadro ddeb9bc332 ADDED: .gitattributes for better WSL/Windows cooperation 2026-03-02 11:36:38 +01:00
Simone Avogadro ec0f6700f8 Improve plugin discoverability and metadata completeness
- Add keywords, skills and commands paths to plugin.json
- Add argument-hint to decompile command for better UX
- Add description to SKILL.md frontmatter for skill auto-matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:30:57 +01:00
Simone Avogadro 3276266788 Improve marketplace metadata compatibility with official Anthropic schema
Add $schema and top-level description fields to align with the dominant
pattern used in anthropics/claude-code and anthropics/claude-plugins-official.
Existing metadata wrapper preserved for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:30:03 +01:00
Simone Avogadro bcbe078c52 Clarified this is a Skill 2026-02-02 21:34:07 +01:00
13 changed files with 1377 additions and 49 deletions

View File

@ -1,18 +1,20 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "android-reverse-engineering-skill",
"description": "Claude Code plugins for Android reverse engineering",
"owner": {
"name": "Simone Avogadro"
},
"metadata": {
"description": "Claude Code plugins for Android reverse engineering",
"version": "1.0.0"
"version": "1.1.0"
},
"plugins": [
{
"name": "android-reverse-engineering",
"source": "./plugins/android-reverse-engineering",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows through libraries, and document extracted APIs.",
"version": "1.0.0",
"version": "1.1.0",
"author": {
"name": "Simone Avogadro"
},

9
.gitattributes vendored Normal file
View File

@ -0,0 +1,9 @@
# Normalize all text files to LF in the repository
* text=auto eol=lf
# Shell scripts — always LF (required for bash execution)
*.sh text eol=lf
# Markdown and JSON — always LF
*.md text eol=lf
*.json text eol=lf

View File

@ -1,24 +1,44 @@
# Android Reverse Engineering & API Extraction — Claude Code Plugin
# Android Reverse Engineering & API Extraction — Claude Code skill
A Claude Code plugin that decompiles Android APK/XAPK/JAR/AAR files and **extracts the HTTP APIs** used by the app — Retrofit endpoints, OkHttp calls, hardcoded URLs, authentication patterns — so you can document and reproduce them without the original source code.
[![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 and **extracts the HTTP APIs** used by the app — Retrofit endpoints, OkHttp calls, hardcoded URLs, authentication patterns — so you can document and reproduce them 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
- **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 |
| **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
See `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md` for detailed installation instructions.
@ -28,12 +48,12 @@ 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
```
The plugin will be permanently available in all future sessions.
The skill will be permanently available in all future sessions.
### From a local clone
@ -43,7 +63,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 +72,7 @@ Then in Claude Code:
### Slash command
```
```text
/decompile path/to/app.apk
```
@ -100,7 +120,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 +138,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
@ -133,9 +157,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:

View File

@ -1,10 +1,13 @@
{
"name": "android-reverse-engineering",
"version": "1.0.0",
"version": "1.1.0",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows through libraries, and document extracted APIs.",
"author": {
"name": "Simone Avogadro"
},
"repository": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill",
"license": "Apache-2.0"
"license": "Apache-2.0",
"keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction"],
"skills": "./skills/",
"commands": "./commands/"
}

View File

@ -2,6 +2,7 @@
allowed-tools: Bash, Read, Glob, Grep, Write, Edit
description: Decompile an Android APK/XAPK/JAR/AAR and analyze its structure
user-invocable: true
argument-hint: <path to APK, XAPK, JAR, or AAR file>
argument: path to APK, XAPK, JAR, or AAR file (optional)
---
@ -45,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>
```
@ -78,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"

View File

@ -1,5 +1,6 @@
---
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
@ -14,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
@ -28,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
@ -38,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.
@ -57,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)
@ -136,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
@ -148,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

View File

@ -133,7 +133,7 @@ Converts Android DEX bytecode to standard Java JAR files.
### GitHub Releases
1. Go to <https://github.com/pxb1988/dex2jar/releases/latest>
1. Go to <https://github.com/ThexXTURBOXx/dex2jar/releases/latest>
2. Download and extract:
```bash

View File

@ -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
}

View File

@ -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 ==="
}

View File

@ -163,6 +163,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
@ -177,20 +179,41 @@ run_jadx() {
args+=("$INPUT_FILE_ABS")
echo "Running: jadx ${args[*]}"
jadx "${args[@]}"
if jadx "${args[@]}"; then
jadx_status=0
else
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_timeout_seconds="${FERNFLOWER_TIMEOUT_SECONDS:-900}"
local ff_jar
if ! ff_jar=$(find_fernflower_jar); then
@ -211,12 +234,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"
@ -233,25 +264,83 @@ run_fernflower() {
ff_args+=("$out_dir")
echo "Running: java -jar $ff_jar ${ff_args[*]}"
java -jar "$ff_jar" "${ff_args[@]}"
if command -v timeout &>/dev/null && [[ "$ff_timeout_seconds" =~ ^[0-9]+$ ]] && (( ff_timeout_seconds > 0 )); then
echo "Fernflower timeout: ${ff_timeout_seconds}s (override with FERNFLOWER_TIMEOUT_SECONDS)"
if timeout "${ff_timeout_seconds}s" java -jar "$ff_jar" "${ff_args[@]}"; then
ff_status=0
else
ff_status=$?
fi
elif java -jar "$ff_jar" "${ff_args[@]}"; then
ff_status=0
else
ff_status=$?
fi
# Fernflower outputs a JAR containing .java files — extract it
local result_jar="$out_dir/$(basename "$jar_to_decompile")"
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 (${ff_timeout_seconds}s)." >&2
fi
echo "Error: Fernflower/Vineflower exited with status $ff_status." >&2
fi
return 1
}
# --- Summary helper ---
@ -259,9 +348,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
}
@ -284,19 +392,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"
@ -314,8 +466,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
@ -370,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

View File

@ -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 ==="

View File

@ -0,0 +1,345 @@
# 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 "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
}
}

View File

@ -332,24 +332,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
@ -369,7 +369,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