This commit is contained in:
Rafael Steil 2026-06-24 18:05:52 -04:00 committed by GitHub
commit 9b160ae3c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1275 additions and 282 deletions

View File

@ -0,0 +1,154 @@
---
name: android-reverse-engineering
description: Decompiles Android APK, XAPK, JAR, and AAR files using jadx or Fernflower/Vineflower. Reverse engineers Android apps, extracts HTTP API endpoints (Retrofit, OkHttp, Ktor, Apollo, Volley), recovers Kotlin class names from R8 obfuscation, and traces 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. Triggers include jadx, fernflower, vineflower, decompile APK, reverse engineer Android, extract API, 反编译APK, 安卓逆向, 提取API.
---
# Android Reverse Engineering (Cursor)
Skill root (repo-relative):
`plugins/android-reverse-engineering/skills/android-reverse-engineering/`
Scripts: `plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/`
References: `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/`
> **Claude Code equivalent:** `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/` maps to the skill root above.
## Platform scripts
Detect the user's OS and pick the matching script suffix:
- **Linux / macOS:** use `bash plugins/.../scripts/*.sh`
- **Windows:** use `& plugins/.../scripts/*.ps1`
Detect via `$env:OS` (Windows), `uname` (Darwin/Linux), or the user's shell. Do not use PowerShell scripts on macOS/Linux unless the user explicitly prefers Git Bash with `.ps1` (rare).
### Prerequisites
Tools must be on **PATH** (or standard fallback locations). Required: **Java JDK 17+**, **jadx**. Optional but recommended: Vineflower/Fernflower (`FERNFLOWER_JAR_PATH` for a JAR), dex2jar.
**Before any decompile work**, run the dependency checker and do not proceed if required tools are missing:
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh
```
On Windows (PowerShell):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1
```
Output includes machine-readable lines: `INSTALL_REQUIRED:<dep>`, `INSTALL_OPTIONAL:<dep>`. Exit code `1` means required tools are missing.
Install missing tools (optional; user may prefer manual PATH setup):
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh <dep>
```
On Windows (PowerShell):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 <dep>
```
See `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md` for installation details.
## Workflow
Full workflow documentation: `plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md`
### Phase 0: Fingerprint (recommended first)
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.sh <file.apk|file.xapk>
```
On Windows (PowerShell):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.ps1 <file.apk|file.xapk>
```
If Flutter / React Native / Cordova / Xamarin is detected, stop; Java decompilation is not the right path.
### Phase 1: Verify dependencies
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh
```
On Windows (PowerShell):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1
```
### Phase 2: Decompile
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh [OPTIONS] <file>
```
On Windows (PowerShell):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 [OPTIONS] <file>
```
Options: `-o`/`-Output <dir>`, `--deobf`/`-Deobf`, `--no-res`/`-NoRes`, `--engine`/`-Engine jadx|fernflower|both`
### Phase 3: Analyze structure
Read `AndroidManifest.xml`, survey `sources/`, grep `BuildConfig.java` files, identify architecture pattern.
### Phase 3.5: Recover Kotlin names (obfuscated Kotlin apps)
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/recover-kotlin-names.sh <output>/sources <output>/mapping
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/lookup-name.sh <output>/mapping --grep '"/api/' <output>/sources
```
On Windows (PowerShell):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/recover-kotlin-names.ps1 <output>/sources <output>/mapping
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/lookup-name.ps1 <output>/mapping --grep '"/api/' <output>/sources
```
### Phase 4: Trace call flows
See `references/call-flow-analysis.md`.
### Phase 5: Extract APIs
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh <output>/sources/
```
On Windows (PowerShell):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 <output>/sources/
```
Targeted flags: `--retrofit`/`-Retrofit`, `--okhttp`/`-OkHttp`, `--ktor`/`-Ktor`, `--apollo`/`-Apollo`, `--volley`/`-Volley`, `--urls`/`-Urls`, `--paths`/`-Paths`, `--auth`/`-Auth`
Produce Tier 1 inventory table for all endpoints; Tier 2 detail only for high-value endpoints (auth, payments, user-requested).
## References
- `references/setup-guide.md`: tool installation
- `references/jadx-usage.md`: jadx CLI
- `references/fernflower-usage.md`: Fernflower/Vineflower CLI
- `references/api-extraction-patterns.md`: search patterns
- `references/kotlin-name-recovery.md`: R8 name recovery
- `references/call-flow-analysis.md`: tracing techniques
## Output deliverables
1. Decompiled source in output directory
2. Architecture summary
3. API documentation (Tier 1 table + Tier 2 for key endpoints)
4. Call flow map (auth and main features)

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*-decompiled/

139
README.md
View File

