SDK Neutralizer v4: JSON registry with 29 SDKs and registry-scan.py

Replaces hardcoded smali patterns with a data-driven SDK registry: 29 JSON
definitions covering ad networks, mediation, attribution, and analytics SDKs
(123 entry points, 156 ad operations, 30 deep patterns, 64 manifest components).

- Add plugins/.../sdk-neutralizer/registry/ with _schema.json + 29 SDK files
  (Adjust, Braze, CleverTap, Guru Fusion, Mintegral, Mixpanel, MobileFuse,
  Moloco, PubMatic, TradPlus, plus the prior 19)
- Add registry-scan.py to consume the registry and emit targets-file +
  manifest-components-file consumed by neutralize.sh
- Extend neutralize.sh with --no-builtin-targets, --targets-file,
  --manifest-components-file, --package, --cleanup-backups
- Extend find-ads.sh and find-trackers.sh with --summary and --json output
- Mark python3 as INSTALL_OPTIONAL in check-neutralize-deps.sh (fallback
  to builtin hardcoded targets when unavailable)
- Bump plugin and marketplace to 1.3.1
- Add .gitignore for __pycache__/, *.pyc, *.pyo
- Update CLAUDE.md, SKILL.md, neutralize.md to reflect registry-driven flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Simone Avogadro 2026-04-27 09:43:39 +02:00
parent 3d68cf392f
commit 6891a3a8a2
47 changed files with 4008 additions and 173 deletions

View File

