From 213818fc27a1009b560a862f7d84d5d1d0864c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Tajchert?= Date: Wed, 29 Apr 2026 01:07:40 +0200 Subject: [PATCH] feat: add Phase 0 fingerprint script for fast pre-decompile triage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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..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. --- .../android-reverse-engineering/SKILL.md | 25 ++ .../scripts/fingerprint.sh | 241 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100755 plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.sh diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md b/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md index 6b31074..9b3b311 100644 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/SKILL.md @@ -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 +``` + +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..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. diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.sh new file mode 100755 index 0000000..c494358 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/fingerprint.sh @@ -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 < + +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//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 " + ;; +esac