@ -1,18 +1,20 @@
# Android Reverse Engineering & API Extraction — Claude Code skill
# Android Reverse Engineering & API Extraction
[![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.
A skill for Claude Code and **Cursor** 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.
> **First-class Kotlin support**: modern Android apps are Kotlin/KMP, heavily obfuscated with R8. This skill recovers the **original Kotlin class names** from metadata R8 cannot strip, and extracts APIs from **Ktor**, **Apollo (GraphQL)** and **Koin** not just the classic Retrofit/OkHttp stack. See [Kotlin name recovery](#kotlin-name-recovery-r8-deobfuscation) below.
> **First-class Kotlin support**: modern Android apps are Kotlin/KMP, heavily obfuscated with R8. This skill recovers the **original Kotlin class names** from metadata R8 cannot strip, and extracts APIs from **Ktor**, **Apollo (GraphQL)** and **Koin**, not just the classic Retrofit/OkHttp stack. See [Kotlin name recovery](#kotlin-name-recovery-r8-deobfuscation) below.
> **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).
> **Cross-platform scripts**: Bash scripts on Linux/macOS and native PowerShell (`*.ps1`) on Windows cover the full workflow: fingerprint, decompile, API extraction, and Kotlin name recovery. See [Usage](#usage) for examples on each platform.
## Table of Contents
# Table of Contents
- [What it does](#what-it-does)
- [Requirements](#requirements)
- [Installation](#installation)
- [Claude Code](#claude-code)
- [Cursor](#cursor)
- [Usage](#usage)
- [Repository Structure](#repository-structure)
- [References](#references)
@ -20,19 +22,19 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files and **extract
- [Disclaimer](#disclaimer)
- [License](#license)
## What it does
# What it does
| Capability | Description |
|------------|-------------|
| **Fingerprint first (Phase 0)** | Triage an APK/XAPK in seconds detect the framework (Flutter / React Native / Cordova / Xamarin / native-Kotlin), HTTP stack, obfuscation level and native libs *before* spending time on a full decompile |
| **Fingerprint first (Phase 0)** | Triage an APK/XAPK in seconds: detect the framework (Flutter / React Native / Cordova / Xamarin / native-Kotlin), HTTP stack, obfuscation level and native libs *before* spending time on a full decompile |
| **Decompile** | APK, XAPK, JAR, and AAR files using jadx and Fernflower/Vineflower (single engine or side-by-side comparison) |
| **Recover Kotlin names** | Rebuild original `*Repository` / `*ViewModel` / `*UseCase` class names from R8-obfuscated binaries using Kotlin metadata that R8 cannot strip |
| **Extract APIs** | Retrofit, OkHttp, Volley **and modern Kotlin/KMP stacks: Ktor, Apollo (GraphQL), Koin DI** endpoints, hardcoded URLs, auth headers, tokens and HMAC request-signing schemes |
| **Extract APIs** | Retrofit, OkHttp, Volley **and modern Kotlin/KMP stacks: Ktor, Apollo (GraphQL), Koin DI**; endpoints, hardcoded URLs, auth headers, tokens and HMAC request-signing schemes |
| **Trace call flows** | From Activities/Fragments through ViewModels and repositories down to HTTP calls |
| **Analyze structure** | Manifest, packages, architecture patterns |
| **Handle obfuscation** | R8-resistant path/URL extraction plus strategies for navigating ProGuard/R8 output |
## Requirements
# Requirements
**Required:**
@ -41,12 +43,14 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files and **extract
**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
- [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
See `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md` for detailed installation instructions.
## Installation
# Installation
## Claude Code
### From GitHub (recommended)
@ -72,9 +76,32 @@ Then in Claude Code:
/plugin install android-reverse-engineering@android-reverse-engineering-skill
```
## Usage
## Cursor
### Slash command
Cursor loads **project skills** from `.cursor/skills/` in the **workspace root** — the folder you open in Cursor, not every repo on disk.
Clone this repository, then pick one setup:
| Goal | What to do |
|------|------------|
| **Use the skill here** (simplest) | Open this repo as your Cursor workspace. Put APKs anywhere on disk and reference them by path. |
| **Use the skill in another project** | Copy (or git submodule) **both** `.cursor/skills/android-reverse-engineering/` **and** `plugins/android-reverse-engineering/` into that project's repo root, then open **that** project in Cursor. The skill's `SKILL.md` references scripts under `plugins/…` relative to the workspace root — copying only `SKILL.md` is not enough. |
| **Both projects at once** | Add this repo and your other project to a [multi-root workspace](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces). |
| **All projects (advanced)** | Install under `~/.cursor/skills/android-reverse-engineering/`, but adjust script paths in `SKILL.md` to a fixed location on disk (e.g. where you cloned this repo). |
The project skill lives at `.cursor/skills/android-reverse-engineering/` and points to the shared scripts under `plugins/android-reverse-engineering/skills/android-reverse-engineering/`.
1. Ensure **Java 17+**, **jadx**, and optionally **dex2jar** / **Vineflower** are on your PATH (see [setup-guide](plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md)).
2. Verify dependencies (run from the workspace root that contains `plugins/`):
- **Linux / macOS:** `bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh`
- **Windows:** `& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1`
3. Invoke via `@android-reverse-engineering` or phrases like "decompile this APK".
Unlike Claude Code, Cursor has no global `/plugin install` — the skill is available only in workspaces where `.cursor/skills/android-reverse-engineering/` is present (unless you use the personal-skill path above).
# Usage
## Slash command
```text
/decompile path/to/app.apk
@ -82,7 +109,7 @@ Then in Claude Code:
This runs the full workflow: dependency check, decompilation, and initial structure analysis.
### Natural language
## Natural language
The skill activates on phrases like:
@ -92,9 +119,7 @@ The skill activates on phrases like:
- "Follow the call flow from LoginActivity"
- "Analyze this AAR library"
### Manual scripts
The scripts can also be used standalone:
## Manual scripts (bash on Linux/macOS)
```bash
# Check dependencies
@ -120,7 +145,7 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri
# Run both engines and compare
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh --engine both --deobf app.apk
# Find API calls defaults to a full scan across every supported stack
# Find API calls (defaults to a full scan across every supported stack)
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --retrofit
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --urls
@ -131,16 +156,31 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --paths # quoted path literals that survive R8 inlining
```
### Kotlin name recovery (R8 deobfuscation)
## Manual scripts (PowerShell on Windows)
Most real-world Kotlin/KMP apps ship through R8, so the decompiled classes come
out as `a.b.c`. R8 renames the JVM symbols but **cannot strip the Kotlin
metadata strings** — the Kotlin runtime (reflection, coroutines) needs the
original fully-qualified names at runtime. This skill mines those
`@DebugMetadata` / `@Metadata` annotations to rebuild an `obfuscated → real`
class-name map. On a typical app it recovers ~100 % of the
`*Repository` / `*ViewModel` / `*UseCase` / `*Impl` classes you actually want to
read.
```powershell
# Check dependencies
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1
# Fingerprint before decompiling (Phase 0)
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.ps1 app.apk
# Decompile
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 app.apk
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.ps1 -Engine both -Deobf app.apk
# Find API calls
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 output/sources/
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.ps1 output/sources/ -Ktor -Apollo -Paths
# Kotlin name recovery
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/recover-kotlin-names.ps1 output/sources output/names
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/lookup-name.ps1 output/names LoginRepository
```
## Kotlin name recovery (R8 deobfuscation)
Most real-world Kotlin/KMP apps ship through R8, so the decompiled classes come out as `a.b.c`. R8 renames the JVM symbols but **cannot strip the Kotlin metadata strings**; the Kotlin runtime (reflection, coroutines) needs the original fully-qualified names at runtime. This skill mines those `@DebugMetadata` / `@Metadata` annotations to rebuild an `obfuscated → real` class-name map. On a typical app it recovers ~100 % of the `*Repository` / `*ViewModel` / `*UseCase` / `*Impl` classes you actually want to read.
```bash
# 1. Build the mapping from the decompiled sources
@ -154,7 +194,7 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/lookup-name.sh output/names/ --grep 'login' output/sources/
```
## Repository Structure
# Repository Structure
```text
android-reverse-engineering-skill/
@ -182,36 +222,45 @@ android-reverse-engineering-skill/
│ │ ├── install-dep.ps1
│ │ ├── decompile.sh
│ │ ├── decompile.ps1
│ │ ├── fingerprint.sh # Phase 0 — pre-decompile triage
│ │ ├── fingerprint.sh # Phase 0: pre-decompile triage
│ │ ├── fingerprint.ps1
│ │ ├── recover-kotlin-names.sh # R8 → real Kotlin class names
│ │ ├── recover-kotlin-names.ps1
│ │ ├── recover_kotlin_names.py
│ │ ├── lookup-name.sh # query the recovered name map
│ │ ├── lookup-name.ps1
│ │ ├── lookup_names.py
│ │ ├── find-api-calls.sh
│ │ └── find-api-calls.ps1
│ └── commands/
│ └── decompile.md # /decompile slash command
├── .cursor/
│ └── skills/
│ └── android-reverse-engineering/
│ └── SKILL.md # Cursor project skill (thin wrapper)
├── LICENSE
└── README.md
```
## References
# References
- [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/ThexXTURBOXx/dex2jar)
- [apktool Android resource decoder](https://apktool.org/)
- [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/ThexXTURBOXx/dex2jar)
- [apktool: Android resource decoder](https://apktool.org/)
## Acknowledgments
# Acknowledgments
Thanks to the contributors who have shaped this skill:
- [@tajchert](https://github.com/tajchert) Phase 0 fingerprinting, R8-resistant Kotlin name recovery (`recover-kotlin-names.sh`, `lookup-name.sh`), and Ktor / Apollo / Koin / HMAC extraction patterns (#16)
- [@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)
- [@tajchert](https://github.com/tajchert): Phase 0 fingerprinting, R8-resistant Kotlin name recovery (`recover-kotlin-names.sh`, `lookup-name.sh`), and Ktor / Apollo / Koin / HMAC extraction patterns (#16)
- [@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
# Disclaimer
This plugin is provided strictly for **lawful purposes**, including but not limited to:
@ -224,6 +273,6 @@ This plugin is provided strictly for **lawful purposes**, including but not limi
The authors disclaim any liability for misuse of this tool.
## License
# License
Apache 2.0 see [LICENSE](LICENSE)
Apache 2.0; see [LICENSE](LICENSE)

View File

@ -35,6 +35,22 @@ After installation on macOS, follow the symlink instructions printed by Homebrew
export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"
```
### Windows (winget)
From PowerShell or Command Prompt:
```powershell
winget install --id Microsoft.OpenJDK.17 --accept-source-agreements --accept-package-agreements
```
Microsoft OpenJDK 17 is added to PATH automatically. If `java` is not found afterward, open a new terminal and run the verify step below.
Alternatively, run the skill's install script (uses winget when available):
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 java
```
### Verify
```bash
@ -86,7 +102,7 @@ jadx --version
## Fernflower / Vineflower (optional, recommended)
Fernflower is the JetBrains Java decompiler. It produces better output than jadx on complex Java constructs, lambdas, and generics. [Vineflower](https://github.com/Vineflower/vineflower) is the actively maintained community fork with published releases prefer it over upstream Fernflower.
Fernflower is the JetBrains Java decompiler. It produces better output than jadx on complex Java constructs, lambdas, and generics. [Vineflower](https://github.com/Vineflower/vineflower) is the actively maintained community fork with published releases; prefer it over upstream Fernflower.
### Option 1: Vineflower from GitHub Releases (recommended)
@ -208,14 +224,76 @@ adb pull /data/app/com.example.app-xxxx/base.apk ./app.apk
---
## Linux / macOS (bash)
On Linux and macOS, use the `*.sh` scripts in `scripts/`. Tools must be on **PATH** before running the skill (or in standard fallback locations; see `check-deps.sh`).
### Verify dependencies
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh
```
### Auto-install (optional)
```bash
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh jadx
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh vineflower
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh dex2jar
```
The install script detects the OS and package manager, installs without sudo when possible (downloads to `~/.local/share/`, symlinks in `~/.local/bin/`), or uses apt/dnf/pacman/brew when necessary.
---
## Windows (PowerShell)
On Windows, use the `*.ps1` scripts in `scripts/`. Tools must be on **PATH** before running the skill (or in standard fallback locations; see `check-deps.ps1`).
### Required PATH entries
| Tool | What to add to PATH |
|------|---------------------|
| Java JDK 17+ | Java `bin` directory (often already on PATH after install) |
| jadx | `jadx\bin` directory (contains `jadx.bat`) |
| dex2jar (optional) | dex-tools root directory (contains `d2j-dex2jar.bat`) |
For Vineflower, set an environment variable instead of PATH:
```powershell
$env:FERNFLOWER_JAR_PATH = 'C:\path\to\vineflower.jar'
```
### Verify dependencies
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.ps1
```
### Auto-install (optional)
```powershell
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 jadx
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 vineflower
& plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.ps1 dex2jar
```
The install script uses `winget`, `scoop`, or `choco` when available, or downloads to `%USERPROFILE%\.local\share\`.
### Cursor IDE
This repository includes a project skill at `.cursor/skills/android-reverse-engineering/`. Invoke it via `@android-reverse-engineering` or natural-language triggers (e.g. "decompile this APK"). The skill reuses scripts and references under `plugins/android-reverse-engineering/skills/android-reverse-engineering/`; no duplication. Use the bash scripts on Linux/macOS and the PowerShell scripts on Windows.
---
## Troubleshooting
| Problem | Solution |
|---|---|
| `jadx: command not found` | Ensure the jadx `bin/` directory is in your `$PATH` |
| `Error: Could not find or load main class` | Java is missing or wrong version — verify with `java -version` |
| `Error: Could not find or load main class` | Java is missing or wrong version; verify with `java -version` |
| jadx runs out of memory on large APKs | Increase heap: `jadx -Xmx4g -d output app.apk` or set `JAVA_OPTS="-Xmx4g"` |
| Decompiled code has many `// Error` comments | Try `--show-bad-code` to see partial output, or use `--deobf` for obfuscated apps |
| Fernflower hangs on a method | Use `-mpm=60` to set a 60-second timeout per method |
| Fernflower JAR not found | Set `FERNFLOWER_JAR_PATH` env variable to the full path of the JAR |
| dex2jar fails with `ZipException` | The APK may have a non-standard ZIP structure — try `jadx` instead |
| dex2jar fails with `ZipException` | The APK may have a non-standard ZIP structure; try `jadx` instead |

View File

@ -1,4 +1,4 @@
# decompile.ps1 Decompile APK/XAPK/JAR/AAR using jadx, fernflower, or both
# decompile.ps1: Decompile APK/XAPK/JAR/AAR using jadx, fernflower, or both
param(
[Alias('o')]
[string]$Output,
@ -48,7 +48,8 @@ Engines:
fernflower -> <output>/fernflower/
Environment:
FERNFLOWER_JAR_PATH Path to fernflower.jar or vineflower.jar
FERNFLOWER_JAR_PATH Path to fernflower.jar or vineflower.jar
FERNFLOWER_TIMEOUT_SECONDS Max seconds for Fernflower run (default: 900)
Examples:
.\decompile.ps1 app-release.apk
@ -95,10 +96,12 @@ $xapkExtractedDir = $null
$xapkApkFiles = @()
if ($extLower -eq 'xapk') {
# Expand-Archive doesn't work on .xapk; use ZipFile
$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
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($inputFileAbs, $xapkExtractedDir)
# Show manifest.json if present
$manifestPath = Join-Path $xapkExtractedDir 'manifest.json'
@ -151,13 +154,13 @@ function Find-Dex2Jar {
return $null
}
# --- jadx decompilation ---
# Returns 0 success, 1 hard failure, 2 partial success (exit non-zero but .java files produced)
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
return 1
}
$jadxArgs = @('-d', $OutDir)
@ -168,89 +171,198 @@ function Invoke-Jadx {
Write-Host "Running: jadx $($jadxArgs -join ' ')"
& jadx @jadxArgs
$jadxStatus = $LASTEXITCODE
$sourcesDir = Join-Path $OutDir 'sources'
$count = 0
if (Test-Path $sourcesDir) {
$count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java').Count
$count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java' -File -ErrorAction SilentlyContinue).Count
Write-Host "jadx output: $sourcesDir\"
Write-Host "Java files decompiled by jadx: $count"
}
return $true
if ($jadxStatus -eq 0) { return 0 }
if ($count -gt 0) {
Write-Host "Warning: jadx exited with status $jadxStatus after writing $count Java files; treating this as partial success." -ForegroundColor Yellow
return 2
}
Write-Host "Error: jadx failed with status $jadxStatus and produced no Java output." -ForegroundColor Red
return 1
}
# --- Fernflower decompilation ---
function Invoke-JavaJarWithTimeout {
param(
[string]$JarPath,
[string[]]$Args,
[int]$TimeoutSeconds
)
$argList = @('-jar', $JarPath) + $Args
$escaped = $argList | ForEach-Object {
if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ }
}
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = 'java'
$psi.Arguments = $escaped -join ' '
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$proc = [System.Diagnostics.Process]::Start($psi)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$timedOut = -not $proc.WaitForExit($TimeoutSeconds * 1000)
if ($timedOut) {
try { $proc.Kill() } catch { }
if ($stdout) { Write-Host $stdout }
if ($stderr) { Write-Host $stderr }
return 124
}
if ($stdout) { Write-Host $stdout }
if ($stderr) { Write-Host $stderr }
return $proc.ExitCode
}
# Returns 0 success, 1 hard failure, 2 partial success
function Invoke-Fernflower {
param([string]$OutDir, [string]$FileAbs, [string]$FileExt)
param([string]$OutDir, [string]$FileAbs, [string]$FileExt, [string]$BaseNameForJar)
$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
return 1
}
New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
$jarToDecompile = $FileAbs
$convertedJar = $null
$intermediateDir = Join-Path $OutDir 'intermediate'
$ffTimeoutSeconds = 900
if ($env:FERNFLOWER_TIMEOUT_SECONDS -match '^\d+$' -and [int]$env:FERNFLOWER_TIMEOUT_SECONDS -gt 0) {
$ffTimeoutSeconds = [int]$env:FERNFLOWER_TIMEOUT_SECONDS
}
# 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
return 1
}
Write-Host "Converting $FileExt to JAR with dex2jar..."
$convertedJar = Join-Path $OutDir "$baseName-dex2jar.jar"
New-Item -ItemType Directory -Path $intermediateDir -Force | Out-Null
$jarBase = if ($BaseNameForJar) { $BaseNameForJar } else { $baseName }
$convertedJar = Join-Path $intermediateDir "$jarBase-dex2jar.jar"
& $d2j -f -o $convertedJar $FileAbs 2>&1 | Write-Host
$d2jStatus = $LASTEXITCODE
if (-not (Test-Path $convertedJar)) {
Write-Host "Error: dex2jar conversion failed." -ForegroundColor Red
return $false
Write-Host "Error: dex2jar conversion failed with status $d2jStatus." -ForegroundColor Red
return 1
}
if ($d2jStatus -ne 0) {
Write-Host "Warning: dex2jar exited with status $d2jStatus but produced $convertedJar; continuing." -ForegroundColor Yellow
}
$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
Write-Host "Fernflower timeout: ${ffTimeoutSeconds}s (override with FERNFLOWER_TIMEOUT_SECONDS)"
$ffStatus = Invoke-JavaJarWithTimeout -JarPath $ffJar -Args $ffArgs -TimeoutSeconds $ffTimeoutSeconds
# Fernflower outputs a JAR containing .java files — extract it
$sourcesDir = Join-Path $OutDir 'sources'
$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
try {
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
[System.IO.Compression.ZipFile]::ExtractToDirectory($resultJar, $sourcesDir)
Remove-Item $resultJar -Force
} catch {
Write-Host "Warning: Fernflower result jar $resultJar could not be extracted; checking for direct folder output." -ForegroundColor Yellow
}
}
New-Item -ItemType Directory -Path $sourcesDir -Force | Out-Null
$count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java' -File -ErrorAction SilentlyContinue).Count
# Vineflower may write sources directly into the destination folder tree
if ($count -eq 0) {
$directEntries = Get-ChildItem -Path $OutDir -Directory |
Where-Object { $_.Name -notin @('sources', 'intermediate') }
$directCount = 0
foreach ($entry in $directEntries) {
$directCount += (Get-ChildItem -Path $entry.FullName -Recurse -Filter '*.java' -File -ErrorAction SilentlyContinue).Count
}
if ($directCount -gt 0) {
foreach ($entry in $directEntries) {
Get-ChildItem -Path $entry.FullName -Recurse -File | ForEach-Object {
$rel = $_.FullName.Substring($entry.FullName.Length).TrimStart('\')
$dest = Join-Path $sourcesDir $rel
$destDir = Split-Path $dest -Parent
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
Move-Item -Path $_.FullName -Destination $dest -Force
}
if ((Get-ChildItem -Path $entry.FullName -Recurse -ErrorAction SilentlyContinue).Count -eq 0) {
Remove-Item $entry.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
}
$count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java' -File -ErrorAction SilentlyContinue).Count
}
}
if ($count -gt 0) {
Write-Host "Fernflower output: $sourcesDir\"
Write-Host "Java files decompiled by Fernflower: $count"
if ($convertedJar -and (Test-Path $convertedJar)) {
Remove-Item $convertedJar -Force
}
if (Test-Path $intermediateDir) {
$remaining = Get-ChildItem -Path $intermediateDir -ErrorAction SilentlyContinue
if (-not $remaining) { Remove-Item $intermediateDir -Force -ErrorAction SilentlyContinue }
}
if ($ffStatus -ne 0) {
if ($ffStatus -eq 124) {
Write-Host "Warning: Fernflower/Vineflower exceeded timeout (${ffTimeoutSeconds}s) but wrote $count Java files; treating as partial success." -ForegroundColor Yellow
} else {
Write-Host "Warning: Fernflower/Vineflower exited with status $ffStatus after writing $count Java files; treating as partial success." -ForegroundColor Yellow
}
return 2
}
return 0
}
# Clean up intermediate dex2jar output
if ($convertedJar -and (Test-Path $convertedJar -ErrorAction SilentlyContinue)) {
Remove-Item $convertedJar -Force
if ($convertedJar -and (Test-Path $convertedJar)) {
Write-Host "Error: Fernflower/Vineflower produced no Java output. Intermediate dex2jar artifact kept at $convertedJar" -ForegroundColor Red
} else {
Write-Host "Error: Fernflower/Vineflower produced no Java output." -ForegroundColor Red
}
return $true
if ($ffStatus -eq 124) {
Write-Host "Error: Fernflower/Vineflower exceeded timeout (${ffTimeoutSeconds}s)." -ForegroundColor Red
} elseif ($ffStatus -ne 0) {
Write-Host "Error: Fernflower/Vineflower exited with status $ffStatus." -ForegroundColor Red
}
return 1
}
# --- 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
$packages = Get-ChildItem -Path $SrcDir -Directory -Recurse -Depth 3 |
ForEach-Object { $_.FullName.Replace("$SrcDir\", '').Replace("$SrcDir/", '') } |
Sort-Object -Unique
if ($packages.Count -eq 0) {
Write-Host "(none)"
} else {
$packages | Select-Object -First 20 | ForEach-Object { Write-Host $_ }
}
}
}
@ -266,19 +378,33 @@ function Invoke-DecompileSingle {
switch ($Engine) {
'jadx' {
Invoke-Jadx -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt
$jadxStatus = Invoke-Jadx -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt
Show-Structure (Join-Path $OutDir 'sources') 'jadx'
if ($jadxStatus -eq 1) { return 1 }
if ($jadxStatus -eq 2) { Write-Host "jadx completed with warnings but produced usable output." }
}
'fernflower' {
Invoke-Fernflower -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt
$ffBase = [IO.Path]::GetFileNameWithoutExtension($FileAbs)
$ffStatus = Invoke-Fernflower -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt -BaseNameForJar $ffBase
Show-Structure (Join-Path $OutDir 'sources') 'fernflower'
if ($ffStatus -eq 1) { return 1 }
if ($ffStatus -eq 2) { Write-Host "Fernflower completed with warnings but produced usable output." }
}
'both' {
Write-Host "--- Pass 1: jadx ---"
Invoke-Jadx -OutDir (Join-Path $OutDir 'jadx') -FileAbs $FileAbs -FileExt $fileExt
$jadxStatus = Invoke-Jadx -OutDir (Join-Path $OutDir 'jadx') -FileAbs $FileAbs -FileExt $fileExt
if ($jadxStatus -eq 1) { return 1 }
if ($jadxStatus -eq 2) {
Write-Host "Continuing to Fernflower because jadx produced usable output despite warnings."
}
Write-Host ""
Write-Host "--- Pass 2: Fernflower ---"
Invoke-Fernflower -OutDir (Join-Path $OutDir 'fernflower') -FileAbs $FileAbs -FileExt $fileExt
$ffBase = [IO.Path]::GetFileNameWithoutExtension($FileAbs)
$ffStatus = Invoke-Fernflower -OutDir (Join-Path $OutDir 'fernflower') -FileAbs $FileAbs -FileExt $fileExt -BaseNameForJar $ffBase
if ($ffStatus -eq 1) { return 1 }
if ($ffStatus -eq 2) {
Write-Host "Continuing with Fernflower output because it produced usable sources despite warnings."
}
Show-Structure (Join-Path $OutDir 'jadx\sources') 'jadx'
Show-Structure (Join-Path $OutDir 'fernflower\sources') 'fernflower'
@ -299,7 +425,7 @@ function Invoke-DecompileSingle {
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-String -Pattern 'JADX WARNING|JADX WARN|JADX ERROR|Code decompiled incorrectly' -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Path -Unique).Count
Write-Host "jadx files with warnings/errors: $jadxErrors"
}
@ -307,6 +433,7 @@ function Invoke-DecompileSingle {
Write-Host "Tip: compare specific classes between jadx/ and fernflower/ to pick the better output."
}
}
return 0
}
# --- Run ---

View File

@ -1,11 +1,14 @@
# find-api-calls.ps1 Search decompiled source for API calls and HTTP endpoints
# find-api-calls.ps1: Search decompiled source for API calls and HTTP endpoints
param(
[Parameter(Position=0)]
[Parameter(Position = 0)]
[string]$SourceDir,
[switch]$Retrofit,
[switch]$OkHttp,
[switch]$Ktor,
[switch]$Apollo,
[switch]$Volley,
[switch]$Urls,
[switch]$Paths,
[switch]$Auth,
[switch]$All,
[Alias('h')]
@ -26,8 +29,11 @@ Arguments:
Options:
-Retrofit Search only for Retrofit annotations
-OkHttp Search only for OkHttp patterns
-Ktor Search only for Ktor client patterns
-Apollo Search only for Apollo (GraphQL) patterns
-Volley Search only for Volley patterns
-Urls Search only for hardcoded URLs
-Paths Extract unique endpoint-shaped path string literals
-Auth Search only for auth-related patterns
-All Search all patterns (default)
-Help Show this help message
@ -39,19 +45,13 @@ Output:
}
if ($Help) { Show-Usage }
if (-not $SourceDir) {
Write-Host "Error: No source directory specified." -ForegroundColor Red
Show-Usage
}
if (-not (Test-Path $SourceDir)) {
if (-not $SourceDir) { Show-Usage }
if (-not (Test-Path $SourceDir -PathType Container)) {
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
$searchAll = (-not $Retrofit -and -not $OkHttp -and -not $Ktor -and -not $Apollo -and -not $Volley -and -not $Urls -and -not $Paths -and -not $Auth) -or $All
function Write-Section {
param([string]$Title)
@ -62,59 +62,188 @@ function Write-Section {
function Search-Sources {
param([string]$Pattern)
Get-ChildItem -Path $SourceDir -Recurse -Include '*.java','*.kt' -File |
Get-ChildItem -Path $SourceDir -Recurse -Include '*.java', '*.kt' -File |
Select-String -Pattern $Pattern -ErrorAction SilentlyContinue |
ForEach-Object {
"$($_.Path):$($_.LineNumber):$($_.Line.Trim())"
}
ForEach-Object { "$($_.Path):$($_.LineNumber):$($_.Line.Trim())" }
}
function Get-AllSourceLines {
Get-ChildItem -Path $SourceDir -Recurse -Include '*.java', '*.kt' -File |
Select-String -Pattern '.' -ErrorAction SilentlyContinue
}
if ($searchAll) {
Write-Section "Summary (counted in a single pass)"
$h = @{
retrofit = 0; okhttp = 0; ktor = 0; apollo = 0; volley = 0
hilt = 0; koin = 0; bearer = 0; hmac = 0
}
$summaryPattern = '@(GET|POST|PUT|DELETE|PATCH|HTTP)\(|Request\.Builder|HttpUrl|\.newCall\(|BearerTokens|defaultRequest \{|client\.(get|post)\(|httpClient\.(get|post)\(|ApolloClient|\.serverUrl\(|StringRequest|JsonObjectRequest|RequestQueue|@HiltAndroidApp|@AndroidEntryPoint|@HiltViewModel|@Provides|@Binds|org\.koin\.|module \{|single<|factory<|"[Bb]earer |HmacSHA|Mac\.getInstance'
foreach ($hit in (Get-ChildItem -Path $SourceDir -Recurse -Include '*.java', '*.kt' -File | Select-String -Pattern $summaryPattern -ErrorAction SilentlyContinue)) {
$line = $hit.Line
if ($line -match '@(GET|POST|PUT|DELETE|PATCH|HTTP)\(') { $h.retrofit++ }
if ($line -match 'Request\.Builder|HttpUrl|\.newCall\(') { $h.okhttp++ }
if ($line -match 'BearerTokens|defaultRequest \{|client\.(get|post)\(|httpClient\.(get|post)\(|HttpClient\.get\(') { $h.ktor++ }
if ($line -match 'ApolloClient|\.serverUrl\(') { $h.apollo++ }
if ($line -match 'StringRequest|JsonObjectRequest|RequestQueue') { $h.volley++ }
if ($line -match '@HiltAndroidApp|@AndroidEntryPoint|@HiltViewModel|@Provides|@Binds') { $h.hilt++ }
if ($line -match 'org\.koin\.|module \{|single<|factory<|singleOf\(|factoryOf\(') { $h.koin++ }
if ($line -match '"Bearer |"bearer |BearerTokens') { $h.bearer++ }
if ($line -match 'HmacSHA|Mac\.getInstance\("Hmac') { $h.hmac++ }
}
Write-Host (" HTTP framework: Retrofit={0,-5} OkHttp={1,-5} Ktor={2,-5} Apollo={3,-5} Volley={4,-5}" -f $h.retrofit, $h.okhttp, $h.ktor, $h.apollo, $h.volley)
Write-Host (" DI framework: Hilt/Dagger={0,-5} Koin={1,-5}" -f $h.hilt, $h.koin)
Write-Host (" Auth signals: Bearer={0,-5} HMAC/Sign={1,-5}" -f $h.bearer, $h.hmac)
Write-Host ""
Write-Host " Run with one of -Retrofit / -OkHttp / -Ktor / -Apollo / -Volley /"
Write-Host " -Paths / -Urls / -Auth to inspect a single section."
}
# --- 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 $Ktor) {
Write-Section "Ktor: Client Calls"
Search-Sources '\b(client|httpClient|HttpClient)\.(get|post|put|delete|patch|head|request)\s*[<(]'
Write-Section "Ktor: Request Building / Default Request"
Search-Sources '(HttpRequestBuilder|defaultRequest\s*\{|\burl\s*\(\s*"|URLBuilder|URLProtocol)'
Write-Section "Ktor: Auth Plugin (Bearer / Refresh)"
Search-Sources '(\bbearer\s*\{|BearerTokens\s*\(|loadTokens\s*\{|refreshTokens\s*\{|\bAuth\s*\)\s*\{)'
}
if ($searchAll -or $Apollo) {
Write-Section "Apollo: GraphQL Client"
Search-Sources '(ApolloClient|\.serverUrl\s*\(|\.subscriptionNetworkTransport|HttpNetworkTransport)'
Write-Section "Apollo: Operations"
Search-Sources '(\.query\s*\(\s*[A-Z]|\.mutation\s*\(\s*[A-Z]|\.subscription\s*\(\s*[A-Z])'
}
if ($searchAll -or $Volley) {
Write-Section "Volley Requests"
Search-Sources '(StringRequest|JsonObjectRequest|JsonArrayRequest|ImageRequest|RequestQueue|Volley\.newRequestQueue)'
}
# --- Hardcoded URLs ---
if ($searchAll -or $Paths) {
Write-Section "Endpoint-Shaped Path Literals (deduplicated)"
$seg = '[A-Za-z0-9_{}.\-]+'
$root = '(api|v[0-9]+|graphql|rest|mobile|auth|oauth|sso|users?|account|session|token|register|signup|signin|logout|password|verify|otp|sms|profile|customer|cart|basket|order|checkout|payment|invoice|product|catalog|inventory|search|category|favo[u]?rites?|wishlist|address|location|delivery|shipping|review|feedback|notification|push|message|chat|track|event|stat[a-z]*|metric|config|settings?|feature|flag|banner|content|media|upload|download|file|image|video|live|stream|webhook|callback)'
$pathsRegex = "`"(/$seg(/$seg)+/?|$root(/$seg)+/?)`""
$exclude = '^(image|video|audio|text|application|content|font|model|multipart|message)/|^/(proc|sys|dev|tmp|etc|usr|var|opt)/'
$allPaths = [System.Collections.Generic.HashSet[string]]::new()
Get-ChildItem -Path $SourceDir -Recurse -Include '*.java', '*.kt' -File | ForEach-Object {
$content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
if ($content) {
[regex]::Matches($content, $pathsRegex) | ForEach-Object {
$val = $_.Value.Trim('"')
if ($val -notmatch $exclude) { [void]$allPaths.Add($_.Value) }
}
}
}
$allPaths | Sort-Object | ForEach-Object { Write-Host $_ }
Write-Host ""
Write-Section "Endpoint-Shaped Path Literals: call sites"
Search-Sources $pathsRegex
}
if ($searchAll -or $Urls) {
Write-Section "Hardcoded URLs (http:// and https://)"
Search-Sources '"https?://[^"]+'
$denylistPath = Join-Path $PSScriptRoot '..\references\third_party_hosts.txt'
$strictUrl = 'https?://(([0-9]{1,3}(\.[0-9]{1,3}){3}|[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,})(:[0-9]{1,5})?(/[^"<>[:space:]]*)?|[A-Za-z0-9-]+(:[0-9]{1,5}(/[^"<>[:space:]]*)?|/[^"<>[:space:]]*))'
$validTlds = 'com|net|org|io|co|app|dev|me|ai|xyz|info|biz|gov|edu|mil|int|tech|cloud|uk|de|fr|it|es|nl|in|us|ca|au|jp|cn|br|ru|eu|ch|se|no|fi|dk|pl|pt|gr|ie|be|at|cz|sg|hk|kr|tw|mx|ar|cl|za|nz'
$urlSet = [System.Collections.Generic.HashSet[string]]::new()
Get-ChildItem -Path $SourceDir -Recurse -Include '*.java', '*.kt' -File | ForEach-Object {
$content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
if ($content) {
[regex]::Matches($content, $strictUrl) | ForEach-Object {
$url = $_.Value
$rest = $url -replace '^https?://', ''
$host = ($rest -split '[/:]')[0]
$hasPathPort = $rest -match '[/:]'
$keep = $false
if ($host -match '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$') { $keep = $true }
elseif (($host -split '\.').Count -ge 3) { $keep = $true }
elseif ($hasPathPort) { $keep = $true }
else {
$parts = $host -split '\.'
if ($parts.Count -eq 2 -and $parts[1] -match "^($validTlds)$") { $keep = $true }
}
if ($keep) { [void]$urlSet.Add($url) }
}
}
}
$urls = $urlSet | Sort-Object
$hosts = $urls | ForEach-Object { ($_ -replace '^https?://', '' -split '[/:]')[0] } | Sort-Object -Unique
$denyRegex = $null
if (Test-Path $denylistPath) {
$denyLines = Get-Content $denylistPath | Where-Object { $_ -notmatch '^\s*(#|$)' }
if ($denyLines) { $denyRegex = ($denyLines -join '|') }
}
$firstHosts = @()
$thirdHosts = @()
foreach ($h in $hosts) {
if ($denyRegex -and $h -match $denyRegex) { $thirdHosts += $h }
else { $firstHosts += $h }
}
Write-Section "Likely First-Party Hosts (frequency-sorted)"
if ($firstHosts.Count) {
$firstHosts | ForEach-Object {
$host = $_
$n = ($urls | Where-Object { $_ -match "://${([regex]::Escape($host))}([/:`"`]|$)" }).Count
[PSCustomObject]@{ Count = $n; Host = $host }
} | Sort-Object Count -Descending | ForEach-Object {
Write-Host (" {0,5} {1}" -f $_.Count, $_.Host)
}
} else {
Write-Host " (none; every URL matched the third-party denylist)"
}
Write-Section "Third-Party Hosts (denylist matches, collapsed)"
if ($thirdHosts.Count) { $thirdHosts | ForEach-Object { Write-Host " $_" } }
else { Write-Host " (none)" }
Write-Section "All First-Party URLs (full strings)"
foreach ($h in $firstHosts) {
$urls | Where-Object { $_ -match "://${([regex]::Escape($h))}([/:`"`]|$)" } | ForEach-Object { Write-Host " $_" }
}
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)'
Search-Sources '(?i)(api[_\-]?key|auth[_\-]?token|bearer|authorization|x-api-key|client[_\-]?secret|access[_\-]?token|refresh[_\-]?token)'
Write-Section "Request Signing (HMAC / signature schemes)"
Search-Sources '(HmacSHA(1|256|512)|Mac\.getInstance\("Hmac|SecretKeySpec\(|Signature\.getInstance\()'
Search-Sources '(?i)(x-signature|x-client-authorization|x-amz-signature|x-hmac|aws4-hmac|signRequest|signatureFor|computeSignature|signaturev[0-9])'
Write-Section "Possible Hardcoded Secrets / Keys"
Search-Sources '(?i)(app[_\-]?secret|client[_\-]?secret|signing[_\-]?key|hmac[_\-]?secret|consumer[_\-]?secret|private[_\-]?key)'
Write-Section "Base URLs and Constants"
Search-Sources '(?i)(BASE_URL|API_URL|SERVER_URL|ENDPOINT|API_BASE|HOST_NAME)'
Write-Section "Ktor Auth (Bearer + Refresh)"
Search-Sources '(BearerTokens|loadTokens\s*\{|refreshTokens\s*\{|\bbearer\s*\{)'
}
Write-Host ""

View File

@ -0,0 +1,274 @@
# fingerprint.ps1: Triage an APK/XAPK before decompiling (PowerShell port of fingerprint.sh)
param(
[Parameter(Position = 0)]
[string]$Input,
[Alias('h')]
[switch]$Help
)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.IO.Compression.FileSystem
function Show-Usage {
Write-Host @"
Usage: fingerprint.ps1 <file.apk|file.xapk>
Prints a one-screen summary:
* mobile framework (with rationale)
* HTTP / DI / serialization stack hints
* obfuscation indicator
* native libraries (consolidated across split APKs)
* notable third-party SDKs found in assets/
"@
exit 0
}
if ($Help) { Show-Usage }
if (-not $Input) { Show-Usage }
if (-not (Test-Path $Input)) {
Write-Host "File not found: $Input" -ForegroundColor Red
exit 1
}
$ext = [IO.Path]::GetExtension($Input).ToLower()
if ($ext -notin @('.apk', '.xapk', '.apks', '.apkm')) {
Write-Host "Unsupported input: $Input" -ForegroundColor Red
exit 1
}
$tmp = Join-Path $env:TEMP "apkfp-$([Guid]::NewGuid().ToString('N').Substring(0, 8))"
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
try {
$apks = @()
if ($ext -eq '.apk') {
$apks = @((Resolve-Path $Input).Path)
} else {
$xapkDir = Join-Path $tmp 'xapk'
New-Item -ItemType Directory -Path $xapkDir -Force | Out-Null
[System.IO.Compression.ZipFile]::ExtractToDirectory((Resolve-Path $Input).Path, $xapkDir)
$apks = Get-ChildItem -Path $xapkDir -Recurse -Filter '*.apk' -File |
Where-Object {
$rel = $_.FullName.Substring($xapkDir.Length).TrimStart('\', '/')
($rel -split '[\\/]').Count -le 2
} |
Select-Object -ExpandProperty FullName
}
if ($apks.Count -eq 0) {
Write-Host "No APK files found in input." -ForegroundColor Red
exit 1
}
$listing = [System.Collections.Generic.List[string]]::new()
$dexStrings = [System.Collections.Generic.HashSet[string]]::new()
function Get-ZipEntries {
param([string]$ZipPath)
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
try {
return $zip.Entries | ForEach-Object { $_.FullName.Replace('\', '/') }
} finally {
$zip.Dispose()
}
} catch {
return @()
}
}
function Get-DexTypeStrings {
param([byte[]]$Bytes)
$text = [System.Text.Encoding]::ASCII.GetString($Bytes)
$matches = [regex]::Matches($text, 'L[a-z][a-zA-Z0-9_]*(/[a-zA-Z0-9_$]+)+;')
$result = [System.Collections.Generic.HashSet[string]]::new()
foreach ($m in $matches) {
$fqn = $m.Value.TrimStart('L').TrimEnd(';')
if ($fqn.Length -ge 8) { [void]$result.Add($fqn) }
}
return $result
}
foreach ($apk in $apks) {
$entries = Get-ZipEntries -ZipPath $apk
foreach ($e in $entries) { $listing.Add($e) }
$dexEntries = $entries | Where-Object { $_ -match '^classes[0-9]*\.dex$' }
foreach ($dexName in $dexEntries) {
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($apk)
try {
$entry = $zip.GetEntry($dexName)
if (-not $entry) { continue }
$ms = New-Object System.IO.MemoryStream
try {
$stream = $entry.Open()
try { $stream.CopyTo($ms) } finally { $stream.Dispose() }
foreach ($s in (Get-DexTypeStrings -Bytes $ms.ToArray())) {
[void]$dexStrings.Add($s)
}
} finally {
$ms.Dispose()
}
} finally {
$zip.Dispose()
}
} catch { }
}
}
$listingText = ($listing | Sort-Object -Unique) -join "`n"
$dexText = ($dexStrings | Sort-Object) -join "`n"
function Test-Has {
param([string]$Pattern)
if ($listingText -match $Pattern) { return $true }
if ($dexText -match $Pattern) { return $true }
return $false
}
$framework = 'unknown'
$rationale = ''
if (Test-Has '^lib/[^/]+/libflutter\.so$') {
$framework = 'Flutter'
$rationale = 'lib/<abi>/libflutter.so present'
if (Test-Has '^lib/[^/]+/libapp\.so$') { $rationale += '; libapp.so contains AOT-compiled Dart' }
} elseif ((Test-Has '^lib/[^/]+/libhermes\.so$') -or (Test-Has '^assets/index\.android\.bundle$') -or (Test-Has '^lib/[^/]+/libreactnativejni\.so$')) {
$framework = 'React Native'
$reasons = @()
if (Test-Has '^lib/[^/]+/libhermes\.so$') { $reasons += 'libhermes.so' }
if (Test-Has '^lib/[^/]+/libreactnativejni\.so$') { $reasons += 'libreactnativejni.so' }
if (Test-Has '^assets/index\.android\.bundle$') { $reasons += 'assets/index.android.bundle' }
$rationale = $reasons -join ' '
} elseif ((Test-Has '^assets/www/index\.html$') -or (Test-Has '^assets/www/cordova\.js$') -or (Test-Has '^assets/public/index\.html$')) {
$framework = 'Cordova / Capacitor (WebView hybrid)'
$rationale = 'assets/www/ or assets/public/ shell present'
} elseif ((Test-Has '^lib/[^/]+/libmonodroid\.so$') -or (Test-Has '^assemblies/')) {
$framework = 'Xamarin / .NET MAUI'
$rationale = 'libmonodroid.so or assemblies/ present; code is in .NET DLLs'
} elseif (Test-Has '^lib/[^/]+/libmaui\.so$') {
$framework = '.NET MAUI'
$rationale = 'libmaui.so present'
} elseif ((Test-Has '^assets/flutter_assets/') -and -not (Test-Has '^lib/[^/]+/libflutter\.so$')) {
$framework = 'Flutter (code-only split?)'
$rationale = 'flutter_assets/ but no libflutter.so in this APK; check splits'
} else {
if (Test-Has 'androidx\.compose') {
$framework = 'Native Android (Kotlin + Jetpack Compose)'
$rationale = 'androidx.compose.* libraries detected'
} elseif (Test-Has '^META-INF/.*\.kotlin_module$') {
$framework = 'Native Android (Kotlin)'
$rationale = 'kotlin_module metadata present, no Compose markers'
} else {
$framework = 'Native Android (Java/Kotlin)'
$rationale = 'no cross-platform framework markers found'
}
}
$http = @()
if (Test-Has 'retrofit2') { $http += 'Retrofit' }
if (Test-Has 'okhttp3') { $http += 'OkHttp' }
if (Test-Has 'io/ktor/') { $http += 'Ktor' }
if (Test-Has 'com/apollographql/') { $http += 'Apollo (GraphQL)' }
if (Test-Has 'com/android/volley') { $http += 'Volley' }
$di = @()
if (Test-Has 'dagger/hilt/') { $di += 'Hilt' }
if (Test-Has '^META-INF/.*dagger.*') { $di += 'Dagger' }
if (Test-Has 'org/koin/') { $di += 'Koin' }
if ($di.Count -eq 0 -and (Test-Has 'javax/inject/')) { $di += 'javax.inject' }
$ser = @()
if (Test-Has 'kotlinx/serialization/') { $ser += 'kotlinx.serialization' }
if (Test-Has 'com/google/gson/') { $ser += 'Gson' }
if (Test-Has 'com/squareup/moshi/') { $ser += 'Moshi' }
if (Test-Has 'com/fasterxml/jackson/') { $ser += 'Jackson' }
$shortDirs = ([regex]::Matches($listingText, '(?m)^[a-z]{1,2}/') | ForEach-Object { $_.Value } | Sort-Object -Unique).Count
if ($shortDirs -gt 30) {
$obfuscation = "HIGH ($shortDirs single/double-letter dirs at root)"
} elseif ($shortDirs -gt 10) {
$obfuscation = "MODERATE ($shortDirs short root dirs)"
} else {
$obfuscation = 'LOW (no significant short-name namespace pollution)'
}
$native = [regex]::Matches($listingText, '(?m)^lib/[^/]+/[^/]+\.so$') |
ForEach-Object { $_.Value } | Sort-Object -Unique
$sdks = @()
if (Test-Has '^assets/com/appsflyer/') { $sdks += 'AppsFlyer' }
if ((Test-Has 'datadog\.buildId') -or (Test-Has 'com/datadog/')) { $sdks += 'Datadog' }
if (Test-Has 'io/sentry/') { $sdks += 'Sentry' }
if (Test-Has 'com/google/firebase/') { $sdks += 'Firebase' }
if (Test-Has 'com/google/android/gms/') { $sdks += 'Google Play Services' }
if (Test-Has 'com/facebook/') { $sdks += 'Facebook SDK' }
if (Test-Has 'com/payu/') { $sdks += 'PayU' }
if (Test-Has 'com/stripe/') { $sdks += 'Stripe' }
if (Test-Has 'com/braintreepayments/') { $sdks += 'Braintree' }
if (Test-Has 'com/storyteller/') { $sdks += 'Storyteller' }
if (Test-Has 'zendesk/') { $sdks += 'Zendesk' }
if (Test-Has 'com/intercom/') { $sdks += 'Intercom' }
if (Test-Has 'com/segment/analytics') { $sdks += 'Segment' }
if (Test-Has 'com/amplitude/') { $sdks += 'Amplitude' }
if (Test-Has 'com/mixpanel/') { $sdks += 'Mixpanel' }
if (Test-Has 'com/onesignal/') { $sdks += 'OneSignal' }
if (Test-Has 'com/microsoft/clarity') { $sdks += 'Microsoft Clarity' }
if (Test-Has 'com/hotjar/') { $sdks += 'Hotjar' }
if (Test-Has 'com/instabug/') { $sdks += 'Instabug' }
$buildConfig = if (Test-Has 'BuildConfig\.class$') {
'present (grep BuildConfig.java after decompile for base URLs / flavor)'
} else {
'not detected in zip listing (still worth grepping after decompile)'
}
$baseName = [IO.Path]::GetFileName($Input)
Write-Host "=== APK Fingerprint: $baseName ==="
Write-Host ""
Write-Host "Framework: $framework"
Write-Host " Rationale: $rationale"
Write-Host "Obfuscation: $obfuscation"
Write-Host ""
Write-Host "HTTP stack: $(if ($http.Count) { $http -join ' ' } else { 'none detected' })"
Write-Host "DI: $(if ($di.Count) { $di -join ' ' } else { 'none detected' })"
Write-Host "Serialization: $(if ($ser.Count) { $ser -join ' ' } else { 'none detected' })"
Write-Host "BuildConfig: $buildConfig"
Write-Host ""
Write-Host "Third-party SDKs: $(if ($sdks.Count) { $sdks -join ' ' } else { 'none detected' })"
Write-Host ""
Write-Host "Native libraries (consolidated across splits):"
if ($native.Count) {
$native | ForEach-Object { Write-Host " $_" }
} else {
Write-Host " (none)"
}
Write-Host ""
Write-Host "Recommended next step:"
switch -Regex ($framework) {
'^Flutter' {
Write-Host " Java decompilation will yield ~no app code. The Dart logic lives in"
Write-Host " libapp.so (AOT). Use tools designed for Flutter:"
Write-Host " - reFlutter / Doldrums / blutter (extract Dart class structure)"
Write-Host " - strings/rabin2 on libapp.so for endpoints & string constants"
}
'^React' {
Write-Host " Java code is just the RN host. Real app logic is in JS/Hermes:"
Write-Host " - if Hermes: hbctool disasm assets/index.android.bundle"
Write-Host " - if JSC: js-beautify the bundle and grep for fetch/axios"
}
'^Cordova' {
Write-Host " All app code is in assets/www/ (or assets/public/). Just unzip and"
Write-Host " inspect the HTML/JS; no Java decompile needed."
}
'^(Xamarin|\.NET)' {
Write-Host " App logic is in .NET DLLs (assemblies/). Use ILSpy or dotPeek;"
Write-Host " jadx will only show the Mono host."
}
default {
Write-Host " Proceed with Phase 2: decompile.ps1 <file>"
}
}
} finally {
if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
}

View File

@ -1,11 +1,11 @@
# install-dep.ps1 Install a single dependency for Android reverse engineering
# 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
# 0: installed successfully
# 1: installation failed
# 2: requires manual action
param(
[Parameter(Position=0)]
[string]$Dep
@ -73,11 +73,13 @@ function Get-GHLatestTag {
function Add-ToUserPath {
param([string]$Dir)
$currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($currentPath -notlike "*$Dir*") {
$pathEntries = if ($currentPath) { $currentPath -split ';' } else { @() }
if ($pathEntries -notcontains $Dir) {
[Environment]::SetEnvironmentVariable('PATH', "$Dir;$currentPath", 'User')
Write-Info "Added $Dir to user PATH. Restart your terminal to apply."
}
if ($env:PATH -notlike "*$Dir*") {
$sessionEntries = $env:PATH -split ';'
if ($sessionEntries -notcontains $Dir) {
$env:PATH = "$Dir;$env:PATH"
}
}
@ -85,21 +87,30 @@ function Add-ToUserPath {
$localBin = Join-Path $env:USERPROFILE '.local\bin'
$localShare = Join-Path $env:USERPROFILE '.local\share'
function Get-JavaMajorVersion {
$javaBin = Get-Command java -ErrorAction SilentlyContinue
if (-not $javaBin) { return $null }
$verOutput = & java -version 2>&1 | Select-Object -First 1
$verStr = "$verOutput"
if ($verStr -match '"(\d+)') {
$ver = [int]$Matches[1]
if ($ver -eq 1 -and $verStr -match '"1\.(\d+)') {
$ver = [int]$Matches[1]
}
return $ver
}
return $null
}
# =====================================================================
# 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
}
}
$ver = Get-JavaMajorVersion
if ($ver -and $ver -ge 17) {
Write-Ok "Java $ver already installed"
return
}
Write-Info "Installing Java JDK 17+..."
@ -274,15 +285,42 @@ function Install-Dex2Jar {
Write-Ok "dex2jar $version installed to $installDir"
}
function Install-ApktoolFromGitHub {
$tag = Get-GHLatestTag "iBotPeaches/Apktool"
if (-not $tag) {
Write-Fail "Could not determine latest apktool version."
Write-Manual "Download from https://github.com/iBotPeaches/Apktool/releases/latest"
}
$version = $tag -replace '^v', ''
$url = "https://github.com/iBotPeaches/Apktool/releases/download/$tag/apktool_$version.jar"
$installDir = Join-Path $localShare 'apktool'
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
Invoke-Download -Url $url -Dest (Join-Path $installDir 'apktool.jar')
New-Item -ItemType Directory -Path $localBin -Force | Out-Null
$wrapperPath = Join-Path $localBin 'apktool.cmd'
Set-Content -Path $wrapperPath -Value "@echo off`r`njava -jar -Duser.language=en `"$installDir\apktool.jar`" %*"
Add-ToUserPath $localBin
Write-Ok "apktool $version installed to $installDir"
}
function Install-Apktool {
if (Get-Command apktool -ErrorAction SilentlyContinue) {
Write-Ok "apktool already installed"
return
}
$javaVer = Get-JavaMajorVersion
if ($hasScoop) {
Write-Info "Installing apktool via scoop..."
scoop install apktool
} elseif ($javaVer -and $javaVer -ge 8) {
Write-Info "Java $javaVer on PATH — installing apktool from GitHub (skipping Chocolatey jre8 dependency)..."
Install-ApktoolFromGitHub
} elseif ($hasChoco) {
Write-Info "Installing apktool via choco..."
choco install apktool -y

View File

@ -0,0 +1,44 @@
# lookup-name.ps1: Query the mapping produced by recover-kotlin-names.ps1
param(
[Parameter(Position = 0)]
[string]$MappingDir,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$QueryArgs,
[Alias('h')]
[switch]$Help
)
$ErrorActionPreference = 'Stop'
function Show-Usage {
Write-Host @"
Usage: lookup-name.ps1 <mapping-dir> <query>
lookup-name.ps1 <mapping-dir> -o <obf-fqn>
lookup-name.ps1 <mapping-dir> -p <real-package-substring>
lookup-name.ps1 <mapping-dir> --grep <regex> <sources-dir>
"@
exit 0
}
if ($Help) { Show-Usage }
if (-not $MappingDir -or $QueryArgs.Count -eq 0) { Show-Usage }
$mapFile = Join-Path $MappingDir 'mapping.json'
if (-not (Test-Path $mapFile)) {
Write-Host "no mapping.json in $MappingDir" -ForegroundColor Red
exit 1
}
$pyScript = Join-Path $PSScriptRoot 'lookup_names.py'
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { $py = Get-Command py -ErrorAction SilentlyContinue }
$allArgs = @($MappingDir) + $QueryArgs
if ($py -and $py.Name -eq 'python') {
& python $pyScript @allArgs
} elseif ($py) {
& py -3 $pyScript @allArgs
} else {
Write-Host "Error: python not found. Install Python 3." -ForegroundColor Red
exit 1
}

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# lookup-name.sh Query the mapping produced by recover-kotlin-names.sh.
# lookup-name.sh: Query the mapping produced by recover-kotlin-names.sh.
#
# Modes:
# lookup-name.sh <mapping-dir> <substring> search by real-FQN substring
@ -27,59 +27,5 @@ EOF
DIR="$1"; shift
[[ ! -f "$DIR/mapping.json" ]] && { echo "no mapping.json in $DIR" >&2; exit 1; }
python3 - "$DIR" "$@" <<'PY'
import json, os, re, sys, subprocess
DIR = sys.argv[1]
args = sys.argv[2:]
MAP = json.load(open(os.path.join(DIR, "mapping.json")))
REV = {}
for o, r in MAP.items():
REV.setdefault(r, []).append(o)
def search(q):
ql = q.lower()
for r in sorted(REV):
if ql in r.lower():
print(r)
for o in sorted(REV[r]):
print(f" {o}")
def by_obf(o):
if o not in MAP:
print(f"no mapping for {o}", file=sys.stderr); sys.exit(1)
print(f"{o} -> {MAP[o]}")
sibs = [s for s in REV[MAP[o]] if s != o]
for s in sorted(sibs):
print(f" sibling: {s}")
def by_pkg(p):
pl = p.lower()
for r in sorted(REV):
if pl in r.rsplit(".", 1)[0].lower():
print(r)
for o in sorted(REV[r]):
print(f" {o}")
def grep_annot(pattern, sources):
res = subprocess.run(
["grep", "-rEn", "--include=*.java", pattern, sources],
capture_output=True, text=True)
for line in res.stdout.splitlines():
try:
path, lineno, content = line.split(":", 2)
except ValueError:
continue
rel = os.path.relpath(path, sources)
obf = rel.replace(os.sep, ".")[:-5]
suffix = f" // {MAP[obf]}" if obf in MAP else ""
print(f"{rel}:{lineno}:{content}{suffix}")
if args[0] == "-o" and len(args) == 2:
by_obf(args[1])
elif args[0] == "-p" and len(args) == 2:
by_pkg(args[1])
elif args[0] == "--grep" and len(args) == 3:
grep_annot(args[1], args[2])
else:
search(" ".join(args))
PY
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
python3 "$SCRIPT_DIR/lookup_names.py" "$DIR" "$@"

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Query the mapping produced by recover_kotlin_names.py."""
import json
import os
import subprocess
import sys
def main():
if len(sys.argv) < 3:
print("Usage: lookup_names.py <mapping-dir> <query>")
print(" lookup_names.py <mapping-dir> -o <obf-fqn>")
print(" lookup_names.py <mapping-dir> -p <real-package-substring>")
print(" lookup_names.py <mapping-dir> --grep <regex> <sources-dir>")
sys.exit(0)
dir_path = sys.argv[1]
args = sys.argv[2:]
map_path = os.path.join(dir_path, "mapping.json")
if not os.path.isfile(map_path):
print(f"no mapping.json in {dir_path}", file=sys.stderr)
sys.exit(1)
with open(map_path) as fh:
mapping = json.load(fh)
rev = {}
for o, r in mapping.items():
rev.setdefault(r, []).append(o)
def search(q):
ql = q.lower()
for r in sorted(rev):
if ql in r.lower():
print(r)
for o in sorted(rev[r]):
print(f" {o}")
def by_obf(o):
if o not in mapping:
print(f"no mapping for {o}", file=sys.stderr)
sys.exit(1)
print(f"{o} -> {mapping[o]}")
for s in sorted(rev[mapping[o]]):
if s != o:
print(f" sibling: {s}")
def by_pkg(p):
pl = p.lower()
for r in sorted(rev):
if pl in r.rsplit(".", 1)[0].lower():
print(r)
for o in sorted(rev[r]):
print(f" {o}")
def grep_annot(pattern, sources):
if sys.platform == "win32":
cmd = [
"powershell", "-NoProfile", "-Command",
f"Get-ChildItem -Path '{sources}' -Recurse -Include *.java -File | "
f"Select-String -Pattern '{pattern}' | "
"ForEach-Object {{ $_.Path + ':' + $_.LineNumber + ':' + $_.Line.Trim() }}"
]
res = subprocess.run(cmd, capture_output=True, text=True)
lines = res.stdout.splitlines()
else:
res = subprocess.run(
["grep", "-rEn", "--include=*.java", pattern, sources],
capture_output=True, text=True)
lines = res.stdout.splitlines()
for line in lines:
try:
path, lineno, content = line.split(":", 2)
except ValueError:
continue
rel = os.path.relpath(path, sources)
obf = rel.replace(os.sep, ".")[:-5]
suffix = f" // {mapping[obf]}" if obf in mapping else ""
print(f"{rel}:{lineno}:{content}{suffix}")
if args[0] == "-o" and len(args) == 2:
by_obf(args[1])
elif args[0] == "-p" and len(args) == 2:
by_pkg(args[1])
elif args[0] == "--grep" and len(args) == 3:
grep_annot(args[1], args[2])
else:
search(" ".join(args))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,41 @@
# recover-kotlin-names.ps1: Rebuild obfuscated -> real Kotlin class-name map
param(
[Parameter(Position = 0)]
[string]$SourceDir,
[Parameter(Position = 1)]
[string]$OutputDir,
[Alias('h')]
[switch]$Help
)
$ErrorActionPreference = 'Stop'
function Show-Usage {
Write-Host @"
Usage: recover-kotlin-names.ps1 <decompiled-sources-dir> [output-dir]
Walks every *.java under <decompiled-sources-dir>, mines @DebugMetadata
and @Metadata annotations, and writes mapping.tsv, mapping.json, by_package/.
"@
exit 0
}
if ($Help) { Show-Usage }
if (-not $SourceDir) { Show-Usage }
if (-not (Test-Path $SourceDir -PathType Container)) {
Write-Host "not a directory: $SourceDir" -ForegroundColor Red
exit 1
}
$pyScript = Join-Path $PSScriptRoot 'recover_kotlin_names.py'
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { $py = Get-Command py -ErrorAction SilentlyContinue }
if ($py -and $py.Name -eq 'python') {
if ($OutputDir) { & python $pyScript $SourceDir $OutputDir } else { & python $pyScript $SourceDir }
} elseif ($py) {
if ($OutputDir) { & py -3 $pyScript $SourceDir $OutputDir } else { & py -3 $pyScript $SourceDir }
} else {
Write-Host "Error: python not found. Install Python 3." -ForegroundColor Red
exit 1
}

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash
# recover-kotlin-names.sh Rebuild a (obfuscated -> real) class-name map
# recover-kotlin-names.sh: Rebuild a (obfuscated -> real) class-name map
# from Kotlin metadata strings left in decompiled sources.
#
# R8 obfuscates JVM symbols but cannot strip the Kotlin metadata strings
# R8 obfuscates JVM symbols but cannot strip the Kotlin metadata strings;
# the Kotlin runtime (reflection, coroutines) needs them at runtime. Two
# annotations carry the original FQN:
#
@ -14,7 +14,7 @@
# class refs of the file.
#
# Typical recovery on a real-world app: 30-50 % of classes regain their real
# names usually 100 % of the *Repository / *ViewModel / *UseCase / *Impl
# names; usually 100 % of the *Repository / *ViewModel / *UseCase / *Impl
# classes you actually want to read.
set -euo pipefail
@ -43,98 +43,5 @@ OUT="${2:-$(dirname "$SRC")/mapping}"
mkdir -p "$OUT/by_package"
python3 - "$SRC" "$OUT" <<'PY'
import os, re, sys, json
from collections import defaultdict
SRC, OUT = sys.argv[1], sys.argv[2]
# @DebugMetadata(c = "com.foo.Bar$Inner$1", ...)
RE_DEBUG = re.compile(r'@DebugMetadata\([^)]*?c\s*=\s*"([^"]+)"', re.S)
# @Metadata(... d2 = { "...Lcom/foo/Bar;..." ...} )
RE_DTWO = re.compile(r'@Metadata\([^)]*?d2\s*=\s*\{([^}]*)\}', re.S)
RE_LCLASS = re.compile(r'L([A-Za-z][\w/$]+);')
# jadx sometimes emits this comment for renamed classes
RE_RENAMED = re.compile(r'/\*\s*renamed from:\s*([\w.$]+)\s*\*/')
# Skip third-party / framework trees — their names are already real.
SKIP_PREFIXES = (
"kotlin.", "kotlinx.", "androidx.", "android.", "java.", "javax.",
"com.google.", "com.facebook.", "com.appsflyer.", "com.datadog.",
"io.ktor.", "io.sentry.", "io.realm.", "okhttp3.", "okio.",
"com.squareup.", "com.bumptech.", "com.airbnb.", "com.payu.",
"com.storyteller.", "zendesk.", "io.intercom.", "com.microsoft.",
"com.tinder.", "com.hotjar.", "com.amplitude.", "com.segment.",
"com.mixpanel.", "com.onesignal.", "com.stripe.", "com.braintreepayments.",
"retrofit2.", "dagger.", "javax.inject.", "org.jetbrains.",
)
mapping = {}
file_real = {}
counts = defaultdict(int)
for dp, _, files in os.walk(SRC):
for f in files:
if not f.endswith(".java"):
continue
path = os.path.join(dp, f)
rel = os.path.relpath(path, SRC)
obf = rel[:-5].replace(os.sep, ".")
if obf.startswith(SKIP_PREFIXES):
continue
try:
text = open(path, "r", errors="replace").read()
except OSError:
continue
real = None
m = RE_DEBUG.search(text)
if m:
real = m.group(1).split("$", 1)[0]
counts["debug_meta"] += 1
if not real:
m = RE_DTWO.search(text)
if m:
for lm in RE_LCLASS.finditer(m.group(1)):
cand = lm.group(1).replace("/", ".").split("$", 1)[0]
if "." in cand and not cand.startswith(("kotlin.", "java.", "android")):
real = cand
counts["d2"] += 1
break
if not real:
m = RE_RENAMED.search(text)
if m:
real = m.group(1)
counts["renamed"] += 1
if real:
mapping[obf] = real
file_real[obf] = path
with open(os.path.join(OUT, "mapping.tsv"), "w") as f:
f.write("obf_fqn\treal_fqn\tfile\n")
for k in sorted(mapping):
f.write(f"{k}\t{mapping[k]}\t{file_real[k]}\n")
with open(os.path.join(OUT, "mapping.json"), "w") as f:
json.dump(mapping, f, indent=2, sort_keys=True)
by_pkg = defaultdict(list)
for obf, real in mapping.items():
pkg = real.rsplit(".", 1)[0] if "." in real else "(default)"
by_pkg[pkg].append((real, obf, file_real[obf]))
for pkg, rows in by_pkg.items():
safe = os.path.basename(pkg).replace(".", "_") or "default"
with open(os.path.join(OUT, "by_package", f"{safe}.txt"), "w") as f:
for real, obf, p in sorted(rows):
f.write(f"{real}\t{obf}\t{p}\n")
print(f"Recovered {len(mapping)} class names")
for k, v in counts.items():
print(f" via {k}: {v}")
print(f"Real packages: {len(by_pkg)}")
print(f"Wrote {OUT}/mapping.tsv, mapping.json, by_package/")
PY
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
python3 "$SCRIPT_DIR/recover_kotlin_names.py" "$SRC" "$OUT"

View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Rebuild obfuscated -> real Kotlin class-name map from decompiled sources."""
import json
import os
import re
import sys
from collections import defaultdict
RE_DEBUG = re.compile(r'@DebugMetadata\([^)]*?c\s*=\s*"([^"]+)"', re.S)
RE_DTWO = re.compile(r'@Metadata\([^)]*?d2\s*=\s*\{([^}]*)\}', re.S)
RE_LCLASS = re.compile(r'L([A-Za-z][\w/$]+);')
RE_RENAMED = re.compile(r'/\*\s*renamed from:\s*([\w.$]+)\s*\*/')
SKIP_PREFIXES = (
"kotlin.", "kotlinx.", "androidx.", "android.", "java.", "javax.",
"com.google.", "com.facebook.", "com.appsflyer.", "com.datadog.",
"io.ktor.", "io.sentry.", "io.realm.", "okhttp3.", "okio.",
"com.squareup.", "com.bumptech.", "com.airbnb.", "com.payu.",
"com.storyteller.", "zendesk.", "io.intercom.", "com.microsoft.",
"com.tinder.", "com.hotjar.", "com.amplitude.", "com.segment.",
"com.mixpanel.", "com.onesignal.", "com.stripe.", "com.braintreepayments.",
"retrofit2.", "dagger.", "javax.inject.", "org.jetbrains.",
)
def main():
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
print("Usage: recover_kotlin_names.py <decompiled-sources-dir> [output-dir]")
sys.exit(0)
src = sys.argv[1]
out = sys.argv[2] if len(sys.argv) > 2 else os.path.join(os.path.dirname(src), "mapping")
if not os.path.isdir(src):
print(f"not a directory: {src}", file=sys.stderr)
sys.exit(1)
os.makedirs(os.path.join(out, "by_package"), exist_ok=True)
mapping = {}
file_real = {}
counts = defaultdict(int)
for dp, _, files in os.walk(src):
for f in files:
if not f.endswith(".java"):
continue
path = os.path.join(dp, f)
rel = os.path.relpath(path, src)
obf = rel[:-5].replace(os.sep, ".")
if obf.startswith(SKIP_PREFIXES):
continue
try:
with open(path, "r", errors="replace") as fh:
text = fh.read()
except OSError:
continue
real = None
m = RE_DEBUG.search(text)
if m:
real = m.group(1).split("$", 1)[0]
counts["debug_meta"] += 1
if not real:
m = RE_DTWO.search(text)
if m:
for lm in RE_LCLASS.finditer(m.group(1)):
cand = lm.group(1).replace("/", ".").split("$", 1)[0]
if "." in cand and not cand.startswith(("kotlin.", "java.", "android")):
real = cand
counts["d2"] += 1
break
if not real:
m = RE_RENAMED.search(text)
if m:
real = m.group(1)
counts["renamed"] += 1
if real:
mapping[obf] = real
file_real[obf] = path
with open(os.path.join(out, "mapping.tsv"), "w") as fh:
fh.write("obf_fqn\treal_fqn\tfile\n")
for k in sorted(mapping):
fh.write(f"{k}\t{mapping[k]}\t{file_real[k]}\n")
with open(os.path.join(out, "mapping.json"), "w") as fh:
json.dump(mapping, fh, indent=2, sort_keys=True)
by_pkg = defaultdict(list)
for obf, real in mapping.items():
pkg = real.rsplit(".", 1)[0] if "." in real else "(default)"
by_pkg[pkg].append((real, obf, file_real[obf]))
for pkg, rows in by_pkg.items():
safe = os.path.basename(pkg).replace(".", "_") or "default"
with open(os.path.join(out, "by_package", f"{safe}.txt"), "w") as fh:
for real, obf, p in sorted(rows):
fh.write(f"{real}\t{obf}\t{p}\n")
print(f"Recovered {len(mapping)} class names")
for k, v in counts.items():
print(f" via {k}: {v}")
print(f"Real packages: {len(by_pkg)}")
print(f"Wrote {out}/mapping.tsv, mapping.json, by_package/")
if __name__ == "__main__":
main()