@ -7,14 +7,14 @@
},
"metadata": {
"description": "Claude Code plugins for Android reverse engineering, tracker detection, ad SDK analysis, and SDK neutralization for enterprise deployment",
"version": "1.2.0"
"version": "1.3.1"
},
"plugins": [
{
"name": "android-reverse-engineering",
"source": "./plugins/android-reverse-engineering",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, analyze tracker/ad SDKs for privacy auditing, and neutralize SDK entry points for enterprise deployment.",
"version": "1.2.0",
"version": "1.3.1",
"author": {
"name": "Simone Avogadro"
},

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*.pyc
*.pyo

View File

@ -21,7 +21,8 @@ A Claude Code Skill (plugin) for Android reverse engineering, API extraction, an
- `plugins/android-reverse-engineering/skills/android-reverse-engineering/` — Core RE skill (5-phase workflow, references, scripts)
- `plugins/android-reverse-engineering/skills/tracker-analysis/` — Tracker/analytics SDK detection skill (4-phase workflow, references, find-trackers.sh)
- `plugins/android-reverse-engineering/skills/ad-analysis/` — Advertising SDK detection skill (3-phase workflow, references, find-ads.sh)
- `plugins/android-reverse-engineering/skills/sdk-neutralizer/` — SDK neutralization skill (6-phase workflow, references, decode-apk.sh, neutralize.sh, merge-splits.sh, rebuild-apk.sh)
- `plugins/android-reverse-engineering/skills/sdk-neutralizer/` — SDK neutralization skill (6-phase workflow, references, decode-apk.sh, neutralize.sh, registry-scan.py, merge-splits.sh, rebuild-apk.sh)
- `plugins/android-reverse-engineering/skills/sdk-neutralizer/registry/` — SDK registry (29 JSON files defining neutralization targets, manifest components, protected patterns)
## Key Scripts
@ -61,13 +62,20 @@ bash find-ads.sh <source-dir> [--admob|--unity|--ironsource|--applovin|--faceboo
SDK neutralizer scripts under `plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/`:
```bash
# Check neutralization dependencies (including apktool >= 2.9.0)
# Check neutralization dependencies (including apktool >= 2.9.0, Python 3.6+ optional)
bash check-neutralize-deps.sh
# Decode APK or XAPK (for XAPK: decodes base APK, preserves splits in .xapk-origin/)
bash decode-apk.sh <file.apk|file.xapk> [-o <decoded-dir>]
# Scan decoded APK against SDK registry (generates targets-file + manifest-components-file)
# Depth: 1=entry_points only, 2=+ad_operations, 3=+deep_patterns
python3 registry-scan.py <decoded-dir> --registry <registry-path> --depth 1|2|3 --category ads|trackers|all --output-dir <decoded-dir>
# Neutralize SDK entry points in decoded APK (dry-run first)
# Registry-driven mode (preferred):
bash neutralize.sh <decoded-dir> --no-builtin-targets --targets-file <decoded-dir>/registry-targets.txt --manifest-components-file <decoded-dir>/registry-manifest.txt [--dry-run] [--package <path>]
# Fallback (builtin targets):
bash neutralize.sh <decoded-dir> [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file <file>] [--replay] [--no-save-manifest]
# Merge XAPK splits into decoded base for single APK output (optional, for XAPK input)
@ -77,6 +85,13 @@ bash merge-splits.sh <decoded-dir> [--abi <abi>] [--all-abis] [--skip-resources]
bash rebuild-apk.sh <decoded-dir> [--auto-keystore|--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign] [--single-apk]
```
SDK registry under `plugins/android-reverse-engineering/skills/sdk-neutralizer/registry/`:
- 29 SDK JSON files + `_schema.json` schema definition
- Covers: AdMob, Unity Ads, IronSource, AppLovin, Meta AN, Vungle, InMobi, Chartboost, Pangle, BidMachine, Smaato, PubNative, Ogury, Fyber, Amazon APS, Facebook, Firebase Analytics, Firebase Crashlytics, AppsFlyer, Adjust, Braze, CleverTap, Guru Fusion, Mintegral, Mixpanel, MobileFuse, Moloco, PubMatic, TradPlus
- Each JSON defines: packages, entry_points, ad_operations, deep_patterns, manifest_components, protected_patterns
- `registry-scan.py` consumes these JSONs to generate neutralization targets
## Architecture
**Plugin structure follows Claude Code skill conventions:**

View File

@ -1,6 +1,6 @@
{
"name": "android-reverse-engineering",
"version": "1.2.0",
"version": "1.3.1",
"description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, analyze tracker/ad SDKs for privacy auditing, and neutralize SDK entry points for enterprise deployment.",
"author": {
"name": "Simone Avogadro"

View File

@ -60,6 +60,25 @@ This searches for ad SDK calls **only in app code** (excluding library packages
Use the reference documents in `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/` for SDK-specific patterns.
### Step 3b: Filter false positives
When analyzing the results, distinguish real SDK presence from false positives:
- **HIGH confidence** — SDK-specific classes/imports are present (e.g., `MobileAds.initialize()`, `import com.unity3d.ads.UnityAds`). The SDK is definitely integrated.
- **MEDIUM confidence** — Only generic ad format names matched (e.g., `InterstitialAd`, `BannerView`, `RewardedAd`). These class names may be from the actual SDK or from custom wrappers. Verify by checking the full import path.
- **LOW confidence** — Only generic strings matched (e.g., "banner", "interstitial" in comments or unrelated code). Check the context.
**Quick verification**: For any SDK flagged as detected, check if its package directory actually exists:
```bash
# Example: verify Unity Ads is really present
find "$SOURCE_DIR" -path "*/com/unity3d/ads" -type d
```
Use `--summary` for a quick confidence-scored overview before diving into raw output:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh "$SOURCE_DIR" --summary
```
### Step 4: Produce report
Generate a structured report with:

View File

@ -48,6 +48,25 @@ For each SDK found in the sweep, perform deep analysis:
Use the reference documents in `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/` for SDK-specific patterns.
### Step 3b: Filter false positives
When analyzing the results, distinguish real SDK presence from false positives:
- **HIGH confidence** — SDK-specific classes/imports are present (e.g., `FirebaseAnalytics.getInstance()`, `import com.adjust.sdk.Adjust`). The SDK is definitely integrated.
- **MEDIUM confidence** — Only generic method names matched (e.g., `.track(`, `.logEvent(`, `.identify(`). These are common method names used by many libraries, not just tracker SDKs. Verify by checking if the actual SDK package exists in the source tree.
- **LOW confidence** — Only string matches (e.g., the word "amplitude" in a comment or unrelated context). Check the surrounding code — if it's not an SDK import/call, it's a false positive.
**Quick verification**: For any SDK flagged as detected, check if its package directory actually exists:
```bash
# Example: verify Firebase is really present, not just a string match
find "$SOURCE_DIR" -path "*/com/google/firebase/analytics" -type d
```
Use `--summary` for a quick confidence-scored overview before diving into raw output:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh "$SOURCE_DIR" --summary
```
### Step 4: Produce report
Generate a structured report with:

View File

@ -76,35 +76,104 @@ Verify the decoded directory contains `smali/` and `AndroidManifest.xml` (the sc
If the output includes `XAPK_ORIGIN:<path>`, inform the user: "This is an XAPK (split APK bundle). The base APK has been decoded for neutralization, and all split APKs are preserved. During rebuild, all APKs (base + splits) will be re-signed with the same key and reassembled into a new XAPK."
### Step 5: Identify targets
### Step 5: Identify targets — Registry Scan
Run entry point detection to find which SDK calls exist in the app code:
The decoded directory contains smali bytecode. Use `registry-scan.py` to match against the SDK registry (29 SDKs, 123 entry points, 156 ad operations).
**5a. Run registry scan:**
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh "${DECODED_DIR}" --entrypoints
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh "${DECODED_DIR}" --entrypoints
python3 ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/registry-scan.py "${DECODED_DIR}" \
--registry "${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/registry/" \
--depth 1 --category all \
--output-dir "${DECODED_DIR}"
```
Present the results and ask the user what to neutralize:
- **Ads only** (`--ads`)
- **Trackers only** (`--trackers`)
- **Both** (`--all`, recommended)
Parse stdout:
- `MATCHED:` lines — present as a table (SDK name, category, target count)
- `UNKNOWN_PACKAGE:` lines — candidates for Step 5c
- `REGISTRY_TARGETS:` / `REGISTRY_MANIFEST:` — paths to generated files
### Step 6: Dry-run preview
**Depth levels**: Ask the user which depth to use:
- **Depth 1** (default, safest): only SDK init/start methods
- **Depth 2**: + ad load/show/cache methods
- **Depth 3**: + bulk-stub internal packages (aggressive, version-dependent)
Always run a dry-run first so the user can review what will be patched:
If the user requests depth 2 or 3, re-run with `--depth 2` or `--depth 3`.
**Fallback** (if Python 3 not available): use builtin hardcoded detection:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh "${DECODED_DIR}" --all --dry-run
```
Show the user the list of methods that would be patched and manifest components that would be disabled. Ask for explicit confirmation before proceeding. Remind the user about possible side effects for any SDK where the stub could cause breakage (especially `getInstance()` returning null).
**5b. Identify wrapper frameworks:**
Search for non-SDK packages that invoke known SDK methods — these are wrapper/bridge classes. Use Grep (auto-approved) to find invocations from app code:
```
Grep: pattern="invoke-.*Lcom/google/android/gms/ads|invoke-.*Lcom/unity3d/ads|invoke-.*Lcom/ironsource|invoke-.*Lcom/applovin"
path=<decoded-dir>/smali*/
```
Filter to non-SDK packages to identify wrappers. If found, add as `--package` targets.
**5c. Unknown SDK discovery (if registry reported UNKNOWN_PACKAGE candidates):**
For significant unknown packages (10+ classes, proper naming), use Claude Code built-in tools:
1. **Glob** to list main classes in the package
2. **Grep** for SDK patterns: `\.method.*(init|initialize|start|load|show)`, `const-string.*http`
3. **Read** key classes to understand the API
4. Classify: ads SDK, tracker, utility, or app code
Present unknown candidates as a table. If the user wants deep analysis:
**5d. Deep analysis (opt-in, requires user confirmation):**
Ask: "I found N unknown SDK candidates. Want me to research them via web search?"
For each confirmed candidate:
1. Web search for the package name
2. Read main smali classes for public API
3. Propose treatment and generate custom targets
**5e. Compile and confirm:**
Present the complete target summary (registry + custom + wrappers) and ask for confirmation:
- `--ads` / `--trackers` / `--all`
- Which SDKs to include/exclude
### Step 6: Dry-run preview
Always run a dry-run first. Use registry-driven mode when available:
```bash
# Registry-driven mode (preferred)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh "${DECODED_DIR}" \
--no-builtin-targets --dry-run \
--targets-file "${DECODED_DIR}/registry-targets.txt" \
--manifest-components-file "${DECODED_DIR}/registry-manifest.txt"
# Fallback (no Python)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh "${DECODED_DIR}" --all --dry-run
```
If wrapper packages were found, add `--package` flags. If custom targets were generated, append them to the targets file first.
Show the user what will be patched. Ask for explicit confirmation. Remind about side effects.
### Step 7: Neutralize
Apply the neutralization:
```bash
# Registry-driven mode (preferred)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh "${DECODED_DIR}" \
--no-builtin-targets \
--targets-file "${DECODED_DIR}/registry-targets.txt" \
--manifest-components-file "${DECODED_DIR}/registry-manifest.txt"
# Fallback (no Python)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh "${DECODED_DIR}" --all
```
@ -112,7 +181,20 @@ Parse the `PATCHED:` and `MANIFEST_DISABLED:` output lines for the report.
### Step 8: Rebuild & sign
**Before rebuilding**, ask the user their signing preference:
**If the input was an XAPK**, ask the user how they want the output:
> The original input was an XAPK (split APK bundle). How would you like to rebuild?
>
> 1. **Merged single APK** (recommended for sideloading) — merges split contents into one APK, installable with standard `adb install`. May be missing some locale/density resources.
> 2. **XAPK bundle** (preserves original structure) — requires `adb install-multiple` to install. All splits preserved exactly.
If the user chooses option 1, run `merge-splits.sh` before rebuilding:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/merge-splits.sh "${DECODED_DIR}"
```
**Then ask the user their signing preference**:
> How would you like to sign the rebuilt APK?
>
@ -136,10 +218,11 @@ bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECO
Parse the output for:
- `KEYSTORE_USED:<path>` — which keystore was used
- `KEYSTORE_SOURCE:<source>` — how it was resolved (debug-standard, debug-previous, debug-generated, custom)
- `KEYSTORE_ALIAS:<alias>` — the key alias used for signing
- `SPLIT_SIGNED:<filename>` — each re-signed split APK (XAPK only)
- `XAPK_ASSEMBLED:<path>` — final XAPK output (XAPK only)
For XAPK output: inform the user that install requires `adb install-multiple` or a split APK installer (SAI).
For XAPK output: inform the user that install requires `adb install-multiple` (unzip the XAPK first, then `adb install-multiple *.apk`).
### Step 9: Report & next steps
@ -151,7 +234,7 @@ Include in the report:
- **Install command**: `adb install <path>` for APK, `adb install-multiple <base.apk> <splits...>` for XAPK
Tell the user what they can do next:
- **Test thoroughly**: for APK: "Install via `adb install <apk>`"; for XAPK: "Install via `adb install-multiple` or use SAI (Split APKs Installer)" — test for crashes, especially features tied to ads or analytics
- **Test thoroughly**: for APK: "Install via `adb install <apk>`"; for XAPK: "Unzip the XAPK, then install via `adb install-multiple *.apk`" — test for crashes, especially features tied to ads or analytics
- **Verify**: "I can re-run entry point detection on the rebuilt APK to confirm neutralization"
- **Custom targets**: "If the app uses obfuscated SDK calls, provide a targets file for additional patching"
- **Deep analysis**: "Run `/find-trackers` or `/find-ads` for full SDK analysis"

View File

@ -28,10 +28,14 @@ Options:
--manifest Search only for AndroidManifest.xml ad markers
--entrypoints Search only for ad SDK calls in app code (excludes library packages)
--all Search all patterns (default)
--summary Output a compact summary table with confidence scoring
--json Output results as machine-readable JSON
-h, --help Show this help message
Output:
Results are printed as file:line:match for easy navigation.
Default: Results are printed as file:line:match for easy navigation.
--summary: Compact table with SDK | Sections | File Matches | Confidence | Status
--json: JSON object with per-SDK detection results and confidence scores
EOF
exit 0
}
@ -53,6 +57,8 @@ SEARCH_CONSENT=false
SEARCH_MANIFEST=false
SEARCH_ENTRYPOINTS=false
SEARCH_ALL=true
OUTPUT_SUMMARY=false
OUTPUT_JSON=false
while [[ $# -gt 0 ]]; do
case "$1" in
@ -72,6 +78,8 @@ while [[ $# -gt 0 ]]; do
--manifest) SEARCH_MANIFEST=true; SEARCH_ALL=false; shift ;;
--entrypoints) SEARCH_ENTRYPOINTS=true; SEARCH_ALL=false; shift ;;
--all) SEARCH_ALL=true; shift ;;
--summary) OUTPUT_SUMMARY=true; shift ;;
--json) OUTPUT_JSON=true; shift ;;
-h|--help) usage ;;
-*) echo "Error: Unknown option $1" >&2; usage ;;
*) SOURCE_DIR="$1"; shift ;;
@ -90,23 +98,99 @@ fi
GREP_OPTS="-rn --include=*.java --include=*.kt"
# Summary/JSON mode
if [[ "$OUTPUT_SUMMARY" == true ]] || [[ "$OUTPUT_JSON" == true ]]; then
SUMMARY_MODE=true
SEARCH_ALL=true
else
SUMMARY_MODE=false
fi
declare -A SDK_CLASS_MATCHES
declare -A SDK_STRING_MATCHES
declare -A SDK_SECTIONS_HIT
section() {
echo
echo "==== $1 ===="
echo
if [[ "$SUMMARY_MODE" == true ]]; then
CURRENT_SECTION="$1"
else
echo
echo "==== $1 ===="
echo
fi
}
run_grep() {
local pattern="$1"
# shellcheck disable=SC2086
grep $GREP_OPTS -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true
local result
result=$(grep $GREP_OPTS -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true)
if [[ "$SUMMARY_MODE" == true ]]; then
if [[ -n "$result" ]]; then
local count
count=$(echo "$result" | wc -l)
_tally_section_result "$count"
fi
else
echo "$result"
fi
}
run_grep_xml() {
local pattern="$1"
grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true
local result
result=$(grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true)
if [[ "$SUMMARY_MODE" == true ]]; then
if [[ -n "$result" ]]; then
local count
count=$(echo "$result" | wc -l)
_tally_section_result "$count"
fi
else
echo "$result"
fi
}
_tally_section_result() {
local count="$1"
local sdk=""
local confidence="class"
case "$CURRENT_SECTION" in
"AdMob"*) sdk="AdMob" ;;
"Unity Ads"*) sdk="UnityAds" ;;
"IronSource"*) sdk="IronSource" ;;
"AppLovin"*) sdk="AppLovin" ;;
"Meta"*|"Meta AN"*) sdk="MetaAN" ;;
"Vungle"*) sdk="Vungle" ;;
"InMobi"*) sdk="InMobi" ;;
"Chartboost"*) sdk="Chartboost" ;;
"Pangle"*) sdk="Pangle" ;;
"Mintegral"*) sdk="Mintegral" ;;
"Cross-SDK"*) sdk="CrossSDK"; confidence="string" ;;
"Mediation"*) sdk="Mediation"; confidence="class" ;;
"Consent"*) sdk="Consent"; confidence="class" ;;
"AndroidManifest"*) sdk="Manifest"; confidence="class" ;;
"Entry Points"*) sdk="EntryPoints"; confidence="class" ;;
*) sdk="Other"; confidence="string" ;;
esac
if [[ "$confidence" == "class" ]]; then
SDK_CLASS_MATCHES["$sdk"]=$(( ${SDK_CLASS_MATCHES["$sdk"]:-0} + count ))
else
SDK_STRING_MATCHES["$sdk"]=$(( ${SDK_STRING_MATCHES["$sdk"]:-0} + count ))
fi
local existing="${SDK_SECTIONS_HIT["$sdk"]:-}"
if [[ -n "$existing" ]]; then
SDK_SECTIONS_HIT["$sdk"]="${existing}, ${CURRENT_SECTION}"
else
SDK_SECTIONS_HIT["$sdk"]="${CURRENT_SECTION}"
fi
}
CURRENT_SECTION=""
# --- AdMob / Google Mobile Ads ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_ADMOB" == true ]]; then
section "AdMob — Initialization"
@ -285,12 +369,102 @@ if [[ "$SEARCH_ALL" == true || "$SEARCH_ENTRYPOINTS" == true ]]; then
ENTRYPOINT_PATTERN='(MobileAds\.initialize|MobileAds\.setRequestConfiguration|InterstitialAd\.load|RewardedAd\.load|RewardedInterstitialAd\.load|AppOpenAd\.load|AdView\.loadAd|AdLoader\.Builder|UnityAds\.initialize|UnityAds\.load|UnityAds\.show|IronSource\.init|IronSource\.loadInterstitial|IronSource\.showInterstitial|IronSource\.showRewardedVideo|IronSource\.loadBanner|AppLovinSdk\.getInstance|AppLovinSdk\.initializeSdk|MaxInterstitialAd|MaxRewardedAd|MaxAdView|AudienceNetworkAds\.initialize|Vungle\.init|Vungle\.loadAd|Vungle\.playAd|InMobiSdk\.init|Chartboost\.startWithAppId|TTAdSdk\.init|PAGSdk\.init|MBridgeSDKFactory\.getMBridgeSDK|LevelPlayInterstitialAd|LevelPlayRewardedAd|LevelPlayBannerAdView)'
# shellcheck disable=SC2086
grep -rn --include="*.java" --include="*.kt" "${EXCLUDE_DIRS[@]}" -E "$ENTRYPOINT_PATTERN" "$SOURCE_DIR" 2>/dev/null || true
echo
echo "NOTE: Only calls from app code are shown above. Library-internal calls are excluded."
echo "If no results appear, ad SDKs may only be invoked internally via mediation adapters."
local ep_result
ep_result=$(grep -rn --include="*.java" --include="*.kt" "${EXCLUDE_DIRS[@]}" -E "$ENTRYPOINT_PATTERN" "$SOURCE_DIR" 2>/dev/null || true)
if [[ "$SUMMARY_MODE" == true ]]; then
if [[ -n "$ep_result" ]]; then
local count
count=$(echo "$ep_result" | wc -l)
_tally_section_result "$count"
fi
else
echo "$ep_result"
echo
echo "NOTE: Only calls from app code are shown above. Library-internal calls are excluded."
echo "If no results appear, ad SDKs may only be invoked internally via mediation adapters."
fi
fi
echo
echo "=== Search complete ==="
# =====================================================================
# Summary / JSON output
# =====================================================================
if [[ "$SUMMARY_MODE" == true ]]; then
AD_SDKS=("AdMob" "UnityAds" "IronSource" "AppLovin" "MetaAN" "Vungle" "InMobi" "Chartboost" "Pangle" "Mintegral")
if [[ "$OUTPUT_JSON" == true ]]; then
printf '{\n "ad_sdks": [\n'
first=true
for sdk in "${AD_SDKS[@]}"; do
class_count=${SDK_CLASS_MATCHES["$sdk"]:-0}
string_count=${SDK_STRING_MATCHES["$sdk"]:-0}
total=$((class_count + string_count))
if [[ $class_count -gt 0 ]]; then
confidence="HIGH"
elif [[ $string_count -gt 0 ]]; then
confidence="MEDIUM"
else
confidence="NONE"
fi
[[ "$confidence" == "NONE" ]] && continue
if [[ "$first" == true ]]; then
first=false
else
printf ',\n'
fi
sections="${SDK_SECTIONS_HIT["$sdk"]:-}"
printf ' {"sdk": "%s", "class_matches": %d, "string_matches": %d, "total_matches": %d, "confidence": "%s", "sections": "%s"}' \
"$sdk" "$class_count" "$string_count" "$total" "$confidence" "$sections"
done
printf '\n ]\n}\n'
else
echo
echo "=== Ad SDK Detection Summary ==="
echo
printf "%-15s | %6s | %7s | %5s | %-10s | %s\n" "SDK" "Class" "String" "Total" "Confidence" "Status"
printf "%-15s-|-%6s-|-%7s-|-%5s-|-%-10s-|-%s\n" "---------------" "------" "-------" "-----" "----------" "--------"
for sdk in "${AD_SDKS[@]}"; do
class_count=${SDK_CLASS_MATCHES["$sdk"]:-0}
string_count=${SDK_STRING_MATCHES["$sdk"]:-0}
total=$((class_count + string_count))
if [[ $class_count -gt 0 ]]; then
confidence="HIGH"
status="DETECTED"
elif [[ $string_count -gt 0 ]]; then
confidence="MEDIUM"
status="LIKELY"
else
confidence="NONE"
status="NOT FOUND"
fi
printf "%-15s | %6d | %7d | %5d | %-10s | %s\n" \
"$sdk" "$class_count" "$string_count" "$total" "$confidence" "$status"
done
ep_count=${SDK_CLASS_MATCHES["EntryPoints"]:-0}
if [[ $ep_count -gt 0 ]]; then
echo
echo "Entry points in app code: $ep_count match(es)"
fi
mediation_count=${SDK_CLASS_MATCHES["Mediation"]:-0}
if [[ $mediation_count -gt 0 ]]; then
echo "Mediation adapters: $mediation_count match(es)"
fi
consent_count=${SDK_CLASS_MATCHES["Consent"]:-0}
if [[ $consent_count -gt 0 ]]; then
echo "Consent framework: $consent_count match(es)"
fi
echo
echo "Confidence: HIGH = SDK-specific classes found, MEDIUM = only generic/string matches, NONE = not detected"
fi
else
echo
echo "=== Search complete ==="
fi

View File

@ -21,6 +21,9 @@ Options:
--deobf Enable deobfuscation of names
--no-res Skip resource decoding (faster, code-only)
--engine ENGINE Decompiler engine: jadx, fernflower, or both (default: jadx)
--timeout SECS Global timeout in seconds (default: 600). 0 = no timeout.
--threads N Number of decompiler threads (passed as -j N to jadx)
--heap SIZE JVM heap size (e.g., 4g, 8g). Default: auto-sized by input file size.
-h, --help Show this help message
Engines:
@ -49,6 +52,9 @@ DEOBF=false
NO_RES=false
ENGINE="jadx"
INPUT_FILE=""
TIMEOUT=600
THREADS=""
HEAP_SIZE=""
while [[ $# -gt 0 ]]; do
case "$1" in
@ -56,6 +62,9 @@ while [[ $# -gt 0 ]]; do
--deobf) DEOBF=true; shift ;;
--no-res) NO_RES=true; shift ;;
--engine) ENGINE="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--threads) THREADS="$2"; shift 2 ;;
--heap) HEAP_SIZE="$2"; shift 2 ;;
-h|--help) usage ;;
-*) echo "Error: Unknown option $1" >&2; usage ;;
*) INPUT_FILE="$1"; shift ;;
@ -98,6 +107,25 @@ if [[ -z "$OUTPUT_DIR" ]]; then
OUTPUT_DIR="${BASENAME}-decompiled"
fi
# --- Auto-size JVM heap based on input file size ---
if [[ -z "$HEAP_SIZE" ]]; then
file_size_bytes=$(stat -c%s "$INPUT_FILE_ABS" 2>/dev/null || stat -f%z "$INPUT_FILE_ABS" 2>/dev/null || echo 0)
file_size_mb=$((file_size_bytes / 1048576))
if [[ "$file_size_mb" -gt 100 ]]; then
HEAP_SIZE="6g"
elif [[ "$file_size_mb" -gt 30 ]]; then
HEAP_SIZE="4g"
else
HEAP_SIZE="2g"
fi
echo "Auto-sized JVM heap: -Xmx${HEAP_SIZE} (input: ${file_size_mb}MB)"
fi
# Export JVM heap for jadx and fernflower
export JAVA_OPTS="-Xmx${HEAP_SIZE}"
# jadx also reads JADX_OPTS
export JADX_OPTS="-Xmx${HEAP_SIZE}"
# --- 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.
@ -106,6 +134,7 @@ XAPK_APK_FILES=()
if [[ "$ext_lower" == "xapk" ]]; then
XAPK_EXTRACTED_DIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-extract-XXXXXX")
trap 'rm -rf "$XAPK_EXTRACTED_DIR"' EXIT
echo "=== Extracting XAPK archive ==="
unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_EXTRACTED_DIR"
@ -179,10 +208,22 @@ run_jadx() {
[[ "$DEOBF" == true ]] && args+=("--deobf")
[[ "$NO_RES" == true ]] && args+=("--no-res")
args+=("--show-bad-code")
[[ -n "$THREADS" ]] && args+=("-j" "$THREADS")
args+=("$INPUT_FILE_ABS")
echo "Running: jadx ${args[*]}"
jadx "${args[@]}"
if [[ "$TIMEOUT" -gt 0 ]] 2>/dev/null; then
if ! timeout "${TIMEOUT}s" jadx "${args[@]}"; then
local exit_code=$?
if [[ $exit_code -eq 124 ]]; then
echo "WARNING: jadx timed out after ${TIMEOUT}s. Partial output may be available." >&2
else
return $exit_code
fi
fi
else
jadx "${args[@]}"
fi
echo "jadx output: $out_dir/sources/"
if [[ -d "$out_dir/sources" ]]; then
@ -237,8 +278,19 @@ run_fernflower() {
ff_args+=("$jar_to_decompile")
ff_args+=("$out_dir")
echo "Running: java -jar $ff_jar ${ff_args[*]}"
java -jar "$ff_jar" "${ff_args[@]}"
echo "Running: java -Xmx${HEAP_SIZE} -jar $ff_jar ${ff_args[*]}"
if [[ "$TIMEOUT" -gt 0 ]] 2>/dev/null; then
if ! timeout "${TIMEOUT}s" java "-Xmx${HEAP_SIZE}" -jar "$ff_jar" "${ff_args[@]}"; then
local exit_code=$?
if [[ $exit_code -eq 124 ]]; then
echo "WARNING: Fernflower timed out after ${TIMEOUT}s. Partial output may be available." >&2
else
return $exit_code
fi
fi
else
java "-Xmx${HEAP_SIZE}" -jar "$ff_jar" "${ff_args[@]}"
fi
# Fernflower outputs a JAR containing .java files — extract it
local result_jar="$out_dir/$(basename "$jar_to_decompile")"
@ -363,11 +415,14 @@ if [[ "$ext_lower" == "xapk" ]]; then
apk_name=$(basename "$apk_file" .apk)
echo
echo "======================================================"
decompile_single "$apk_file" "$OUTPUT_DIR/$apk_name" "$apk_name.apk"
if ! decompile_single "$apk_file" "$OUTPUT_DIR/$apk_name" "$apk_name.apk"; then
echo "WARNING: Failed to decompile $apk_name.apk, continuing with remaining APKs..." >&2
fi
done
# Cleanup extracted XAPK
rm -rf "$XAPK_EXTRACTED_DIR"
trap - EXIT
echo
echo "=== XAPK decompilation complete ==="

View File

@ -98,86 +98,135 @@ If the input is an XAPK, inform the user that it's a split APK bundle. Let them
### Phase 3: Identify Targets
Target identification has three sub-phases. The goal is to find all SDK entry points to neutralize while minimizing manual approval prompts.
Target identification has four sub-phases. The goal is to combine deterministic registry matching with heuristic discovery for maximum coverage.
#### Phase 3a — Built-in Catalog Detection
**Depth levels** control how aggressively SDKs are neutralized:
- **Depth 1** (default, safest): Only SDK entry points (init, start). Disables SDK initialization.
- **Depth 2**: Entry points + ad operations (load, show, cache). Safety net if init stub is bypassed.
- **Depth 3** (most aggressive): All above + deep patterns (bulk-stub internal packages). Version-dependent.
Use `neutralize.sh --dry-run` to detect known SDK targets. This is more reliable than `find-ads.sh`/`find-trackers.sh` because it searches smali directly (not Java source) and matches the exact patterns that will be patched.
Ask the user which depth level to use. Default to depth 1 unless they request more.
**Action**: Run dry-run detection.
#### Phase 3a — Registry Scan (Known SDKs)
Run `registry-scan.py` to match the decoded APK against the SDK registry (29 SDKs, 123 entry points, 156 ad operations, 30 deep patterns).
**Action**: Run registry scan.
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/registry-scan.py "<decoded-dir>" \
--registry "${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/registry/" \
--depth 1 --category all \
--output-dir "<decoded-dir>"
```
Parse stdout for:
- `MATCHED:<sdk_id>:<display_name>:<category>:<n_targets>` — matched SDK with target count
- `UNKNOWN_PACKAGE:<package>:<class_count>` — unknown packages (candidates for Phase 3b)
- `REGISTRY_TARGETS:<path>` — generated targets file for neutralize.sh
- `REGISTRY_MANIFEST:<path>` — generated manifest components file
Present matched SDKs as a table:
| SDK | Category | Depth | Targets | Manifest Components |
|---|---|---|---|---|
| Google AdMob | ads | 1 | 2 entry points | 3 components |
| Firebase Analytics | analytics | 1 | 8 entry points | 7 components |
| AppsFlyer | attribution | 1 | 19 entry points | 3 components |
| ... | | | | |
If the user requests **depth 2 or 3**, re-run registry-scan.py with the higher depth level.
**Fallback**: If Python 3 is not available, fall back to the builtin catalog:
```bash
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run
```
Parse the output for:
- `DRY_RUN:WOULD_PATCH:` lines — smali methods that would be stubbed
- `DRY_RUN:WOULD_DISABLE:` lines — manifest components that would be disabled
**NOTE**: Do NOT use `find-ads.sh` or `find-trackers.sh` here — those scripts search Java/Kotlin source (`.java`, `.kt`), not smali. The decoded directory from Phase 2 contains only smali bytecode, so those scripts will find nothing.
#### Phase 3b — Custom/Proprietary SDK Discovery (if needed)
#### Phase 3b — Unknown SDK Discovery
Activate this sub-phase when:
- The user mentions a specific SDK not in the built-in catalog (e.g., `guru/ads/fusion`, `com/proprietary/analytics`)
- The dry-run found few or no targets but the user expects more
- The user asks to find "all" trackers/ads, including custom/proprietary ones
- `registry-scan.py` reported `UNKNOWN_PACKAGE:` candidates
- The user asks to discover SDKs beyond the registry
- Few matches in Phase 3a but the user expects more
**CRITICAL — Use Claude Code built-in tools, NOT bash commands.** Glob, Grep, and Read are auto-approved and require no manual user approval. Using `bash find`, `bash grep`, or `bash head` forces the user to approve each command individually.
The registry scan automatically filters unknowns:
- Excludes obfuscated packages (single-letter names like `a/`, `b/c/`)
- Excludes known utility libraries (okhttp, retrofit, gson, protobuf, kotlinx, androidx, etc.)
- Excludes the app's own package (from AndroidManifest)
- Only includes packages with 10+ classes and 3+ name segments
**Discovery workflow using built-in tools:**
**CRITICAL — Use Claude Code built-in tools, NOT bash commands.** Glob, Grep, and Read are auto-approved.
1. **Find smali files by package pattern** — use Glob:
**Discovery workflow** for each unknown package candidate:
1. **List main classes** — use Glob:
```
Glob: **/smali*/com/guru/**/*.smali
Glob: **/smali*/com/proprietary/analytics/**/*.smali
Glob: **/smali*/com/vendor/sdk/**/*.smali
```
2. **Find SDK method signatures** — use Grep on the matched files:
2. **Search for SDK patterns** — use Grep:
```
Grep: pattern="^\.method.*(init|track|log|show|load|send|report|emit)"
Grep: pattern="^\.method.*getInstance"
Grep: pattern="\.method.*(init|initialize|start|load|show)" path=<smali-dir>/com/vendor/sdk/
Grep: pattern="const-string.*http" path=<smali-dir>/com/vendor/sdk/
```
3. **Find SDK invocations from app code** — use Grep on the app's own smali:
3. **Check manifest** for components (activities, services, providers, receivers) with that package.
4. **Classify**: "probable SDK ads", "probable SDK tracker", "utility library", "app code"
Present results as a table:
| Package | Classes | SDK Patterns | Classification | Suggested Action |
|---|---|---|---|---|
| `com/vendor/analytics` | 45 | init, logEvent, URL endpoints | Tracker | Deep analysis |
| `com/vendor/mediator` | 120 | no direct init | Mediator/wrapper | Check SDK refs |
| `org/example/util` | 8 | no SDK patterns | Utility library | Ignore |
#### Phase 3c — Deep Analysis (opt-in, explicit confirmation required)
**IMPORTANT**: Before proceeding, present the candidates and ask:
> "I identified N SDK candidates for deep analysis. This involves web search and smali reverse engineering. Which ones should I analyze? (list numbers or 'all')"
**Only after user confirmation**, for each selected SDK:
1. **Web search**: Search for the package name to identify the SDK:
- `"com.vendor.sdk" android SDK`
- `site:maven.org "com.vendor.sdk"`
- `"com.vendor.sdk" gradle dependency`
2. **Read main smali classes**: Identify the public API (init, config, entry points).
3. **Propose to the user**: "This appears to be **X SDK** version Y, used for Z. Entry points found: `init()`, `start()`, `logEvent()`. Neutralize it?"
4. **If confirmed**, generate entry for targets file:
```
Grep: pattern="invoke-(static|virtual|direct).*Lcom/guru/ads/"
Grep: pattern="invoke-(static|virtual|direct).*Lcom/proprietary/analytics/"
# [X SDK] discovered via deep analysis
com/vendor/sdk/MainClass:init
com/vendor/sdk/MainClass:start
```
4. **Examine specific files** — use Read to inspect method bodies and confirm they are SDK entry points worth neutralizing.
5. **Optionally**, propose a draft registry JSON entry for future inclusion.
**Build the custom targets file** with discovered entry points, one per line, in the format:
#### Phase 3d — Compile & Confirm
```
<smali-class-path>:<method-name>
```
Merge all target sources:
- **Registry targets** from Phase 3a (`registry-targets.txt`)
- **Custom discovery** from Phase 3b/3c (append to `custom-targets.txt`)
- **User-provided** `--targets-file` if any
For example:
```
smali/com/guru/ads/fusion/FusionAd.smali:initialize
smali/com/guru/ads/fusion/FusionAd.smali:showAd
smali/com/guru/ads/fusion/FusionTracker.smali:trackEvent
```
Present the complete summary:
**Action**: Write the targets file.
| SDK | Category | Source | Depth | Targets | Manifest Components |
|---|---|---|---|---|---|
| Google AdMob | ads | Registry | 1 | 2 methods | 3 components |
| Firebase Analytics | analytics | Registry | 1 | 8 methods | 7 components |
| guru/ads/fusion | ads | Discovery | - | 3 methods | 0 components |
| ... | | | | | |
```
Write: <decoded-dir>/custom-targets.txt
```
**Ads vs Trackers distinction**: Always present ads and trackers separately — they have different implications (revenue impact vs privacy).
#### Phase 3c — Compile Target List and Confirm
Present a summary table of all targets to the user:
| SDK | Category | Source | Entry Points |
|---|---|---|---|
| AdMob | Ads | Built-in catalog | 5 methods, 2 components |
| Firebase Analytics | Trackers | Built-in catalog | 3 methods, 1 component |
| guru/ads/fusion | Ads | Custom discovery | 3 methods |
| ... | | | |
Ask which categories/SDKs to neutralize:
Ask for final confirmation:
- Which categories/SDKs to neutralize
- `--ads` — only ad SDKs
- `--trackers` — only tracker/analytics SDKs
- `--all` — both (default)
@ -187,34 +236,55 @@ Ask which categories/SDKs to neutralize:
Run the neutralization script. **Always run a dry-run first** to preview changes.
**Action**: Dry-run, then apply.
#### Registry-driven mode (preferred, requires Python 3.6+)
Uses `registry-targets.txt` and `registry-manifest.txt` generated by Phase 3a. The `--no-builtin-targets` flag disables hardcoded targets to rely entirely on the registry.
```bash
# Preview changes (no files modified)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run
# Preview (dry-run)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> \
--no-builtin-targets --dry-run \
--targets-file <decoded-dir>/registry-targets.txt \
--manifest-components-file <decoded-dir>/registry-manifest.txt
# Apply changes (with backups)
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all
# Apply
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> \
--no-builtin-targets \
--targets-file <decoded-dir>/registry-targets.txt \
--manifest-components-file <decoded-dir>/registry-manifest.txt
```
**If Phase 3b produced custom targets**, add `--targets-file` to both commands:
**If Phase 3b/3c produced custom targets**, add a second `--targets-file` or append to the registry file:
```bash
# Preview with custom targets
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run --targets-file <decoded-dir>/custom-targets.txt
# Append custom targets to registry targets
cat <decoded-dir>/custom-targets.txt >> <decoded-dir>/registry-targets.txt
```
# Apply with custom targets
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --targets-file <decoded-dir>/custom-targets.txt
#### Fallback mode (no Python)
If Python 3 is not available, use the builtin hardcoded targets:
```bash
# Preview
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run
# Apply
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all
```
Parse the output for `PATCHED:` and `MANIFEST_DISABLED:` lines to build the report.
Options:
- `--ads` / `--trackers` / `--all` — target selection
#### Options reference
- `--no-builtin-targets` — skip hardcoded target functions, rely on `--targets-file` + `--manifest-components-file`
- `--targets-file <file>` — load targets from file (registry-scan.py output or custom)
- `--manifest-components-file <file>` — load manifest components from file (registry-scan.py output)
- `--ads` / `--trackers` / `--all` — target selection (for builtin mode)
- `--dry-run` — preview only
- `--no-backup` — skip `.smali.bak` creation
- `--no-manifest` — skip manifest patching
- `--targets-file <file>` — additional custom targets
- `--package <path>` — neutralize all methods in a package recursively
- `--replay` — replay patches from a previous `neutralize-manifest.json` (useful after re-decode)
- `--no-save-manifest` — skip saving `neutralize-manifest.json`
@ -231,7 +301,7 @@ Rebuild the decoded directory back into a signed APK (or XAPK if the original wa
> How would you like to rebuild the neutralized app?
>
> 1. **Merged single APK** (recommended for sideloading) — merges split contents into one APK, installable with standard `adb install`. May be missing some locale/density resources.
> 2. **XAPK bundle** (preserves original structure) — requires `adb install-multiple` or SAI app to install. All splits preserved exactly.
> 2. **XAPK bundle** (preserves original structure) — requires `adb install-multiple` to install. All splits preserved exactly.
**If the user chooses option 1 (merged single APK):**
@ -376,7 +446,7 @@ If the original input was an XAPK and the user chose merged single APK output, i
- Signed with: auto-detected debug key / generated debug key / custom keystore
- Keystore used: `<path>` (source: `KEYSTORE_SOURCE:` value)
- Install via: `adb install <path>` (APK / merged APK) or `adb install-multiple <base.apk> <split1.apk> ...` (XAPK)
- For XAPK: can also use SAI (Split APKs Installer) or unzip and `adb install-multiple *.apk`
- For XAPK: unzip the XAPK and run `adb install-multiple *.apk`
```
**Next steps to suggest:**

View File

@ -0,0 +1,188 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "sdk-registry-entry.schema.json",
"title": "SDK Neutralization Registry Entry",
"description": "Defines which public API methods of a third-party SDK can be safely neutralized (stubbed) in a decompiled Android APK.",
"type": "object",
"required": ["sdk_id", "display_name", "vendor", "category", "packages", "targets"],
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string"
},
"sdk_id": {
"type": "string",
"pattern": "^[a-z0-9-]+$",
"description": "Unique lowercase identifier for this SDK (used as filename stem)"
},
"display_name": {
"type": "string",
"description": "Human-readable SDK name"
},
"description": {
"type": "string"
},
"vendor": {
"type": "string"
},
"category": {
"type": "string",
"enum": ["ads", "analytics", "attribution", "crash_reporting", "ads_mediation", "social"]
},
"known_versions": {
"type": "array",
"items": { "type": "string" },
"description": "SDK versions tested against (for reference only)"
},
"packages": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "Root Java/Kotlin packages of this SDK"
},
"obfuscation": {
"type": "object",
"properties": {
"public_api": {
"type": "string",
"enum": ["readable", "mixed", "obfuscated"]
},
"internals": {
"type": "string",
"enum": ["readable", "mixed", "obfuscated"]
},
"internal_prefix": {
"type": "string",
"description": "If known, the obfuscation prefix pattern (e.g. 'zz', single letters)"
}
},
"additionalProperties": false
},
"targets": {
"type": "object",
"additionalProperties": false,
"properties": {
"entry_points": {
"type": "array",
"items": { "$ref": "#/definitions/class_target" },
"description": "Level 1: SDK init methods. Stubbing these disables the entire SDK."
},
"ad_operations": {
"type": "array",
"items": { "$ref": "#/definitions/class_target" },
"description": "Level 2: load/show/cache methods. Safety net if init stub is bypassed."
},
"deep_patterns": {
"type": "array",
"items": { "$ref": "#/definitions/deep_pattern" },
"description": "Level 3: Broad internal patterns. Use for well-known SDK internals."
}
}
},
"manifest_components": {
"type": "array",
"items": { "$ref": "#/definitions/manifest_component" },
"description": "AndroidManifest components to disable/remove"
},
"protected_patterns": {
"type": "array",
"items": { "$ref": "#/definitions/protected_pattern" },
"description": "Methods/patterns that must NEVER be stubbed in this SDK"
}
},
"definitions": {
"class_target": {
"type": "object",
"required": ["class", "methods"],
"additionalProperties": false,
"properties": {
"class": {
"type": "string",
"description": "Fully qualified Java class name (dot-separated)"
},
"methods": {
"type": "array",
"items": { "$ref": "#/definitions/method_target" },
"minItems": 1
}
}
},
"method_target": {
"type": "object",
"required": ["name", "stub"],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "Method name"
},
"signature": {
"type": "string",
"description": "Smali-style signature or '*' for all overloads"
},
"stub": {
"type": "string",
"enum": ["return-void", "return-false/0", "return-null"],
"description": "Stub type to apply"
},
"note": {
"type": "string"
}
}
},
"deep_pattern": {
"type": "object",
"required": ["package_glob", "rule"],
"additionalProperties": false,
"properties": {
"package_glob": {
"type": "string",
"description": "Glob pattern for packages (e.g. 'com.google.android.gms.internal.ads.**')"
},
"rule": {
"type": "string",
"enum": ["stub_all_void", "stub_all_concrete", "stub_all_non_getter"],
"description": "How aggressively to stub matched classes"
},
"note": {
"type": "string"
}
}
},
"manifest_component": {
"type": "object",
"required": ["type", "class", "action"],
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"enum": ["activity", "service", "receiver", "provider"]
},
"class": {
"type": "string"
},
"action": {
"type": "string",
"enum": ["disable", "remove"]
},
"note": {
"type": "string"
}
}
},
"protected_pattern": {
"type": "object",
"required": ["pattern", "reason"],
"additionalProperties": false,
"properties": {
"pattern": {
"type": "string",
"description": "Glob or regex pattern for methods to protect"
},
"reason": {
"type": "string"
}
}
}
}
}

View File

@ -0,0 +1,47 @@
{
"$schema": "./_schema.json",
"sdk_id": "adjust",
"display_name": "Adjust",
"description": "Adjust mobile attribution and analytics SDK",
"vendor": "Adjust",
"category": "attribution",
"known_versions": [],
"packages": [
"com.adjust.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "com.adjust.sdk.Adjust",
"methods": [
{ "name": "onCreate", "signature": "*", "stub": "return-void", "note": "Main SDK init" },
{ "name": "trackEvent", "signature": "*", "stub": "return-void", "note": "Event tracking" },
{ "name": "setEnabled", "signature": "*", "stub": "return-void" },
{ "name": "addSessionCallbackParameter", "signature": "*", "stub": "return-void" },
{ "name": "addSessionPartnerParameter", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": [
{
"package_glob": "com.adjust.sdk.**",
"rule": "stub_all_void",
"note": "Internal SDK classes"
}
]
},
"manifest_components": [
{
"type": "receiver",
"class": "com.adjust.sdk.AdjustReferrerReceiver",
"action": "disable",
"note": "Install referrer receiver"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,49 @@
{
"$schema": "./_schema.json",
"sdk_id": "amazon-ads",
"display_name": "Amazon Ads",
"description": "Amazon Mobile Ads SDK (APS / TAM)",
"vendor": "Amazon",
"category": "ads",
"known_versions": [],
"packages": [
"com.amazon.device.ads"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.amazon.device.ads.AdRegistration",
"methods": [
{ "name": "setAppKey", "signature": "*", "stub": "return-void", "note": "SDK config — all overloads" },
{ "name": "enableLogging", "signature": "*", "stub": "return-void" },
{ "name": "enableTesting", "signature": "*", "stub": "return-void" },
{ "name": "setMRAIDPolicy", "signature": "*", "stub": "return-void" },
{ "name": "setMRAIDSupportedVersions", "signature": "*", "stub": "return-void" },
{ "name": "setAdNetworkInfo", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [
{
"class": "com.amazon.device.ads.DTBAdRequest",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": []
},
"manifest_components": [
{
"type": "activity",
"class": "com.amazon.device.ads.DTBAdActivity",
"action": "disable",
"note": "Ad display activity"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,98 @@
{
"$schema": "./_schema.json",
"sdk_id": "applovin",
"display_name": "AppLovin / MAX",
"description": "AppLovin SDK and MAX mediation platform",
"vendor": "AppLovin",
"category": "ads_mediation",
"known_versions": ["13.3.0"],
"packages": [
"com.applovin.sdk",
"com.applovin.mediation",
"com.applovin.impl",
"com.applovin.adview"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated",
"internal_prefix": "single-letter"
},
"targets": {
"entry_points": [
{
"class": "com.applovin.sdk.AppLovinSdk",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Main SDK init" },
{ "name": "initializeSdk", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [
{
"class": "com.applovin.mediation.MaxInterstitialAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "showAd", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.applovin.mediation.MaxRewardedAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "showAd", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.applovin.mediation.MaxAdView",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" },
{ "name": "startAutoRefresh", "signature": "*", "stub": "return-void" },
{ "name": "stopAutoRefresh", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.applovin.impl.**",
"rule": "stub_all_concrete",
"note": "Internal obfuscated implementation — version-dependent"
},
{
"package_glob": "com.applovin.mediation.adapters.**",
"rule": "stub_all_void",
"note": "Third-party adapters bundled with MAX"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.applovin.adview.AppLovinFullscreenActivity",
"action": "disable",
"note": "Fullscreen ad display activity"
},
{
"type": "activity",
"class": "com.applovin.sdk.AppLovinWebViewActivity",
"action": "disable",
"note": "WebView-based ad activity"
},
{
"type": "provider",
"class": "com.applovin.sdk.AppLovinInitProvider",
"action": "disable",
"note": "Auto-init ContentProvider"
}
],
"protected_patterns": [
{
"pattern": "*.getActivity()*",
"reason": "Returns Activity reference — null causes NPE"
}
]
}

View File

@ -0,0 +1,98 @@
{
"$schema": "./_schema.json",
"sdk_id": "appsflyer",
"display_name": "AppsFlyer",
"description": "AppsFlyer mobile attribution and analytics SDK",
"vendor": "AppsFlyer",
"category": "attribution",
"known_versions": [],
"packages": [
"com.appsflyer"
],
"obfuscation": {
"public_api": "mixed",
"internals": "obfuscated",
"internal_prefix": "AF"
},
"targets": {
"entry_points": [
{
"class": "com.appsflyer.internal.AFa1uSDK",
"methods": [
{ "name": "start", "signature": "*", "stub": "return-void", "note": "Main SDK start — all overloads" }
]
},
{
"class": "com.appsflyer.unity.AppsFlyerAndroidWrapper",
"methods": [
{ "name": "initSdk", "signature": "*", "stub": "return-void", "note": "Unity wrapper init" },
{ "name": "startTracking", "signature": "*", "stub": "return-void" },
{ "name": "logEvent", "signature": "*", "stub": "return-void" },
{ "name": "setCustomerUserId", "signature": "*", "stub": "return-void" },
{ "name": "setCurrencyCode", "signature": "*", "stub": "return-void" },
{ "name": "setAdditionalData", "signature": "*", "stub": "return-void" },
{ "name": "sendPushNotificationData", "signature": "*", "stub": "return-void" },
{ "name": "setUserEmails", "signature": "*", "stub": "return-void" },
{ "name": "setDebug", "signature": "*", "stub": "return-void" },
{ "name": "getConversionData", "signature": "*", "stub": "return-void" },
{ "name": "attributeAndOpenStore", "signature": "*", "stub": "return-void" },
{ "name": "recordLocation", "signature": "*", "stub": "return-void" },
{ "name": "recordCrossPromoteImpression", "signature": "*", "stub": "return-void" },
{ "name": "setPhoneNumber", "signature": "*", "stub": "return-void" },
{ "name": "setSharingFilterForPartners", "signature": "*", "stub": "return-void" },
{ "name": "setPartnerData", "signature": "*", "stub": "return-void" },
{ "name": "setDisableNetworkData", "signature": "*", "stub": "return-void" },
{ "name": "performLatestCPI", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": [
{
"package_glob": "com.appsflyer.internal.**",
"rule": "stub_all_void",
"note": "Internal obfuscated classes (AF* prefix) — 649+ methods, V1-proven safe"
},
{
"package_glob": "com.appsflyer.share.**",
"rule": "stub_all_void",
"note": "Sharing module — 11 methods, V1-proven safe"
},
{
"package_glob": "com.appsflyer.unity.**",
"rule": "stub_all_void",
"note": "Unity wrapper — 99+ methods, V1-proven safe"
}
]
},
"manifest_components": [
{
"type": "receiver",
"class": "com.appsflyer.MultipleInstallBroadcastReceiver",
"action": "disable",
"note": "Install referrer receiver"
},
{
"type": "receiver",
"class": "com.appsflyer.SingleInstallBroadcastReceiver",
"action": "disable",
"note": "Single install referrer receiver"
},
{
"type": "receiver",
"class": "com.appsflyer.internal.AFSingleInstallBroadcastReceiver",
"action": "disable",
"note": "Internal single install referrer receiver"
}
],
"protected_patterns": [
{
"pattern": "*.getActivity()*",
"reason": "Returns Activity reference — null causes NPE"
},
{
"pattern": "*.getApplicationContext()*",
"reason": "Returns Context — null causes NPE"
}
]
}

View File

@ -0,0 +1,59 @@
{
"$schema": "./_schema.json",
"sdk_id": "bidmachine",
"display_name": "BidMachine",
"description": "BidMachine programmatic ad SDK",
"vendor": "BidMachine",
"category": "ads",
"known_versions": [],
"packages": [
"io.bidmachine"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "io.bidmachine.BidMachine",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Main SDK init" }
]
}
],
"ad_operations": [
{
"class": "io.bidmachine.interstitial.InterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "io.bidmachine.rewarded.RewardedAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "io.bidmachine.banner.BannerView",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": []
},
"manifest_components": [
{
"type": "provider",
"class": "io.bidmachine.BidMachineInitProvider",
"action": "disable",
"note": "Auto-init ContentProvider"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,54 @@
{
"$schema": "./_schema.json",
"sdk_id": "braze",
"display_name": "Braze",
"description": "Braze (formerly Appboy) customer engagement SDK",
"vendor": "Braze",
"category": "analytics",
"known_versions": [],
"packages": [
"com.braze",
"com.appboy"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "com.braze.Braze",
"methods": [
{ "name": "configure", "signature": "*", "stub": "return-void", "note": "SDK configuration" },
{ "name": "logCustomEvent", "signature": "*", "stub": "return-void" },
{ "name": "changeUser", "signature": "*", "stub": "return-void" },
{ "name": "logPurchase", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.appboy.Appboy",
"methods": [
{ "name": "configure", "signature": "*", "stub": "return-void", "note": "Legacy Appboy init" },
{ "name": "logCustomEvent", "signature": "*", "stub": "return-void" },
{ "name": "changeUser", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": []
},
"manifest_components": [
{
"type": "service",
"class": "com.braze.push.BrazeFirebaseMessagingService",
"action": "disable",
"note": "Firebase messaging service for push notifications"
}
],
"protected_patterns": [
{
"pattern": "*.getInstance()*",
"reason": "Singleton accessor — null causes NPE if called by app code"
}
]
}

View File

@ -0,0 +1,66 @@
{
"$schema": "./_schema.json",
"sdk_id": "chartboost",
"display_name": "Chartboost",
"description": "Chartboost ad SDK",
"vendor": "Chartboost",
"category": "ads",
"known_versions": [],
"packages": [
"com.chartboost.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.chartboost.sdk.Chartboost",
"methods": [
{ "name": "startWithAppId", "signature": "*", "stub": "return-void", "note": "Main SDK init" }
]
}
],
"ad_operations": [
{
"class": "com.chartboost.sdk.ads.Interstitial",
"methods": [
{ "name": "cache", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.chartboost.sdk.ads.Rewarded",
"methods": [
{ "name": "cache", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.chartboost.sdk.ads.Banner",
"methods": [
{ "name": "cache", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "detach", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.chartboost.sdk.internal.**",
"rule": "stub_all_concrete",
"note": "Internal obfuscated classes"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.chartboost.sdk.CBImpressionActivity",
"action": "disable",
"note": "Ad impression display activity"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,54 @@
{
"$schema": "./_schema.json",
"sdk_id": "clevertap",
"display_name": "CleverTap",
"description": "CleverTap customer engagement and analytics SDK",
"vendor": "CleverTap",
"category": "analytics",
"known_versions": [],
"packages": [
"com.clevertap.android.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "com.clevertap.android.sdk.CleverTapAPI",
"methods": [
{ "name": "pushEvent", "signature": "*", "stub": "return-void", "note": "Push analytics event" },
{ "name": "onUserLogin", "signature": "*", "stub": "return-void" },
{ "name": "pushProfile", "signature": "*", "stub": "return-void" },
{ "name": "recordEvent", "signature": "*", "stub": "return-void" },
{ "name": "recordScreen", "signature": "*", "stub": "return-void" },
{ "name": "pushNotificationClickedEvent", "signature": "*", "stub": "return-void" },
{ "name": "pushNotificationViewedEvent", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": []
},
"manifest_components": [
{
"type": "receiver",
"class": "com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver",
"action": "disable",
"note": "Push notification receiver"
},
{
"type": "service",
"class": "com.clevertap.android.sdk.pushnotification.CTNotificationIntentService",
"action": "disable",
"note": "Notification intent service"
}
],
"protected_patterns": [
{
"pattern": "*.getDefaultInstance()*",
"reason": "Singleton accessor — null causes NPE if called by app code"
}
]
}

View File

@ -0,0 +1,147 @@
{
"$schema": "./_schema.json",
"sdk_id": "facebook",
"display_name": "Facebook SDK",
"description": "Meta SDK — core, ads (Audience Network), login, share, gaming services",
"vendor": "Meta",
"category": "social",
"known_versions": ["17.0.2"],
"packages": [
"com.facebook",
"com.facebook.ads",
"com.facebook.login",
"com.facebook.share",
"com.facebook.appevents",
"com.facebook.gamingservices",
"com.facebook.bolts",
"com.facebook.internal",
"com.facebook.unity"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "com.facebook.FacebookSdk",
"methods": [
{ "name": "sdkInitialize", "signature": "*", "stub": "return-void", "note": "Main SDK init — all overloads" },
{ "name": "fullyInitialize", "signature": "*", "stub": "return-void" },
{ "name": "setApplicationId", "signature": "*", "stub": "return-void" },
{ "name": "setClientToken", "signature": "*", "stub": "return-void" },
{ "name": "setAutoLogAppEventsEnabled", "signature": "*", "stub": "return-void" },
{ "name": "setAdvertiserIDCollectionEnabled", "signature": "*", "stub": "return-void" },
{ "name": "setAutoInitEnabled", "signature": "*", "stub": "return-void" },
{ "name": "setIsDebugEnabled", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.facebook.ads.AudienceNetworkAds",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Audience Network init" },
{ "name": "buildInitSettings", "signature": "*", "stub": "return-null" }
]
},
{
"class": "com.facebook.ads.internal.dynamicloading.DynamicLoaderFactory",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Dynamic loader init" },
{ "name": "getDynamicLoader", "signature": "*", "stub": "return-null" },
{ "name": "makeLoaderUnsafe", "signature": "*", "stub": "return-null" },
{ "name": "doCallInitialize", "signature": "*", "stub": "return-void" },
{ "name": "setFallbackMode", "signature": "*", "stub": "return-void" },
{ "name": "setUseLegacyClassLoader", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.facebook.ads.internal.api.InitSettingsBuilder",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.facebook.unity.FB",
"methods": [
{ "name": "Init", "signature": "*", "stub": "return-void", "note": "Unity bridge init" }
]
}
],
"ad_operations": [
{
"class": "com.facebook.ads.internal.api.InterstitialAdApi",
"methods": [
{ "name": "buildLoadAdConfig", "signature": "*", "stub": "return-null" },
{ "name": "buildShowAdConfig", "signature": "*", "stub": "return-null" }
]
},
{
"class": "com.facebook.ads.internal.api.RewardedInterstitialAdApi",
"methods": [
{ "name": "buildLoadAdConfig", "signature": "*", "stub": "return-null" },
{ "name": "buildShowAdConfig", "signature": "*", "stub": "return-null" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.facebook.appevents.**",
"rule": "stub_all_void",
"note": "Event tracking — 670+ methods, all V1-proven safe"
},
{
"package_glob": "com.facebook.internal.**",
"rule": "stub_all_void",
"note": "Internal utilities — 460+ methods, V1-proven safe"
},
{
"package_glob": "com.facebook.login.**",
"rule": "stub_all_void",
"note": "Login flow — 392+ methods, safe if app does not use FB Login as feature"
},
{
"package_glob": "com.facebook.ads.internal.**",
"rule": "stub_all_void",
"note": "Ads internal — 313+ methods, V1-proven safe"
},
{
"package_glob": "com.facebook.share.**",
"rule": "stub_all_void",
"note": "Share API — 263+ methods, V1-proven safe"
},
{
"package_glob": "com.facebook.gamingservices.**",
"rule": "stub_all_void",
"note": "Gaming services — 181+ methods, V1-proven safe"
},
{
"package_glob": "com.facebook.bolts.**",
"rule": "stub_all_void",
"note": "Task framework — 66 methods, V1-proven safe"
},
{
"package_glob": "com.facebook.unity.**",
"rule": "stub_all_void",
"note": "FB Unity wrappers — 147+ methods, V1-proven safe"
}
]
},
"manifest_components": [
{
"type": "provider",
"class": "com.facebook.internal.FacebookInitProvider",
"action": "disable",
"note": "Auto-init ContentProvider"
}
],
"protected_patterns": [
{
"pattern": "*.getActivity()*",
"reason": "Returns Activity reference — null causes NPE"
},
{
"pattern": "*.getApplicationContext()*",
"reason": "Returns Context — null causes NPE"
}
]
}

View File

@ -0,0 +1,91 @@
{
"$schema": "./_schema.json",
"sdk_id": "firebase-analytics",
"display_name": "Firebase Analytics",
"description": "Google Firebase Analytics (Google Analytics for Firebase)",
"vendor": "Google",
"category": "analytics",
"known_versions": [],
"packages": [
"com.google.firebase.analytics"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.google.firebase.analytics.FirebaseAnalytics",
"methods": [
{ "name": "logEvent", "signature": "*", "stub": "return-void", "note": "Log analytics event" },
{ "name": "setUserId", "signature": "*", "stub": "return-void" },
{ "name": "setUserProperty", "signature": "*", "stub": "return-void" },
{ "name": "setAnalyticsCollectionEnabled", "signature": "*", "stub": "return-void" },
{ "name": "setCurrentScreen", "signature": "*", "stub": "return-void" },
{ "name": "setDefaultEventParameters", "signature": "*", "stub": "return-void" },
{ "name": "resetAnalyticsData", "signature": "*", "stub": "return-void" },
{ "name": "setSessionTimeoutDuration", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": [
{
"package_glob": "com.google.android.gms.measurement.**",
"rule": "stub_all_void",
"note": "Analytics measurement internals"
}
]
},
"manifest_components": [
{
"type": "provider",
"class": "com.google.firebase.provider.FirebaseInitProvider",
"action": "disable",
"note": "Firebase auto-init ContentProvider"
},
{
"type": "service",
"class": "com.google.android.gms.measurement.AppMeasurementService",
"action": "disable",
"note": "Analytics measurement service"
},
{
"type": "receiver",
"class": "com.google.android.gms.measurement.AppMeasurementReceiver",
"action": "disable",
"note": "Analytics measurement receiver"
},
{
"type": "provider",
"class": "com.google.android.gms.measurement.AppMeasurementContentProvider",
"action": "disable",
"note": "Analytics measurement content provider"
},
{
"type": "receiver",
"class": "com.google.android.gms.measurement.AppMeasurementInstallReferrerReceiver",
"action": "disable",
"note": "Install referrer tracking receiver"
},
{
"type": "service",
"class": "com.google.android.gms.measurement.AppMeasurementJobService",
"action": "disable",
"note": "Analytics background job service"
},
{
"type": "receiver",
"class": "com.google.firebase.iid.FirebaseInstanceIdReceiver",
"action": "disable",
"note": "Firebase Instance ID receiver"
}
],
"protected_patterns": [
{
"pattern": "*.getInstance()*",
"reason": "Singleton accessor — null causes NPE if called by app code"
}
]
}

View File

@ -0,0 +1,41 @@
{
"$schema": "./_schema.json",
"sdk_id": "firebase-crashlytics",
"display_name": "Firebase Crashlytics",
"description": "Google Firebase Crashlytics crash reporting",
"vendor": "Google",
"category": "crash_reporting",
"known_versions": [],
"packages": [
"com.google.firebase.crashlytics"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.google.firebase.crashlytics.FirebaseCrashlytics",
"methods": [
{ "name": "log", "signature": "*", "stub": "return-void", "note": "Log message" },
{ "name": "recordException", "signature": "*", "stub": "return-void", "note": "Record non-fatal exception" },
{ "name": "setUserId", "signature": "*", "stub": "return-void" },
{ "name": "setCustomKey", "signature": "*", "stub": "return-void" },
{ "name": "setCrashlyticsCollectionEnabled", "signature": "*", "stub": "return-void" },
{ "name": "sendUnsentReports", "signature": "*", "stub": "return-void" },
{ "name": "deleteUnsentReports", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": []
},
"manifest_components": [],
"protected_patterns": [
{
"pattern": "*.getInstance()*",
"reason": "Singleton accessor — null causes NPE if called by app code"
}
]
}

View File

@ -0,0 +1,37 @@
{
"$schema": "./_schema.json",
"sdk_id": "fyber",
"display_name": "Fyber / Digital Turbine",
"description": "Fyber (Inneractive / Digital Turbine) ad SDK",
"vendor": "Digital Turbine",
"category": "ads",
"known_versions": [],
"packages": [
"com.fyber.inneractive.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.fyber.inneractive.sdk.external.InneractiveAdManager",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Main SDK init" }
]
}
],
"ad_operations": [
{
"class": "com.fyber.inneractive.sdk.external.InneractiveAdRequest",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": []
},
"manifest_components": [],
"protected_patterns": []
}

View File

@ -0,0 +1,123 @@
{
"$schema": "./_schema.json",
"sdk_id": "google-admob",
"display_name": "Google AdMob",
"description": "Google Mobile Ads SDK",
"vendor": "Google",
"category": "ads",
"known_versions": ["24.4.0"],
"packages": [
"com.google.android.gms.ads"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated",
"internal_prefix": "zz"
},
"targets": {
"entry_points": [
{
"class": "com.google.android.gms.ads.MobileAds",
"methods": [
{
"name": "initialize",
"signature": "(Landroid/content/Context;)V",
"stub": "return-void",
"note": "Main SDK init — disables entire ad pipeline"
},
{
"name": "initialize",
"signature": "(Landroid/content/Context;Lcom/google/android/gms/ads/initialization/OnInitializationCompleteListener;)V",
"stub": "return-void",
"note": "Init with callback"
}
]
}
],
"ad_operations": [
{
"class": "com.google.android.gms.ads.AdLoader",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "loadAds", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.google.android.gms.ads.rewarded.RewardedAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.google.android.gms.ads.interstitial.InterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.google.android.gms.ads.appopen.AppOpenAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.google.android.gms.ads.AdView",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "resume", "signature": "*", "stub": "return-void" },
{ "name": "pause", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.google.android.gms.ads.nativead.NativeAd",
"methods": [
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.google.android.gms.ads.rewarded.RewardedInterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.google.android.gms.internal.ads.**",
"rule": "stub_all_concrete",
"note": "Internal obfuscated classes (zz* prefix) — safe but version-dependent"
}
]
},
"manifest_components": [
{
"type": "provider",
"class": "com.google.android.gms.ads.MobileAdsInitProvider",
"action": "disable",
"note": "Auto-init ContentProvider — starts SDK before Application.onCreate"
},
{
"type": "activity",
"class": "com.google.android.gms.ads.AdActivity",
"action": "disable",
"note": "Fullscreen ad display activity"
},
{
"type": "service",
"class": "com.google.android.gms.ads.AdService",
"action": "disable",
"note": "Background ad service"
}
],
"protected_patterns": [
{
"pattern": "*.getActivity()*",
"reason": "Returns Activity reference — null causes NPE"
},
{
"pattern": "*.getApplicationContext()*",
"reason": "Returns Context — null causes NPE"
}
]
}

View File

@ -0,0 +1,111 @@
{
"$schema": "./_schema.json",
"sdk_id": "guru-fusion",
"display_name": "Guru Fusion Ads",
"description": "Guru Fusion Ads SDK — ad mediation wrapper for AdMob, IronSource, AppLovin MAX",
"vendor": "Guru",
"category": "ads_mediation",
"known_versions": [],
"packages": [
"guru.ads.fusion"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "guru.ads.fusion.core.GuruAdsSdk",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Main SDK init with callback" }
]
},
{
"class": "guru.ads.fusion.admob.GuruAdMobAdsSdk",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "AdMob backend init" }
]
},
{
"class": "guru.ads.fusion.ironsource.GuruIronSourceAdsSdk",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "IronSource backend init" }
]
},
{
"class": "guru.ads.fusion.max.GuruMaxAdsSdk",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "AppLovin MAX backend init" }
]
}
],
"ad_operations": [
{
"class": "guru.ads.fusion.admob.GuruAdMobInterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "guru.ads.fusion.admob.GuruAdMobRewardedAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "guru.ads.fusion.admob.GuruAdMobBannerAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "guru.ads.fusion.ironsource.GuruIronSourceInterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "guru.ads.fusion.ironsource.GuruIronSourceRewardedAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "guru.ads.fusion.max.GuruMaxInterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "guru.ads.fusion.max.GuruMaxRewardedAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "guru.ads.fusion.max.GuruMaxBannerAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "guru.ads.fusion.engine.**",
"rule": "stub_all_void",
"note": "Ad engine internals"
}
]
},
"manifest_components": [],
"protected_patterns": []
}

View File

@ -0,0 +1,47 @@
{
"$schema": "./_schema.json",
"sdk_id": "inmobi",
"display_name": "InMobi",
"description": "InMobi ad SDK",
"vendor": "InMobi",
"category": "ads",
"known_versions": [],
"packages": [
"com.inmobi.sdk",
"com.inmobi.ads"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.inmobi.sdk.InMobiSdk",
"methods": [
{ "name": "init", "signature": "*", "stub": "return-void", "note": "Main SDK init — all overloads" }
]
}
],
"ad_operations": [
{
"class": "com.inmobi.ads.InMobiInterstitial",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.inmobi.ads.InMobiBanner",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": []
},
"manifest_components": [],
"protected_patterns": []
}

View File

@ -0,0 +1,84 @@
{
"$schema": "./_schema.json",
"sdk_id": "ironsource",
"display_name": "IronSource / LevelPlay",
"description": "IronSource mediation SDK (also known as LevelPlay)",
"vendor": "Unity (IronSource)",
"category": "ads_mediation",
"known_versions": ["8.11.0"],
"packages": [
"com.ironsource.mediationsdk",
"com.ironsource.sdk",
"com.ironsource.adapters"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated",
"internal_prefix": "single-letter"
},
"targets": {
"entry_points": [
{
"class": "com.ironsource.mediationsdk.IronSource",
"methods": [
{ "name": "init", "signature": "*", "stub": "return-void", "note": "Main SDK init — all overloads" }
]
}
],
"ad_operations": [
{
"class": "com.ironsource.mediationsdk.IronSource",
"methods": [
{ "name": "loadInterstitial", "signature": "*", "stub": "return-void" },
{ "name": "showInterstitial", "signature": "*", "stub": "return-void" },
{ "name": "loadRewardedVideo", "signature": "*", "stub": "return-void" },
{ "name": "showRewardedVideo", "signature": "*", "stub": "return-void" },
{ "name": "loadBanner", "signature": "*", "stub": "return-void" },
{ "name": "destroyBanner", "signature": "*", "stub": "return-void" },
{ "name": "setConsent", "signature": "*", "stub": "return-void" },
{ "name": "setMetaData", "signature": "*", "stub": "return-void" },
{ "name": "isInterstitialReady", "signature": "*", "stub": "return-false/0", "note": "Always report no ad available" },
{ "name": "isRewardedVideoAvailable", "signature": "*", "stub": "return-false/0", "note": "Always report no ad available" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.ironsource.mediationsdk.sdk.**",
"rule": "stub_all_void",
"note": "Internal SDK classes — obfuscated, version-dependent"
},
{
"package_glob": "com.ironsource.adapters.**",
"rule": "stub_all_void",
"note": "Third-party ad network adapters bundled with IronSource"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.ironsource.sdk.controller.InterstitialActivity",
"action": "disable",
"note": "Interstitial ad display activity"
},
{
"type": "activity",
"class": "com.ironsource.sdk.controller.ControllerActivity",
"action": "disable",
"note": "SDK controller activity"
},
{
"type": "provider",
"class": "com.ironsource.lifecycle.IronsourceLifecycleProvider",
"action": "disable",
"note": "Lifecycle auto-init provider"
}
],
"protected_patterns": [
{
"pattern": "*.getActivity()*",
"reason": "Returns Activity reference — null causes NPE"
}
]
}

View File

@ -0,0 +1,89 @@
{
"$schema": "./_schema.json",
"sdk_id": "meta-audience-network",
"display_name": "Meta Audience Network",
"description": "Meta Audience Network (Facebook Ads) — ad-specific subset. Full SDK coverage in facebook.json.",
"vendor": "Meta",
"category": "ads",
"known_versions": ["6.20.0"],
"packages": [
"com.facebook.ads"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "com.facebook.ads.AudienceNetworkAds",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Audience Network init" },
{ "name": "isInitialized", "signature": "*", "stub": "return-false/0" }
]
}
],
"ad_operations": [
{
"class": "com.facebook.ads.InterstitialAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isAdLoaded", "signature": "*", "stub": "return-false/0" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.facebook.ads.RewardedVideoAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isAdLoaded", "signature": "*", "stub": "return-false/0" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.facebook.ads.AdView",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.facebook.ads.NativeAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.facebook.ads.internal.**",
"rule": "stub_all_void",
"note": "Ads internals — 313+ methods, V1-proven safe"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.facebook.ads.AudienceNetworkActivity",
"action": "disable",
"note": "Audience Network ad activity"
},
{
"type": "activity",
"class": "com.facebook.ads.InterstitialAdActivity",
"action": "disable",
"note": "Interstitial ad display activity"
},
{
"type": "provider",
"class": "com.facebook.ads.AudienceNetworkContentProvider",
"action": "disable",
"note": "Audience Network content provider"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,124 @@
{
"$schema": "./_schema.json",
"sdk_id": "mintegral",
"display_name": "Mintegral",
"description": "Mintegral ad SDK",
"vendor": "Mintegral",
"category": "ads",
"known_versions": [],
"packages": [
"com.mbridge.msdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.mbridge.msdk.out.MBridgeSDKFactory",
"methods": [
{ "name": "getMBridgeSDK", "signature": "*", "stub": "return-null", "note": "Factory — returns SDK singleton" }
]
},
{
"class": "com.mbridge.msdk.MBridgeSDK",
"methods": [
{ "name": "init", "signature": "*", "stub": "return-void", "note": "Main SDK init — all overloads" },
{ "name": "initAsync", "signature": "*", "stub": "return-void", "note": "Async SDK init — all overloads" },
{ "name": "preload", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [
{
"class": "com.mbridge.msdk.out.MBInterstitialHandler",
"methods": [
{ "name": "preload", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.mbridge.msdk.out.MBRewardVideoHandler",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.mbridge.msdk.out.MBBannerView",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "loadFromBid", "signature": "*", "stub": "return-void" },
{ "name": "release", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.mbridge.msdk.out.MBNewInterstitialHandler",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "loadFormSelfFilling", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.mbridge.msdk.out.MBBidRewardVideoHandler",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.mbridge.msdk.out.MBBidInterstitialVideoHandler",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.mbridge.msdk.foundation.**",
"rule": "stub_all_void",
"note": "Internal foundation classes"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.mbridge.msdk.activity.MBCommonActivity",
"action": "disable",
"note": "Common ad display activity"
},
{
"type": "activity",
"class": "com.mbridge.msdk.reward.player.MBRewardVideoActivity",
"action": "disable",
"note": "Legacy reward video activity"
},
{
"type": "activity",
"class": "com.mbridge.msdk.newreward.player.MBRewardVideoActivity",
"action": "disable",
"note": "New reward video activity"
},
{
"type": "activity",
"class": "com.mbridge.msdk.out.LoadingActivity",
"action": "disable",
"note": "Ad loading activity"
},
{
"type": "receiver",
"class": "com.mbridge.msdk.foundation.same.broadcast.NetWorkChangeReceiver",
"action": "disable",
"note": "Network change broadcast receiver"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,51 @@
{
"$schema": "./_schema.json",
"sdk_id": "mixpanel",
"display_name": "Mixpanel",
"description": "Mixpanel analytics SDK for mobile",
"vendor": "Mixpanel",
"category": "analytics",
"known_versions": [],
"packages": [
"com.mixpanel.android"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "com.mixpanel.android.mpmetrics.MixpanelAPI",
"methods": [
{ "name": "track", "signature": "*", "stub": "return-void", "note": "Track event" },
{ "name": "trackMap", "signature": "*", "stub": "return-void" },
{ "name": "identify", "signature": "*", "stub": "return-void" },
{ "name": "timeEvent", "signature": "*", "stub": "return-void" },
{ "name": "registerSuperProperties", "signature": "*", "stub": "return-void" },
{ "name": "flush", "signature": "*", "stub": "return-void" },
{ "name": "alias", "signature": "*", "stub": "return-void" },
{ "name": "reset", "signature": "*", "stub": "return-void" },
{ "name": "optOutTracking", "signature": "*", "stub": "return-void" },
{ "name": "optInTracking", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": []
},
"manifest_components": [
{
"type": "service",
"class": "com.mixpanel.android.mpmetrics.MixpanelFCMMessagingService",
"action": "disable",
"note": "FCM push notification service"
}
],
"protected_patterns": [
{
"pattern": "*.getInstance()*",
"reason": "Singleton accessor — null causes NPE if called by app code"
}
]
}

View File

@ -0,0 +1,99 @@
{
"$schema": "./_schema.json",
"sdk_id": "mobilefuse",
"display_name": "MobileFuse",
"description": "MobileFuse ad SDK",
"vendor": "MobileFuse",
"category": "ads",
"known_versions": [],
"packages": [
"com.mobilefuse.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.mobilefuse.sdk.MobileFuse",
"methods": [
{ "name": "init", "signature": "*", "stub": "return-void", "note": "Main SDK init — all overloads" },
{ "name": "initSdkServices", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [
{
"class": "com.mobilefuse.sdk.MobileFuseInterstitialAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "showAd", "signature": "*", "stub": "return-void" },
{ "name": "isLoaded", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.mobilefuse.sdk.MobileFuseRewardedAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "showAd", "signature": "*", "stub": "return-void" },
{ "name": "isLoaded", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.mobilefuse.sdk.MobileFuseBannerAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.mobilefuse.sdk.MobileFuseNativeAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.mobilefuse.sdk.internal.**",
"rule": "stub_all_void",
"note": "Internal SDK classes"
}
]
},
"manifest_components": [
{
"type": "provider",
"class": "com.mobilefuse.sdk.MobileFuseSdkInitProvider",
"action": "disable",
"note": "Auto-init ContentProvider"
},
{
"type": "activity",
"class": "com.mobilefuse.sdk.MobileFuseFullscreenActivity",
"action": "disable",
"note": "Fullscreen ad activity"
},
{
"type": "activity",
"class": "com.mobilefuse.sdk.MobileFuseFullscreenTransparentActivity",
"action": "disable",
"note": "Transparent fullscreen ad overlay"
},
{
"type": "activity",
"class": "com.mobilefuse.sdk.experimental.NativeInterstitialActivity",
"action": "disable",
"note": "Native interstitial activity"
},
{
"type": "activity",
"class": "com.mobilefuse.sdk.ad.rendering.splashad.MobileFuseSplashAdActivity",
"action": "disable",
"note": "Splash ad activity"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,66 @@
{
"$schema": "./_schema.json",
"sdk_id": "moloco",
"display_name": "Moloco",
"description": "Moloco programmatic ad SDK",
"vendor": "Moloco",
"category": "ads",
"known_versions": [],
"packages": [
"com.moloco.sdk"
],
"obfuscation": {
"public_api": "mixed",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.moloco.sdk.Init",
"methods": [
{ "name": "registerAllExtensions", "signature": "*", "stub": "return-void", "note": "Protobuf init registration" }
]
}
],
"ad_operations": [],
"deep_patterns": [
{
"package_glob": "com.moloco.sdk.internal.**",
"rule": "stub_all_void",
"note": "Internal SDK classes"
},
{
"package_glob": "com.moloco.sdk.xenoss.**",
"rule": "stub_all_void",
"note": "Ad renderer internals"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.moloco.sdk.xenoss.sdkdevkit.android.adrenderer.internal.templates.renderer.fullscreen.FullscreenWebviewActivity",
"action": "disable",
"note": "Fullscreen webview ad activity"
},
{
"type": "activity",
"class": "com.moloco.sdk.xenoss.sdkdevkit.android.adrenderer.internal.vast.VastActivity",
"action": "disable",
"note": "VAST video ad activity"
},
{
"type": "activity",
"class": "com.moloco.sdk.xenoss.sdkdevkit.android.adrenderer.internal.staticrenderer.StaticAdActivity",
"action": "disable",
"note": "Static ad activity"
},
{
"type": "activity",
"class": "com.moloco.sdk.xenoss.sdkdevkit.android.adrenderer.internal.mraid.MraidActivity",
"action": "disable",
"note": "MRAID ad activity"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,57 @@
{
"$schema": "./_schema.json",
"sdk_id": "ogury",
"display_name": "Ogury",
"description": "Ogury ad and consent SDK",
"vendor": "Ogury",
"category": "ads",
"known_versions": [],
"packages": [
"com.ogury.sdk",
"com.ogury.ed",
"com.ogury.cm",
"com.ogury.core"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.ogury.sdk.Ogury",
"methods": [
{ "name": "start", "signature": "*", "stub": "return-void", "note": "Main SDK init" }
]
}
],
"ad_operations": [
{
"class": "com.ogury.ed.OguryInterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isLoaded", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.ogury.ed.OguryOptinVideoAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isLoaded", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.ogury.ed.OguryBannerAdView",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": []
},
"manifest_components": [],
"protected_patterns": []
}

View File

@ -0,0 +1,88 @@
{
"$schema": "./_schema.json",
"sdk_id": "pangle",
"display_name": "Pangle",
"description": "Pangle (ByteDance / TikTok) ad SDK",
"vendor": "ByteDance",
"category": "ads",
"known_versions": [],
"packages": [
"com.bytedance.sdk.openadsdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.bytedance.sdk.openadsdk.api.init.PAGSdk",
"methods": [
{ "name": "init", "signature": "*", "stub": "return-void", "note": "Main SDK init" }
]
}
],
"ad_operations": [
{
"class": "com.bytedance.sdk.openadsdk.api.interstitial.PAGInterstitialAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.bytedance.sdk.openadsdk.api.reward.PAGRewardedAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.bytedance.sdk.openadsdk.api.banner.PAGBannerAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.bytedance.sdk.openadsdk.core.**",
"rule": "stub_all_concrete",
"note": "Internal implementation"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.bytedance.sdk.openadsdk.activity.TTFullScreenVideoActivity",
"action": "disable",
"note": "Fullscreen video ad activity"
},
{
"type": "activity",
"class": "com.bytedance.sdk.openadsdk.activity.TTRewardVideoActivity",
"action": "disable",
"note": "Rewarded video ad activity"
},
{
"type": "activity",
"class": "com.bytedance.sdk.openadsdk.activity.TTInterstitialActivity",
"action": "disable",
"note": "Interstitial ad activity"
},
{
"type": "activity",
"class": "com.bytedance.sdk.openadsdk.activity.TTAdActivity",
"action": "disable",
"note": "Generic ad activity"
},
{
"type": "activity",
"class": "com.bytedance.sdk.openadsdk.activity.TTDelegateActivity",
"action": "disable",
"note": "Delegate activity for ad interactions"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,72 @@
{
"$schema": "./_schema.json",
"sdk_id": "pubmatic",
"display_name": "PubMatic OpenWrap",
"description": "PubMatic OpenWrap SDK for programmatic advertising",
"vendor": "PubMatic",
"category": "ads",
"known_versions": [],
"packages": [
"com.pubmatic.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.pubmatic.sdk.openwrap.core.POBBid",
"methods": [
{ "name": "invalidate", "signature": "*", "stub": "return-void", "note": "Invalidate bid" }
]
}
],
"ad_operations": [
{
"class": "com.pubmatic.sdk.rewardedad.POBRewardedAd",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.pubmatic.sdk.nativead.POBNativeAdView",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.pubmatic.sdk.openwrap.**",
"rule": "stub_all_void",
"note": "OpenWrap core engine internals"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.pubmatic.sdk.webrendering.mraid.POBVideoPlayerActivity",
"action": "disable",
"note": "Video player activity"
},
{
"type": "activity",
"class": "com.pubmatic.sdk.webrendering.ui.POBFullScreenActivity",
"action": "disable",
"note": "Fullscreen ad activity"
},
{
"type": "activity",
"class": "com.pubmatic.sdk.common.browser.POBInternalBrowserActivity",
"action": "disable",
"note": "Internal browser activity"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,47 @@
{
"$schema": "./_schema.json",
"sdk_id": "pubnative",
"display_name": "PubNative / HyBid",
"description": "PubNative HyBid SDK for programmatic advertising",
"vendor": "Verve Group",
"category": "ads",
"known_versions": [],
"packages": [
"net.pubnative.lite.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "mixed"
},
"targets": {
"entry_points": [
{
"class": "net.pubnative.lite.sdk.HyBid",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Main SDK init" }
]
}
],
"ad_operations": [
{
"class": "net.pubnative.lite.sdk.interstitial.HyBidInterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "net.pubnative.lite.sdk.rewarded.HyBidRewardedAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "show", "signature": "*", "stub": "return-void" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
}
],
"deep_patterns": []
},
"manifest_components": [],
"protected_patterns": []
}

View File

@ -0,0 +1,59 @@
{
"$schema": "./_schema.json",
"sdk_id": "smaato",
"display_name": "Smaato",
"description": "Smaato ad SDK",
"vendor": "Smaato",
"category": "ads",
"known_versions": [],
"packages": [
"com.smaato.sdk"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.smaato.sdk.core.SmaatoSdk",
"methods": [
{ "name": "init", "signature": "*", "stub": "return-void", "note": "Main SDK init — all overloads" }
]
}
],
"ad_operations": [
{
"class": "com.smaato.sdk.interstitial.Interstitial",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "showAd", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.smaato.sdk.rewarded.RewardedInterstitial",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "showAd", "signature": "*", "stub": "return-void" }
]
},
{
"class": "com.smaato.sdk.banner.BannerView",
"methods": [
{ "name": "loadAd", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": []
},
"manifest_components": [
{
"type": "receiver",
"class": "com.smaato.sdk.core.SmaatoBroadcastReceiver",
"action": "disable",
"note": "Broadcast receiver for ad events"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,78 @@
{
"$schema": "./_schema.json",
"sdk_id": "tradplus",
"display_name": "TradPlus",
"description": "TradPlus ad mediation SDK",
"vendor": "TradPlus",
"category": "ads_mediation",
"known_versions": [],
"packages": [
"com.tradplus.ads"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.tradplus.ads.open.TradPlusSdk",
"methods": [
{ "name": "initSdk", "signature": "*", "stub": "return-void", "note": "Main SDK init" },
{ "name": "setDebugMode", "signature": "*", "stub": "return-void" },
{ "name": "setGDPRDataCollection", "signature": "*", "stub": "return-void" },
{ "name": "setCCPADoNotSell", "signature": "*", "stub": "return-void" },
{ "name": "setCOPPAIsAgeRestrictedUser", "signature": "*", "stub": "return-void" },
{ "name": "setEUTraffic", "signature": "*", "stub": "return-void" },
{ "name": "checkCurrentArea", "signature": "*", "stub": "return-void" }
]
}
],
"ad_operations": [],
"deep_patterns": [
{
"package_glob": "com.tradplus.ads.core.**",
"rule": "stub_all_void",
"note": "Core ad management internals"
},
{
"package_glob": "com.tradplus.ads.mgr.**",
"rule": "stub_all_void",
"note": "Ad manager internals"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.tradplus.ads.mgr.interstitial.views.InterNativeActivity",
"action": "disable",
"note": "Fullscreen native interstitial activity"
},
{
"type": "activity",
"class": "com.tradplus.ads.mgr.interstitial.views.InterNativeAPI26Activity",
"action": "disable",
"note": "Native interstitial for API 26+"
},
{
"type": "activity",
"class": "com.tradplus.crosspro.ui.CPAdActivity",
"action": "disable",
"note": "Cross-promotion ad activity"
},
{
"type": "activity",
"class": "com.tradplus.crosspro.ui.ApkConfirmDialogActivity",
"action": "disable",
"note": "APK install confirm dialog"
},
{
"type": "activity",
"class": "com.tradplus.crosspro.ui.ApkConfirmDialogCNActivity",
"action": "disable",
"note": "APK install confirm dialog (CN)"
}
],
"protected_patterns": []
}

View File

@ -0,0 +1,71 @@
{
"$schema": "./_schema.json",
"sdk_id": "unity-ads",
"display_name": "Unity Ads",
"description": "Unity Ads SDK for in-app advertising",
"vendor": "Unity",
"category": "ads",
"known_versions": ["4.15.0"],
"packages": [
"com.unity3d.ads",
"com.unity3d.services"
],
"obfuscation": {
"public_api": "readable",
"internals": "readable"
},
"targets": {
"entry_points": [
{
"class": "com.unity3d.ads.UnityAds",
"methods": [
{ "name": "initialize", "signature": "*", "stub": "return-void", "note": "Main SDK init — all overloads" }
]
}
],
"ad_operations": [
{
"class": "com.unity3d.ads.UnityAds",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void", "note": "Load ad placement — all overloads" },
{ "name": "show", "signature": "*", "stub": "return-void", "note": "Show ad — all overloads" },
{ "name": "isInitialized", "signature": "*", "stub": "return-false/0" },
{ "name": "isReady", "signature": "*", "stub": "return-false/0" }
]
}
],
"deep_patterns": [
{
"package_glob": "com.unity3d.services.**",
"rule": "stub_all_void",
"note": "Unity Services internal — readable, stable across versions"
}
]
},
"manifest_components": [
{
"type": "activity",
"class": "com.unity3d.ads.adunit.AdUnitActivity",
"action": "disable",
"note": "Legacy ad unit activity"
},
{
"type": "activity",
"class": "com.unity3d.ads.adunit.AdUnitTransparentActivity",
"action": "disable",
"note": "Legacy transparent ad overlay"
},
{
"type": "activity",
"class": "com.unity3d.services.ads.adunit.AdUnitActivity",
"action": "disable",
"note": "Services ad unit activity (newer SDK)"
}
],
"protected_patterns": [
{
"pattern": "*.getActivity()*",
"reason": "Returns Activity reference — null causes NPE"
}
]
}

View File

@ -0,0 +1,67 @@
{
"$schema": "./_schema.json",
"sdk_id": "vungle",
"display_name": "Vungle / Liftoff",
"description": "Vungle (Liftoff) ad SDK",
"vendor": "Liftoff",
"category": "ads",
"known_versions": [],
"packages": [
"com.vungle.ads"
],
"obfuscation": {
"public_api": "readable",
"internals": "obfuscated"
},
"targets": {
"entry_points": [
{
"class": "com.vungle.ads.VungleAds",
"methods": [
{ "name": "init", "signature": "*", "stub": "return-void", "note": "Main SDK init" }
]
}
],
"ad_operations": [
{
"class": "com.vungle.ads.InterstitialAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "play", "signature": "*", "stub": "return-void" },
{ "name": "canPlayAd", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.vungle.ads.RewardedAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "play", "signature": "*", "stub": "return-void" },
{ "name": "canPlayAd", "signature": "*", "stub": "return-false/0" }
]
},
{
"class": "com.vungle.ads.BannerAd",
"methods": [
{ "name": "load", "signature": "*", "stub": "return-void" },
{ "name": "destroy", "signature": "*", "stub": "return-void" }
]
}
],
"deep_patterns": []
},
"manifest_components": [
{
"type": "activity",
"class": "com.vungle.warren.ui.VungleActivity",
"action": "disable",
"note": "Legacy Vungle (warren) ad activity"
},
{
"type": "activity",
"class": "com.vungle.ads.internal.ui.VungleActivity",
"action": "disable",
"note": "New Vungle ads activity"
}
],
"protected_patterns": []
}

View File

@ -128,6 +128,26 @@ else
missing_optional+=("zip")
fi
# --- Python 3.6+ (optional, for registry-scan.py) ---
if command -v python3 &>/dev/null; then
py_version=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
py_major=$(echo "$py_version" | cut -d. -f1)
py_minor=$(echo "$py_version" | cut -d. -f2)
if [[ "$py_major" -ge 3 ]] && [[ "$py_minor" -ge 6 ]]; then
echo "[OK] Python $py_version (for registry-scan.py — SDK registry scanning)"
else
echo "[WARN] Python $py_version found but 3.6+ required for registry-scan.py"
echo " Without Python 3.6+, neutralize.sh falls back to builtin hardcoded targets."
echo " Install: apt install python3 / brew install python3"
missing_optional+=("python3")
fi
else
echo "[WARN] python3 not found (optional — required for registry-scan.py SDK registry scanning)"
echo " Without python3, neutralize.sh falls back to builtin hardcoded targets."
echo " Install: apt install python3 / brew install python3"
missing_optional+=("python3")
fi
# --- Machine-readable summary ---
echo
if [[ ${#missing_required[@]} -gt 0 ]]; then

View File

@ -115,6 +115,7 @@ if [[ "$ext_lower" == "xapk" ]]; then
IS_XAPK=true
echo "=== Extracting XAPK archive ==="
XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX")
trap cleanup_xapk EXIT
unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_TMPDIR"
# Show manifest.json if present

View File

@ -118,8 +118,10 @@ for split_apk in "$XAPK_ORIGIN_DIR/splits/"*.apk; do
[[ -f "$split_apk" ]] || continue
split_name=$(basename "$split_apk")
# Check if it's an ABI split by looking for lib/ inside
if unzip -l "$split_apk" 2>/dev/null | grep -q "lib/[^/]*/"; then
# Check if it's an ABI split — filename pattern first, then content-based fallback
if [[ "$split_name" =~ ^config\.(arm64_v8a|armeabi_v7a|x86|x86_64)\.apk$ ]]; then
abi_splits+=("$split_apk")
elif unzip -l "$split_apk" 2>/dev/null | grep -qE '\blib/[a-z0-9_-]+/.*\.so'; then
abi_splits+=("$split_apk")
# Check if it's a feature split (contains classes.dex or AndroidManifest.xml with split name)
elif unzip -l "$split_apk" 2>/dev/null | grep -qE "(classes[0-9]*\.dex|^.*AndroidManifest\.xml)" && \
@ -221,7 +223,7 @@ else
for so_file in "$TMPDIR_NATIVE/lib/$abi/"*; do
[[ -f "$so_file" ]] || continue
cp "$so_file" "$DECODED_DIR/lib/$abi/"
(( lib_count++ ))
(( lib_count++ )) || true
done
ok "Merged $lib_count native libraries for $abi"
echo "MERGE_ABI:$abi ($lib_count native libraries)"
@ -265,7 +267,7 @@ else
# Count extracted files (excluding directories)
file_count=0
while IFS= read -r -d '' f; do
(( file_count++ ))
(( file_count++ )) || true
done < <(find "$TMPDIR_RES" -type f -print0 2>/dev/null)
if (( file_count == 0 )); then
@ -287,7 +289,7 @@ else
target="$DECODED_DIR/res/$subdir_name/$(basename "$res_file")"
if [[ ! -f "$target" ]]; then
cp "$res_file" "$target"
(( res_copied++ ))
(( res_copied++ )) || true
fi
done
done
@ -303,7 +305,7 @@ else
if [[ ! -f "$target" ]]; then
mkdir -p "$(dirname "$target")"
cp "$asset_file" "$target"
(( assets_copied++ ))
(( assets_copied++ )) || true
fi
done < <(find "$TMPDIR_RES/assets" -type f -print0 2>/dev/null)
fi

View File

@ -27,12 +27,32 @@ Options:
--trackers Neutralize only tracker/analytics SDK entry points
--all Neutralize both ads and trackers (default)
--dry-run Show what would be patched without modifying files
--backup Create .smali.bak backups before patching (default)
--no-backup Do not create backup files
--backup Create .smali.bak backups before patching
--no-backup Do not create backup files (default)
--cleanup-backups Remove all .smali.bak files from the decoded directory
--manifest Patch AndroidManifest.xml to disable SDK components (default)
--no-manifest Skip manifest patching
--targets-file <file> Load additional targets from a file (one per line:
<smali-class>:<method-name>)
--targets-file <file> Load additional targets from a file (one per line).
Format: <class-path>:<method-name>
com/example/MyClass:methodName (searches all smali dirs)
com/example/pkg/**:* (wildcard — all methods in package)
com/example/Class:* (all methods in class)
L prefix and ; suffix are auto-stripped:
Lcom/example/MyClass;:init (also valid)
Lines starting with # are ignored.
--manifest-components-file <file>
Load additional manifest components from a file.
Format: one per line, class_substring|sdk_name
Components are appended to the builtin lists.
--no-builtin-targets Skip hardcoded patch_ad_targets() and patch_tracker_targets().
Use when relying entirely on --targets-file (e.g., from
registry-scan.py). Builtin manifest components are also
skipped; use --manifest-components-file for registry-driven
manifest patching.
--package <path> Neutralize ALL methods in a smali package recursively.
Stubs every non-abstract, non-native, non-constructor
method. Can be specified multiple times.
Example: --package guru/ads --package com/appsflyer
--replay Replay patches from a previous neutralize-manifest.json
--save-manifest Save neutralize-manifest.json after patching (default)
--no-save-manifest Do not save neutralize-manifest.json
@ -57,9 +77,13 @@ NEUTRALIZE_ADS=false
NEUTRALIZE_TRACKERS=false
NEUTRALIZE_ALL=true
DRY_RUN=false
DO_BACKUP=true
DO_BACKUP=false
DO_CLEANUP_BACKUPS=false
DO_MANIFEST=true
TARGETS_FILE=""
MANIFEST_COMPONENTS_FILE=""
NO_BUILTIN_TARGETS=false
PACKAGE_PATHS=()
DO_REPLAY=false
DO_SAVE_MANIFEST=true
@ -71,6 +95,7 @@ while [[ $# -gt 0 ]]; do
--dry-run) DRY_RUN=true; shift ;;
--backup) DO_BACKUP=true; shift ;;
--no-backup) DO_BACKUP=false; shift ;;
--cleanup-backups) DO_CLEANUP_BACKUPS=true; shift ;;
--manifest) DO_MANIFEST=true; shift ;;
--no-manifest) DO_MANIFEST=false; shift ;;
--targets-file)
@ -80,6 +105,21 @@ while [[ $# -gt 0 ]]; do
exit 1
fi
TARGETS_FILE="$1"; shift ;;
--manifest-components-file)
shift
if [[ $# -eq 0 ]]; then
echo "Error: --manifest-components-file requires a file argument" >&2
exit 1
fi
MANIFEST_COMPONENTS_FILE="$1"; shift ;;
--no-builtin-targets) NO_BUILTIN_TARGETS=true; shift ;;
--package)
shift
if [[ $# -eq 0 ]]; then
echo "Error: --package requires a package path argument" >&2
exit 1
fi
PACKAGE_PATHS+=("$1"); shift ;;
--replay) DO_REPLAY=true; shift ;;
--save-manifest) DO_SAVE_MANIFEST=true; shift ;;
--no-save-manifest) DO_SAVE_MANIFEST=false; shift ;;
@ -169,39 +209,101 @@ patch_method() {
found = 0
}
# Match .method line containing our target method name
/^\.method / && $0 ~ "[ ;]" method "\\(" {
# count_param_registers(descriptor_params, is_static)
# Counts the number of registers required for method parameters.
# Instance methods have an implicit "this" (p0) taking 1 register.
# J (long) and D (double) each take 2 registers; all others take 1.
function count_param_registers(params, is_static, i, c, count) {
count = 0
if (!is_static) count = 1 # p0 = this
i = 1
while (i <= length(params)) {
c = substr(params, i, 1)
if (c == "J" || c == "D") {
count += 2; i++
} else if (c == "L") {
count += 1
# Skip to ";"
while (i <= length(params) && substr(params, i, 1) != ";") i++
i++ # skip the ";"
} else if (c == "[") {
# Array: skip all leading "[", then the base type
while (i <= length(params) && substr(params, i, 1) == "[") i++
c = substr(params, i, 1)
if (c == "L") {
count += 1
while (i <= length(params) && substr(params, i, 1) != ";") i++
i++
} else {
# Primitive array
count += 1; i++
}
} else {
# Primitive: Z, B, C, S, I, F
count += 1; i++
}
}
return count
}
# Match .method line containing our target method name (skip abstract/native)
/^\.method / && !/ abstract / && !/ native / && $0 ~ "[ ;]" method "\\(" {
in_target = 1
found = 1
# Extract return type from method descriptor
# The descriptor is the last part: (params)ReturnType
# Determine if static
is_static = ($0 ~ / static /) ? 1 : 0
# Extract descriptor: everything between "(" and ")" is params,
# everything after ")" is return type
line = $0
# Find the closing paren and get what follows
idx = index(line, ")")
if (idx > 0) {
ret_type = substr(line, idx + 1)
# Remove trailing whitespace
open_idx = index(line, "(")
close_idx = index(line, ")")
if (open_idx > 0 && close_idx > open_idx) {
params = substr(line, open_idx + 1, close_idx - open_idx - 1)
ret_type = substr(line, close_idx + 1)
gsub(/[[:space:]]+$/, "", ret_type)
} else {
params = ""
ret_type = "V"
}
# Determine stub type
# Count registers needed for parameters (including "this" for instance methods)
param_regs = count_param_registers(params, is_static)
# Determine stub type and registers needed for the stub itself
if (ret_type == "V") {
stub_type = "return-void"
stub_body = " .registers 1\n\n return-void"
stub_regs = 0 # return-void needs no registers beyond params
} else if (ret_type == "Z" || ret_type == "I" || ret_type == "S" || \
ret_type == "B" || ret_type == "C" || ret_type == "F") {
stub_type = "const/4+return"
stub_body = " .registers 1\n\n const/4 v0, 0x0\n\n return v0"
stub_regs = 1 # needs v0
} else if (ret_type == "J" || ret_type == "D") {
stub_type = "const-wide+return-wide"
stub_body = " .registers 2\n\n const-wide/16 v0, 0x0\n\n return-wide v0"
stub_regs = 2 # needs v0, v1
} else {
# Object or array return type (L...; or [...)
stub_type = "const/4+return-object"
stub_body = " .registers 1\n\n const/4 v0, 0x0\n\n return-object v0"
stub_regs = 1 # needs v0
}
# Total registers = max(param_regs, stub_regs + param_regs)
# In Dalvik, .registers = local_vars + param_registers
# We need stub_regs local vars + param_regs for parameters
total_regs = stub_regs + param_regs
if (total_regs < 1) total_regs = 1 # minimum 1 register
# Build stub body
if (ret_type == "V") {
stub_body = " .registers " total_regs "\n\n return-void"
} else if (ret_type == "J" || ret_type == "D") {
stub_body = " .registers " total_regs "\n\n const-wide/16 v0, 0x0\n\n return-wide v0"
} else if (ret_type == "Z" || ret_type == "I" || ret_type == "S" || \
ret_type == "B" || ret_type == "C" || ret_type == "F") {
stub_body = " .registers " total_regs "\n\n const/4 v0, 0x0\n\n return v0"
} else {
stub_body = " .registers " total_regs "\n\n const/4 v0, 0x0\n\n return-object v0"
}
if (dry_run == "true") {
@ -232,21 +334,24 @@ patch_method() {
# Outside target method — print line unchanged
{ print }
' "$file" > "$tmp_file" 2> >(while IFS= read -r line; do
echo "$line"
if [[ "$line" == PATCHED:* ]]; then
METHODS_PATCHED=$((METHODS_PATCHED + 1))
patched=true
echo "$line" >> "$PATCH_LOG_FILE"
elif [[ "$line" == DRY_RUN:WOULD_PATCH:* ]]; then
METHODS_PATCHED=$((METHODS_PATCHED + 1))
patched=true
fi
done)
' "$file" > "$tmp_file" 2>"${tmp_file}.stderr"
# Process stderr in the parent shell (not a subshell) so variables propagate
if grep -q '^PATCHED:\|^DRY_RUN:WOULD_PATCH:' "${tmp_file}.stderr" 2>/dev/null; then
patched=true
local patch_count
patch_count=$(grep -c '^PATCHED:\|^DRY_RUN:WOULD_PATCH:' "${tmp_file}.stderr")
METHODS_PATCHED=$((METHODS_PATCHED + patch_count))
# Append PATCHED lines to the patch log (not DRY_RUN lines)
grep '^PATCHED:' "${tmp_file}.stderr" >> "$PATCH_LOG_FILE" 2>/dev/null || true
fi
# Emit stderr lines to stderr for user visibility
cat "${tmp_file}.stderr" >&2
rm -f "${tmp_file}.stderr"
if [[ "$DRY_RUN" == false ]] && [[ "$patched" == true ]]; then
if [[ "$DO_BACKUP" == true ]]; then
cp "$file" "${file}.bak"
[[ -f "${file}.bak" ]] || cp "$file" "${file}.bak"
fi
mv "$tmp_file" "$file"
else
@ -282,6 +387,92 @@ find_and_patch() {
done
}
# =====================================================================
# patch_all_methods() — Stub ALL non-abstract, non-native, non-constructor
# methods in a smali file
#
# Arguments:
# $1 — smali file path
# $2 — SDK name (for reporting)
# =====================================================================
patch_all_methods() {
local file="$1"
local sdk_name="$2"
if [[ ! -f "$file" ]]; then
return
fi
# Extract class descriptor from the .class line
local class_desc
class_desc=$(grep -m1 '^\.class ' "$file" | grep -oP 'L[^ ;]+;' | head -1)
if [[ -z "$class_desc" ]]; then
return
fi
# Extract all method names that are patchable:
# - Not abstract, not native
# - Not <init> or <clinit> (constructors)
local methods
methods=$(awk '
/^\.method / {
# Skip abstract and native methods
if ($0 ~ / abstract / || $0 ~ / native /) next
# Extract method name from descriptor
# Format: .method [access] methodName(params)RetType
match($0, /[ ]([^ (]+)\(/, arr)
if (arr[1] != "" && arr[1] != "<init>" && arr[1] != "<clinit>") {
print arr[1]
}
}
' "$file" | sort -u)
if [[ -z "$methods" ]]; then
return
fi
while IFS= read -r method_name; do
[[ -z "$method_name" ]] && continue
patch_method "$file" "$method_name" "$sdk_name" "$class_desc"
done <<< "$methods"
}
# =====================================================================
# patch_packages() — Neutralize all methods in specified packages
# =====================================================================
patch_packages() {
if [[ ${#PACKAGE_PATHS[@]} -eq 0 ]]; then
return
fi
for pkg_path in "${PACKAGE_PATHS[@]}"; do
# Normalize: remove trailing slashes
pkg_path="${pkg_path%/}"
local sdk_label="Package:${pkg_path}"
echo "--- Neutralizing package: $pkg_path ---"
local pkg_file_count=0
for smali_dir in "${SMALI_DIRS[@]}"; do
local pkg_dir="$smali_dir/$pkg_path"
if [[ ! -d "$pkg_dir" ]]; then
continue
fi
# Find all .smali files recursively
while IFS= read -r -d '' smali_file; do
patch_all_methods "$smali_file" "$sdk_label"
pkg_file_count=$((pkg_file_count + 1))
done < <(find "$pkg_dir" -name "*.smali" -print0 2>/dev/null)
done
echo " Processed $pkg_file_count smali file(s) in $pkg_path"
done
echo
}
# =====================================================================
# SDK Target Lists
# =====================================================================
@ -317,12 +508,12 @@ patch_ad_targets() {
# IronSource / LevelPlay
find_and_patch "com/ironsource/mediationsdk/IronSource" \
"init,loadInterstitial,showInterstitial,showRewardedVideo,loadBanner" \
"init,loadInterstitial,showInterstitial,showRewardedVideo,loadRewardedVideo,loadBanner,showISDemandOnlyInterstitial,showISDemandOnlyRewardedVideo" \
"IronSource" "Lcom/ironsource/mediationsdk/IronSource;"
# AppLovin / MAX
find_and_patch "com/applovin/sdk/AppLovinSdk" \
"getInstance,initializeSdk" \
"getInstance,initialize,initializeSdk" \
"AppLovin" "Lcom/applovin/sdk/AppLovinSdk;"
# Meta Audience Network
@ -334,6 +525,9 @@ patch_ad_targets() {
find_and_patch "com/vungle/warren/Vungle" \
"init,loadAd,playAd" \
"Vungle" "Lcom/vungle/warren/Vungle;"
find_and_patch "com/vungle/ads/VungleAds" \
"init" \
"Vungle" "Lcom/vungle/ads/VungleAds;"
find_and_patch "com/vungle/ads/VungleInterstitial" \
"load,show" \
"Vungle" "Lcom/vungle/ads/VungleInterstitial;"
@ -355,24 +549,64 @@ patch_ad_targets() {
"load" \
"InMobi" "Lcom/inmobi/ads/InMobiBanner;"
# Chartboost
# Chartboost (legacy API)
find_and_patch "com/chartboost/sdk/Chartboost" \
"startWithAppId,cacheInterstitial,showInterstitial,cacheRewardedVideo,showRewardedVideo" \
"Chartboost" "Lcom/chartboost/sdk/Chartboost;"
# Chartboost (new ads API)
find_and_patch "com/chartboost/sdk/ads/Interstitial" \
"cache,show" \
"Chartboost" "Lcom/chartboost/sdk/ads/Interstitial;"
find_and_patch "com/chartboost/sdk/ads/Rewarded" \
"cache,show" \
"Chartboost" "Lcom/chartboost/sdk/ads/Rewarded;"
# Pangle / TikTok (legacy API)
find_and_patch "com/bytedance/sdk/openadsdk/TTAdSdk" \
"init" \
"Pangle" "Lcom/bytedance/sdk/openadsdk/TTAdSdk;"
# Pangle new API
find_and_patch "com/pgl/sys/ces/PAGSdk" \
find_and_patch "com/bytedance/sdk/openadsdk/api/init/PAGSdk" \
"init" \
"Pangle" "Lcom/pgl/sys/ces/PAGSdk;"
"Pangle" "Lcom/bytedance/sdk/openadsdk/api/init/PAGSdk;"
# Mintegral
find_and_patch "com/mbridge/msdk/MBridgeSDKFactory" \
"getMBridgeSDK" \
"Mintegral" "Lcom/mbridge/msdk/MBridgeSDKFactory;"
find_and_patch "com/mbridge/msdk/MBridgeSDK" \
"init,initAsync,preload" \
"Mintegral" "Lcom/mbridge/msdk/MBridgeSDK;"
# BidMachine
find_and_patch "io/bidmachine/BidMachine" \
"initialize" \
"BidMachine" "Lio/bidmachine/BidMachine;"
# Smaato
find_and_patch "com/smaato/sdk/core/SmaatoSdk" \
"init" \
"Smaato" "Lcom/smaato/sdk/core/SmaatoSdk;"
# Verve / HyBid (PubNative)
find_and_patch "net/pubnative/lite/sdk/HyBid" \
"initialize" \
"Verve" "Lnet/pubnative/lite/sdk/HyBid;"
# Ogury
find_and_patch "com/ogury/sdk/Ogury" \
"start" \
"Ogury" "Lcom/ogury/sdk/Ogury;"
# Fyber / DT Exchange (InnerActive)
find_and_patch "com/fyber/inneractive/sdk/external/InneractiveAdManager" \
"initialize" \
"Fyber" "Lcom/fyber/inneractive/sdk/external/InneractiveAdManager;"
# Amazon APS
find_and_patch "com/amazon/device/ads/AdRegistration" \
"setAppKey,enableTesting,enableLogging" \
"AmazonAPS" "Lcom/amazon/device/ads/AdRegistration;"
}
patch_tracker_targets() {
@ -423,6 +657,16 @@ patch_tracker_targets() {
find_and_patch "com/flurry/android/FlurryAgent" \
"logEvent,setUserId,onStartSession,onEndSession" \
"Flurry" "Lcom/flurry/android/FlurryAgent;"
# Facebook SDK core
find_and_patch "com/facebook/FacebookSdk" \
"sdkInitialize,fullyInitialize,setAutoInitEnabled,setAutoLogAppEventsEnabled" \
"Facebook" "Lcom/facebook/FacebookSdk;"
# Firebase Crashlytics
find_and_patch "com/google/firebase/crashlytics/FirebaseCrashlytics" \
"log,recordException,setUserId,setCustomKey,setCrashlyticsCollectionEnabled" \
"Firebase" "Lcom/google/firebase/crashlytics/FirebaseCrashlytics;"
}
# =====================================================================
@ -486,11 +730,22 @@ patch_manifest() {
local -a components_to_disable=()
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_ADS" == true ]]; then
components_to_disable+=("${AD_COMPONENTS[@]}")
if [[ "$NO_BUILTIN_TARGETS" == false ]]; then
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_ADS" == true ]]; then
components_to_disable+=("${AD_COMPONENTS[@]}")
fi
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_TRACKERS" == true ]]; then
components_to_disable+=("${TRACKER_COMPONENTS[@]}")
fi
fi
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_TRACKERS" == true ]]; then
components_to_disable+=("${TRACKER_COMPONENTS[@]}")
# Load additional components from file (registry-scan.py output)
if [[ -n "$MANIFEST_COMPONENTS_FILE" ]] && [[ -f "$MANIFEST_COMPONENTS_FILE" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" ]] && continue
[[ "$line" == \#* ]] && continue
components_to_disable+=("$line")
done < "$MANIFEST_COMPONENTS_FILE"
fi
if [[ "$DO_BACKUP" == true ]] && [[ "$DRY_RUN" == false ]]; then
@ -553,6 +808,8 @@ patch_custom_targets() {
[[ "$line" == \#* ]] && continue
# Format: Lcom/example/Class;:methodName or com/example/Class:methodName
# Wildcards: com/example/pkg/**:* (all methods in package)
# com/example/Class:* (all methods in class)
local class_part="${line%%:*}"
local method_part="${line##*:}"
@ -565,9 +822,42 @@ patch_custom_targets() {
class_part="${class_part#L}"
class_part="${class_part%;}"
# Build class descriptor for reporting
local class_desc="L${class_part};"
# Handle package wildcard: com/example/pkg/** — treat as --package
if [[ "$class_part" == *"/**" ]]; then
local pkg_path="${class_part%/**}"
echo " Wildcard target: neutralizing package $pkg_path"
for smali_dir in "${SMALI_DIRS[@]}"; do
local pkg_dir="$smali_dir/$pkg_path"
if [[ ! -d "$pkg_dir" ]]; then
continue
fi
while IFS= read -r -d '' smali_file; do
if [[ "$method_part" == "*" ]]; then
patch_all_methods "$smali_file" "Custom"
else
local file_class_desc
file_class_desc=$(grep -m1 '^\.class ' "$smali_file" | grep -oP 'L[^ ;]+;' | head -1)
patch_method "$smali_file" "$method_part" "Custom" "${file_class_desc:-Lunknown;}"
fi
done < <(find "$pkg_dir" -name "*.smali" -print0 2>/dev/null)
done
continue
fi
# Handle class wildcard: com/example/Class:* — all methods in one class
if [[ "$method_part" == "*" ]]; then
local class_desc="L${class_part};"
for smali_dir in "${SMALI_DIRS[@]}"; do
local smali_file="$smali_dir/$class_part.smali"
if [[ -f "$smali_file" ]]; then
patch_all_methods "$smali_file" "Custom"
fi
done
continue
fi
# Standard target: specific class + method
local class_desc="L${class_part};"
find_and_patch "$class_part" "$method_part" "Custom" "$class_desc"
done < "$TARGETS_FILE"
}
@ -630,6 +920,19 @@ if [[ "$DO_REPLAY" == true ]]; then
echo
fi
# =====================================================================
# Cleanup backups (if requested)
# =====================================================================
if [[ "$DO_CLEANUP_BACKUPS" == true ]]; then
bak_count=0
while IFS= read -r -d '' bakfile; do
rm -f "$bakfile"
bak_count=$((bak_count + 1))
done < <(find "$DECODED_DIR" -name "*.smali.bak" -print0 2>/dev/null)
echo "Cleaned up $bak_count .smali.bak file(s) from $DECODED_DIR"
fi
# =====================================================================
# Main
# =====================================================================
@ -644,16 +947,21 @@ echo "Smali directories: ${SMALI_DIRS[*]}"
echo
# Patch SDK targets
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_ADS" == true ]]; then
echo "--- Neutralizing Ad SDK entry points ---"
patch_ad_targets
if [[ "$NO_BUILTIN_TARGETS" == true ]]; then
echo "--- Skipping builtin targets (--no-builtin-targets) ---"
echo
fi
else
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_ADS" == true ]]; then
echo "--- Neutralizing Ad SDK entry points ---"
patch_ad_targets
echo
fi
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_TRACKERS" == true ]]; then
echo "--- Neutralizing Tracker SDK entry points ---"
patch_tracker_targets
echo
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_TRACKERS" == true ]]; then
echo "--- Neutralizing Tracker SDK entry points ---"
patch_tracker_targets
echo
fi
fi
# Patch custom targets
@ -663,6 +971,12 @@ if [[ -n "$TARGETS_FILE" ]]; then
echo
fi
# Patch packages (--package flag)
if [[ ${#PACKAGE_PATHS[@]} -gt 0 ]]; then
echo "--- Neutralizing packages ---"
patch_packages
fi
# Patch manifest
patch_manifest

View File

@ -51,6 +51,7 @@ Output:
VERIFY_OK:<output-apk>
KEYSTORE_USED:<path>
KEYSTORE_SOURCE:debug-standard|debug-previous|debug-generated|custom
KEYSTORE_ALIAS:<alias>
SPLIT_SIGNED:<filename> (XAPK only)
XAPK_ASSEMBLED:<output-xapk> (XAPK only)
EOF
@ -231,6 +232,8 @@ build_apk() {
local no_res_flag="${1:-}"
# Clean previous build artifacts
rm -rf "$DECODED_DIR/dist/" 2>/dev/null || true
# Remove .smali.bak files that cause apktool warnings during rebuild
find "$DECODED_DIR" -name "*.smali.bak" -delete 2>/dev/null || true
if apktool b $no_res_flag "$DECODED_DIR" 2>&1; then
return 0
fi
@ -387,6 +390,7 @@ fi
echo "KEYSTORE_USED:$KEYSTORE"
echo "KEYSTORE_SOURCE:$KEYSTORE_SOURCE"
echo "KEYSTORE_ALIAS:$KEY_ALIAS"
info "Signing APK with $SIGNER..."
@ -559,7 +563,6 @@ echo
echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading."
if [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == false ]]; then
echo "Install via: adb install-multiple <base.apk> <split1.apk> <split2.apk> ..."
echo " or: use a split APK installer (e.g., SAI — Split APKs Installer)"
echo " or: unzip the XAPK and run: adb install-multiple *.apk"
elif [[ "$IS_XAPK" == true ]] && [[ "$SINGLE_APK" == true ]]; then
echo "Install via: adb install $OUTPUT"

View File

@ -0,0 +1,517 @@
#!/usr/bin/env python3
"""registry-scan.py — Scan decoded APK against SDK registry for neutralization targets.
Loads SDK registry JSONs, scans smali directories for matching packages,
and generates targets-file + manifest-components-file for neutralize.sh.
Also discovers unknown SDK packages not covered by the registry.
Requires Python 3.6+. No external dependencies.
Exit codes:
0 success (matches found)
1 error (invalid input, missing files)
2 no matches found
"""
import argparse
import json
import os
import re
import sys
from collections import defaultdict
from fnmatch import fnmatch
from pathlib import Path
# =====================================================================
# Constants
# =====================================================================
# Category mapping for --category filter
ADS_CATEGORIES = {"ads", "ads_mediation"}
TRACKER_CATEGORIES = {"analytics", "attribution", "crash_reporting", "social"}
# Known non-SDK library packages to exclude from unknown discovery
KNOWN_LIBRARY_PACKAGES = {
"android", "androidx", "kotlin", "kotlinx",
"com/google/protobuf", "com/google/gson", "com/google/common",
"com/google/android/material", "com/google/android/play",
"com/google/android/exoplayer", "com/google/android/exoplayer2",
"com/google/android/datatransport", "com/google/android/recaptcha",
"com/google/crypto", "com/google/flatbuffers",
"com/squareup/okhttp3", "com/squareup/okhttp", "com/squareup/moshi",
"com/squareup/wire", "com/squareup/picasso",
"okhttp3", "okio",
"retrofit2", "retrofit",
"com/jakewharton",
"dagger", "com/google/dagger",
"javax/inject", "javax/annotation",
"io/reactivex", "io/reactivex/rxjava3",
"com/bumptech/glide",
"com/airbnb/lottie",
"org/json", "org/intellij", "org/jetbrains",
"org/apache",
"com/google/firebase/components", "com/google/firebase/inject",
"com/google/firebase/encoders", "com/google/firebase/installations",
"com/google/firebase/sessions", "com/google/firebase/messaging",
"com/google/firebase/datatransport",
"com/google/android/gms/base", "com/google/android/gms/common",
"com/google/android/gms/tasks", "com/google/android/gms/flags",
"com/google/android/gms/dynamic", "com/google/android/gms/dynamite",
"com/google/android/gms/security", "com/google/android/gms/cloudmessaging",
"com/google/android/gms/phenotype",
"com/google/android/ump",
"bolts",
"com/github",
}
# Minimum class count to consider a package as a potential SDK
MIN_CLASSES_FOR_UNKNOWN = 10
# Minimum package depth (segments) to consider as SDK (not obfuscated)
MIN_PACKAGE_DEPTH = 3
# =====================================================================
# Registry loading
# =====================================================================
def load_registry(registry_dir):
"""Load all SDK JSON files from registry directory."""
sdks = []
registry_path = Path(registry_dir)
if not registry_path.is_dir():
print(f"Error: Registry directory not found: {registry_dir}", file=sys.stderr)
sys.exit(1)
for json_file in sorted(registry_path.glob("*.json")):
if json_file.name.startswith("_"):
continue
try:
with open(json_file, "r", encoding="utf-8") as f:
sdk = json.load(f)
sdks.append(sdk)
except (json.JSONDecodeError, IOError) as e:
print(f"Warning: Skipping {json_file.name}: {e}", file=sys.stderr)
return sdks
def filter_sdks_by_category(sdks, category):
"""Filter SDKs by category: ads, trackers, or all."""
if category == "all":
return sdks
elif category == "ads":
return [s for s in sdks if s.get("category") in ADS_CATEGORIES]
elif category == "trackers":
return [s for s in sdks if s.get("category") in TRACKER_CATEGORIES]
return sdks
# =====================================================================
# Smali scanning
# =====================================================================
def find_smali_dirs(decoded_dir):
"""Find all smali directories (multidex support)."""
decoded = Path(decoded_dir)
dirs = sorted(decoded.glob("smali*"))
return [d for d in dirs if d.is_dir()]
def get_app_package(decoded_dir):
"""Extract app package from AndroidManifest.xml."""
manifest = Path(decoded_dir) / "AndroidManifest.xml"
if not manifest.exists():
return None
try:
content = manifest.read_text(encoding="utf-8")
match = re.search(r'<manifest[^>]+package="([^"]+)"', content)
if match:
return match.group(1).replace(".", "/")
except IOError:
pass
return None
def scan_packages(smali_dirs):
"""Scan smali directories and return a map of package -> class count."""
package_classes = defaultdict(int)
for smali_dir in smali_dirs:
for smali_file in smali_dir.rglob("*.smali"):
rel = smali_file.relative_to(smali_dir)
parts = rel.parts[:-1] # directory parts = package
if parts:
package = "/".join(parts)
package_classes[package] += 1
return package_classes
def check_package_exists(smali_dirs, package_dot):
"""Check if a dot-separated package exists in any smali directory."""
package_path = package_dot.replace(".", "/")
for smali_dir in smali_dirs:
pkg_dir = smali_dir / package_path
if pkg_dir.is_dir():
return True
return False
def find_sdk_matches(sdks, smali_dirs):
"""Match registry SDKs against smali directories. Returns list of (sdk, matched_packages)."""
matches = []
for sdk in sdks:
matched_pkgs = []
for pkg in sdk.get("packages", []):
if check_package_exists(smali_dirs, pkg):
matched_pkgs.append(pkg)
if matched_pkgs:
matches.append((sdk, matched_pkgs))
return matches
# =====================================================================
# Target generation
# =====================================================================
def java_to_smali_class(java_class):
"""Convert Java class name to smali path: com.example.Foo -> com/example/Foo"""
return java_class.replace(".", "/")
def generate_targets(sdk, depth):
"""Generate targets-file lines for a matched SDK at given depth level.
Depth 1: entry_points only
Depth 2: entry_points + ad_operations
Depth 3: entry_points + ad_operations + deep_patterns
"""
lines = []
targets = sdk.get("targets", {})
protected = sdk.get("protected_patterns", [])
sdk_name = sdk.get("display_name", sdk.get("sdk_id", "Unknown"))
# Collect protected method patterns
protected_methods = set()
for pp in protected:
pattern = pp.get("pattern", "")
# Extract method name from patterns like "*.getActivity()*"
m = re.search(r'\*\.(\w+)\(', pattern)
if m:
protected_methods.add(m.group(1))
# Level 1: entry_points
for class_target in targets.get("entry_points", []):
cls = java_to_smali_class(class_target["class"])
for method in class_target.get("methods", []):
method_name = method["name"]
if method_name in protected_methods:
continue
lines.append(f"# [{sdk_name}] entry_point")
lines.append(f"{cls}:{method_name}")
# Level 2: ad_operations
if depth >= 2:
for class_target in targets.get("ad_operations", []):
cls = java_to_smali_class(class_target["class"])
for method in class_target.get("methods", []):
method_name = method["name"]
if method_name in protected_methods:
continue
lines.append(f"# [{sdk_name}] ad_operation")
lines.append(f"{cls}:{method_name}")
# Level 3: deep_patterns (package wildcards)
if depth >= 3:
for dp in targets.get("deep_patterns", []):
glob_pattern = dp["package_glob"]
# Convert "com.example.pkg.**" -> "com/example/pkg/**:*"
pkg_path = glob_pattern.replace(".", "/").rstrip("*").rstrip("/")
lines.append(f"# [{sdk_name}] deep_pattern: {dp.get('rule', 'stub_all_void')}")
lines.append(f"{pkg_path}/**:*")
return lines
def generate_manifest_components(sdk):
"""Generate manifest-components-file lines for a matched SDK."""
lines = []
sdk_name = sdk.get("display_name", sdk.get("sdk_id", "Unknown"))
for comp in sdk.get("manifest_components", []):
cls = comp["class"]
lines.append(f"{cls}|{sdk_name}")
return lines
# =====================================================================
# Unknown package discovery
# =====================================================================
def is_obfuscated_package(package):
"""Check if a package looks obfuscated (single-letter segments, too short)."""
parts = package.split("/")
# Single-letter segments are obfuscated (a/, b/c/, etc.)
if all(len(p) <= 2 for p in parts):
return True
# First two segments are single letters
if len(parts) >= 2 and len(parts[0]) <= 1 and len(parts[1]) <= 1:
return True
return False
def is_known_library(package):
"""Check if a package matches known non-SDK libraries."""
for known in KNOWN_LIBRARY_PACKAGES:
if package == known or package.startswith(known + "/"):
return True
return False
def find_unknown_packages(package_classes, matched_sdk_packages, app_package):
"""Find packages that might be unknown SDKs.
Filters:
- Exclude matched SDK packages
- Exclude app package
- Exclude known libraries (androidx, kotlin, okhttp, etc.)
- Exclude obfuscated packages (single-letter names)
- Minimum class count threshold
- Minimum package depth (3+ segments)
"""
# Build set of all matched SDK root packages (as paths)
sdk_roots = set()
for pkg_dot in matched_sdk_packages:
sdk_roots.add(pkg_dot.replace(".", "/"))
unknowns = []
# Aggregate class counts at the 3-segment level for better grouping
aggregated = defaultdict(int)
for pkg, count in package_classes.items():
parts = pkg.split("/")
if len(parts) >= 3:
root = "/".join(parts[:3])
else:
root = pkg
aggregated[root] += count
for package, class_count in sorted(aggregated.items(), key=lambda x: -x[1]):
# Check minimum class count
if class_count < MIN_CLASSES_FOR_UNKNOWN:
continue
# Check package depth
parts = package.split("/")
if len(parts) < MIN_PACKAGE_DEPTH:
continue
# Skip obfuscated
if is_obfuscated_package(package):
continue
# Skip known libraries
if is_known_library(package):
continue
# Skip app package
if app_package and (package == app_package or package.startswith(app_package + "/")):
continue
# Skip matched SDK packages
is_matched = False
for sdk_root in sdk_roots:
if package == sdk_root or package.startswith(sdk_root + "/"):
is_matched = True
break
if is_matched:
continue
unknowns.append((package, class_count))
return unknowns
# =====================================================================
# Report generation
# =====================================================================
def generate_report(matches, unknowns, depth, category):
"""Generate JSON report of scan results."""
report = {
"scan_config": {
"depth": depth,
"category": category,
},
"matched_sdks": [],
"unknown_packages": [],
}
for sdk, matched_pkgs in matches:
targets = sdk.get("targets", {})
n_entry = sum(len(ct.get("methods", [])) for ct in targets.get("entry_points", []))
n_ops = sum(len(ct.get("methods", [])) for ct in targets.get("ad_operations", []))
n_deep = len(targets.get("deep_patterns", []))
n_manifest = len(sdk.get("manifest_components", []))
# Count targets at current depth
n_targets = n_entry
if depth >= 2:
n_targets += n_ops
if depth >= 3:
n_targets += n_deep
report["matched_sdks"].append({
"sdk_id": sdk["sdk_id"],
"display_name": sdk["display_name"],
"category": sdk["category"],
"matched_packages": matched_pkgs,
"targets_count": n_targets,
"manifest_components_count": n_manifest,
"depth_breakdown": {
"entry_points": n_entry,
"ad_operations": n_ops,
"deep_patterns": n_deep,
},
})
for package, class_count in unknowns:
report["unknown_packages"].append({
"package": package,
"class_count": class_count,
})
return report
# =====================================================================
# Main
# =====================================================================
def main():
parser = argparse.ArgumentParser(
description="Scan decoded APK against SDK registry for neutralization targets."
)
parser.add_argument("decoded_dir", help="Path to apktool-decoded APK directory")
parser.add_argument(
"--registry", required=True,
help="Path to SDK registry directory containing JSON files"
)
parser.add_argument(
"--depth", type=int, choices=[1, 2, 3], default=1,
help="Neutralization depth: 1=entry_points, 2=+ad_operations, 3=+deep_patterns (default: 1)"
)
parser.add_argument(
"--category", choices=["ads", "trackers", "all"], default="all",
help="Filter SDKs by category (default: all)"
)
parser.add_argument(
"--output-dir",
help="Output directory for generated files (default: decoded-dir)"
)
args = parser.parse_args()
decoded_dir = args.decoded_dir
output_dir = args.output_dir or decoded_dir
# Validate decoded directory
if not os.path.isdir(decoded_dir):
print(f"Error: Directory not found: {decoded_dir}", file=sys.stderr)
sys.exit(1)
# Find smali directories
smali_dirs = find_smali_dirs(decoded_dir)
if not smali_dirs:
print(f"Error: No smali/ directory found in {decoded_dir}", file=sys.stderr)
sys.exit(1)
# Load and filter registry
sdks = load_registry(args.registry)
if not sdks:
print("Error: No SDK entries loaded from registry", file=sys.stderr)
sys.exit(1)
sdks = filter_sdks_by_category(sdks, args.category)
# Scan for package existence
package_classes = scan_packages(smali_dirs)
app_package = get_app_package(decoded_dir)
# Find SDK matches
matches = find_sdk_matches(sdks, smali_dirs)
# Collect all matched SDK packages for unknown discovery
all_matched_packages = set()
for sdk, matched_pkgs in matches:
for pkg in sdk.get("packages", []):
all_matched_packages.add(pkg)
# Generate targets file
targets_lines = []
targets_lines.append("# Auto-generated by registry-scan.py")
targets_lines.append(f"# Depth: {args.depth} | Category: {args.category}")
targets_lines.append("")
for sdk, matched_pkgs in matches:
sdk_targets = generate_targets(sdk, args.depth)
if sdk_targets:
targets_lines.extend(sdk_targets)
targets_lines.append("")
# Generate manifest components file
manifest_lines = []
for sdk, matched_pkgs in matches:
manifest_lines.extend(generate_manifest_components(sdk))
# Find unknown packages
unknowns = find_unknown_packages(package_classes, all_matched_packages, app_package)
# Generate report
report = generate_report(matches, unknowns, args.depth, args.category)
# Write output files
os.makedirs(output_dir, exist_ok=True)
targets_path = os.path.join(output_dir, "registry-targets.txt")
with open(targets_path, "w", encoding="utf-8") as f:
f.write("\n".join(targets_lines) + "\n")
manifest_path = os.path.join(output_dir, "registry-manifest.txt")
with open(manifest_path, "w", encoding="utf-8") as f:
f.write("\n".join(manifest_lines) + "\n")
report_path = os.path.join(output_dir, "registry-report.json")
with open(report_path, "w", encoding="utf-8") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
f.write("\n")
# Machine-readable stdout
for sdk, matched_pkgs in matches:
targets = sdk.get("targets", {})
n_entry = sum(len(ct.get("methods", [])) for ct in targets.get("entry_points", []))
n_ops = sum(len(ct.get("methods", [])) for ct in targets.get("ad_operations", []))
n_deep = len(targets.get("deep_patterns", []))
n_targets = n_entry
if args.depth >= 2:
n_targets += n_ops
if args.depth >= 3:
n_targets += n_deep
print(f"MATCHED:{sdk['sdk_id']}:{sdk['display_name']}:{sdk['category']}:{n_targets}")
for package, class_count in unknowns:
print(f"UNKNOWN_PACKAGE:{package}:{class_count}")
print(f"REGISTRY_TARGETS:{targets_path}")
print(f"REGISTRY_MANIFEST:{manifest_path}")
if not matches:
sys.exit(2)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -26,10 +26,14 @@ Options:
--manifest Search only for AndroidManifest.xml markers
--entrypoints Search only for tracker SDK calls in app code (excludes library packages)
--all Search all patterns (default)
--summary Output a compact summary table with confidence scoring
--json Output results as machine-readable JSON
-h, --help Show this help message
Output:
Results are printed as file:line:match for easy navigation.
Default: Results are printed as file:line:match for easy navigation.
--summary: Compact table with SDK | Sections | File Matches | Confidence | Status
--json: JSON object with per-SDK detection results and confidence scores
EOF
exit 0
}
@ -49,6 +53,8 @@ SEARCH_ENDPOINTS=false
SEARCH_MANIFEST=false
SEARCH_ENTRYPOINTS=false
SEARCH_ALL=true
OUTPUT_SUMMARY=false
OUTPUT_JSON=false
while [[ $# -gt 0 ]]; do
case "$1" in
@ -66,6 +72,8 @@ while [[ $# -gt 0 ]]; do
--manifest) SEARCH_MANIFEST=true; SEARCH_ALL=false; shift ;;
--entrypoints) SEARCH_ENTRYPOINTS=true; SEARCH_ALL=false; shift ;;
--all) SEARCH_ALL=true; shift ;;
--summary) OUTPUT_SUMMARY=true; shift ;;
--json) OUTPUT_JSON=true; shift ;;
-h|--help) usage ;;
-*) echo "Error: Unknown option $1" >&2; usage ;;
*) SOURCE_DIR="$1"; shift ;;
@ -84,23 +92,102 @@ fi
GREP_OPTS="-rn --include=*.java --include=*.kt"
# Summary/JSON mode: redirect output to temp file, count results per SDK
if [[ "$OUTPUT_SUMMARY" == true ]] || [[ "$OUTPUT_JSON" == true ]]; then
SUMMARY_MODE=true
# Force --all for summary mode to get complete picture
SEARCH_ALL=true
else
SUMMARY_MODE=false
fi
# Associative arrays for summary tracking (SDK -> match count, sections with hits)
declare -A SDK_CLASS_MATCHES # SDK-specific class/import matches (HIGH confidence)
declare -A SDK_STRING_MATCHES # SDK string/generic matches (MEDIUM confidence)
declare -A SDK_SECTIONS_HIT # comma-separated list of sections with hits
section() {
echo
echo "==== $1 ===="
echo
if [[ "$SUMMARY_MODE" == true ]]; then
CURRENT_SECTION="$1"
else
echo
echo "==== $1 ===="
echo
fi
}
run_grep() {
local pattern="$1"
# shellcheck disable=SC2086
grep $GREP_OPTS -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true
local result
result=$(grep $GREP_OPTS -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true)
if [[ "$SUMMARY_MODE" == true ]]; then
if [[ -n "$result" ]]; then
local count
count=$(echo "$result" | wc -l)
# Determine which SDK this section belongs to and confidence level
_tally_section_result "$count"
fi
else
echo "$result"
fi
}
run_grep_xml() {
local pattern="$1"
grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true
local result
result=$(grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true)
if [[ "$SUMMARY_MODE" == true ]]; then
if [[ -n "$result" ]]; then
local count
count=$(echo "$result" | wc -l)
_tally_section_result "$count"
fi
else
echo "$result"
fi
}
# Maps section name to SDK and tallies results
_tally_section_result() {
local count="$1"
local sdk=""
local confidence="class" # default to HIGH (class-level match)
case "$CURRENT_SECTION" in
"Firebase Analytics"*) sdk="Firebase" ;;
"Adjust"*) sdk="Adjust" ;;
"AppsFlyer"*) sdk="AppsFlyer" ;;
"Mixpanel"*) sdk="Mixpanel" ;;
"Amplitude"*) sdk="Amplitude" ;;
"Segment"*) sdk="Segment" ;;
"Braze"*) sdk="Braze" ;;
"CleverTap"*) sdk="CleverTap" ;;
"Flurry"*) sdk="Flurry" ;;
"Generic"*) sdk="Generic"; confidence="string" ;;
"Known Tracker Endpoints"*) sdk="Endpoints"; confidence="class" ;;
"AndroidManifest"*) sdk="Manifest"; confidence="class" ;;
"Entry Points"*) sdk="EntryPoints"; confidence="class" ;;
*) sdk="Other"; confidence="string" ;;
esac
if [[ "$confidence" == "class" ]]; then
SDK_CLASS_MATCHES["$sdk"]=$(( ${SDK_CLASS_MATCHES["$sdk"]:-0} + count ))
else
SDK_STRING_MATCHES["$sdk"]=$(( ${SDK_STRING_MATCHES["$sdk"]:-0} + count ))
fi
# Track which sections had hits
local existing="${SDK_SECTIONS_HIT["$sdk"]:-}"
if [[ -n "$existing" ]]; then
SDK_SECTIONS_HIT["$sdk"]="${existing}, ${CURRENT_SECTION}"
else
SDK_SECTIONS_HIT["$sdk"]="${CURRENT_SECTION}"
fi
}
CURRENT_SECTION=""
# --- Firebase Analytics ---
if [[ "$SEARCH_ALL" == true || "$SEARCH_FIREBASE" == true ]]; then
section "Firebase Analytics — Initialization"
@ -257,12 +344,103 @@ if [[ "$SEARCH_ALL" == true || "$SEARCH_ENTRYPOINTS" == true ]]; then
ENTRYPOINT_PATTERN='(FirebaseAnalytics\.getInstance|FirebaseAnalytics\.newInstance|FirebaseApp\.initializeApp|\.logEvent\s*\(|\.setUserId\s*\(|\.setUserProperty\s*\(|\.setAnalyticsCollectionEnabled|Adjust\.onCreate|Adjust\.trackEvent|AdjustConfig\s*\(|AppsFlyerLib\.getInstance|\.init\s*\(.*AF_DEV_KEY|\.start\s*\(.*AppsFlyerLib|MixpanelAPI\.getInstance|\.track\s*\(.*MixpanelAPI|\.identify\s*\(.*Mixpanel|Amplitude\.getInstance|\.logEvent\s*\(.*Amplitude|Analytics\.with\s*\(|\.track\s*\(.*Segment|\.identify\s*\(.*Segment|Braze\.configure|\.logCustomEvent\s*\(|\.changeUser\s*\(.*Braze|CleverTapAPI\.getDefaultInstance|\.pushEvent\s*\(|\.onUserLogin\s*\(|FlurryAgent\.logEvent|FlurryAgent\.setUserId|FlurryAgent\.Builder)'
# shellcheck disable=SC2086
grep -rn --include="*.java" --include="*.kt" "${EXCLUDE_DIRS[@]}" -E "$ENTRYPOINT_PATTERN" "$SOURCE_DIR" 2>/dev/null || true
echo
echo "NOTE: Only calls from app code are shown above. Library-internal calls are excluded."
echo "If no results appear, tracker SDKs may only be initialized internally (e.g., via ContentProvider)."
local ep_result
ep_result=$(grep -rn --include="*.java" --include="*.kt" "${EXCLUDE_DIRS[@]}" -E "$ENTRYPOINT_PATTERN" "$SOURCE_DIR" 2>/dev/null || true)
if [[ "$SUMMARY_MODE" == true ]]; then
if [[ -n "$ep_result" ]]; then
local count
count=$(echo "$ep_result" | wc -l)
_tally_section_result "$count"
fi
else
echo "$ep_result"
echo
echo "NOTE: Only calls from app code are shown above. Library-internal calls are excluded."
echo "If no results appear, tracker SDKs may only be initialized internally (e.g., via ContentProvider)."
fi
fi
echo
echo "=== Search complete ==="
# =====================================================================
# Summary / JSON output
# =====================================================================
if [[ "$SUMMARY_MODE" == true ]]; then
# Known tracker SDKs to report on (exclude meta categories)
TRACKER_SDKS=("Firebase" "Adjust" "AppsFlyer" "Mixpanel" "Amplitude" "Segment" "Braze" "CleverTap" "Flurry")
if [[ "$OUTPUT_JSON" == true ]]; then
# JSON output
printf '{\n "trackers": [\n'
first=true
for sdk in "${TRACKER_SDKS[@]}"; do
class_count=${SDK_CLASS_MATCHES["$sdk"]:-0}
string_count=${SDK_STRING_MATCHES["$sdk"]:-0}
total=$((class_count + string_count))
# Determine confidence
if [[ $class_count -gt 0 ]]; then
confidence="HIGH"
elif [[ $string_count -gt 0 ]]; then
confidence="MEDIUM"
else
confidence="NONE"
fi
[[ "$confidence" == "NONE" ]] && continue
if [[ "$first" == true ]]; then
first=false
else
printf ',\n'
fi
sections="${SDK_SECTIONS_HIT["$sdk"]:-}"
printf ' {"sdk": "%s", "class_matches": %d, "string_matches": %d, "total_matches": %d, "confidence": "%s", "sections": "%s"}' \
"$sdk" "$class_count" "$string_count" "$total" "$confidence" "$sections"
done
printf '\n ]\n}\n'
else
# Summary table output
echo
echo "=== Tracker Detection Summary ==="
echo
printf "%-15s | %6s | %7s | %5s | %-10s | %s\n" "SDK" "Class" "String" "Total" "Confidence" "Status"
printf "%-15s-|-%6s-|-%7s-|-%5s-|-%-10s-|-%s\n" "---------------" "------" "-------" "-----" "----------" "--------"
for sdk in "${TRACKER_SDKS[@]}"; do
class_count=${SDK_CLASS_MATCHES["$sdk"]:-0}
string_count=${SDK_STRING_MATCHES["$sdk"]:-0}
total=$((class_count + string_count))
if [[ $class_count -gt 0 ]]; then
confidence="HIGH"
status="DETECTED"
elif [[ $string_count -gt 0 ]]; then
confidence="MEDIUM"
status="LIKELY"
else
confidence="NONE"
status="NOT FOUND"
fi
printf "%-15s | %6d | %7d | %5d | %-10s | %s\n" \
"$sdk" "$class_count" "$string_count" "$total" "$confidence" "$status"
done
# Additional info
ep_count=${SDK_CLASS_MATCHES["EntryPoints"]:-0}
if [[ $ep_count -gt 0 ]]; then
echo
echo "Entry points in app code: $ep_count match(es)"
fi
manifest_count=${SDK_CLASS_MATCHES["Manifest"]:-0}
if [[ $manifest_count -gt 0 ]]; then
echo "Manifest markers: $manifest_count match(es)"
fi
echo
echo "Confidence: HIGH = SDK-specific classes found, MEDIUM = only generic/string matches, NONE = not detected"
fi
else
echo
echo "=== Search complete ==="
fi