reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$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.5.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.5.0",
|
||||
"author": {
|
||||
"name": "Simone Avogadro"
|
||||
},
|
||||
"repository": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction"],
|
||||
"category": "security"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
vendor-new/android-reverse-engineering-skill/.gitattributes
vendored
Normal file
9
vendor-new/android-reverse-engineering-skill/.gitattributes
vendored
Normal 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
|
||||
190
vendor-new/android-reverse-engineering-skill/LICENSE
Normal file
190
vendor-new/android-reverse-engineering-skill/LICENSE
Normal file
@@ -0,0 +1,190 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding any notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2025 Simone Avogadro
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
51
vendor-new/android-reverse-engineering-skill/PRIVACY.md
Normal file
51
vendor-new/android-reverse-engineering-skill/PRIVACY.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Privacy Policy
|
||||
|
||||
_Last updated: 2026-10-11_
|
||||
|
||||
The **android-reverse-engineering** plugin for Claude Code is an open-source set of
|
||||
instructions and local scripts. It does not include any server, account, or
|
||||
telemetry operated by the author.
|
||||
|
||||
## Data the plugin collects
|
||||
|
||||
None. The plugin does not collect, store, or transmit personal data, usage
|
||||
statistics, or the files you analyze.
|
||||
|
||||
## Local processing
|
||||
|
||||
APK, XAPK, JAR, and AAR files are decompiled and searched entirely on your
|
||||
machine with locally installed tools (jadx, Vineflower/Fernflower, dex2jar).
|
||||
Output is written to a directory on your machine and nowhere else.
|
||||
|
||||
The scripts do not read credentials, tokens, or API keys from your environment
|
||||
or files. The "auth" search options only scan the decompiled source you point
|
||||
them at for authentication-related strings.
|
||||
|
||||
## Network access
|
||||
|
||||
The only network requests the plugin's scripts make are dependency downloads,
|
||||
and only when you run `install-dep.sh` / `install-dep.ps1`:
|
||||
|
||||
- release metadata and archives from GitHub (`api.github.com`, `github.com`)
|
||||
for jadx, Vineflower, and dex2jar;
|
||||
- your system package manager (for example Homebrew, apt, winget, scoop, or
|
||||
Chocolatey) when installing Java, jadx, or optional tools such as Apktool
|
||||
and adb.
|
||||
|
||||
These requests go directly to those services and are subject to their own
|
||||
privacy policies. No data about you or your analyzed files is included.
|
||||
|
||||
The tools themselves (jadx, Vineflower, dex2jar, Apktool, adb, Java) are
|
||||
third-party software under their own licenses and terms; see
|
||||
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
## Claude
|
||||
|
||||
Like any Claude Code plugin, the content Claude reads while using this plugin
|
||||
(for example decompiled source) is processed by Claude under Anthropic's terms
|
||||
and privacy policy, not by the plugin author.
|
||||
|
||||
## Contact
|
||||
|
||||
Questions or concerns: open an issue at
|
||||
https://github.com/SimoneAvogadro/android-reverse-engineering-skill/issues
|
||||
231
vendor-new/android-reverse-engineering-skill/README.md
Normal file
231
vendor-new/android-reverse-engineering-skill/README.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Android Reverse Engineering & API Extraction — Claude Code skill
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0) [](https://github.com/SimoneAvogadro/android-reverse-engineering-skill/stargazers) [](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.
|
||||
|
||||
> **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).
|
||||
|
||||
## 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
|
||||
|
||||
| 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 |
|
||||
| **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 |
|
||||
| **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
|
||||
|
||||
**Required:**
|
||||
|
||||
- Java JDK 17+
|
||||
- [jadx](https://github.com/skylot/jadx) (CLI)
|
||||
|
||||
**Optional (recommended):**
|
||||
|
||||
- [Vineflower](https://github.com/Vineflower/vineflower) or [Fernflower](https://github.com/JetBrains/fernflower) — better output on complex Java code
|
||||
- [dex2jar](https://github.com/ThexXTURBOXx/dex2jar) — needed to use Fernflower on APK/DEX files
|
||||
|
||||
See `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md` for detailed installation instructions.
|
||||
|
||||
## Installation
|
||||
|
||||
### From GitHub (recommended)
|
||||
|
||||
Inside Claude Code, run:
|
||||
|
||||
```text
|
||||
/plugin marketplace add SimoneAvogadro/android-reverse-engineering-skill
|
||||
/plugin install android-reverse-engineering@android-reverse-engineering-skill
|
||||
```
|
||||
|
||||
The skill will be permanently available in all future sessions.
|
||||
|
||||
### From a local clone
|
||||
|
||||
```bash
|
||||
git clone https://github.com/SimoneAvogadro/android-reverse-engineering-skill.git
|
||||
```
|
||||
|
||||
Then in Claude Code:
|
||||
|
||||
```text
|
||||
/plugin marketplace add /path/to/android-reverse-engineering-skill
|
||||
/plugin install android-reverse-engineering@android-reverse-engineering-skill
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Slash command
|
||||
|
||||
```text
|
||||
/decompile path/to/app.apk
|
||||
```
|
||||
|
||||
This runs the full workflow: dependency check, decompilation, and initial structure analysis.
|
||||
|
||||
### Natural language
|
||||
|
||||
The skill activates on phrases like:
|
||||
|
||||
- "Decompile this APK"
|
||||
- "Reverse engineer this Android app"
|
||||
- "Extract API endpoints from this app"
|
||||
- "Follow the call flow from LoginActivity"
|
||||
- "Analyze this AAR library"
|
||||
|
||||
### Manual scripts
|
||||
|
||||
The scripts can also be used standalone:
|
||||
|
||||
```bash
|
||||
# Check dependencies
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/check-deps.sh
|
||||
|
||||
# Install a missing dependency (auto-detects OS and package manager)
|
||||
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
|
||||
|
||||
# Fingerprint an APK/XAPK BEFORE decompiling (Phase 0 triage):
|
||||
# framework, HTTP stack, obfuscation level, native libs, notable SDKs
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.sh app.apk
|
||||
|
||||
# Decompile APK with jadx (default)
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh app.apk
|
||||
|
||||
# Decompile XAPK (auto-extracts and decompiles each APK inside)
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh app-bundle.xapk
|
||||
|
||||
# Decompile with Fernflower
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/decompile.sh --engine fernflower library.jar
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# Modern Kotlin/KMP stacks and obfuscation-resistant extraction
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --ktor # Ktor client
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --apollo # Apollo / GraphQL
|
||||
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)
|
||||
|
||||
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
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/recover-kotlin-names.sh output/sources/ output/names/
|
||||
# → output/names/mapping.tsv, mapping.json, by_package/
|
||||
|
||||
# 2. Query it: resolve an obfuscated name, search by real name, or grep
|
||||
# the sources with each hit annotated with its recovered class name
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/lookup-name.sh output/names/ LoginRepository
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/lookup-name.sh output/names/ -o a.b.c
|
||||
bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/lookup-name.sh output/names/ --grep 'login' output/sources/
|
||||
```
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```text
|
||||
android-reverse-engineering-skill/
|
||||
├── .claude-plugin/
|
||||
│ └── marketplace.json # Marketplace catalog
|
||||
├── plugins/
|
||||
│ └── android-reverse-engineering/
|
||||
│ ├── .claude-plugin/
|
||||
│ │ └── plugin.json # Plugin manifest
|
||||
│ ├── skills/
|
||||
│ │ └── android-reverse-engineering/
|
||||
│ │ ├── SKILL.md # Core workflow (Phase 0–5)
|
||||
│ │ ├── references/
|
||||
│ │ │ ├── setup-guide.md
|
||||
│ │ │ ├── jadx-usage.md
|
||||
│ │ │ ├── fernflower-usage.md
|
||||
│ │ │ ├── api-extraction-patterns.md
|
||||
│ │ │ ├── kotlin-name-recovery.md
|
||||
│ │ │ ├── third_party_hosts.txt # denylist for first/third-party bucketing
|
||||
│ │ │ └── call-flow-analysis.md
|
||||
│ │ └── scripts/
|
||||
│ │ ├── check-deps.sh # Bash
|
||||
│ │ ├── check-deps.ps1 # PowerShell
|
||||
│ │ ├── install-dep.sh
|
||||
│ │ ├── install-dep.ps1
|
||||
│ │ ├── decompile.sh
|
||||
│ │ ├── decompile.ps1
|
||||
│ │ ├── fingerprint.sh # Phase 0 — pre-decompile triage
|
||||
│ │ ├── recover-kotlin-names.sh # R8 → real Kotlin class names
|
||||
│ │ ├── lookup-name.sh # query the recovered name map
|
||||
│ │ ├── find-api-calls.sh
|
||||
│ │ └── find-api-calls.ps1
|
||||
│ └── commands/
|
||||
│ └── decompile.md # /decompile slash command
|
||||
├── LICENSE
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 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/)
|
||||
|
||||
## 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)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This plugin is provided strictly for **lawful purposes**, including but not limited to:
|
||||
|
||||
- Security research and authorized penetration testing
|
||||
- Interoperability analysis permitted under applicable law (e.g., EU Directive 2009/24/EC, US DMCA §1201(f))
|
||||
- Malware analysis and incident response
|
||||
- Educational use and CTF competitions
|
||||
|
||||
**You are solely responsible** for ensuring that your use of this tool complies with all applicable laws, regulations, and terms of service. Unauthorized reverse engineering of software you do not own or do not have permission to analyze may violate intellectual property laws and computer fraud statutes in your jurisdiction.
|
||||
|
||||
The authors disclaim any liability for misuse of this tool.
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0 — see [LICENSE](LICENSE).
|
||||
|
||||
The decompilers and other tools this plugin drives (jadx, Vineflower/Fernflower, dex2jar, Apktool, adb, Java) are third-party software, not bundled with the plugin and distributed under their own licenses — see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Privacy: [PRIVACY.md](PRIVACY.md).
|
||||
@@ -0,0 +1,33 @@
|
||||
# Third-Party Software Notices
|
||||
|
||||
This plugin is licensed under the [Apache License 2.0](LICENSE). That license
|
||||
covers only the plugin's own files (instructions, reference docs, and scripts).
|
||||
|
||||
The plugin **does not bundle or redistribute** any third-party software. Its
|
||||
scripts invoke tools that you install yourself, either manually or through
|
||||
`install-dep.sh` / `install-dep.ps1`, which download them from their official
|
||||
upstream sources or your system package manager. Each tool is distributed by
|
||||
its own authors under its own license, and your use of it is governed by that
|
||||
license, not by this plugin's.
|
||||
|
||||
| Tool | Used for | License | Project |
|
||||
|---|---|---|---|
|
||||
| jadx | DEX/APK to Java decompilation (default engine) | Apache-2.0 | https://github.com/skylot/jadx |
|
||||
| Vineflower | Java decompilation (Fernflower fork) | Apache-2.0 | https://github.com/Vineflower/vineflower |
|
||||
| Fernflower | Java decompilation (JetBrains) | Apache-2.0 | https://github.com/JetBrains/fernflower |
|
||||
| dex2jar | DEX to JAR conversion | Apache-2.0 | https://github.com/ThexXTURBOXx/dex2jar |
|
||||
| Apktool | Android resource decoding (optional) | Apache-2.0 | https://apktool.org/ |
|
||||
| Android SDK Platform-Tools (`adb`) | Pulling APKs from a device (optional) | Android Software Development Kit License | https://developer.android.com/tools/releases/platform-tools |
|
||||
| Java (OpenJDK 17+) | Runtime required by the tools above | GPL-2.0 with Classpath Exception (varies by distribution) | https://openjdk.org/ |
|
||||
|
||||
When `install-dep` uses a package manager (Homebrew, apt, dnf, pacman, winget,
|
||||
scoop, Chocolatey), the package comes from that manager's repositories and
|
||||
under its maintainers' packaging terms.
|
||||
|
||||
Licenses listed here are provided for convenience and may change upstream;
|
||||
the license published by each project is authoritative.
|
||||
|
||||
## Your responsibility
|
||||
|
||||
These tools decompile software. Make sure you have the right to analyze the
|
||||
files you process. See the [Disclaimer](README.md#disclaimer) in the README.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 779 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "android-reverse-engineering",
|
||||
"version": "1.5.0",
|
||||
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows through libraries, and document extracted APIs.",
|
||||
"author": {
|
||||
"name": "Simone Avogadro"
|
||||
},
|
||||
"homepage": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill",
|
||||
"repository": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill",
|
||||
"documentationUrl": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill#readme",
|
||||
"license": "Apache-2.0",
|
||||
"supportUrl": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill/issues/new",
|
||||
"termsOfServiceUrl": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill/blob/master/LICENSE",
|
||||
"privacyPolicyUrl": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill/blob/master/PRIVACY.md",
|
||||
"keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction"],
|
||||
"skills": "./skills/",
|
||||
"commands": "./commands/"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
allowed-tools: Bash(bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh), Bash(bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh *), Read, Glob, Grep
|
||||
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)
|
||||
---
|
||||
|
||||
# /decompile
|
||||
|
||||
Decompile an Android application and perform initial structure analysis.
|
||||
|
||||
## Instructions
|
||||
|
||||
You are starting the Android reverse engineering workflow. Follow these steps:
|
||||
|
||||
### Step 1: Get the target file
|
||||
|
||||
If the user provided a file path as an argument, use that. Otherwise, ask the user for the path to the APK, XAPK, JAR, or AAR file they want to decompile.
|
||||
|
||||
### Step 2: Check and install dependencies
|
||||
|
||||
Run the dependency check:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/check-deps.sh
|
||||
```
|
||||
|
||||
Parse the output looking for `INSTALL_REQUIRED:` and `INSTALL_OPTIONAL:` lines.
|
||||
|
||||
**If required dependencies are missing**, install them one by one:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh java
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh jadx
|
||||
```
|
||||
|
||||
The install script auto-detects the OS and installs without sudo when possible (user-local install to `~/.local/`). If sudo is needed, it will prompt — if the user declines or sudo is unavailable, the script prints exact manual instructions (exit code 2). Show those instructions to the user and stop.
|
||||
|
||||
**For optional dependencies** (`INSTALL_OPTIONAL:vineflower`, `INSTALL_OPTIONAL:dex2jar`, etc.), ask the user if they want to install them. Recommend vineflower and dex2jar for better results.
|
||||
|
||||
After any installations, re-run `check-deps.sh` to verify. Do not proceed until all required dependencies pass.
|
||||
|
||||
### Step 3: Decompile
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
For obfuscated apps (if the user mentions it or you detect single-letter package names), add `--deobf`:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh --deobf <file>
|
||||
```
|
||||
|
||||
### Step 4: Analyze structure
|
||||
|
||||
After decompilation completes:
|
||||
|
||||
1. Read `AndroidManifest.xml` from the resources directory. For XAPK, check the base APK's output first.
|
||||
2. If XAPK, review `xapk-manifest.json` in the output directory to understand the split structure.
|
||||
3. List the top-level package structure
|
||||
4. Identify the app's main Activity, Application class, and architecture pattern
|
||||
5. Report a summary to the user
|
||||
|
||||
### 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"
|
||||
- **Re-decompile with Fernflower**: If jadx output has warnings, offer to re-run with `--engine both` for comparison
|
||||
|
||||
Refer to the full skill documentation in `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/SKILL.md` for the complete workflow.
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
name: android-reverse-engineering
|
||||
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
|
||||
|
||||
Decompile Android APK, XAPK, JAR, and AAR files using jadx and Fernflower/Vineflower, trace call flows through application code and libraries, and produce structured documentation of extracted APIs. Two decompiler engines are supported — jadx for broad Android coverage and Fernflower for higher-quality output on complex Java code — and can be used together for comparison.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This skill requires **Java JDK 17+** and **jadx** to be installed. **Fernflower/Vineflower** and **dex2jar** are optional but recommended for better decompilation quality. Run the dependency checker to verify:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
### Phase 0: Fingerprint the App (recommended before anything else)
|
||||
|
||||
Before installing tools or decompiling, run a fast triage to determine what
|
||||
kind of app you are looking at. **Decompiling Java is mostly useless for
|
||||
Flutter, React Native, Cordova/Capacitor, and Xamarin apps** — the real code
|
||||
lives elsewhere. The fingerprint script tells you which.
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/fingerprint.sh <file.apk|file.xapk>
|
||||
```
|
||||
|
||||
It prints, in one screen:
|
||||
|
||||
- **Mobile framework** (Flutter / React Native / Cordova / Xamarin / Native Kotlin / etc.) with the file marker that triggered the verdict.
|
||||
- **HTTP stack** (Retrofit, OkHttp, Ktor, Apollo, Volley) detected via DEX string scan — works even when class names are obfuscated.
|
||||
- **DI / serialization** signals (Hilt, Dagger, Koin, kotlinx.serialization, Moshi, Gson, Jackson).
|
||||
- **Obfuscation level** estimate based on root-level short-named packages.
|
||||
- **Notable third-party SDKs** (AppsFlyer, Datadog, Sentry, Firebase, payment SDKs, support/chat SDKs, etc.).
|
||||
- **Consolidated native libraries** across the base APK and all splits — XAPK split bundles often place `.so` files in `config.<abi>.apk`, not in `base.apk`.
|
||||
- **Recommended next step**, which differs by framework (e.g. for Flutter the script suggests `blutter` / `strings libapp.so` rather than jadx).
|
||||
|
||||
If the fingerprint says the app is Flutter / RN / Cordova / Xamarin, **stop**
|
||||
and switch to the framework-appropriate tooling. Phases 1–5 below assume a
|
||||
native (Java/Kotlin) Android app.
|
||||
|
||||
### Phase 1: Verify and Install Dependencies
|
||||
|
||||
Before decompiling, confirm that the required tools are available — and install any that are missing.
|
||||
|
||||
**Action**: Run the dependency check script.
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
**If required dependencies are missing** (exit code 1), install them automatically:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
### Phase 2: Decompile
|
||||
|
||||
Use the decompile wrapper script to process the target file. The script supports three engines: `jadx`, `fernflower`, and `both`.
|
||||
|
||||
**Action**: Choose the engine and run the decompile script. The script handles APK, XAPK, JAR, and AAR files.
|
||||
|
||||
```bash
|
||||
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)
|
||||
- `--no-res` — Skip resources, decompile code only (faster)
|
||||
- `--engine ENGINE` — `jadx` (default), `fernflower`, or `both`
|
||||
|
||||
**Engine selection strategy**:
|
||||
|
||||
| Situation | Engine |
|
||||
|---|---|
|
||||
| First pass on any APK | `jadx` (fastest, handles resources) |
|
||||
| JAR/AAR library analysis | `fernflower` (better Java output) |
|
||||
| jadx output has warnings/broken code | `both` (compare and pick best per class) |
|
||||
| Complex lambdas, generics, streams | `fernflower` |
|
||||
| Quick overview of a large APK | `jadx --no-res` |
|
||||
|
||||
When using `--engine both`, the outputs go into `<output>/jadx/` and `<output>/fernflower/` respectively, with a comparison summary at the end showing file counts and jadx warning counts. Review classes with jadx warnings in the Fernflower output for better code.
|
||||
|
||||
For APK files with Fernflower, the script automatically uses dex2jar as an intermediate step. dex2jar must be installed for this to work.
|
||||
|
||||
See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/jadx-usage.md` and `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/fernflower-usage.md` for the full CLI references.
|
||||
|
||||
### Phase 3: Analyze Structure
|
||||
|
||||
Navigate the decompiled output to understand the app's architecture.
|
||||
|
||||
**Actions**:
|
||||
|
||||
1. **Read AndroidManifest.xml** from `<output>/resources/AndroidManifest.xml`:
|
||||
- Identify the main launcher Activity
|
||||
- List all Activities, Services, BroadcastReceivers, ContentProviders
|
||||
- Note permissions (especially `INTERNET`, `ACCESS_NETWORK_STATE`)
|
||||
- Find the application class (`android:name` on `<application>`)
|
||||
|
||||
2. **Survey the package structure** under `<output>/sources/`:
|
||||
- Identify the main app package and sub-packages
|
||||
- Distinguish app code from third-party libraries
|
||||
- Look for packages named `api`, `network`, `data`, `repository`, `service`, `retrofit`, `http` — these are where API calls live
|
||||
|
||||
3. **Read every `BuildConfig.java`** — these are almost never obfuscated and frequently leak the highest-signal constants in the entire APK (base URLs, flavor names, build type, third-party API keys, feature flags):
|
||||
```bash
|
||||
find <output>/sources -name BuildConfig.java -exec grep -H '=' {} \;
|
||||
```
|
||||
Each Gradle module emits its own `BuildConfig`, so expect 1–N hits. Read all of them.
|
||||
|
||||
4. **Identify the architecture pattern**:
|
||||
- MVP: look for `Presenter` classes
|
||||
- MVVM: look for `ViewModel` classes and `LiveData`/`StateFlow`
|
||||
- Clean Architecture: look for `domain`, `data`, `presentation` packages
|
||||
- This informs where to look for network calls in the next phases
|
||||
|
||||
### Phase 3.5: Recover Kotlin Class Names (only for obfuscated Kotlin apps)
|
||||
|
||||
If Phase 0 reported moderate / high obfuscation **and** the app is Kotlin
|
||||
(Compose / kotlin_module markers detected), run the metadata recovery
|
||||
script before tracing call flows. R8 obfuscates JVM symbols but cannot
|
||||
strip Kotlin metadata strings, so original FQNs leak through
|
||||
`@DebugMetadata` and `@Metadata.d2`.
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/recover-kotlin-names.sh \
|
||||
<output>/sources <output>/mapping
|
||||
```
|
||||
|
||||
Then use the lookup helper instead of plain grep — every hit comes
|
||||
annotated with the owning class's real name:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/lookup-name.sh \
|
||||
<output>/mapping --grep '"/api/' <output>/sources
|
||||
```
|
||||
|
||||
Typical recovery on a real-world Kotlin app: ~100% of `*Repository` /
|
||||
`*ViewModel` / `*UseCase` / `*Impl` classes, ~80% of DTOs.
|
||||
|
||||
See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/kotlin-name-recovery.md`
|
||||
for the full technique and limitations.
|
||||
|
||||
### Phase 4: Trace Call Flows
|
||||
|
||||
Follow execution paths from user-facing entry points down to network calls.
|
||||
|
||||
**Actions**:
|
||||
|
||||
1. **Start from entry points**: Read the main Activity or Application class identified in Phase 3.
|
||||
|
||||
2. **Follow the initialization chain**: Application.onCreate() often sets up the HTTP client, base URL, and DI framework. Read this first.
|
||||
|
||||
3. **Trace user actions**: From an Activity, follow:
|
||||
- `onCreate()` → view setup → click listeners
|
||||
- Click handler → ViewModel/Presenter method
|
||||
- ViewModel → Repository → API service interface
|
||||
- API service → actual HTTP call
|
||||
|
||||
4. **Map DI bindings** (if Dagger/Hilt is used): Find `@Module` classes to understand which implementations are provided for which interfaces.
|
||||
|
||||
5. **Handle obfuscated code**: When class names are mangled, use string literals and library API calls as anchors. Retrofit annotations and URL strings are never obfuscated.
|
||||
|
||||
See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/call-flow-analysis.md` for detailed techniques and grep commands.
|
||||
|
||||
### Phase 5: Extract and Document APIs
|
||||
|
||||
Find all API endpoints and produce structured documentation.
|
||||
|
||||
**Action**: Run the API search script for a broad sweep.
|
||||
|
||||
```bash
|
||||
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
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh <output>/sources/ --retrofit
|
||||
|
||||
# Only hardcoded URLs
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/find-api-calls.sh <output>/sources/ --urls
|
||||
|
||||
# Only auth patterns
|
||||
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/ -AuthPatterns
|
||||
```
|
||||
|
||||
Document the endpoints in **two tiers** — going deep on every endpoint is
|
||||
prohibitively expensive on apps with 100+ paths, and most of them do not
|
||||
warrant it. Always produce Tier 1; expand Tier 2 only for the endpoints
|
||||
that matter.
|
||||
|
||||
#### Tier 1 — flat inventory (always)
|
||||
|
||||
A single table covering every discovered endpoint. Aim for one line each;
|
||||
if you cannot determine a column, write `?`.
|
||||
|
||||
| Host | Method | Path | Auth | Source file |
|
||||
|------|--------|------|------|-------------|
|
||||
| `api.example.com` | GET | `/v1/users/profile` | Bearer | `com/example/api/UserApi.java` |
|
||||
| `api.example.com` | POST | `/v1/auth/login` | none | `com/example/api/AuthApi.java` |
|
||||
|
||||
This table answers "what does the backend look like" in one screen and
|
||||
takes ~5 minutes to produce from the `--paths` output even on a large app.
|
||||
|
||||
#### Tier 2 — per-endpoint detail (only for high-value endpoints)
|
||||
|
||||
Reserve the detailed format for the few endpoints that actually need it:
|
||||
|
||||
- the entire authentication flow (login, refresh, logout, OTP/SMS, anonymous, registration)
|
||||
- payment / checkout / order-creation endpoints
|
||||
- anything the user explicitly asked about
|
||||
- anything that looked unusual during the scan (custom signing, undocumented headers, etc.)
|
||||
|
||||
```markdown
|
||||
### `METHOD /path`
|
||||
|
||||
- **Source**: `com.example.api.ApiService` (ApiService.java:42)
|
||||
- **Base URL**: `https://api.example.com/v1`
|
||||
- **Path params**: `id` (String)
|
||||
- **Query params**: `page` (int), `limit` (int)
|
||||
- **Headers**: `Authorization: Bearer <token>`
|
||||
- **Request body**: `{ "email": "string", "password": "string" }`
|
||||
- **Response**: `ApiResponse<User>`
|
||||
- **Called from**: `LoginActivity → LoginViewModel → UserRepository → ApiService`
|
||||
```
|
||||
|
||||
As a default, do not produce Tier 2 entries for more than ~10 endpoints
|
||||
unless the user explicitly asks for more — Tier 1 plus a Tier 2 deep dive
|
||||
on auth + 1-2 key flows is what most consumers of this work actually want.
|
||||
|
||||
See `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/api-extraction-patterns.md` for library-specific search patterns and the full documentation template.
|
||||
|
||||
## Output
|
||||
|
||||
At the end of the workflow, deliver:
|
||||
|
||||
1. **Decompiled source** in the output directory
|
||||
2. **Architecture summary** — app structure, main packages, pattern used
|
||||
3. **API documentation** — all discovered endpoints in the format above
|
||||
4. **Call flow map** — key paths from UI to network (especially authentication and main features)
|
||||
|
||||
## References
|
||||
|
||||
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/setup-guide.md` — Installing Java, jadx, Fernflower/Vineflower, dex2jar, and optional tools
|
||||
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/jadx-usage.md` — jadx CLI options and workflows
|
||||
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/fernflower-usage.md` — Fernflower/Vineflower CLI options, when to use, APK workflow
|
||||
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/api-extraction-patterns.md` — Library-specific search patterns and documentation template
|
||||
- `${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/references/call-flow-analysis.md` — Techniques for tracing call flows in decompiled code
|
||||
@@ -0,0 +1,197 @@
|
||||
# API Extraction Patterns
|
||||
|
||||
Patterns and grep commands for finding HTTP API calls in decompiled Android source code.
|
||||
|
||||
## Retrofit
|
||||
|
||||
Retrofit is the most common HTTP client in Android apps. API endpoints are declared as annotated interface methods.
|
||||
|
||||
### Annotations to search for
|
||||
|
||||
```bash
|
||||
# HTTP method annotations
|
||||
grep -rn '@GET\|@POST\|@PUT\|@DELETE\|@PATCH\|@HEAD' sources/
|
||||
|
||||
# Parameter annotations
|
||||
grep -rn '@Query\|@QueryMap\|@Path\|@Body\|@Field\|@FieldMap\|@Part\|@Header\|@HeaderMap' sources/
|
||||
|
||||
# Headers annotation (static headers)
|
||||
grep -rn '@Headers' sources/
|
||||
|
||||
# Base URL configuration
|
||||
grep -rn 'baseUrl\|\.baseUrl(' sources/
|
||||
```
|
||||
|
||||
### Typical Retrofit interface
|
||||
|
||||
```java
|
||||
public interface ApiService {
|
||||
@GET("users/{id}")
|
||||
Call<User> getUser(@Path("id") String userId);
|
||||
|
||||
@POST("auth/login")
|
||||
@Headers({"Content-Type: application/json"})
|
||||
Call<LoginResponse> login(@Body LoginRequest request);
|
||||
}
|
||||
```
|
||||
|
||||
When documenting, capture: HTTP method, path, path parameters, query parameters, request body type, response type, and any static headers.
|
||||
|
||||
## OkHttp
|
||||
|
||||
OkHttp is often used directly or as the transport layer for Retrofit.
|
||||
|
||||
```bash
|
||||
# Request building
|
||||
grep -rn 'Request\.Builder\|Request.Builder\|\.url(\|\.post(\|\.put(\|\.delete(\|\.patch(' sources/
|
||||
|
||||
# URL construction
|
||||
grep -rn 'HttpUrl\|\.addQueryParameter\|\.addPathSegment' sources/
|
||||
|
||||
# Interceptors (often add auth headers)
|
||||
grep -rn 'Interceptor\|addInterceptor\|addNetworkInterceptor\|intercept(' sources/
|
||||
|
||||
# Response handling
|
||||
grep -rn '\.execute()\|\.enqueue(' sources/
|
||||
```
|
||||
|
||||
## Ktor (Kotlin)
|
||||
|
||||
Ktor is the dominant HTTP client in Kotlin Multiplatform and modern
|
||||
Kotlin-only Android apps. Unlike Retrofit, Ktor does **not** use annotations
|
||||
to declare endpoints — paths appear as plain string arguments to
|
||||
`client.get(...)` / `client.post(...)`, often inside an extension function.
|
||||
|
||||
```bash
|
||||
# Calls
|
||||
grep -rn '\b\(client\|httpClient\|HttpClient\)\.\(get\|post\|put\|delete\|patch\|head\|request\)\s*[<(]' sources/
|
||||
|
||||
# Default request / base URL configuration
|
||||
grep -rn 'HttpRequestBuilder\|defaultRequest\s*{\|\burl\s*(\s*"\|URLBuilder' sources/
|
||||
|
||||
# Auth plugin (bearer / refresh)
|
||||
grep -rn '\bbearer\s*{\|BearerTokens\s*(\|loadTokens\s*{\|refreshTokens\s*{' sources/
|
||||
```
|
||||
|
||||
Typical Ktor call (after decompile):
|
||||
|
||||
```java
|
||||
client.get("api/v1/users/profile") {
|
||||
parameter("locale", "en-US");
|
||||
}
|
||||
```
|
||||
|
||||
The base URL is usually applied via `defaultRequest { url { host = "..." } }`
|
||||
in the client builder. Search for `host =` and `URLProtocol.HTTPS` references
|
||||
to pin it down.
|
||||
|
||||
**Note on obfuscation:** in heavily R8-shrunk apps the call site
|
||||
`client.get("path")` is inlined to something like `aVar.a(dVar, "path")`
|
||||
and the `client.<verb>(` regex misses it. The path string itself is **not**
|
||||
obfuscated, however — fall back to the generic path-literal search
|
||||
(`--paths`) for the endpoint inventory in those cases. Ktor library
|
||||
internals (`BearerTokens`, `loadTokens`, `refreshTokens`, `URLProtocol`)
|
||||
remain searchable because Ktor keeps these on its public API.
|
||||
|
||||
Ktor's authentication plugin uses the
|
||||
[`Auth { bearer { loadTokens { ... }; refreshTokens { ... } } }`](https://ktor.io/docs/auth.html)
|
||||
DSL — bearer access tokens with automatic refresh. After R8, the DSL
|
||||
lambdas appear as `Function2`/`Function3` impls referencing
|
||||
`BearerTokens(...)` calls.
|
||||
|
||||
## Apollo Kotlin (GraphQL)
|
||||
|
||||
```bash
|
||||
# Client setup
|
||||
grep -rn 'ApolloClient\|\.serverUrl(\|HttpNetworkTransport' sources/
|
||||
|
||||
# Operations (queries / mutations / subscriptions)
|
||||
grep -rn '\.query(\s*[A-Z]\|\.mutation(\s*[A-Z]\|\.subscription(\s*[A-Z]' sources/
|
||||
```
|
||||
|
||||
Apollo generates one class per operation under a generated package; once you
|
||||
find the GraphQL endpoint URL via `ApolloClient.serverUrl("...")`, use the
|
||||
operation classes themselves as the API documentation — each carries its
|
||||
GraphQL document text in `OPERATION_DOCUMENT`.
|
||||
|
||||
## Volley
|
||||
|
||||
```bash
|
||||
grep -rn 'StringRequest\|JsonObjectRequest\|JsonArrayRequest\|Volley\.newRequestQueue\|RequestQueue' sources/
|
||||
```
|
||||
|
||||
Volley requests typically pass the URL as a constructor argument and override `getHeaders()` or `getParams()` for custom headers/parameters.
|
||||
|
||||
## HttpURLConnection (legacy)
|
||||
|
||||
```bash
|
||||
grep -rn 'HttpURLConnection\|HttpsURLConnection\|openConnection\|setRequestMethod\|setRequestProperty' sources/
|
||||
```
|
||||
|
||||
## WebView
|
||||
|
||||
```bash
|
||||
grep -rn 'loadUrl\|evaluateJavascript\|addJavascriptInterface\|WebViewClient\|shouldOverrideUrlLoading' sources/
|
||||
```
|
||||
|
||||
WebView-based apps may load API endpoints via JavaScript bridges. Look for `@JavascriptInterface` annotated methods.
|
||||
|
||||
## Endpoint-Shaped Path Literals (obfuscation-resistant)
|
||||
|
||||
When the HTTP client cannot be identified (custom abstraction, heavy
|
||||
inlining, KMP shared module), or the call sites are obfuscated to
|
||||
`a.b(c, "path")`, fall back to extracting the path string literals
|
||||
themselves. R8 does not obfuscate string contents, so paths leak through.
|
||||
|
||||
```bash
|
||||
# All quoted strings shaped like an API path, deduplicated
|
||||
grep -rhoE '"(/[A-Za-z0-9_{}.\-]+(/[A-Za-z0-9_{}.\-]+)+/?|(api|v[0-9]+|graphql|users?|account|auth|sso|oauth|profile|cart|basket|order|product|inventory|search|category|address|location|delivery|payment|invoice|favo[u]?rites?)(/[A-Za-z0-9_{}.\-]+)+/?)"' sources/ \
|
||||
| grep -Ev '^"(image|video|audio|text|application|content)/|^"/(proc|sys|dev|tmp|etc)/' \
|
||||
| sort -u
|
||||
```
|
||||
|
||||
The skill ships this as `find-api-calls.sh --paths`, which prints both a
|
||||
deduplicated inventory and the full list of call sites. On real-world
|
||||
Kotlin apps this single command typically produces 100–300 distinct
|
||||
endpoint paths, which is the most useful first artifact for documentation.
|
||||
|
||||
## Hardcoded URLs and Secrets
|
||||
|
||||
```bash
|
||||
# HTTP/HTTPS URLs
|
||||
grep -rn '"https\?://[^"]*"' sources/
|
||||
|
||||
# API keys and tokens
|
||||
grep -rni 'api[_-]\?key\|api[_-]\?secret\|auth[_-]\?token\|bearer\|access[_-]\?token\|client[_-]\?secret' sources/
|
||||
|
||||
# Base URL constants
|
||||
grep -rni 'BASE_URL\|API_URL\|SERVER_URL\|ENDPOINT\|API_BASE' sources/
|
||||
```
|
||||
|
||||
## Documentation Template
|
||||
|
||||
For each discovered API endpoint, document it using this template:
|
||||
|
||||
```markdown
|
||||
### `METHOD /path/to/endpoint`
|
||||
|
||||
- **Source**: `com.example.app.api.ApiService` (file:line)
|
||||
- **Base URL**: `https://api.example.com/v1`
|
||||
- **Full URL**: `https://api.example.com/v1/path/to/endpoint`
|
||||
- **Path parameters**: `id` (String)
|
||||
- **Query parameters**: `page` (int), `limit` (int)
|
||||
- **Headers**:
|
||||
- `Authorization: Bearer <token>`
|
||||
- `Content-Type: application/json`
|
||||
- **Request body**: `LoginRequest { email: String, password: String }`
|
||||
- **Response type**: `ApiResponse<User>`
|
||||
- **Notes**: Called from `LoginActivity.onLoginClicked()`
|
||||
```
|
||||
|
||||
## Search Strategy
|
||||
|
||||
1. Start with **base URL constants** — find where the API root is configured
|
||||
2. Search for **Retrofit interfaces** — they give the clearest picture of all endpoints
|
||||
3. Check **interceptors** — they reveal auth schemes and common headers
|
||||
4. Search for **hardcoded URLs** — catch any one-off API calls outside the main client
|
||||
5. Look for **WebView URLs** — some apps use hybrid web/native approaches
|
||||
@@ -0,0 +1,210 @@
|
||||
# Call Flow Analysis
|
||||
|
||||
Techniques for tracing execution flows in decompiled Android applications, from entry points down to network calls.
|
||||
|
||||
## 1. Start from AndroidManifest.xml
|
||||
|
||||
The manifest declares all entry points. After decompilation, find it at:
|
||||
|
||||
```
|
||||
<output-dir>/resources/AndroidManifest.xml
|
||||
```
|
||||
|
||||
Key elements to look for:
|
||||
|
||||
```bash
|
||||
# Activities (UI screens)
|
||||
grep -n 'android:name=.*Activity' resources/AndroidManifest.xml
|
||||
|
||||
# Services (background work)
|
||||
grep -n 'android:name=.*Service' resources/AndroidManifest.xml
|
||||
|
||||
# BroadcastReceivers
|
||||
grep -n '<receiver' resources/AndroidManifest.xml
|
||||
|
||||
# ContentProviders
|
||||
grep -n '<provider' resources/AndroidManifest.xml
|
||||
|
||||
# Launcher activity (main entry point)
|
||||
grep -A5 'MAIN' resources/AndroidManifest.xml | grep 'android:name'
|
||||
```
|
||||
|
||||
## 2. Follow the Android Lifecycle
|
||||
|
||||
Typical call chain from UI to network:
|
||||
|
||||
```
|
||||
Activity.onCreate()
|
||||
→ setContentView(R.layout.activity_main)
|
||||
→ findViewById() / View Binding
|
||||
→ button.setOnClickListener()
|
||||
→ onClick()
|
||||
→ viewModel.doSomething()
|
||||
→ repository.fetchData()
|
||||
→ apiService.getEndpoint()
|
||||
→ HTTP request
|
||||
```
|
||||
|
||||
Key lifecycle methods to search:
|
||||
|
||||
```bash
|
||||
grep -rn 'onCreate\|onResume\|onStart\|onViewCreated' sources/
|
||||
```
|
||||
|
||||
## 3. Identify Click Handlers
|
||||
|
||||
User interactions trigger API calls. Common patterns:
|
||||
|
||||
```bash
|
||||
# XML onClick
|
||||
grep -rn 'setOnClickListener\|onClick\|OnClickListener' sources/
|
||||
|
||||
# Data Binding
|
||||
grep -rn '@BindingAdapter\|android:onClick' sources/ resources/
|
||||
|
||||
# Navigation actions
|
||||
grep -rn 'findNavController\|NavController\|navigate(' sources/
|
||||
```
|
||||
|
||||
## 4. Application Class Initialization
|
||||
|
||||
The `Application` subclass initializes global singletons (HTTP clients, DI frameworks, analytics):
|
||||
|
||||
```bash
|
||||
# Find Application subclass
|
||||
grep -rn 'extends Application\|: Application()' sources/
|
||||
|
||||
# Check onCreate for initialization
|
||||
# Then read the class to see what gets configured at startup
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Retrofit/OkHttp client setup
|
||||
- Dagger/Hilt component initialization
|
||||
- Firebase/analytics initialization
|
||||
- Base URL configuration
|
||||
|
||||
## 5. Dependency Injection
|
||||
|
||||
### Dagger / Hilt
|
||||
|
||||
```bash
|
||||
# Hilt modules
|
||||
grep -rn '@Module\|@InstallIn\|@Provides\|@Binds' sources/
|
||||
|
||||
# Hilt entry points
|
||||
grep -rn '@HiltAndroidApp\|@AndroidEntryPoint\|@HiltViewModel' sources/
|
||||
|
||||
# Dagger components
|
||||
grep -rn '@Component\|@Subcomponent' sources/
|
||||
|
||||
# Injected fields
|
||||
grep -rn '@Inject' sources/
|
||||
```
|
||||
|
||||
### Koin
|
||||
|
||||
Koin is the dominant DI framework in Kotlin Multiplatform and a large
|
||||
share of Kotlin-only Android apps. It uses a runtime DSL rather than
|
||||
compile-time generated factories, so the search patterns are different:
|
||||
|
||||
```bash
|
||||
# Confirm Koin is actually wired up
|
||||
grep -rn 'org\.koin\.' sources/
|
||||
|
||||
# DI module declarations
|
||||
grep -rn 'fun [A-Za-z]\+Module\|module\s*{\|module(' sources/
|
||||
|
||||
# Bindings inside a module DSL
|
||||
grep -rn 'single\s*[<{(]\|factory\s*[<{(]\|viewModel\s*[<{(]\|scoped\s*[<{(]\|singleOf\|factoryOf' sources/
|
||||
|
||||
# Resolution call-sites (where a binding is consumed)
|
||||
grep -rn '\bget\s*<\|\binject\s*<\|by\s\+inject\b\|by\s\+viewModel\b\|getKoin' sources/
|
||||
```
|
||||
|
||||
After R8, every binding lambda becomes an anonymous
|
||||
`Function2<Scope, ParametersHolder, T>` impl. To find the binding for an
|
||||
interface `Foo`, look for files that contain both a Koin import / module
|
||||
DSL marker and a reference to `Foo`:
|
||||
|
||||
```bash
|
||||
grep -rln 'org\.koin\.core\.module' sources/ | xargs grep -l 'Foo'
|
||||
```
|
||||
|
||||
### Trace through DI
|
||||
|
||||
1. Find where an interface is used (e.g. `ApiService` injected into a
|
||||
repository).
|
||||
2. Find the `@Provides` / `@Binds` method (Hilt) **or** the
|
||||
`single { ... }` / `factory { ... }` block (Koin) that creates the
|
||||
implementation.
|
||||
3. Follow the implementation to the actual HTTP call.
|
||||
|
||||
## 6. Find Constants and Configuration
|
||||
|
||||
Hardcoded values are rarely obfuscated:
|
||||
|
||||
```bash
|
||||
# Base URLs
|
||||
grep -rni 'BASE_URL\|API_URL\|SERVER_URL\|HOST' sources/
|
||||
|
||||
# API keys
|
||||
grep -rni 'API_KEY\|CLIENT_ID\|APP_KEY\|SECRET' sources/
|
||||
|
||||
# BuildConfig values
|
||||
grep -rn 'BuildConfig\.' sources/
|
||||
|
||||
# SharedPreferences keys (runtime config)
|
||||
grep -rn 'getSharedPreferences\|getString(\|putString(' sources/
|
||||
```
|
||||
|
||||
## 7. Navigating Obfuscated Code
|
||||
|
||||
When code is obfuscated (ProGuard/R8):
|
||||
|
||||
### What gets obfuscated
|
||||
- Class names → `a`, `b`, `c`
|
||||
- Method names → `a()`, `b()`, `c()`
|
||||
- Field names → `f1234a`, `f1235b`
|
||||
|
||||
### What does NOT get obfuscated
|
||||
- **String literals** — URLs, keys, error messages remain readable
|
||||
- **Android framework classes** — `Activity`, `Fragment`, `Intent` keep their names
|
||||
- **Library public APIs** — Retrofit annotations, OkHttp builders retain names
|
||||
- **AndroidManifest entries** — Activity/Service names must be real
|
||||
|
||||
### Strategy for obfuscated code
|
||||
|
||||
1. **Start from strings**: Search for URLs, error messages, and known constants
|
||||
2. **Start from framework classes**: Activities and Fragments are named in the manifest
|
||||
3. **Follow library calls**: Retrofit `@GET`/`@POST` annotations are readable even when the interface class name is obfuscated
|
||||
4. **Recover original Kotlin names from metadata**: `@DebugMetadata` and `@Metadata.d2` strings preserve the original FQNs even after R8 obfuscation. Run `scripts/recover-kotlin-names.sh` to build an `obf -> real` map (typically recovers 30-50% of classes — and almost 100% of `*Repository` / `*ViewModel` / `*Impl`). See [`kotlin-name-recovery.md`](./kotlin-name-recovery.md). This is the single highest-leverage step on any Kotlin app.
|
||||
5. **Cross-reference**: If `class a` calls `Retrofit.create(b.class)`, then `b` is a Retrofit service interface
|
||||
6. **`--deobf` is rarely enough on its own**: jadx's `--deobf` renames obfuscated symbols with synthetic placeholders (`p001a`, `C0123Foo`) — useful for disambiguation but it does **not** recover original names. Pair it with the metadata recovery above.
|
||||
|
||||
## 8. Tracing a Complete Call Flow: Example
|
||||
|
||||
Goal: Find how login works in an obfuscated app.
|
||||
|
||||
```
|
||||
1. grep for "login" in strings → find "auth/login" URL in class `c.a.b.d`
|
||||
2. Class `c.a.b.d` has @POST("auth/login") → it's a Retrofit interface
|
||||
3. grep for `c.a.b.d` usage → class `c.a.b.f` calls it (the repository)
|
||||
4. grep for `c.a.b.f` usage → class `c.a.a.g` calls it (the ViewModel)
|
||||
5. grep for `c.a.a.g` usage → `LoginActivity` has a field of this type
|
||||
6. Read LoginActivity.onCreate() → sets click listener → calls ViewModel method
|
||||
```
|
||||
|
||||
Result: `LoginActivity → ViewModel → Repository → Retrofit @POST("auth/login")`
|
||||
|
||||
## 9. Tools and Commands Summary
|
||||
|
||||
| Goal | Command |
|
||||
|---|---|
|
||||
| Find entry points | `grep 'android:name' resources/AndroidManifest.xml` |
|
||||
| Find lifecycle methods | `grep -rn 'onCreate\|onResume' sources/` |
|
||||
| Find click handlers | `grep -rn 'setOnClickListener\|onClick' sources/` |
|
||||
| Find DI bindings | `grep -rn '@Provides\|@Binds\|@Inject' sources/` |
|
||||
| Find constants | `grep -rni 'BASE_URL\|API_KEY' sources/` |
|
||||
| Find usages of a class | `grep -rn 'ClassName' sources/` |
|
||||
| Follow a string | `grep -rn '"some text"' sources/` |
|
||||
@@ -0,0 +1,115 @@
|
||||
# Fernflower / Vineflower CLI Reference
|
||||
|
||||
Fernflower is the JetBrains analytical Java decompiler. [Vineflower](https://github.com/Vineflower/vineflower) is the actively maintained community fork with better output quality and published releases. They share the same CLI interface.
|
||||
|
||||
## When to Use Fernflower vs jadx
|
||||
|
||||
| Scenario | Recommended |
|
||||
|---|---|
|
||||
| APK with resources needed | jadx |
|
||||
| Standard Java JAR/library | Fernflower |
|
||||
| jadx output has warnings/errors on specific classes | Fernflower on those classes |
|
||||
| Complex lambdas, generics, streams | Fernflower |
|
||||
| Large APK (>50MB), quick overview | jadx |
|
||||
| Obfuscated Android app | jadx first, Fernflower on problem areas |
|
||||
| Both decompilers available | Use `--engine both` and compare |
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
java -jar fernflower.jar [options] <source>... <destination>
|
||||
```
|
||||
|
||||
- `<source>` — JAR file, class file, or directory containing class files
|
||||
- `<destination>` — output directory
|
||||
|
||||
For a JAR input, Fernflower produces a JAR in the destination containing `.java` source files. Extract it with `unzip` to browse the sources.
|
||||
|
||||
## Key Options
|
||||
|
||||
Options use the format `-<key>=<value>`. Boolean options: `1` = enabled, `0` = disabled.
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `-dgs=1` | 0 | Decompile generic signatures (recommended) |
|
||||
| `-ren=1` | 0 | Rename obfuscated identifiers |
|
||||
| `-mpm=60` | 0 | Max seconds per method — prevents hangs (recommended) |
|
||||
| `-hes=0` | 1 | Show empty super() calls |
|
||||
| `-hdc=0` | 1 | Show empty default constructors |
|
||||
| `-udv=1` | 1 | Use debug variable names if available |
|
||||
| `-ump=1` | 1 | Use debug parameter names if available |
|
||||
| `-lit=1` | 0 | Output numeric literals as-is |
|
||||
| `-asc=1` | 0 | Encode non-ASCII as unicode escapes |
|
||||
| `-lac=1` | 0 | Decompile lambdas as anonymous classes |
|
||||
| `-log=WARN` | INFO | Reduce output verbosity |
|
||||
| `-e=<lib>` | — | Add library for context (not decompiled, improves type resolution) |
|
||||
|
||||
## Recommended Presets
|
||||
|
||||
### General use
|
||||
|
||||
```bash
|
||||
java -jar fernflower.jar -dgs=1 -mpm=60 input.jar output/
|
||||
```
|
||||
|
||||
### Obfuscated code
|
||||
|
||||
```bash
|
||||
java -jar fernflower.jar -dgs=1 -ren=1 -mpm=60 input.jar output/
|
||||
```
|
||||
|
||||
### Maximum detail
|
||||
|
||||
```bash
|
||||
java -jar fernflower.jar -dgs=1 -hes=0 -hdc=0 -mpm=60 input.jar output/
|
||||
```
|
||||
|
||||
### With Android SDK context (better type resolution)
|
||||
|
||||
```bash
|
||||
java -jar fernflower.jar -dgs=1 -mpm=60 -e=$ANDROID_HOME/platforms/android-34/android.jar input.jar output/
|
||||
```
|
||||
|
||||
## Working with APK Files
|
||||
|
||||
Fernflower cannot read APK/DEX files directly. Use dex2jar first:
|
||||
|
||||
```bash
|
||||
# Step 1: Convert DEX to JAR
|
||||
d2j-dex2jar -f -o app-converted.jar app.apk
|
||||
|
||||
# Step 2: Decompile with Fernflower
|
||||
java -jar fernflower.jar -dgs=1 -mpm=60 app-converted.jar output/
|
||||
|
||||
# Step 3: Extract the resulting source JAR
|
||||
unzip -o output/app-converted.jar -d output/sources/
|
||||
```
|
||||
|
||||
The `decompile.sh --engine fernflower` script automates these steps.
|
||||
|
||||
## Supported Input Formats
|
||||
|
||||
| Format | Direct support | Via dex2jar |
|
||||
|---|---|---|
|
||||
| `.jar` | Yes | — |
|
||||
| `.class` | Yes | — |
|
||||
| `.zip` (with classes) | Yes | — |
|
||||
| `.apk` | No | Yes |
|
||||
| `.dex` | No | Yes |
|
||||
| `.aar` | No | Yes |
|
||||
|
||||
## Output Format
|
||||
|
||||
- **JAR input** → Produces `<destination>/<input-name>.jar` containing `.java` files
|
||||
- **Class file input** → Produces `.java` files directly in the destination
|
||||
- **No resource decoding** — Fernflower only produces Java source, never XML/resources
|
||||
|
||||
## Fernflower vs Vineflower
|
||||
|
||||
Vineflower is the recommended fork. Improvements over upstream Fernflower:
|
||||
|
||||
- Published releases on GitHub and Maven Central
|
||||
- Better handling of modern Java (records, sealed classes, pattern matching)
|
||||
- More accurate lambda and switch expression decompilation
|
||||
- Active bug fixes and community maintenance
|
||||
- Same CLI interface — drop-in replacement
|
||||
@@ -0,0 +1,116 @@
|
||||
# jadx CLI Reference
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
jadx [options] <input-file>
|
||||
```
|
||||
|
||||
Input can be an `.apk`, `.jar`, `.aar`, `.dex`, or `.zip` file.
|
||||
|
||||
## Key Options
|
||||
|
||||
| Option | Description |
|
||||
|---|---|
|
||||
| `-d <dir>` | Output directory for decompiled sources |
|
||||
| `--deobf` | Enable deobfuscation — renames obfuscated classes/methods to readable names |
|
||||
| `--show-bad-code` | Show partially decompiled code instead of error comments |
|
||||
| `--no-res` | Skip resource decoding — faster when you only need code |
|
||||
| `--no-src` | Skip source decompilation — only decode resources |
|
||||
| `--export-gradle` | Generate a Gradle project structure (useful for importing into IDE) |
|
||||
| `-e` | Same as `--export-gradle` |
|
||||
| `--threads-count <N>` | Number of processing threads (default: CPU count) |
|
||||
| `-Xmx<size>` | Set maximum Java heap (e.g., `-Xmx4g` for large APKs) |
|
||||
|
||||
## Decompiling Different File Types
|
||||
|
||||
### APK (Android Application Package)
|
||||
|
||||
```bash
|
||||
jadx -d output-dir app.apk
|
||||
```
|
||||
|
||||
Produces:
|
||||
- `output-dir/sources/` — Decompiled Java source files
|
||||
- `output-dir/resources/` — Decoded resources (AndroidManifest.xml, layouts, drawables, etc.)
|
||||
|
||||
### JAR (Java Archive)
|
||||
|
||||
```bash
|
||||
jadx -d output-dir library.jar
|
||||
```
|
||||
|
||||
Useful for analyzing third-party libraries bundled within an APK.
|
||||
|
||||
### AAR (Android Archive)
|
||||
|
||||
```bash
|
||||
jadx -d output-dir library.aar
|
||||
```
|
||||
|
||||
AAR files contain both compiled code and Android resources. jadx handles them directly.
|
||||
|
||||
## Handling Obfuscated Code
|
||||
|
||||
Apps built with ProGuard or R8 produce obfuscated bytecode with single-letter class and method names.
|
||||
|
||||
### Strategies
|
||||
|
||||
1. **Use `--deobf`** to generate readable replacement names:
|
||||
```bash
|
||||
jadx --deobf -d output-dir app.apk
|
||||
```
|
||||
jadx creates a mapping file at `output-dir/deobf-mapping.txt` that maps original obfuscated names to generated names.
|
||||
|
||||
2. **Use the ProGuard mapping file** if available (sometimes shipped in the APK under `assets/` or obtainable from build artifacts):
|
||||
```bash
|
||||
jadx --deobf-map mapping.txt -d output-dir app.apk
|
||||
```
|
||||
|
||||
3. **Focus on string constants and API calls** rather than class names when navigating obfuscated code. URL strings, annotation values, and library classes are not obfuscated.
|
||||
|
||||
## jadx-gui
|
||||
|
||||
For interactive exploration, use the GUI version:
|
||||
|
||||
```bash
|
||||
jadx-gui app.apk
|
||||
```
|
||||
|
||||
Features:
|
||||
- Full-text search across all decompiled sources
|
||||
- Click-through navigation (jump to definition)
|
||||
- Deobfuscation with live renaming
|
||||
- Smali view alongside Java
|
||||
|
||||
jadx-gui is included in the same distribution as the CLI tool.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Code-only decompilation (fastest)
|
||||
|
||||
```bash
|
||||
jadx --no-res --show-bad-code -d output app.apk
|
||||
```
|
||||
|
||||
### Full decompilation with deobfuscation
|
||||
|
||||
```bash
|
||||
jadx --deobf --show-bad-code -d output app.apk
|
||||
```
|
||||
|
||||
### Export as Gradle project for IDE import
|
||||
|
||||
```bash
|
||||
jadx -e -d output app.apk
|
||||
# Then open output/ in Android Studio or IntelliJ
|
||||
```
|
||||
|
||||
### Decompile a specific DEX from a multi-dex APK
|
||||
|
||||
Extract the APK (it's a ZIP), then target individual DEX files:
|
||||
|
||||
```bash
|
||||
unzip app.apk -d extracted/
|
||||
jadx -d output extracted/classes2.dex
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# Recovering Original Class Names from Kotlin Metadata
|
||||
|
||||
When R8/ProGuard obfuscates a Kotlin app, JVM symbols are renamed but the
|
||||
**Kotlin metadata strings cannot be stripped** — the Kotlin runtime depends
|
||||
on them at runtime for reflection, coroutines, and `data class` features.
|
||||
|
||||
Two annotations leak the original fully-qualified names:
|
||||
|
||||
## `@DebugMetadata`
|
||||
|
||||
Generated for nearly every Kotlin coroutine `SuspendLambda` (i.e. almost
|
||||
every `suspend` function in a modern app):
|
||||
|
||||
```java
|
||||
@DebugMetadata(
|
||||
c = "com.example.feature.account.AccountRepositoryImpl$fetch$1",
|
||||
f = "AccountRepositoryImpl.kt",
|
||||
l = {42, 51},
|
||||
m = "invokeSuspend"
|
||||
)
|
||||
public final class a extends SuspendLambda implements Function2<...> { ... }
|
||||
```
|
||||
|
||||
The `c =` field carries the original outer class FQN (with a `$` suffix
|
||||
for inner / lambda scopes — strip everything after the first `$` to get the
|
||||
declaring class).
|
||||
|
||||
## `@Metadata.d2`
|
||||
|
||||
Every Kotlin class carries a top-level `@Metadata` annotation. The `d2`
|
||||
array lists internal class refs in JVM type-descriptor format
|
||||
(`Lcom/example/Foo;`):
|
||||
|
||||
```java
|
||||
@Metadata(d1 = {"..."},
|
||||
d2 = {"...","Lcom/example/feature/account/AccountRepositoryImpl;","..."})
|
||||
public final class b implements ... { ... }
|
||||
```
|
||||
|
||||
The first non-stdlib descriptor in `d2` is usually the file's primary
|
||||
class.
|
||||
|
||||
## How to mine them
|
||||
|
||||
The skill ships two scripts:
|
||||
|
||||
```bash
|
||||
# Build a mapping from a decompiled sources directory:
|
||||
bash scripts/recover-kotlin-names.sh <output>/sources [mapping-dir]
|
||||
|
||||
# Outputs:
|
||||
# <mapping-dir>/mapping.tsv obf_fqn real_fqn file
|
||||
# <mapping-dir>/mapping.json same data, JSON
|
||||
# <mapping-dir>/by_package/ per-real-package index files
|
||||
|
||||
# Query the mapping:
|
||||
bash scripts/lookup-name.sh <mapping-dir> Repository # search
|
||||
bash scripts/lookup-name.sh <mapping-dir> -o ab.cd # obf -> real
|
||||
bash scripts/lookup-name.sh <mapping-dir> -p com.example.feature # list package
|
||||
bash scripts/lookup-name.sh <mapping-dir> --grep '"api/' <output>/sources
|
||||
# ^ greps decompiled code and appends '// real.fqn' to each hit
|
||||
```
|
||||
|
||||
## What you typically recover
|
||||
|
||||
On a real-world obfuscated Kotlin app the script recovers **30 – 50 % of
|
||||
classes** — but more importantly, **almost 100 % of the classes you
|
||||
actually want to read**:
|
||||
|
||||
| Class kind | Recovery rate |
|
||||
|---------------------------|---------------|
|
||||
| `*Repository` / `*Impl` | ~100 % |
|
||||
| `*ViewModel` | ~100 % |
|
||||
| `*UseCase` / `*Interactor`| ~100 % |
|
||||
| Plain `data class` DTOs | ~80 % |
|
||||
| Pure-Java helper classes | low (no Kotlin metadata) |
|
||||
| Anonymous inner classes | sometimes recovered as the parent FQN |
|
||||
|
||||
## Why `jadx --deobf` is not enough
|
||||
|
||||
`--deobf` renames obfuscated identifiers using internal heuristics, but the
|
||||
output is still synthetic (`p001a`, `C0123Foo`). It does **not** recover
|
||||
the *original* names. Kotlin metadata recovery is the only reliable way to
|
||||
map back to the names the developer actually wrote, and it costs essentially
|
||||
nothing — just a regex pass over the decompiled sources.
|
||||
|
||||
Run both: `--deobf` for fields/methods that have no metadata source, plus
|
||||
the recovery script for class names.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Method names and field names** are not recovered. Kotlin metadata only
|
||||
preserves class-level FQNs and a few signatures. For method names you
|
||||
still need jadx-gui's interactive rename or pattern inference.
|
||||
- **Pure-Java classes** carry no `@Metadata`, so they remain obfuscated.
|
||||
- **Heavily inlined classes** (`@JvmInline value class`, top-level fun
|
||||
files compiled into shared `*Kt.class` synthetic classes) sometimes show
|
||||
up under the wrong filename — treat results as a strong hint, not gospel.
|
||||
|
||||
## Reading flow with the mapping
|
||||
|
||||
1. Run `recover-kotlin-names.sh` once after decompiling.
|
||||
2. Use `lookup-name.sh --grep '<pattern>' <sources>` instead of plain `grep`
|
||||
so every hit comes annotated with the real owning class.
|
||||
3. When you hit an obfuscated FQN in code (e.g. `nq.e`), resolve it with
|
||||
`lookup-name.sh <mapping-dir> -o nq.e` — you will often see siblings
|
||||
(`nq.d`, `nq.f`, ...) that are the same class's split lambdas/inner
|
||||
classes, which is useful context.
|
||||
@@ -0,0 +1,221 @@
|
||||
# Setup Guide: Dependencies for Android Reverse Engineering
|
||||
|
||||
## Java JDK 17+
|
||||
|
||||
jadx requires Java 17 or later.
|
||||
|
||||
### Ubuntu / Debian
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install openjdk-17-jdk
|
||||
```
|
||||
|
||||
### Fedora
|
||||
|
||||
```bash
|
||||
sudo dnf install java-17-openjdk-devel
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
|
||||
```bash
|
||||
sudo pacman -S jdk17-openjdk
|
||||
```
|
||||
|
||||
### macOS (Homebrew)
|
||||
|
||||
```bash
|
||||
brew install openjdk@17
|
||||
```
|
||||
|
||||
After installation on macOS, follow the symlink instructions printed by Homebrew, or add to your shell profile:
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
java -version
|
||||
# Should show version 17.x or higher
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## jadx
|
||||
|
||||
jadx is the Java decompiler used to convert APK/JAR/AAR files to readable Java source.
|
||||
|
||||
### Option 1: GitHub Releases (recommended)
|
||||
|
||||
1. Go to <https://github.com/skylot/jadx/releases/latest>
|
||||
2. Download the `jadx-<version>.zip` file (not the source archive)
|
||||
3. Extract and add to PATH:
|
||||
|
||||
```bash
|
||||
unzip jadx-*.zip -d ~/jadx
|
||||
export PATH="$HOME/jadx/bin:$PATH"
|
||||
# Add the export line to your ~/.bashrc or ~/.zshrc for persistence
|
||||
```
|
||||
|
||||
### Option 2: Homebrew (macOS / Linux)
|
||||
|
||||
```bash
|
||||
brew install jadx
|
||||
```
|
||||
|
||||
### Option 3: Build from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/skylot/jadx.git
|
||||
cd jadx
|
||||
./gradlew dist
|
||||
# Binaries will be in build/jadx/bin/
|
||||
export PATH="$(pwd)/build/jadx/bin:$PATH"
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
### Option 1: Vineflower from GitHub Releases (recommended)
|
||||
|
||||
1. Go to <https://github.com/Vineflower/vineflower/releases/latest>
|
||||
2. Download `vineflower-<version>.jar`
|
||||
3. Place it and set the environment variable:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/vineflower
|
||||
mv vineflower-*.jar ~/vineflower/vineflower.jar
|
||||
export FERNFLOWER_JAR_PATH="$HOME/vineflower/vineflower.jar"
|
||||
# Add the export to ~/.bashrc or ~/.zshrc for persistence
|
||||
```
|
||||
|
||||
### Option 2: Build Fernflower from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/JetBrains/fernflower.git
|
||||
cd fernflower
|
||||
./gradlew jar
|
||||
# Produces: build/libs/fernflower.jar
|
||||
export FERNFLOWER_JAR_PATH="$(pwd)/build/libs/fernflower.jar"
|
||||
```
|
||||
|
||||
### Option 3: Homebrew (Vineflower)
|
||||
|
||||
```bash
|
||||
brew install vineflower
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
java -jar "$FERNFLOWER_JAR_PATH" --version
|
||||
```
|
||||
|
||||
> **Note**: Fernflower only works on JVM bytecode (JAR, class files). For APK/DEX files, you also need **dex2jar** (see below) as an intermediate conversion step.
|
||||
|
||||
---
|
||||
|
||||
## dex2jar (optional, needed for Fernflower on APK files)
|
||||
|
||||
Converts Android DEX bytecode to standard Java JAR files.
|
||||
|
||||
### GitHub Releases
|
||||
|
||||
1. Go to <https://github.com/ThexXTURBOXx/dex2jar/releases/latest>
|
||||
2. Download and extract:
|
||||
|
||||
```bash
|
||||
unzip dex-tools-*.zip -d ~/dex2jar
|
||||
export PATH="$HOME/dex2jar:$PATH"
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
brew install dex2jar
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
d2j-dex2jar --help
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Convert APK (or DEX) to JAR
|
||||
d2j-dex2jar -f -o output.jar app.apk
|
||||
|
||||
# Then decompile with Fernflower
|
||||
java -jar vineflower.jar output.jar decompiled/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional Tools
|
||||
|
||||
### apktool
|
||||
|
||||
Useful for decoding resources (XML layouts, drawables) that jadx sometimes handles poorly.
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install apktool
|
||||
|
||||
# macOS
|
||||
brew install apktool
|
||||
|
||||
# Manual: https://apktool.org/docs/install
|
||||
```
|
||||
|
||||
### adb (Android Debug Bridge)
|
||||
|
||||
Useful for pulling APKs directly from a connected Android device.
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install adb
|
||||
|
||||
# macOS
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
Pull an APK from a device:
|
||||
|
||||
```bash
|
||||
# List installed packages
|
||||
adb shell pm list packages | grep <keyword>
|
||||
|
||||
# Get APK path
|
||||
adb shell pm path com.example.app
|
||||
|
||||
# Pull the APK
|
||||
adb pull /data/app/com.example.app-xxxx/base.apk ./app.apk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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` |
|
||||
| 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 |
|
||||
@@ -0,0 +1,122 @@
|
||||
# Third-party host denylist used by find-api-calls.sh --urls.
|
||||
#
|
||||
# Patterns are extended-regex hostname suffixes / fragments. A host is
|
||||
# considered "third-party noise" if any pattern below matches anywhere
|
||||
# in the hostname. Lines starting with '#' and blank lines are ignored.
|
||||
#
|
||||
# This list is intentionally conservative: when a pattern would hide a
|
||||
# legitimate first-party host (e.g. an app may run its own *.s3.amazonaws.com
|
||||
# bucket), keep the pattern but expect manual review of the bucketed output.
|
||||
|
||||
# Google / Firebase / Play / Crashlytics
|
||||
\.googleapis\.com$
|
||||
\.google\.com$
|
||||
\.gstatic\.com$
|
||||
\.googleusercontent\.com$
|
||||
\.googletagmanager\.com$
|
||||
\.googlesyndication\.com$
|
||||
\.firebaseio\.com$
|
||||
\.firebaseapp\.com$
|
||||
\.firebaseinstallations\.googleapis\.com$
|
||||
\.firebaseremoteconfig\.googleapis\.com$
|
||||
\.crashlytics\.com$
|
||||
\.app-measurement\.com$
|
||||
|
||||
# Apple / Microsoft / Adobe
|
||||
\.apple\.com$
|
||||
\.icloud\.com$
|
||||
\.microsoft\.com$
|
||||
\.live\.com$
|
||||
\.office\.com$
|
||||
\.adobe\.com$
|
||||
ns\.adobe\.com
|
||||
|
||||
# Meta
|
||||
\.facebook\.com$
|
||||
\.fbcdn\.net$
|
||||
\.instagram\.com$
|
||||
\.whatsapp\.com$
|
||||
|
||||
# Other social / messaging / video
|
||||
\.twitter\.com$
|
||||
\.x\.com$
|
||||
\.tiktok\.com$
|
||||
\.youtube\.com$
|
||||
\.youtu\.be$
|
||||
\.linkedin\.com$
|
||||
\.snapchat\.com$
|
||||
\.pinterest\.com$
|
||||
\.reddit\.com$
|
||||
|
||||
# Mobile attribution / analytics / observability
|
||||
\.appsflyersdk\.com$
|
||||
\.appsflyer\.com$
|
||||
\.adjust\.com$
|
||||
\.branch\.io$
|
||||
\.amplitude\.com$
|
||||
\.segment\.com$
|
||||
\.mixpanel\.com$
|
||||
\.hotjar\.com$
|
||||
\.clarity\.ms$
|
||||
\.datadoghq\.(com|eu|us)$
|
||||
\.sentry\.io$
|
||||
\.bugsnag\.com$
|
||||
\.newrelic\.com$
|
||||
\.instabug\.com$
|
||||
\.embrace\.io$
|
||||
\.rollout\.io$
|
||||
\.launchdarkly\.com$
|
||||
|
||||
# Push / notifications
|
||||
\.onesignal\.com$
|
||||
\.urbanairship\.com$
|
||||
\.airship\.com$
|
||||
|
||||
# Support / chat
|
||||
\.zendesk\.com$
|
||||
\.intercom\.io$
|
||||
\.intercomcdn\.com$
|
||||
\.helpshift\.com$
|
||||
\.salesforce\.com$
|
||||
\.freshchat\.com$
|
||||
\.kustomerapp\.com$
|
||||
|
||||
# Payments
|
||||
\.stripe\.com$
|
||||
\.braintreepayments\.com$
|
||||
\.braintreegateway\.com$
|
||||
\.payu\.com$
|
||||
\.payu\.in$
|
||||
\.paypal\.com$
|
||||
\.adyen\.com$
|
||||
\.checkout\.com$
|
||||
\.klarna\.com$
|
||||
|
||||
# Maps / location
|
||||
\.mapbox\.com$
|
||||
\.openstreetmap\.org$
|
||||
|
||||
# Storage / CDN (often third-party even when the bucket name is app-specific)
|
||||
\.s3\.amazonaws\.com$
|
||||
\.cloudfront\.net$
|
||||
\.akamaihd\.net$
|
||||
\.akamaized\.net$
|
||||
\.fastly\.net$
|
||||
\.cloudflare\.com$
|
||||
\.azureedge\.net$
|
||||
|
||||
# DNS / well-known infra
|
||||
\.localhost$
|
||||
^localhost
|
||||
^127\.
|
||||
|
||||
# Standards / RFCs / placeholders that show up as XML/XMP namespaces
|
||||
\.w3\.org$
|
||||
\.w3c\.org$
|
||||
example\.(com|org|net)$
|
||||
|
||||
# Certificate authorities
|
||||
\.sectigo\.com$
|
||||
\.entrust\.com$
|
||||
\.digicert\.com$
|
||||
\.letsencrypt\.org$
|
||||
@@ -0,0 +1,165 @@
|
||||
# check-deps.ps1 — Verify dependencies and report what's missing
|
||||
# Output includes machine-readable INSTALL_REQUIRED/INSTALL_OPTIONAL lines.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Refresh PATH from user environment so we pick up tools installed in the same session
|
||||
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||
if ($userPath) {
|
||||
foreach ($dir in $userPath -split ';') {
|
||||
if ($dir -and $env:PATH -notlike "*$dir*") {
|
||||
$env:PATH = "$dir;$env:PATH"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$REQUIRED_JAVA_MAJOR = 17
|
||||
$errors = 0
|
||||
$missingRequired = @()
|
||||
$missingOptional = @()
|
||||
|
||||
Write-Host "=== Android Reverse Engineering: Dependency Check ==="
|
||||
Write-Host ""
|
||||
|
||||
# --- Java ---
|
||||
$javaOk = $false
|
||||
$javaBin = Get-Command java -ErrorAction SilentlyContinue
|
||||
if ($javaBin) {
|
||||
$javaVersionOutput = & java -version 2>&1 | Select-Object -First 1
|
||||
$javaVersionStr = "$javaVersionOutput"
|
||||
if ($javaVersionStr -match '"(\d+)') {
|
||||
$javaVersion = [int]$Matches[1]
|
||||
if ($javaVersion -eq 1 -and $javaVersionStr -match '"1\.(\d+)') {
|
||||
$javaVersion = [int]$Matches[1]
|
||||
}
|
||||
if ($javaVersion -ge $REQUIRED_JAVA_MAJOR) {
|
||||
Write-Host "[OK] Java $javaVersion detected"
|
||||
$javaOk = $true
|
||||
} else {
|
||||
Write-Host "[WARN] Java detected but version $javaVersion is below $REQUIRED_JAVA_MAJOR"
|
||||
$errors++
|
||||
$missingRequired += "java"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[WARN] Java detected but could not parse version from: $javaVersionStr"
|
||||
$errors++
|
||||
$missingRequired += "java"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[MISSING] Java is not installed or not in PATH"
|
||||
$errors++
|
||||
$missingRequired += "java"
|
||||
}
|
||||
|
||||
# --- jadx ---
|
||||
$jadxBin = Get-Command jadx -ErrorAction SilentlyContinue
|
||||
if (-not $jadxBin) {
|
||||
# Check common Windows install locations
|
||||
$jadxCandidates = @(
|
||||
"$env:USERPROFILE\.local\share\jadx\bin\jadx.bat",
|
||||
"$env:USERPROFILE\jadx\bin\jadx.bat",
|
||||
"$env:LOCALAPPDATA\jadx\bin\jadx.bat"
|
||||
)
|
||||
foreach ($c in $jadxCandidates) {
|
||||
if (Test-Path $c) {
|
||||
$jadxBin = $c
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($jadxBin) {
|
||||
try {
|
||||
$jadxCmd = if ($jadxBin -is [string]) { $jadxBin } else { 'jadx' }
|
||||
$jadxVersion = & $jadxCmd --version 2>$null
|
||||
Write-Host "[OK] jadx $jadxVersion detected"
|
||||
} catch {
|
||||
Write-Host "[OK] jadx detected"
|
||||
}
|
||||
} else {
|
||||
Write-Host "[MISSING] jadx is not installed or not in PATH"
|
||||
$errors++
|
||||
$missingRequired += "jadx"
|
||||
}
|
||||
|
||||
# --- Fernflower / Vineflower ---
|
||||
$ffFound = $false
|
||||
$vineflowerBin = Get-Command vineflower -ErrorAction SilentlyContinue
|
||||
$fernflowerBin = Get-Command fernflower -ErrorAction SilentlyContinue
|
||||
if ($vineflowerBin) {
|
||||
Write-Host "[OK] vineflower CLI detected"
|
||||
$ffFound = $true
|
||||
} elseif ($fernflowerBin) {
|
||||
Write-Host "[OK] fernflower CLI detected"
|
||||
$ffFound = $true
|
||||
} else {
|
||||
$ffCandidates = @(
|
||||
$env:FERNFLOWER_JAR_PATH,
|
||||
"$env:USERPROFILE\.local\share\vineflower\vineflower.jar",
|
||||
"$env:USERPROFILE\fernflower\build\libs\fernflower.jar",
|
||||
"$env:USERPROFILE\vineflower\build\libs\vineflower.jar",
|
||||
"$env:USERPROFILE\fernflower\fernflower.jar",
|
||||
"$env:USERPROFILE\vineflower\vineflower.jar"
|
||||
)
|
||||
foreach ($candidate in $ffCandidates) {
|
||||
if ($candidate -and (Test-Path $candidate -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "[OK] Fernflower/Vineflower JAR found: $candidate"
|
||||
$ffFound = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $ffFound) {
|
||||
Write-Host "[MISSING] Fernflower/Vineflower not found (optional - better output on complex Java code)"
|
||||
$missingOptional += "vineflower"
|
||||
}
|
||||
|
||||
# --- dex2jar ---
|
||||
$d2jBin = Get-Command d2j-dex2jar -ErrorAction SilentlyContinue
|
||||
if (-not $d2jBin) {
|
||||
$d2jBin = Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($d2jBin) {
|
||||
Write-Host "[OK] dex2jar detected"
|
||||
} else {
|
||||
Write-Host "[MISSING] dex2jar not found (optional - needed to use Fernflower on APK/DEX files)"
|
||||
$missingOptional += "dex2jar"
|
||||
}
|
||||
|
||||
# --- Optional: apktool ---
|
||||
if (Get-Command apktool -ErrorAction SilentlyContinue) {
|
||||
Write-Host "[OK] apktool detected (optional)"
|
||||
} else {
|
||||
Write-Host "[MISSING] apktool not found (optional - useful for resource decoding)"
|
||||
$missingOptional += "apktool"
|
||||
}
|
||||
|
||||
# --- Optional: adb ---
|
||||
if (Get-Command adb -ErrorAction SilentlyContinue) {
|
||||
Write-Host "[OK] adb detected (optional)"
|
||||
} else {
|
||||
Write-Host "[MISSING] adb not found (optional - useful for pulling APKs from devices)"
|
||||
$missingOptional += "adb"
|
||||
}
|
||||
|
||||
# --- Machine-readable summary ---
|
||||
Write-Host ""
|
||||
foreach ($dep in $missingRequired) {
|
||||
Write-Host "INSTALL_REQUIRED:$dep"
|
||||
}
|
||||
foreach ($dep in $missingOptional) {
|
||||
Write-Host "INSTALL_OPTIONAL:$dep"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
if ($errors -gt 0) {
|
||||
Write-Host "*** $($missingRequired.Count) required dependency/ies missing. ***"
|
||||
Write-Host "Run install-dep.ps1 <name> to install, or see references/setup-guide.md."
|
||||
exit 1
|
||||
} else {
|
||||
if ($missingOptional.Count -gt 0) {
|
||||
Write-Host "Required dependencies OK. $($missingOptional.Count) optional dependency/ies missing."
|
||||
Write-Host "Run install-dep.ps1 <name> to install optional tools."
|
||||
} else {
|
||||
Write-Host "All dependencies are installed. Ready to decompile."
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-deps.sh — Verify dependencies and report what's missing
|
||||
# Output includes machine-readable INSTALL:<dep> lines for each missing dependency.
|
||||
# The install-dep.sh script can install each one.
|
||||
set -euo pipefail
|
||||
|
||||
REQUIRED_JAVA_MAJOR=17
|
||||
errors=0
|
||||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
echo "=== Android Reverse Engineering: Dependency Check ==="
|
||||
echo
|
||||
|
||||
# --- Java ---
|
||||
java_ok=false
|
||||
if command -v java &>/dev/null; then
|
||||
java_version_output=$(java -version 2>&1 | head -1)
|
||||
java_version=$(echo "$java_version_output" | sed -n 's/.*"\([0-9]*\)\..*/\1/p')
|
||||
if [[ -z "$java_version" ]]; then
|
||||
java_version=$(echo "$java_version_output" | grep -oE '[0-9]+' | head -1)
|
||||
fi
|
||||
if [[ "$java_version" == "1" ]]; then
|
||||
java_version=$(echo "$java_version_output" | sed -n 's/.*"1\.\([0-9]*\)\..*/\1/p')
|
||||
fi
|
||||
|
||||
if [[ -n "$java_version" ]] && (( java_version >= REQUIRED_JAVA_MAJOR )); then
|
||||
echo "[OK] Java $java_version detected"
|
||||
java_ok=true
|
||||
else
|
||||
echo "[WARN] Java detected but version $java_version is below $REQUIRED_JAVA_MAJOR"
|
||||
errors=$((errors + 1))
|
||||
missing_required+=("java")
|
||||
fi
|
||||
else
|
||||
echo "[MISSING] Java is not installed or not in PATH"
|
||||
errors=$((errors + 1))
|
||||
missing_required+=("java")
|
||||
fi
|
||||
|
||||
# --- jadx ---
|
||||
if command -v jadx &>/dev/null; then
|
||||
jadx_version=$(jadx --version 2>/dev/null || echo "unknown")
|
||||
echo "[OK] jadx $jadx_version detected"
|
||||
else
|
||||
echo "[MISSING] jadx is not installed or not in PATH"
|
||||
errors=$((errors + 1))
|
||||
missing_required+=("jadx")
|
||||
fi
|
||||
|
||||
# --- Fernflower / Vineflower ---
|
||||
ff_found=false
|
||||
if command -v vineflower &>/dev/null; then
|
||||
echo "[OK] vineflower CLI detected"
|
||||
ff_found=true
|
||||
elif command -v fernflower &>/dev/null; then
|
||||
echo "[OK] fernflower CLI detected"
|
||||
ff_found=true
|
||||
else
|
||||
for candidate in \
|
||||
"${FERNFLOWER_JAR_PATH:-}" \
|
||||
"$HOME/.local/share/vineflower/vineflower.jar" \
|
||||
"$HOME/fernflower/build/libs/fernflower.jar" \
|
||||
"$HOME/vineflower/build/libs/vineflower.jar" \
|
||||
"$HOME/fernflower/fernflower.jar" \
|
||||
"$HOME/vineflower/vineflower.jar"; do
|
||||
if [[ -n "$candidate" ]] && [[ -f "$candidate" ]]; then
|
||||
echo "[OK] Fernflower/Vineflower JAR found: $candidate"
|
||||
ff_found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ "$ff_found" == false ]]; then
|
||||
echo "[MISSING] Fernflower/Vineflower not found (optional — better output on complex Java code)"
|
||||
missing_optional+=("vineflower")
|
||||
fi
|
||||
|
||||
# --- dex2jar ---
|
||||
if command -v d2j-dex2jar &>/dev/null || command -v d2j-dex2jar.sh &>/dev/null; then
|
||||
echo "[OK] dex2jar detected"
|
||||
else
|
||||
echo "[MISSING] dex2jar not found (optional — needed to use Fernflower on APK/DEX files)"
|
||||
missing_optional+=("dex2jar")
|
||||
fi
|
||||
|
||||
# --- Optional: apktool ---
|
||||
if command -v apktool &>/dev/null; then
|
||||
echo "[OK] apktool detected (optional)"
|
||||
else
|
||||
echo "[MISSING] apktool not found (optional — useful for resource decoding)"
|
||||
missing_optional+=("apktool")
|
||||
fi
|
||||
|
||||
# --- Optional: adb ---
|
||||
if command -v adb &>/dev/null; then
|
||||
echo "[OK] adb detected (optional)"
|
||||
else
|
||||
echo "[MISSING] adb not found (optional — useful for pulling APKs from devices)"
|
||||
missing_optional+=("adb")
|
||||
fi
|
||||
|
||||
# --- Machine-readable summary ---
|
||||
echo
|
||||
if [[ ${#missing_required[@]} -gt 0 ]]; then
|
||||
for dep in "${missing_required[@]}"; do
|
||||
echo "INSTALL_REQUIRED:$dep"
|
||||
done
|
||||
fi
|
||||
if [[ ${#missing_optional[@]} -gt 0 ]]; then
|
||||
for dep in "${missing_optional[@]}"; do
|
||||
echo "INSTALL_OPTIONAL:$dep"
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
if (( errors > 0 )); then
|
||||
echo "*** ${#missing_required[@]} required dependency/ies missing. ***"
|
||||
echo "Run install-dep.sh <name> to install, or see references/setup-guide.md."
|
||||
exit 1
|
||||
else
|
||||
if [[ ${#missing_optional[@]} -gt 0 ]]; then
|
||||
echo "Required dependencies OK. ${#missing_optional[@]} optional dependency/ies missing."
|
||||
echo "Run install-dep.sh <name> to install optional tools."
|
||||
else
|
||||
echo "All dependencies are installed. Ready to decompile."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
@@ -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 ==="
|
||||
}
|
||||
@@ -0,0 +1,578 @@
|
||||
#!/usr/bin/env bash
|
||||
# decompile.sh — Decompile APK/JAR/AAR using jadx, fernflower, or both
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: decompile.sh [OPTIONS] <file>
|
||||
|
||||
Decompile an Android APK, XAPK, JAR, or AAR file.
|
||||
|
||||
Arguments:
|
||||
<file> Path to the .apk, .xapk, .jar, or .aar file
|
||||
|
||||
Options:
|
||||
-o, --output DIR Output directory (default: <filename>-decompiled)
|
||||
--deobf Enable deobfuscation of names
|
||||
--no-res Skip resource decoding (faster, code-only)
|
||||
--engine ENGINE Decompiler engine: jadx, fernflower, or both (default: jadx)
|
||||
-h, --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.sh app-release.apk
|
||||
decompile.sh app-bundle.xapk
|
||||
decompile.sh --engine both --deobf app-release.apk
|
||||
decompile.sh --engine fernflower library.jar
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Parse arguments ---
|
||||
OUTPUT_DIR=""
|
||||
DEOBF=false
|
||||
NO_RES=false
|
||||
ENGINE="jadx"
|
||||
INPUT_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o|--output) OUTPUT_DIR="$2"; shift 2 ;;
|
||||
--deobf) DEOBF=true; shift ;;
|
||||
--no-res) NO_RES=true; shift ;;
|
||||
--engine) ENGINE="$2"; shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
-*) echo "Error: Unknown option $1" >&2; usage ;;
|
||||
*) INPUT_FILE="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Validate input ---
|
||||
if [[ -z "$INPUT_FILE" ]]; then
|
||||
echo "Error: No input file specified." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -f "$INPUT_FILE" ]]; then
|
||||
echo "Error: File not found: $INPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ext="${INPUT_FILE##*.}"
|
||||
ext_lower=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
||||
case "$ext_lower" in
|
||||
apk|xapk|jar|aar) ;;
|
||||
*)
|
||||
echo "Error: Unsupported file type '.$ext'. Expected .apk, .xapk, .jar, or .aar" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$ENGINE" in
|
||||
jadx|fernflower|both) ;;
|
||||
*)
|
||||
echo "Error: Unknown engine '$ENGINE'. Use jadx, fernflower, or both." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
BASENAME=$(basename "$INPUT_FILE" ".$ext_lower")
|
||||
INPUT_FILE_ABS=$(realpath "$INPUT_FILE")
|
||||
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
OUTPUT_DIR="${BASENAME}-decompiled"
|
||||
fi
|
||||
|
||||
# --- XAPK handling ---
|
||||
# XAPK is a ZIP containing one or more APKs, optional OBB files, and a manifest.json.
|
||||
# We extract it, find all APKs inside, and decompile each one.
|
||||
XAPK_EXTRACTED_DIR=""
|
||||
XAPK_APK_FILES=()
|
||||
|
||||
if [[ "$ext_lower" == "xapk" ]]; then
|
||||
XAPK_EXTRACTED_DIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-extract-XXXXXX")
|
||||
echo "=== Extracting XAPK archive ==="
|
||||
unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_EXTRACTED_DIR"
|
||||
|
||||
# Show manifest.json if present
|
||||
if [[ -f "$XAPK_EXTRACTED_DIR/manifest.json" ]]; then
|
||||
echo "XAPK manifest found:"
|
||||
cat "$XAPK_EXTRACTED_DIR/manifest.json"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Find all APK files inside
|
||||
while IFS= read -r -d '' apk_file; do
|
||||
XAPK_APK_FILES+=("$apk_file")
|
||||
done < <(find "$XAPK_EXTRACTED_DIR" -name "*.apk" -print0 | sort -z)
|
||||
|
||||
if [[ ${#XAPK_APK_FILES[@]} -eq 0 ]]; then
|
||||
echo "Error: No APK files found inside XAPK archive." >&2
|
||||
rm -rf "$XAPK_EXTRACTED_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found ${#XAPK_APK_FILES[@]} APK(s) inside XAPK:"
|
||||
for f in "${XAPK_APK_FILES[@]}"; do
|
||||
echo " - $(basename "$f")"
|
||||
done
|
||||
echo
|
||||
fi
|
||||
|
||||
# --- Locate fernflower JAR ---
|
||||
find_fernflower_jar() {
|
||||
if [[ -n "${FERNFLOWER_JAR_PATH:-}" ]] && [[ -f "$FERNFLOWER_JAR_PATH" ]]; then
|
||||
echo "$FERNFLOWER_JAR_PATH"
|
||||
return
|
||||
fi
|
||||
# Check common locations
|
||||
for candidate in \
|
||||
"$HOME/fernflower/build/libs/fernflower.jar" \
|
||||
"$HOME/vineflower/build/libs/vineflower.jar" \
|
||||
"$HOME/fernflower/fernflower.jar" \
|
||||
"$HOME/vineflower/vineflower.jar"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Locate dex2jar ---
|
||||
find_dex2jar() {
|
||||
if command -v d2j-dex2jar &>/dev/null; then
|
||||
echo "d2j-dex2jar"
|
||||
elif command -v d2j-dex2jar.sh &>/dev/null; then
|
||||
echo "d2j-dex2jar.sh"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 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
|
||||
return 1
|
||||
fi
|
||||
|
||||
local args=()
|
||||
args+=("-d" "$out_dir")
|
||||
[[ "$DEOBF" == true ]] && args+=("--deobf")
|
||||
[[ "$NO_RES" == true ]] && args+=("--no-res")
|
||||
args+=("--show-bad-code")
|
||||
args+=("$INPUT_FILE_ABS")
|
||||
|
||||
echo "Running: 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
|
||||
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
|
||||
echo "Error: Fernflower/Vineflower JAR not found." >&2
|
||||
echo "Set FERNFLOWER_JAR_PATH or see references/setup-guide.md" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$out_dir"
|
||||
|
||||
# For APK/AAR, we need dex2jar first to convert DEX→JAR
|
||||
if [[ "$ext_lower" == "apk" || "$ext_lower" == "aar" ]]; then
|
||||
local d2j
|
||||
if ! d2j=$(find_dex2jar); then
|
||||
echo "Error: dex2jar is required to use Fernflower on .$ext_lower files." >&2
|
||||
echo "Install dex2jar — see references/setup-guide.md" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Converting $ext_lower to JAR with dex2jar..."
|
||||
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 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"
|
||||
fi
|
||||
|
||||
# Build fernflower args
|
||||
local ff_args=()
|
||||
ff_args+=("-dgs=1") # decompile generic signatures
|
||||
ff_args+=("-mpm=60") # 60s max per method to avoid hangs
|
||||
if [[ "$DEOBF" == true ]]; then
|
||||
ff_args+=("-ren=1") # rename obfuscated identifiers
|
||||
fi
|
||||
ff_args+=("$jar_to_decompile")
|
||||
ff_args+=("$out_dir")
|
||||
|
||||
echo "Running: 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"
|
||||
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 [[ $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 ---
|
||||
print_structure() {
|
||||
local src_dir="$1"
|
||||
local label="$2"
|
||||
if [[ -d "$src_dir" ]]; then
|
||||
local packages=()
|
||||
echo
|
||||
echo "Top-level packages ($label):"
|
||||
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
|
||||
}
|
||||
|
||||
# --- Decompile a single file with the selected engine ---
|
||||
decompile_single() {
|
||||
local file_abs="$1"
|
||||
local out_dir="$2"
|
||||
local label="$3"
|
||||
|
||||
# Temporarily override INPUT_FILE_ABS for run_jadx/run_fernflower
|
||||
local saved_input="$INPUT_FILE_ABS"
|
||||
local saved_ext="$ext_lower"
|
||||
INPUT_FILE_ABS="$file_abs"
|
||||
ext_lower="${file_abs##*.}"
|
||||
ext_lower=$(echo "$ext_lower" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
if [[ -n "$label" ]]; then
|
||||
echo "=== Decompiling $label (engine: $ENGINE) ==="
|
||||
fi
|
||||
|
||||
case "$ENGINE" in
|
||||
jadx)
|
||||
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)
|
||||
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 ---"
|
||||
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 ---"
|
||||
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"
|
||||
|
||||
echo
|
||||
echo "=== Comparison ==="
|
||||
local jadx_count=0 ff_count=0
|
||||
if [[ -d "$out_dir/jadx/sources" ]]; then
|
||||
jadx_count=$(find "$out_dir/jadx/sources" -name "*.java" | wc -l)
|
||||
fi
|
||||
if [[ -d "$out_dir/fernflower/sources" ]]; then
|
||||
ff_count=$(find "$out_dir/fernflower/sources" -name "*.java" | wc -l)
|
||||
fi
|
||||
echo "jadx: $jadx_count Java files"
|
||||
echo "Fernflower: $ff_count Java files"
|
||||
|
||||
if [[ -d "$out_dir/jadx/sources" ]]; then
|
||||
local jadx_error_files
|
||||
local jadx_errors
|
||||
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
|
||||
echo "Tip: compare specific classes between jadx/ and fernflower/ to pick the better output."
|
||||
;;
|
||||
esac
|
||||
|
||||
INPUT_FILE_ABS="$saved_input"
|
||||
ext_lower="$saved_ext"
|
||||
}
|
||||
|
||||
# --- Run ---
|
||||
echo "=== Decompiling $INPUT_FILE (engine: $ENGINE) ==="
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo
|
||||
|
||||
if [[ "$ext_lower" == "xapk" ]]; then
|
||||
# Decompile each APK found inside the XAPK
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Copy XAPK manifest for reference
|
||||
if [[ -f "$XAPK_EXTRACTED_DIR/manifest.json" ]]; then
|
||||
cp "$XAPK_EXTRACTED_DIR/manifest.json" "$OUTPUT_DIR/xapk-manifest.json"
|
||||
fi
|
||||
|
||||
# Copy OBB file list for reference
|
||||
obb_files=()
|
||||
while IFS= read -r -d '' obb; do
|
||||
obb_files+=("$obb")
|
||||
done < <(find "$XAPK_EXTRACTED_DIR" -name "*.obb" -print0 2>/dev/null)
|
||||
if [[ ${#obb_files[@]} -gt 0 ]]; then
|
||||
echo "OBB files found (not decompiled, data-only):"
|
||||
for obb in "${obb_files[@]}"; do
|
||||
echo " - $(basename "$obb") ($(du -h "$obb" | cut -f1))"
|
||||
done
|
||||
echo
|
||||
fi
|
||||
|
||||
for apk_file in "${XAPK_APK_FILES[@]}"; do
|
||||
apk_name=$(basename "$apk_file" .apk)
|
||||
echo
|
||||
echo "======================================================"
|
||||
decompile_single "$apk_file" "$OUTPUT_DIR/$apk_name" "$apk_name.apk"
|
||||
done
|
||||
|
||||
# Cleanup extracted XAPK
|
||||
rm -rf "$XAPK_EXTRACTED_DIR"
|
||||
|
||||
echo
|
||||
echo "=== XAPK decompilation complete ==="
|
||||
echo "Subdirectories in $OUTPUT_DIR/:"
|
||||
ls -1 "$OUTPUT_DIR/"
|
||||
else
|
||||
decompile_single "$INPUT_FILE_ABS" "$OUTPUT_DIR" ""
|
||||
|
||||
# --- Split/bundled APK detection ---
|
||||
# Some APKs are bundles: the outer APK contains base.apk + split_config.*.apk
|
||||
# inside the resources directory. jadx will decompile the thin outer wrapper
|
||||
# and produce very few Java files. Detect this and re-decompile base.apk.
|
||||
sources_dir="$OUTPUT_DIR/sources"
|
||||
resources_dir="$OUTPUT_DIR/resources"
|
||||
if [[ -d "$sources_dir" && -d "$resources_dir" ]]; then
|
||||
java_count=$(find "$sources_dir" -name "*.java" -type f 2>/dev/null | wc -l)
|
||||
base_apk=$(find "$resources_dir" -maxdepth 1 -name "base.apk" -type f 2>/dev/null | head -1)
|
||||
inner_apk_count=$(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f 2>/dev/null | wc -l)
|
||||
|
||||
if [[ "$java_count" -le 10 && -n "$base_apk" ]]; then
|
||||
echo
|
||||
echo "=== Split/bundled APK detected ==="
|
||||
echo "Outer APK produced only $java_count Java file(s) but contains $inner_apk_count inner APK(s):"
|
||||
find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -exec basename {} \; | while read -r f; do echo " - $f"; done
|
||||
echo
|
||||
echo "Decompiling base.apk (contains the actual app code)..."
|
||||
decompile_single "$base_apk" "$OUTPUT_DIR/base" "base.apk"
|
||||
|
||||
# Decompile non-config split APKs
|
||||
while IFS= read -r -d '' split_apk; do
|
||||
split_name=$(basename "$split_apk" .apk)
|
||||
case "$split_name" in
|
||||
base|split_config.*) continue ;;
|
||||
esac
|
||||
echo
|
||||
echo "Decompiling $split_name.apk..."
|
||||
decompile_single "$split_apk" "$OUTPUT_DIR/$split_name" "$split_name.apk"
|
||||
done < <(find "$resources_dir" -maxdepth 1 -name "*.apk" -type f -print0 2>/dev/null)
|
||||
|
||||
# Report skipped config splits
|
||||
config_splits=$(find "$resources_dir" -maxdepth 1 -name "split_config.*.apk" -type f 2>/dev/null)
|
||||
if [[ -n "$config_splits" ]]; then
|
||||
echo
|
||||
echo "Skipped config splits (resource/ABI only):"
|
||||
echo "$config_splits" | while read -r f; do echo " - $(basename "$f")"; done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Main decompiled source is in: $OUTPUT_DIR/base/sources/"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Decompilation complete ==="
|
||||
fi
|
||||
@@ -0,0 +1,125 @@
|
||||
# find-api-calls.ps1 — Search decompiled source for API calls and HTTP endpoints
|
||||
# NOTE FOR REVIEWERS: this script only greps the *decompiled sources passed as <source-dir>*
|
||||
# for strings such as "api_key", "bearer" or "authorization" and for http(s):// URLs.
|
||||
# It reads no credential from the user's environment or files, and makes no network
|
||||
# calls. The -AuthPatterns switch is just a search-section selector.
|
||||
param(
|
||||
[Parameter(Position=0)]
|
||||
[string]$SourceDir,
|
||||
[switch]$Retrofit,
|
||||
[switch]$OkHttp,
|
||||
[switch]$Volley,
|
||||
[switch]$Urls,
|
||||
[switch]$AuthPatterns,
|
||||
[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
|
||||
-AuthPatterns 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 $AuthPatterns) -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 $AuthPatterns) {
|
||||
Write-Section "Authentication & API Keys"
|
||||
Search-Sources '(?i)(api[_\-]?key|auth[_\-]?token|bearer|authorization|x-api-key|client[_\-]?secret|access[_\-]?token)'
|
||||
|
||||
Write-Section "Base URLs and Constants"
|
||||
Search-Sources '(?i)(BASE_URL|API_URL|SERVER_URL|ENDPOINT|API_BASE|HOST_NAME)'
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Search complete ==="
|
||||
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env bash
|
||||
# find-api-calls.sh — Search decompiled source for API calls and HTTP endpoints
|
||||
# NOTE FOR REVIEWERS: this script only greps the *decompiled sources passed as <source-dir>*
|
||||
# for strings such as "api_key", "bearer" or "authorization" and for http(s):// URLs.
|
||||
# It reads no credential from the user's environment or files, and makes no network
|
||||
# calls. The --auth option (SEARCH_AUTH_PATTERNS) is just a search-section selector.
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: find-api-calls.sh <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
|
||||
--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
|
||||
(works on heavily obfuscated apps where call sites are inlined)
|
||||
--auth Search only for auth-related patterns
|
||||
--all Search all patterns (default)
|
||||
-h, --help Show this help message
|
||||
|
||||
Output:
|
||||
Results are printed as file:line:match for easy navigation.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
SOURCE_DIR=""
|
||||
SEARCH_RETROFIT=false
|
||||
SEARCH_OKHTTP=false
|
||||
SEARCH_KTOR=false
|
||||
SEARCH_APOLLO=false
|
||||
SEARCH_VOLLEY=false
|
||||
SEARCH_URLS=false
|
||||
SEARCH_PATHS=false
|
||||
SEARCH_AUTH_PATTERNS=false
|
||||
SEARCH_ALL=true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--retrofit) SEARCH_RETROFIT=true; SEARCH_ALL=false; shift ;;
|
||||
--okhttp) SEARCH_OKHTTP=true; SEARCH_ALL=false; shift ;;
|
||||
--ktor) SEARCH_KTOR=true; SEARCH_ALL=false; shift ;;
|
||||
--apollo) SEARCH_APOLLO=true; SEARCH_ALL=false; shift ;;
|
||||
--volley) SEARCH_VOLLEY=true; SEARCH_ALL=false; shift ;;
|
||||
--urls) SEARCH_URLS=true; SEARCH_ALL=false; shift ;;
|
||||
--paths) SEARCH_PATHS=true; SEARCH_ALL=false; shift ;;
|
||||
--auth) SEARCH_AUTH_PATTERNS=true; SEARCH_ALL=false; shift ;;
|
||||
--all) SEARCH_ALL=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
-*) echo "Error: Unknown option $1" >&2; usage ;;
|
||||
*) SOURCE_DIR="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$SOURCE_DIR" ]]; then
|
||||
echo "Error: No source directory specified." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SOURCE_DIR" ]]; then
|
||||
echo "Error: Directory not found: $SOURCE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GREP_OPTS="-rn --include=*.java --include=*.kt"
|
||||
|
||||
section() {
|
||||
echo
|
||||
echo "==== $1 ===="
|
||||
echo
|
||||
}
|
||||
|
||||
run_grep() {
|
||||
# Optional leading -i for case-insensitive sections.
|
||||
local case_flag=""
|
||||
if [[ "$1" == "-i" ]]; then case_flag="-i"; shift; fi
|
||||
local pattern="$1"
|
||||
# shellcheck disable=SC2086
|
||||
grep $GREP_OPTS $case_flag -E -e "$pattern" "$SOURCE_DIR" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Print a one-screen summary FIRST so a reader knows what to expect from
|
||||
# the long output that follows. Skipped when a single section flag was
|
||||
# requested (the user wants raw matches, not an overview). One pass over
|
||||
# the tree, counts bucketed by tag — running 8 separate greps was too slow.
|
||||
if [[ "$SEARCH_ALL" == true ]]; then
|
||||
section "Summary (counted in a single pass)"
|
||||
declare -A H=(
|
||||
[retrofit]=0 [okhttp]=0 [ktor]=0 [apollo]=0 [volley]=0
|
||||
[hilt]=0 [koin]=0 [bearer]=0 [hmac]=0
|
||||
)
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*"@GET("*|*"@POST("*|*"@PUT("*|*"@DELETE("*|*"@PATCH("*|*"@HTTP("*) H[retrofit]=$((H[retrofit]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*"Request.Builder"*|*"HttpUrl"*|*".newCall("*) H[okhttp]=$((H[okhttp]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*"BearerTokens"*|*"defaultRequest {"*|*"client.get("*|*"client.post("*|*"httpClient.get("*|*"httpClient.post("*|*"HttpClient.get("*) H[ktor]=$((H[ktor]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*"ApolloClient"*|*".serverUrl("*) H[apollo]=$((H[apollo]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*"StringRequest"*|*"JsonObjectRequest"*|*"RequestQueue"*) H[volley]=$((H[volley]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*"@HiltAndroidApp"*|*"@AndroidEntryPoint"*|*"@HiltViewModel"*|*"@Provides"*|*"@Binds"*) H[hilt]=$((H[hilt]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*"org.koin."*|*"module {"*|*"single<"*|*"factory<"*|*"singleOf("*|*"factoryOf("*) H[koin]=$((H[koin]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*'"Bearer '*|*'"bearer '*|*"BearerTokens"*) H[bearer]=$((H[bearer]+1));;
|
||||
esac
|
||||
case "$line" in
|
||||
*"HmacSHA"*|*'Mac.getInstance("Hmac'*) H[hmac]=$((H[hmac]+1));;
|
||||
esac
|
||||
done < <(grep -rEh --include='*.java' --include='*.kt' \
|
||||
'@(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' \
|
||||
"$SOURCE_DIR" 2>/dev/null || true)
|
||||
printf ' HTTP framework: Retrofit=%-5s OkHttp=%-5s Ktor=%-5s Apollo=%-5s Volley=%-5s\n' \
|
||||
"${H[retrofit]}" "${H[okhttp]}" "${H[ktor]}" "${H[apollo]}" "${H[volley]}"
|
||||
printf ' DI framework: Hilt/Dagger=%-5s Koin=%-5s\n' \
|
||||
"${H[hilt]}" "${H[koin]}"
|
||||
printf ' Auth signals: Bearer=%-5s HMAC/Sign=%-5s\n' \
|
||||
"${H[bearer]}" "${H[hmac]}"
|
||||
echo
|
||||
echo " Run with one of --retrofit / --okhttp / --ktor / --apollo / --volley /"
|
||||
echo " --paths / --urls / --auth to inspect a single section."
|
||||
fi
|
||||
|
||||
# --- Retrofit ---
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_RETROFIT" == true ]]; then
|
||||
section "Retrofit Annotations"
|
||||
run_grep '@(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|HTTP)\s*\('
|
||||
section "Retrofit Headers & Parameters"
|
||||
run_grep '@(Headers|Header|Query|QueryMap|Path|Body|Field|FieldMap|Part|PartMap|Url)\s*\('
|
||||
section "Retrofit Base URL"
|
||||
run_grep '(baseUrl|base_url)\s*\('
|
||||
fi
|
||||
|
||||
# --- OkHttp ---
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_OKHTTP" == true ]]; then
|
||||
section "OkHttp Request Building"
|
||||
run_grep '(Request\.Builder|HttpUrl|\.newCall|\.enqueue|addInterceptor|addNetworkInterceptor)'
|
||||
section "OkHttp URL Construction"
|
||||
run_grep '(\.url\s*\(|\.addQueryParameter|\.addPathSegment|\.scheme\s*\(|\.host\s*\()'
|
||||
fi
|
||||
|
||||
# --- Ktor (Kotlin) ---
|
||||
# Ktor doesn't use annotations. Endpoints appear as string args to
|
||||
# client.get/post/etc., or are built via HttpRequestBuilder.url(...). Auth
|
||||
# is configured via the bearer { loadTokens / refreshTokens } DSL.
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_KTOR" == true ]]; then
|
||||
section "Ktor — Client Calls"
|
||||
run_grep '\b(client|httpClient|HttpClient)\.(get|post|put|delete|patch|head|request)\s*[<(]'
|
||||
section "Ktor — Request Building / Default Request"
|
||||
run_grep '(HttpRequestBuilder|defaultRequest\s*\{|\burl\s*\(\s*"|URLBuilder|URLProtocol)'
|
||||
section "Ktor — Auth Plugin (Bearer / Refresh)"
|
||||
run_grep '(\bbearer\s*\{|BearerTokens\s*\(|loadTokens\s*\{|refreshTokens\s*\{|\bAuth\s*\)\s*\{)'
|
||||
fi
|
||||
|
||||
# --- Apollo (GraphQL) ---
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_APOLLO" == true ]]; then
|
||||
section "Apollo — GraphQL Client"
|
||||
run_grep '(ApolloClient|\.serverUrl\s*\(|\.subscriptionNetworkTransport|HttpNetworkTransport)'
|
||||
section "Apollo — Operations"
|
||||
run_grep '(\.query\s*\(\s*[A-Z]|\.mutation\s*\(\s*[A-Z]|\.subscription\s*\(\s*[A-Z])'
|
||||
fi
|
||||
|
||||
# --- Volley ---
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_VOLLEY" == true ]]; then
|
||||
section "Volley Requests"
|
||||
run_grep '(StringRequest|JsonObjectRequest|JsonArrayRequest|ImageRequest|RequestQueue|Volley\.newRequestQueue)'
|
||||
fi
|
||||
|
||||
# --- Endpoint-shaped path literals ---
|
||||
# Survives R8 obfuscation: even when call sites are inlined to a.b(c, "path"),
|
||||
# the path strings themselves are not obfuscated. This produces a deduplicated
|
||||
# inventory of likely API endpoints that other modes miss.
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_PATHS" == true ]]; then
|
||||
section "Endpoint-Shaped Path Literals (deduplicated)"
|
||||
# Quoted strings that begin with /<segment> or <segment>/ where the leading
|
||||
# segment is a typical API root word. Cap segment count and length to keep
|
||||
# the regex grounded.
|
||||
# An endpoint-shaped string is one of:
|
||||
# "/seg/seg..." — absolute path with >= 2 segments
|
||||
# "api-root/seg/seg..." — relative path starting with a known
|
||||
# API root keyword and containing >= 1
|
||||
# '/' followed by another segment
|
||||
# Segments are URL-safe chars plus {} for path-template placeholders.
|
||||
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)'
|
||||
PATHS_REGEX="\"(/${SEG}(/${SEG})+/?|${ROOT}(/${SEG})+/?)\""
|
||||
# Filter out frequent false positives (MIME types, /proc, /sys, /dev).
|
||||
EXCLUDE='^"(image|video|audio|text|application|content|font|model|multipart|message)/|^"/(proc|sys|dev|tmp|etc|usr|var|opt)/'
|
||||
# Print a flat unique list rather than file:line — this is the inventory.
|
||||
grep -rhoE --include='*.java' --include='*.kt' "$PATHS_REGEX" "$SOURCE_DIR" 2>/dev/null \
|
||||
| grep -Ev "$EXCLUDE" \
|
||||
| sort -u || true
|
||||
echo
|
||||
section "Endpoint-Shaped Path Literals — call sites"
|
||||
grep $GREP_OPTS -E "$PATHS_REGEX" "$SOURCE_DIR" 2>/dev/null \
|
||||
| grep -Ev ":[0-9]+:.*${EXCLUDE#^}" || true
|
||||
fi
|
||||
|
||||
# --- Hardcoded URLs ---
|
||||
# A loose grep for http(s)://... drowns in compression-dictionary garbage and
|
||||
# in third-party SDK URLs (Google, Firebase, AppsFlyer, Datadog, ...). The
|
||||
# strict regex requires a syntactically valid hostname and rejects strings
|
||||
# containing whitespace, angle brackets, or non-printable bytes. Hosts are
|
||||
# then bucketed into "first-party candidates" vs "third-party (denylist)".
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_URLS" == true ]]; then
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DENYLIST="$HERE/../references/third_party_hosts.txt"
|
||||
# Accept three host shapes, all rejecting whitespace / angle brackets /
|
||||
# non-printables in the path:
|
||||
# * IPv4 literal (dev/staging endpoints, high signal) 192.168.0.1
|
||||
# * dotted host: >=2 labels ending in a 2+ letter TLD (incl apex) example.com
|
||||
# * bare single-label host, BUT only when followed by ':port' or localhost:3000
|
||||
# '/path' — keeps internal hosts (localhost, internal-backend) svc/health
|
||||
# while still dropping Kotlin-stdlib dictionary fragments like
|
||||
# "http://An Introduction..." (bare word, no port/path follows).
|
||||
STRICT_URL='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:]]*))'
|
||||
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
# Extraction (STRICT_URL) is deliberately permissive; this awk pass drops the
|
||||
# residual Kotlin-stdlib dictionary noise WITHOUT losing the high-signal
|
||||
# shapes a strict-only regex discards (IPs, apex domains, internal hosts).
|
||||
# Decision table, top-down, on the host (authority before any :port / /path):
|
||||
# * IPv4 literal -> keep (dict fragments are words,
|
||||
# never dotted-quads)
|
||||
# * >=3 labels (sub.domain.tld) -> keep (any TLD; same tolerance the
|
||||
# original strict regex had)
|
||||
# * any host WITH a :port or /path -> keep (structured = high signal:
|
||||
# localhost:3000, svc/health)
|
||||
# * bare 2-label apex, no port/path -> keep ONLY if the TLD is a real one,
|
||||
# compared as a whole field (kills
|
||||
# "www.this" / "this.introduction",
|
||||
# keeps "mytrackera-api.com")
|
||||
# Trade-off: a first-party host referenced bare with an uncommon TLD (e.g.
|
||||
# https://foo.store with no path) is dropped — give it a path/port, or add the
|
||||
# TLD to the list below, if you hit that case.
|
||||
{ grep -rhoE --include='*.java' --include='*.kt' "$STRICT_URL" "$SOURCE_DIR" 2>/dev/null || true; } \
|
||||
| sort -u \
|
||||
| awk '
|
||||
{ rest=$0; sub(/^https?:\/\//,"",rest)
|
||||
host=rest; sub(/[/:].*/,"",host)
|
||||
haspathport = (rest ~ /[/:]/)
|
||||
if (host ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) { print; next } # IPv4
|
||||
n = split(host, a, ".")
|
||||
if (n >= 3) { print; next } # sub.domain.tld
|
||||
if (haspathport) { print; next } # has :port or /path
|
||||
if (n == 2 && a[2] ~ /^(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)$/) print # real apex TLD
|
||||
}' > "$TMP"
|
||||
|
||||
# Extract host: strip scheme, take part up to first ':' or '/'.
|
||||
HOSTS_TMP="$(mktemp)"
|
||||
sed -E 's#^https?://##; s#[/:].*$##' "$TMP" | sort -u > "$HOSTS_TMP"
|
||||
|
||||
if [[ -f "$DENYLIST" ]]; then
|
||||
# Build a single combined regex from the denylist (one line each).
|
||||
DENY_REGEX="$(grep -vE '^\s*(#|$)' "$DENYLIST" | tr '\n' '|' | sed 's/|$//')"
|
||||
THIRD_HOSTS=$(grep -E "$DENY_REGEX" "$HOSTS_TMP" || true)
|
||||
FIRST_HOSTS=$(grep -vE "$DENY_REGEX" "$HOSTS_TMP" || true)
|
||||
else
|
||||
THIRD_HOSTS=""
|
||||
FIRST_HOSTS=$(cat "$HOSTS_TMP")
|
||||
fi
|
||||
|
||||
section "Likely First-Party Hosts (frequency-sorted)"
|
||||
if [[ -n "$FIRST_HOSTS" ]]; then
|
||||
while IFS= read -r h; do
|
||||
[[ -z "$h" ]] && continue
|
||||
n=$(grep -cE "://${h//./\\.}([/:\"]|$)" "$TMP" || true)
|
||||
printf ' %5d %s\n' "$n" "$h"
|
||||
done <<< "$FIRST_HOSTS" | sort -rn -k1
|
||||
else
|
||||
echo " (none — every URL matched the third-party denylist)"
|
||||
fi
|
||||
|
||||
section "Third-Party Hosts (denylist matches, collapsed)"
|
||||
if [[ -n "$THIRD_HOSTS" ]]; then
|
||||
echo "$THIRD_HOSTS" | sed 's/^/ /'
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
|
||||
section "All First-Party URLs (full strings)"
|
||||
if [[ -n "$FIRST_HOSTS" ]]; then
|
||||
while IFS= read -r h; do
|
||||
[[ -z "$h" ]] && continue
|
||||
grep -E "://${h//./\\.}([/:\"]|$)" "$TMP" | sed 's/^/ /'
|
||||
done <<< "$FIRST_HOSTS"
|
||||
fi
|
||||
|
||||
rm -f "$HOSTS_TMP" "$TMP"
|
||||
trap - EXIT
|
||||
|
||||
section "HttpURLConnection"
|
||||
run_grep '(openConnection|setRequestMethod|HttpURLConnection|HttpsURLConnection)'
|
||||
section "WebView URLs"
|
||||
run_grep '(loadUrl|loadData|evaluateJavascript|addJavascriptInterface|WebViewClient|WebChromeClient)'
|
||||
fi
|
||||
|
||||
# --- Auth patterns ---
|
||||
if [[ "$SEARCH_ALL" == true || "$SEARCH_AUTH_PATTERNS" == true ]]; then
|
||||
section "Authentication & API Keys"
|
||||
run_grep -i '(api[_-]?key|auth[_-]?token|bearer|authorization|x-api-key|client[_-]?secret|access[_-]?token|refresh[_-]?token)'
|
||||
|
||||
# Request-signing schemes: a hardcoded HMAC / RSA secret in an APK is a
|
||||
# security finding worth surfacing prominently. These patterns catch the
|
||||
# common shapes of homegrown / SDK-issued request signers.
|
||||
section "Request Signing (HMAC / signature schemes)"
|
||||
run_grep '(HmacSHA(1|256|512)|Mac\.getInstance\("Hmac|SecretKeySpec\(|Signature\.getInstance\()'
|
||||
run_grep -i '(x-signature|x-client-authorization|x-amz-signature|x-hmac|aws4-hmac|signRequest|signatureFor|computeSignature|signaturev[0-9])'
|
||||
|
||||
# Hardcoded high-entropy strings adjacent to "secret"/"key" assignments
|
||||
# are the canonical leaked-credential pattern.
|
||||
section "Possible Hardcoded Secrets / Keys"
|
||||
run_grep -i '(app[_-]?secret|client[_-]?secret|signing[_-]?key|hmac[_-]?secret|consumer[_-]?secret|private[_-]?key)'
|
||||
|
||||
section "Base URLs and Constants"
|
||||
run_grep -i '(BASE_URL|API_URL|SERVER_URL|ENDPOINT|API_BASE|HOST_NAME)'
|
||||
|
||||
# Ktor BearerTokens / refresh DSL — common on Kotlin apps and lives on
|
||||
# Ktor's public API, so it survives R8 unchanged.
|
||||
section "Ktor Auth (Bearer + Refresh)"
|
||||
run_grep '(BearerTokens|loadTokens\s*\{|refreshTokens\s*\{|\bbearer\s*\{)'
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Search complete ==="
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env bash
|
||||
# fingerprint.sh — Triage an APK/XAPK before decompiling.
|
||||
#
|
||||
# Detects mobile framework (Flutter, React Native, Cordova/Capacitor,
|
||||
# Xamarin, KMP/native), HTTP-stack hints, obfuscation level, native libs,
|
||||
# and notable third-party SDKs.
|
||||
#
|
||||
# Decompiling Java is mostly useless for Flutter / RN / Xamarin / Cordova
|
||||
# apps — different tools are needed. Run this BEFORE Phase 2 to choose
|
||||
# the right path.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: fingerprint.sh <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/
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
[[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]] && usage
|
||||
INPUT="$1"
|
||||
[[ ! -f "$INPUT" ]] && { echo "File not found: $INPUT" >&2; exit 1; }
|
||||
|
||||
TMP="$(mktemp -d -t apkfp.XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# Resolve to a list of APKs (handle XAPK = ZIP of APKs)
|
||||
APKS=()
|
||||
case "${INPUT,,}" in
|
||||
*.xapk|*.apks|*.apkm)
|
||||
unzip -q -o "$INPUT" -d "$TMP/xapk"
|
||||
while IFS= read -r p; do APKS+=("$p"); done < <(find "$TMP/xapk" -maxdepth 2 -type f -name '*.apk')
|
||||
;;
|
||||
*.apk)
|
||||
APKS=("$INPUT")
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported input: $INPUT" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Aggregate ZIP listings from every APK in the bundle (split-aware view)
|
||||
LISTING="$TMP/listing.txt"
|
||||
: > "$LISTING"
|
||||
for apk in "${APKS[@]}"; do
|
||||
unzip -l -- "$apk" 2>/dev/null | awk '{print $NF}' >> "$LISTING"
|
||||
done
|
||||
|
||||
# Most class-level libs live inside classes*.dex, not as visible zip paths.
|
||||
# Extract the type-name strings out of each dex with `strings` and append them
|
||||
# to the listing so `has()` can match e.g. 'io/ktor/' or 'org/koin/'.
|
||||
DEX_STRINGS="$TMP/dex_strings.txt"
|
||||
: > "$DEX_STRINGS"
|
||||
for apk in "${APKS[@]}"; do
|
||||
for dex in $(unzip -Z1 -- "$apk" 2>/dev/null | grep -E '^classes[0-9]*\.dex$' || true); do
|
||||
# DEX type descriptors look like "Lcom/foo/Bar;". Extract the inner
|
||||
# slash-separated FQN so callers can match e.g. 'io/ktor/' directly.
|
||||
unzip -p -- "$apk" "$dex" 2>/dev/null \
|
||||
| strings -n 8 \
|
||||
| grep -oE 'L[a-z][a-zA-Z0-9_]*(/[a-zA-Z0-9_$]+)+;' \
|
||||
| sed -E 's/^L//; s/;$//' \
|
||||
>> "$DEX_STRINGS" || true
|
||||
done
|
||||
done
|
||||
sort -u "$DEX_STRINGS" -o "$DEX_STRINGS"
|
||||
|
||||
has() { grep -qE "$1" "$LISTING" || grep -qE "$1" "$DEX_STRINGS"; }
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Framework detection (priority order — first match wins)
|
||||
# ----------------------------------------------------------------------
|
||||
FRAMEWORK="unknown"
|
||||
RATIONALE=""
|
||||
|
||||
if has '^lib/[^/]+/libflutter\.so$'; then
|
||||
FRAMEWORK="Flutter"
|
||||
RATIONALE="lib/<abi>/libflutter.so present"
|
||||
has '^lib/[^/]+/libapp\.so$' && RATIONALE+="; libapp.so contains AOT-compiled Dart"
|
||||
elif has '^lib/[^/]+/libhermes\.so$' || has '^assets/index\.android\.bundle$' || has '^lib/[^/]+/libreactnativejni\.so$'; then
|
||||
FRAMEWORK="React Native"
|
||||
reasons=()
|
||||
has '^lib/[^/]+/libhermes\.so$' && reasons+=("libhermes.so")
|
||||
has '^lib/[^/]+/libreactnativejni\.so$' && reasons+=("libreactnativejni.so")
|
||||
has '^assets/index\.android\.bundle$' && reasons+=("assets/index.android.bundle")
|
||||
RATIONALE="${reasons[*]}"
|
||||
elif has '^assets/www/index\.html$' || has '^assets/www/cordova\.js$' || has '^assets/public/index\.html$'; then
|
||||
FRAMEWORK="Cordova / Capacitor (WebView hybrid)"
|
||||
RATIONALE="assets/www/ or assets/public/ shell present"
|
||||
elif has '^lib/[^/]+/libmonodroid\.so$' || has '^assemblies/'; then
|
||||
FRAMEWORK="Xamarin / .NET MAUI"
|
||||
RATIONALE="libmonodroid.so or assemblies/ present — code is in .NET DLLs"
|
||||
elif has '^lib/[^/]+/libmaui\.so$'; then
|
||||
FRAMEWORK=".NET MAUI"
|
||||
RATIONALE="libmaui.so present"
|
||||
elif has '^assets/flutter_assets/' && ! has '^lib/[^/]+/libflutter\.so$'; then
|
||||
FRAMEWORK="Flutter (code-only split?)"
|
||||
RATIONALE="flutter_assets/ but no libflutter.so in this APK — check splits"
|
||||
else
|
||||
# Native: distinguish Compose vs classic Android by androidx.compose presence
|
||||
if has 'androidx\.compose'; then
|
||||
FRAMEWORK="Native Android (Kotlin + Jetpack Compose)"
|
||||
RATIONALE="androidx.compose.* libraries detected"
|
||||
elif has '^META-INF/.*\.kotlin_module$'; then
|
||||
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"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# HTTP / DI / serialization stack hints
|
||||
# ----------------------------------------------------------------------
|
||||
http=()
|
||||
has 'retrofit2' && http+=("Retrofit")
|
||||
has 'okhttp3' && http+=("OkHttp")
|
||||
has 'io/ktor/' && http+=("Ktor")
|
||||
has 'com/apollographql/' && http+=("Apollo (GraphQL)")
|
||||
has 'com/android/volley' && http+=("Volley")
|
||||
|
||||
di=()
|
||||
has 'dagger/hilt/' && di+=("Hilt")
|
||||
has '^META-INF/.*dagger.*' && di+=("Dagger")
|
||||
has 'org/koin/' && di+=("Koin")
|
||||
has 'javax/inject/' && [[ ${#di[@]} -eq 0 ]] && di+=("javax.inject")
|
||||
|
||||
ser=()
|
||||
has 'kotlinx/serialization/' && ser+=("kotlinx.serialization")
|
||||
has 'com/google/gson/' && ser+=("Gson")
|
||||
has 'com/squareup/moshi/' && ser+=("Moshi")
|
||||
has 'com/fasterxml/jackson/' && ser+=("Jackson")
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Obfuscation indicator (R8/ProGuard) — count single-letter dex packages
|
||||
# ----------------------------------------------------------------------
|
||||
# Note: pipefail is on, so guard greps that may legitimately return 0 matches.
|
||||
short_dirs=$( { grep -oE '^[a-z]{1,2}/' "$LISTING" || true; } | sort -u | wc -l | tr -d ' ')
|
||||
if [[ "$short_dirs" -gt 30 ]]; then
|
||||
OBFUSCATION="HIGH ($short_dirs single/double-letter dirs at root)"
|
||||
elif [[ "$short_dirs" -gt 10 ]]; then
|
||||
OBFUSCATION="MODERATE ($short_dirs short root dirs)"
|
||||
else
|
||||
OBFUSCATION="LOW (no significant short-name namespace pollution)"
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Native libraries (consolidated)
|
||||
# ----------------------------------------------------------------------
|
||||
NATIVE=$(grep -E '^lib/[^/]+/[^/]+\.so$' "$LISTING" | sort -u || true)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Notable third-party SDKs (assets-based markers)
|
||||
# ----------------------------------------------------------------------
|
||||
sdks=()
|
||||
has '^assets/com/appsflyer/' && sdks+=("AppsFlyer")
|
||||
has 'datadog\.buildId|com/datadog/' && sdks+=("Datadog")
|
||||
has 'io/sentry/' && sdks+=("Sentry")
|
||||
has 'com/google/firebase/' && sdks+=("Firebase")
|
||||
has 'com/google/android/gms/' && sdks+=("Google Play Services")
|
||||
has 'com/facebook/' && sdks+=("Facebook SDK")
|
||||
has 'com/payu/' && sdks+=("PayU")
|
||||
has 'com/stripe/' && sdks+=("Stripe")
|
||||
has 'com/braintreepayments/' && sdks+=("Braintree")
|
||||
has 'com/storyteller/' && sdks+=("Storyteller")
|
||||
has 'zendesk/' && sdks+=("Zendesk")
|
||||
has 'com/intercom/' && sdks+=("Intercom")
|
||||
has 'com/segment/analytics' && sdks+=("Segment")
|
||||
has 'com/amplitude/' && sdks+=("Amplitude")
|
||||
has 'com/mixpanel/' && sdks+=("Mixpanel")
|
||||
has 'com/onesignal/' && sdks+=("OneSignal")
|
||||
has 'com/microsoft/clarity' && sdks+=("Microsoft Clarity")
|
||||
has 'com/hotjar/' && sdks+=("Hotjar")
|
||||
has 'com/instabug/' && sdks+=("Instabug")
|
||||
|
||||
# BuildConfig.java is almost never obfuscated and often holds base URLs / flavor.
|
||||
if has 'BuildConfig\.class$'; then
|
||||
BUILDCONFIG="present (grep BuildConfig.java after decompile for base URLs / flavor)"
|
||||
else
|
||||
BUILDCONFIG="not detected in zip listing (still worth grepping after decompile)"
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Summary
|
||||
# ----------------------------------------------------------------------
|
||||
echo "=== APK Fingerprint: $(basename "$INPUT") ==="
|
||||
echo
|
||||
echo "Framework: $FRAMEWORK"
|
||||
echo " Rationale: $RATIONALE"
|
||||
echo "Obfuscation: $OBFUSCATION"
|
||||
echo
|
||||
echo "HTTP stack: ${http[*]:-none detected}"
|
||||
echo "DI: ${di[*]:-none detected}"
|
||||
echo "Serialization: ${ser[*]:-none detected}"
|
||||
echo "BuildConfig: $BUILDCONFIG"
|
||||
echo
|
||||
echo "Third-party SDKs: ${sdks[*]:-none detected}"
|
||||
echo
|
||||
echo "Native libraries (consolidated across splits):"
|
||||
if [[ -n "$NATIVE" ]]; then
|
||||
echo "$NATIVE" | sed 's/^/ /'
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Recommendation
|
||||
# ----------------------------------------------------------------------
|
||||
echo "Recommended next step:"
|
||||
case "$FRAMEWORK" in
|
||||
Flutter*)
|
||||
echo " Java decompilation will yield ~no app code. The Dart logic lives in"
|
||||
echo " libapp.so (AOT). Use tools designed for Flutter:"
|
||||
echo " - reFlutter / Doldrums / blutter (extract Dart class structure)"
|
||||
echo " - strings/rabin2 on libapp.so for endpoints & string constants"
|
||||
;;
|
||||
React*)
|
||||
echo " Java code is just the RN host. Real app logic is in JS/Hermes:"
|
||||
echo " - if Hermes: hbctool disasm assets/index.android.bundle"
|
||||
echo " - if JSC: js-beautify the bundle and grep for 'fetch('/'axios'"
|
||||
;;
|
||||
Cordova*)
|
||||
echo " All app code is in assets/www/ (or assets/public/). Just unzip and"
|
||||
echo " inspect the HTML/JS — no Java decompile needed."
|
||||
;;
|
||||
Xamarin*|.NET*)
|
||||
echo " App logic is in .NET DLLs (assemblies/). Use ILSpy or dotPeek;"
|
||||
echo " jadx will only show the Mono host."
|
||||
;;
|
||||
*)
|
||||
echo " Proceed with Phase 2: bash scripts/decompile.sh <file>"
|
||||
;;
|
||||
esac
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env bash
|
||||
# install-dep.sh — Install a single dependency for Android reverse engineering
|
||||
# Usage: install-dep.sh <dependency>
|
||||
# Dependencies: java, jadx, vineflower, dex2jar, apktool, adb
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — installed successfully
|
||||
# 1 — installation failed
|
||||
# 2 — requires manual action (e.g. sudo needed but not available)
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: install-dep.sh <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 your OS and package manager, then:
|
||||
- Installs directly if possible (brew, or user-local install)
|
||||
- Uses sudo if available and needed
|
||||
- Prints manual instructions if neither option works
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
DEP="$1"
|
||||
|
||||
# --- Detect environment ---
|
||||
OS="unknown"
|
||||
PKG_MANAGER="none"
|
||||
HAS_SUDO=false
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) OS="linux" ;;
|
||||
Darwin) OS="macos" ;;
|
||||
esac
|
||||
|
||||
# Detect package manager
|
||||
if command -v brew &>/dev/null; then
|
||||
PKG_MANAGER="brew"
|
||||
elif command -v apt-get &>/dev/null; then
|
||||
PKG_MANAGER="apt"
|
||||
elif command -v dnf &>/dev/null; then
|
||||
PKG_MANAGER="dnf"
|
||||
elif command -v pacman &>/dev/null; then
|
||||
PKG_MANAGER="pacman"
|
||||
fi
|
||||
|
||||
# Check sudo availability
|
||||
if command -v sudo &>/dev/null; then
|
||||
if sudo -n true 2>/dev/null; then
|
||||
HAS_SUDO=true
|
||||
else
|
||||
# sudo exists but may need password — we'll try it and let it prompt
|
||||
HAS_SUDO=true
|
||||
fi
|
||||
fi
|
||||
|
||||
info() { echo "[INFO] $*"; }
|
||||
ok() { echo "[OK] $*"; }
|
||||
fail() { echo "[FAIL] $*" >&2; }
|
||||
manual() {
|
||||
echo "[MANUAL] $*" >&2
|
||||
echo " Cannot install automatically. Please install manually and retry." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# --- Helper: install via system package manager (needs sudo on Linux) ---
|
||||
pkg_install() {
|
||||
local pkg="$1"
|
||||
case "$PKG_MANAGER" in
|
||||
brew)
|
||||
info "Installing $pkg via Homebrew..."
|
||||
brew install "$pkg"
|
||||
;;
|
||||
apt)
|
||||
if [[ "$HAS_SUDO" == true ]]; then
|
||||
info "Installing $pkg via apt..."
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq "$pkg"
|
||||
else
|
||||
manual "Run: sudo apt-get install $pkg"
|
||||
fi
|
||||
;;
|
||||
dnf)
|
||||
if [[ "$HAS_SUDO" == true ]]; then
|
||||
info "Installing $pkg via dnf..."
|
||||
sudo dnf install -y "$pkg"
|
||||
else
|
||||
manual "Run: sudo dnf install $pkg"
|
||||
fi
|
||||
;;
|
||||
pacman)
|
||||
if [[ "$HAS_SUDO" == true ]]; then
|
||||
info "Installing $pkg via pacman..."
|
||||
sudo pacman -S --noconfirm "$pkg"
|
||||
else
|
||||
manual "Run: sudo pacman -S $pkg"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
manual "No supported package manager found. Install $pkg manually."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- Helper: download a file ---
|
||||
download() {
|
||||
local url="$1" dest="$2"
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -fsSL -o "$dest" "$url"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -q -O "$dest" "$url"
|
||||
else
|
||||
fail "Neither curl nor wget available."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Helper: get latest GitHub release tag ---
|
||||
gh_latest_tag() {
|
||||
local repo="$1"
|
||||
local url="https://api.github.com/repos/$repo/releases/latest"
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -fsSL "$url" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/'
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -q -O - "$url" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/'
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Helper: add a line to shell profile if not already present ---
|
||||
add_to_profile() {
|
||||
local line="$1"
|
||||
local profile=""
|
||||
if [[ -f "$HOME/.zshrc" ]]; then
|
||||
profile="$HOME/.zshrc"
|
||||
elif [[ -f "$HOME/.bashrc" ]]; then
|
||||
profile="$HOME/.bashrc"
|
||||
elif [[ -f "$HOME/.profile" ]]; then
|
||||
profile="$HOME/.profile"
|
||||
fi
|
||||
|
||||
if [[ -n "$profile" ]]; then
|
||||
if ! grep -qF "$line" "$profile" 2>/dev/null; then
|
||||
echo "$line" >> "$profile"
|
||||
info "Added to $profile: $line"
|
||||
info "Run 'source $profile' or start a new shell to apply."
|
||||
fi
|
||||
else
|
||||
info "Add this to your shell profile: $line"
|
||||
fi
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Dependency installers
|
||||
# =====================================================================
|
||||
|
||||
install_java() {
|
||||
if command -v java &>/dev/null; then
|
||||
local ver
|
||||
ver=$(java -version 2>&1 | head -1 | sed -n 's/.*"\([0-9]*\)\..*/\1/p')
|
||||
if [[ -n "$ver" ]] && (( ver >= 17 )); then
|
||||
ok "Java $ver already installed"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Installing Java JDK 17+..."
|
||||
case "$PKG_MANAGER" in
|
||||
brew) brew install openjdk@17 ;;
|
||||
apt) pkg_install "openjdk-17-jdk" ;;
|
||||
dnf) pkg_install "java-17-openjdk-devel" ;;
|
||||
pacman) pkg_install "jdk17-openjdk" ;;
|
||||
*) manual "Install Java JDK 17+ from https://adoptium.net/" ;;
|
||||
esac
|
||||
|
||||
# Verify
|
||||
if command -v java &>/dev/null; then
|
||||
ok "Java installed: $(java -version 2>&1 | head -1)"
|
||||
else
|
||||
fail "Java installation may require PATH update."
|
||||
if [[ "$PKG_MANAGER" == "brew" ]]; then
|
||||
add_to_profile 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"'
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_jadx() {
|
||||
if command -v jadx &>/dev/null; then
|
||||
ok "jadx already installed: $(jadx --version 2>/dev/null || echo 'unknown')"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try brew first (cleanest)
|
||||
if [[ "$PKG_MANAGER" == "brew" ]]; then
|
||||
info "Installing jadx via Homebrew..."
|
||||
brew install jadx
|
||||
ok "jadx installed via Homebrew"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# User-local install from GitHub releases (no sudo needed)
|
||||
info "Installing jadx from GitHub releases..."
|
||||
local tag
|
||||
tag=$(gh_latest_tag "skylot/jadx")
|
||||
if [[ -z "$tag" ]]; then
|
||||
fail "Could not determine latest jadx version."
|
||||
manual "Download from https://github.com/skylot/jadx/releases/latest"
|
||||
fi
|
||||
|
||||
local version="${tag#v}"
|
||||
local url="https://github.com/skylot/jadx/releases/download/${tag}/jadx-${version}.zip"
|
||||
local tmp_zip
|
||||
tmp_zip=$(mktemp /tmp/jadx-XXXXXX.zip)
|
||||
|
||||
info "Downloading jadx $version..."
|
||||
download "$url" "$tmp_zip"
|
||||
|
||||
local install_dir="$HOME/.local/share/jadx"
|
||||
rm -rf "$install_dir"
|
||||
mkdir -p "$install_dir"
|
||||
unzip -qo "$tmp_zip" -d "$install_dir"
|
||||
rm -f "$tmp_zip"
|
||||
chmod +x "$install_dir/bin/jadx" "$install_dir/bin/jadx-gui" 2>/dev/null || true
|
||||
|
||||
# Add to PATH
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
ln -sf "$install_dir/bin/jadx" "$HOME/.local/bin/jadx"
|
||||
ln -sf "$install_dir/bin/jadx-gui" "$HOME/.local/bin/jadx-gui"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
add_to_profile 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
|
||||
if command -v jadx &>/dev/null; then
|
||||
ok "jadx $version installed to $install_dir"
|
||||
else
|
||||
ok "jadx $version installed to $install_dir"
|
||||
info "Run: export PATH=\"\$HOME/.local/bin:\$PATH\" to use it now"
|
||||
fi
|
||||
}
|
||||
|
||||
install_vineflower() {
|
||||
# Check if already available
|
||||
if command -v vineflower &>/dev/null || command -v fernflower &>/dev/null; then
|
||||
ok "Vineflower/Fernflower CLI already installed"
|
||||
return 0
|
||||
fi
|
||||
for candidate in \
|
||||
"${FERNFLOWER_JAR_PATH:-}" \
|
||||
"$HOME/vineflower/vineflower.jar" \
|
||||
"$HOME/fernflower/fernflower.jar" \
|
||||
"$HOME/fernflower/build/libs/fernflower.jar" \
|
||||
"$HOME/vineflower/build/libs/vineflower.jar"; do
|
||||
if [[ -n "$candidate" ]] && [[ -f "$candidate" ]]; then
|
||||
ok "Vineflower/Fernflower JAR already exists: $candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Try brew
|
||||
if [[ "$PKG_MANAGER" == "brew" ]]; then
|
||||
info "Installing vineflower via Homebrew..."
|
||||
if brew install vineflower 2>/dev/null; then
|
||||
ok "Vineflower installed via Homebrew"
|
||||
return 0
|
||||
fi
|
||||
info "Homebrew formula not available, falling back to direct download."
|
||||
fi
|
||||
|
||||
# Download JAR from GitHub releases (no sudo needed)
|
||||
info "Installing Vineflower from GitHub releases..."
|
||||
local tag
|
||||
tag=$(gh_latest_tag "Vineflower/vineflower")
|
||||
if [[ -z "$tag" ]]; then
|
||||
fail "Could not determine latest Vineflower version."
|
||||
manual "Download from https://github.com/Vineflower/vineflower/releases/latest"
|
||||
fi
|
||||
|
||||
local version="${tag#v}"
|
||||
local url="https://github.com/Vineflower/vineflower/releases/download/${tag}/vineflower-${version}.jar"
|
||||
local install_dir="$HOME/.local/share/vineflower"
|
||||
mkdir -p "$install_dir"
|
||||
|
||||
info "Downloading Vineflower $version..."
|
||||
download "$url" "$install_dir/vineflower.jar"
|
||||
|
||||
# Create wrapper script
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
cat > "$HOME/.local/bin/vineflower" <<'WRAPPER'
|
||||
#!/usr/bin/env bash
|
||||
exec java -jar "$HOME/.local/share/vineflower/vineflower.jar" "$@"
|
||||
WRAPPER
|
||||
chmod +x "$HOME/.local/bin/vineflower"
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
export FERNFLOWER_JAR_PATH="$install_dir/vineflower.jar"
|
||||
add_to_profile 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
add_to_profile "export FERNFLOWER_JAR_PATH=\"$install_dir/vineflower.jar\""
|
||||
|
||||
ok "Vineflower $version installed to $install_dir/vineflower.jar"
|
||||
info "FERNFLOWER_JAR_PATH set to $install_dir/vineflower.jar"
|
||||
}
|
||||
|
||||
install_dex2jar() {
|
||||
if command -v d2j-dex2jar &>/dev/null || command -v d2j-dex2jar.sh &>/dev/null; then
|
||||
ok "dex2jar already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try brew
|
||||
if [[ "$PKG_MANAGER" == "brew" ]]; then
|
||||
info "Installing dex2jar via Homebrew..."
|
||||
if brew install dex2jar 2>/dev/null; then
|
||||
ok "dex2jar installed via Homebrew"
|
||||
return 0
|
||||
fi
|
||||
info "Homebrew formula not available, falling back to direct download."
|
||||
fi
|
||||
|
||||
# Download from GitHub (no sudo needed)
|
||||
info "Installing dex2jar from GitHub releases..."
|
||||
local tag
|
||||
tag=$(gh_latest_tag "ThexXTURBOXx/dex2jar")
|
||||
if [[ -z "$tag" ]]; then
|
||||
# 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/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/ThexXTURBOXx/dex2jar/releases/download/${tag}/dex-tools-v${version}.zip"
|
||||
download "$url" "$tmp_zip" || {
|
||||
fail "Download failed."
|
||||
manual "Download from https://github.com/ThexXTURBOXx/dex2jar/releases/latest"
|
||||
}
|
||||
fi
|
||||
|
||||
local install_dir="$HOME/.local/share/dex2jar"
|
||||
rm -rf "$install_dir"
|
||||
mkdir -p "$install_dir"
|
||||
unzip -qo "$tmp_zip" -d "$install_dir"
|
||||
rm -f "$tmp_zip"
|
||||
|
||||
# The zip may contain a top-level directory — find the actual bin location
|
||||
local bin_dir=""
|
||||
if [[ -f "$install_dir/d2j-dex2jar.sh" ]]; then
|
||||
bin_dir="$install_dir"
|
||||
else
|
||||
bin_dir=$(find "$install_dir" -name "d2j-dex2jar.sh" -exec dirname {} \; | head -1)
|
||||
fi
|
||||
|
||||
if [[ -z "$bin_dir" ]]; then
|
||||
fail "Could not find d2j-dex2jar.sh in extracted archive."
|
||||
manual "Download and extract manually from https://github.com/ThexXTURBOXx/dex2jar/releases"
|
||||
fi
|
||||
|
||||
chmod +x "$bin_dir"/*.sh 2>/dev/null || true
|
||||
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
for script in "$bin_dir"/d2j-*.sh; do
|
||||
local name
|
||||
name=$(basename "$script" .sh)
|
||||
ln -sf "$script" "$HOME/.local/bin/$name"
|
||||
done
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
add_to_profile 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
|
||||
ok "dex2jar $version installed to $install_dir"
|
||||
}
|
||||
|
||||
install_apktool() {
|
||||
if command -v apktool &>/dev/null; then
|
||||
ok "apktool already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
brew) info "Installing apktool via Homebrew..."; brew install apktool ;;
|
||||
apt) pkg_install "apktool" ;;
|
||||
*) manual "Install apktool from https://apktool.org/docs/install" ;;
|
||||
esac
|
||||
|
||||
if command -v apktool &>/dev/null; then
|
||||
ok "apktool installed"
|
||||
else
|
||||
fail "apktool installation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_adb() {
|
||||
if command -v adb &>/dev/null; then
|
||||
ok "adb already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
brew) info "Installing adb via Homebrew..."; brew install android-platform-tools ;;
|
||||
apt) pkg_install "adb" ;;
|
||||
dnf) pkg_install "android-tools" ;;
|
||||
pacman) pkg_install "android-tools" ;;
|
||||
*) manual "Install Android SDK Platform Tools from https://developer.android.com/tools/releases/platform-tools" ;;
|
||||
esac
|
||||
|
||||
if command -v adb &>/dev/null; then
|
||||
ok "adb installed"
|
||||
else
|
||||
fail "adb installation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Dispatch
|
||||
# =====================================================================
|
||||
|
||||
case "$DEP" in
|
||||
java) install_java ;;
|
||||
jadx) install_jadx ;;
|
||||
vineflower|fernflower) install_vineflower ;;
|
||||
dex2jar) install_dex2jar ;;
|
||||
apktool) install_apktool ;;
|
||||
adb) install_adb ;;
|
||||
*)
|
||||
echo "Error: Unknown dependency '$DEP'" >&2
|
||||
echo "Available: java, jadx, vineflower, dex2jar, apktool, adb" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# lookup-name.sh — Query the mapping produced by recover-kotlin-names.sh.
|
||||
#
|
||||
# Modes:
|
||||
# lookup-name.sh <mapping-dir> <substring> search by real-FQN substring
|
||||
# lookup-name.sh <mapping-dir> -o <obf> resolve obf -> real
|
||||
# lookup-name.sh <mapping-dir> -p <pkg> list a real package
|
||||
# lookup-name.sh <mapping-dir> --grep <regex> <sources-dir>
|
||||
# grep decompiled sources and annotate each hit with the real class name
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: lookup-name.sh <mapping-dir> <query>
|
||||
lookup-name.sh <mapping-dir> -o <obf-fqn>
|
||||
lookup-name.sh <mapping-dir> -p <real-package-substring>
|
||||
lookup-name.sh <mapping-dir> --grep <regex> <sources-dir>
|
||||
|
||||
<mapping-dir> is the directory produced by recover-kotlin-names.sh
|
||||
(must contain mapping.json).
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
[[ $# -lt 2 ]] && usage
|
||||
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
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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 —
|
||||
# the Kotlin runtime (reflection, coroutines) needs them at runtime. Two
|
||||
# annotations carry the original FQN:
|
||||
#
|
||||
# * @DebugMetadata(c = "<full.qualified.Name>", f = "<File.kt>", ...)
|
||||
# emitted for almost every `suspend` function (every coroutine
|
||||
# SuspendLambda).
|
||||
#
|
||||
# * @Metadata(... d2 = {"...L<pkg/Class>;..."} ...) listing internal
|
||||
# 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
|
||||
# classes you actually want to read.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: recover-kotlin-names.sh <decompiled-sources-dir> [output-dir]
|
||||
|
||||
Walks every *.java under <decompiled-sources-dir>, mines @DebugMetadata
|
||||
and @Metadata annotations, and writes:
|
||||
|
||||
<output-dir>/mapping.tsv tab-separated obf_fqn <TAB> real_fqn <TAB> file
|
||||
<output-dir>/mapping.json same data as JSON { obf_fqn: real_fqn, ... }
|
||||
<output-dir>/by_package/ one file per real package, listing
|
||||
real_fqn <TAB> obf_fqn <TAB> file
|
||||
|
||||
If [output-dir] is omitted, files are written next to the sources dir.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
[[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]] && usage
|
||||
SRC="$1"
|
||||
OUT="${2:-$(dirname "$SRC")/mapping}"
|
||||
[[ ! -d "$SRC" ]] && { echo "not a directory: $SRC" >&2; exit 1; }
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user