feat: add Phase 0 fingerprint script for fast pre-decompile triage

Decompiling Java is wasted effort for Flutter, React Native, Cordova/
Capacitor, and Xamarin apps — their code lives in libapp.so, the JS bundle,
assets/www/, or .NET DLLs respectively. The previous workflow jumped
straight to Phase 1 (install deps) and Phase 2 (decompile), so the agent
had no way to know which path to take until after a full jadx run.

The new fingerprint.sh inspects an APK/XAPK in seconds and reports:

* Detected mobile framework with the file marker that triggered it
* HTTP stack hints (Retrofit, OkHttp, Ktor, Apollo, Volley) via DEX
  string scanning — survives R8 obfuscation
* DI and serialization libraries
* Obfuscation level estimate
* Notable third-party SDKs found in assets/ and DEX
* Consolidated native libraries across base + split APKs (split bundles
  often place .so files only in config.<abi>.apk)
* A framework-specific recommendation for the next step

SKILL.md documents this as Phase 0 and explicitly tells the agent to
stop and switch tooling if the app is non-native.

PowerShell port (fingerprint.ps1) intentionally not included — happy to
add if needed; behavior is straightforward to mirror.
This commit is contained in:
Michał Tajchert 2026-04-29 01:07:40 +02:00
parent 6a31ed3fa2
commit 213818fc27
2 changed files with 266 additions and 0 deletions

View File

@ -24,6 +24,31 @@ If anything is missing, follow the installation instructions in `${CLAUDE_PLUGIN
## 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 15 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.

View File

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