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:
parent
6a31ed3fa2
commit
213818fc27
|
|
@ -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 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue