SDK Neutralizer v2: fix duplicate attrs, XAPK support, version check, build fallback, new patterns, replay
Six improvements from real-world testing on Water Sort Puzzle XAPK: - Fix manifest patching creating duplicate android:enabled attributes when the component already has android:enabled="true" (3-way check logic) - Add decode-apk.sh with XAPK support (auto-extracts base APK, skips splits) - Add apktool >= 2.9.0 minimum version check in check-neutralize-deps.sh - Add --no-res fallback in rebuild-apk.sh when apktool build fails on resources - Add 13 missing manifest components (Vungle new SDK, Meta AN provider, AppLovin/BidMachine/IronSource init providers, Amazon APS, Pangle/Mintegral activities, Smaato, AppsFlyer internal receiver) - Add patch persistence via neutralize-manifest.json and --replay flag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f216ec0914
commit
e06f794112
21
CLAUDE.md
21
CLAUDE.md
|
|
@ -4,10 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
A Claude Code Skill (plugin) for Android reverse engineering, API extraction, and privacy auditing. It provides three skills:
|
||||
A Claude Code Skill (plugin) for Android reverse engineering, API extraction, and privacy auditing. It provides four skills:
|
||||
- **android-reverse-engineering**: 5-phase workflow — dependency verification, APK/XAPK/JAR/AAR decompilation (jadx and/or Fernflower), manifest/structure analysis, call flow tracing, and HTTP API endpoint extraction
|
||||
- **tracker-analysis**: 4-phase workflow — detect analytics/tracker SDKs (Firebase, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry), analyze init/events/user ID/consent, report data exfiltration endpoints
|
||||
- **ad-analysis**: 3-phase workflow — detect ad SDKs (AdMob, Unity, IronSource, AppLovin, Meta AN, Vungle, InMobi, Chartboost, Pangle, Mintegral), map ad formats and mediation setup, report privacy/consent
|
||||
- **sdk-neutralizer**: 6-phase workflow — decode APK, identify tracker/ad SDK entry points, neutralize by replacing smali method bodies with stubs, disable manifest components, rebuild and sign APK for enterprise sideloading
|
||||
|
||||
## Repository Structure
|
||||
|
||||
|
|
@ -16,9 +17,11 @@ A Claude Code Skill (plugin) for Android reverse engineering, API extraction, an
|
|||
- `plugins/android-reverse-engineering/commands/decompile.md` — `/decompile` slash command
|
||||
- `plugins/android-reverse-engineering/commands/find-trackers.md` — `/find-trackers` slash command
|
||||
- `plugins/android-reverse-engineering/commands/find-ads.md` — `/find-ads` slash command
|
||||
- `plugins/android-reverse-engineering/commands/neutralize.md` — `/neutralize` slash command
|
||||
- `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, rebuild-apk.sh)
|
||||
|
||||
## Key Scripts
|
||||
|
||||
|
|
@ -52,6 +55,22 @@ Ad analysis script under `plugins/android-reverse-engineering/skills/ad-analysis
|
|||
bash find-ads.sh <source-dir> [--admob|--unity|--ironsource|--applovin|--facebook|--formats|--mediation|--consent|--entrypoints|--all]
|
||||
```
|
||||
|
||||
SDK neutralizer scripts under `plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/`:
|
||||
|
||||
```bash
|
||||
# Check neutralization dependencies (including apktool >= 2.9.0)
|
||||
bash check-neutralize-deps.sh
|
||||
|
||||
# Decode APK or XAPK (extracts base APK from XAPK automatically)
|
||||
bash decode-apk.sh <file.apk|file.xapk> [-o <decoded-dir>]
|
||||
|
||||
# Neutralize SDK entry points in decoded APK (dry-run first)
|
||||
bash neutralize.sh <decoded-dir> [--ads|--trackers|--all] [--dry-run] [--no-backup] [--no-manifest] [--targets-file <file>] [--replay] [--no-save-manifest]
|
||||
|
||||
# Rebuild and sign neutralized APK
|
||||
bash rebuild-apk.sh <decoded-dir> [--debug-key|--keystore <file>] [-o <output>] [--no-sign] [--no-res] [--zipalign]
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
**Plugin structure follows Claude Code skill conventions:**
|
||||
|
|
|
|||
81
README.md
81
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# Android Reverse Engineering & API Extraction — Claude Code skill
|
||||
|
||||
A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts HTTP APIs**, and **audits privacy** by detecting tracker/analytics and advertising SDKs — so you can document endpoints, understand data collection, and assess ad monetization without the original source code.
|
||||
A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts HTTP APIs**, **audits privacy** by detecting tracker/analytics and advertising SDKs, and **neutralizes SDK telemetry** at the smali bytecode level for enterprise deployment — so you can document endpoints, understand data collection, assess ad monetization, and produce sanitized APKs without the original source code.
|
||||
|
||||
## What it does
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts H
|
|||
- **Traces call flows** from Activities/Fragments through ViewModels and repositories down to HTTP calls
|
||||
- **Detects tracker/analytics SDKs**: Firebase Analytics, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry — with deep analysis of init, events, user identification, consent, and data exfiltration endpoints
|
||||
- **Detects advertising SDKs**: AdMob, Unity Ads, IronSource/LevelPlay, AppLovin/MAX, Meta Audience Network, Vungle, InMobi, Chartboost, Pangle, Mintegral — with ad format mapping, mediation analysis, and consent framework detection
|
||||
- **Neutralizes SDK entry points**: replaces tracker/ad SDK method bodies with no-op stubs at the smali level, disables manifest components, and rebuilds a signed APK for enterprise sideloading
|
||||
- **Analyzes** app structure: manifest, packages, architecture patterns
|
||||
- **Handles obfuscated code**: strategies for navigating ProGuard/R8 output
|
||||
|
||||
|
|
@ -22,6 +23,10 @@ A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts H
|
|||
- [Vineflower](https://github.com/Vineflower/vineflower) or [Fernflower](https://github.com/JetBrains/fernflower) — better output on complex Java code
|
||||
- [dex2jar](https://github.com/pxb1988/dex2jar) — needed to use Fernflower on APK/DEX files
|
||||
|
||||
**For SDK neutralization (`/neutralize`):**
|
||||
- [apktool](https://apktool.org/) (required) — APK decode/rebuild
|
||||
- apksigner or jarsigner (required) — APK signing
|
||||
|
||||
See `plugins/android-reverse-engineering/skills/android-reverse-engineering/references/setup-guide.md` for detailed installation instructions.
|
||||
|
||||
## Installation
|
||||
|
|
@ -37,6 +42,29 @@ Inside Claude Code, run:
|
|||
|
||||
The skill will be permanently available in all future sessions.
|
||||
|
||||
### Permissions
|
||||
|
||||
Claude Code will ask for approval when the skill runs bash scripts (e.g., `neutralize.sh`, `find-ads.sh`, `apktool`). This is standard Claude Code security behaviour — `allowed-tools` in skills declares which tools may be used, but does not bypass your permission settings.
|
||||
|
||||
To avoid repeated prompts, you can either:
|
||||
|
||||
- **Per-session**: when prompted, select *"Yes, and don't ask again for: bash:\*"*
|
||||
- **Permanent**: add the following to your `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(bash */sdk-neutralizer/scripts/*)",
|
||||
"Bash(bash */ad-analysis/scripts/*)",
|
||||
"Bash(bash */tracker-analysis/scripts/*)",
|
||||
"Bash(bash */android-reverse-engineering/scripts/*)",
|
||||
"Bash(apktool *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### From a local clone
|
||||
|
||||
```bash
|
||||
|
|
@ -69,6 +97,13 @@ Detects analytics/tracker SDKs and produces a privacy report with init patterns,
|
|||
```
|
||||
Detects advertising SDKs and produces a report with ad formats, mediation setup, ad unit IDs, and consent framework analysis.
|
||||
|
||||
```
|
||||
/neutralize path/to/app.apk
|
||||
```
|
||||
Neutralizes tracker/ad SDK entry points in the APK, producing a sanitized APK for enterprise sideloading with telemetry disabled.
|
||||
|
||||
> **Warning**: SDK neutralization modifies bytecode and can cause crashes, broken features, or unexpected behaviour. The APK signature is invalidated. Ensure you have authorization to modify the application and that your use complies with applicable laws and the app's EULA. See the [Disclaimer](#disclaimer) for details.
|
||||
|
||||
### Natural language
|
||||
|
||||
The skills activate on phrases like:
|
||||
|
|
@ -82,6 +117,9 @@ The skills activate on phrases like:
|
|||
- "What analytics SDKs does this app use?"
|
||||
- "Detect ad networks in this app"
|
||||
- "Show me the ad mediation setup"
|
||||
- "Neutralize trackers in this APK"
|
||||
- "Remove telemetry from this app"
|
||||
- "Sanitize this APK for enterprise deployment"
|
||||
|
||||
### Manual scripts
|
||||
|
||||
|
|
@ -121,6 +159,18 @@ bash plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-tr
|
|||
bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/
|
||||
bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/ --admob
|
||||
bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/ --mediation
|
||||
|
||||
# Neutralize SDK entry points (decode → patch → rebuild)
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh app.apk -o app-decoded
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all --dry-run
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --all
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/rebuild-apk.sh app-decoded --debug-key
|
||||
|
||||
# XAPK support — decode-apk.sh extracts the base APK automatically
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh app-bundle.xapk -o app-decoded
|
||||
|
||||
# Replay previous patches after re-decoding
|
||||
bash plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh app-decoded --replay
|
||||
```
|
||||
|
||||
## Repository Structure
|
||||
|
|
@ -155,18 +205,29 @@ android-reverse-engineering-skill/
|
|||
│ │ │ │ └── data-exfiltration-patterns.md
|
||||
│ │ │ └── scripts/
|
||||
│ │ │ └── find-trackers.sh
|
||||
│ │ └── ad-analysis/ # Advertising SDK detection
|
||||
│ │ ├── SKILL.md # 3-phase workflow
|
||||
│ │ ├── ad-analysis/ # Advertising SDK detection
|
||||
│ │ │ ├── SKILL.md # 3-phase workflow
|
||||
│ │ │ ├── references/
|
||||
│ │ │ │ ├── ad-sdk-catalog.md
|
||||
│ │ │ │ ├── mediation-patterns.md
|
||||
│ │ │ │ └── ad-format-patterns.md
|
||||
│ │ │ └── scripts/
|
||||
│ │ │ └── find-ads.sh
|
||||
│ │ └── sdk-neutralizer/ # SDK neutralization for enterprise
|
||||
│ │ ├── SKILL.md # 6-phase workflow
|
||||
│ │ ├── references/
|
||||
│ │ │ ├── ad-sdk-catalog.md
|
||||
│ │ │ ├── mediation-patterns.md
|
||||
│ │ │ └── ad-format-patterns.md
|
||||
│ │ │ ├── neutralization-guide.md
|
||||
│ │ │ └── smali-patterns.md
|
||||
│ │ └── scripts/
|
||||
│ │ └── find-ads.sh
|
||||
│ │ ├── check-neutralize-deps.sh
|
||||
│ │ ├── decode-apk.sh
|
||||
│ │ ├── neutralize.sh
|
||||
│ │ └── rebuild-apk.sh
|
||||
│ └── commands/
|
||||
│ ├── decompile.md # /decompile slash command
|
||||
│ ├── find-trackers.md # /find-trackers slash command
|
||||
│ └── find-ads.md # /find-ads slash command
|
||||
│ ├── find-ads.md # /find-ads slash command
|
||||
│ └── neutralize.md # /neutralize slash command
|
||||
├── LICENSE
|
||||
└── README.md
|
||||
```
|
||||
|
|
@ -187,6 +248,10 @@ This plugin is provided strictly for **lawful purposes**, including but not limi
|
|||
- Interoperability analysis permitted under applicable law (e.g., EU Directive 2009/24/EC, US DMCA §1201(f))
|
||||
- Malware analysis and incident response
|
||||
- Educational use and CTF competitions
|
||||
- Enterprise privacy compliance and data minimisation (GDPR Art. 5(1)(c))
|
||||
- Authorized internal distribution of sanitized applications
|
||||
|
||||
**SDK neutralization** modifies APK bytecode and invalidates the original signature. The resulting APK will fail Play Integrity checks and is intended only for enterprise sideloading via MDM or authorized internal distribution. Using this feature to circumvent digital rights management for unauthorized purposes is prohibited.
|
||||
|
||||
**You are solely responsible** for ensuring that your use of this tool complies with all applicable laws, regulations, and terms of service. Unauthorized reverse engineering of software you do not own or do not have permission to analyze may violate intellectual property laws and computer fraud statutes in your jurisdiction.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
---
|
||||
allowed-tools: Bash, Read, Glob, Grep, Write, Edit
|
||||
description: Neutralize tracker/ad SDK entry points in an Android APK for enterprise deployment
|
||||
user-invocable: true
|
||||
argument-hint: <path to APK file>
|
||||
argument: path to APK file (optional)
|
||||
---
|
||||
|
||||
# /neutralize
|
||||
|
||||
Neutralize tracker and ad SDK entry points in an Android APK, producing a sanitized APK for enterprise sideloading.
|
||||
|
||||
## Instructions
|
||||
|
||||
You are starting the SDK neutralization workflow. Follow these steps:
|
||||
|
||||
### Step 1: Responsible use warning
|
||||
|
||||
**This step is mandatory and must not be skipped.**
|
||||
|
||||
Before doing anything else, warn the user clearly about the implications of SDK neutralization:
|
||||
|
||||
> **Before we proceed, please be aware of the following:**
|
||||
>
|
||||
> **Side effects** — Neutralizing SDK entry points can cause the app to crash (NullPointerException from stubbed methods), lose features (rewarded ads, A/B testing, analytics-gated content), or behave unexpectedly at startup. The original APK signature will be invalidated — Play Integrity will fail.
|
||||
>
|
||||
> **Legal/EULA implications** — Modifying an APK may violate the app's Terms of Service, SDK provider agreements, and intellectual property laws depending on your jurisdiction. Legitimate uses include authorized enterprise deployment, security research, and privacy compliance (EU Directive 2009/24/EC, GDPR data minimisation), but you are responsible for verifying you have proper authorization.
|
||||
>
|
||||
> **Please confirm**: Do you have authorization to modify this application, and do you understand the potential side effects?
|
||||
|
||||
**Wait for the user to explicitly confirm before proceeding.** If the user declines or expresses doubt, do not continue — suggest they consult their legal/compliance team first.
|
||||
|
||||
### Step 2: Get the APK/XAPK file
|
||||
|
||||
If the user provided a path as an argument, use that. Otherwise, ask the user for the path to the APK or XAPK file.
|
||||
|
||||
Verify the file exists and is an APK or XAPK:
|
||||
|
||||
```bash
|
||||
file "$APK_PATH"
|
||||
```
|
||||
|
||||
### Step 3: Check dependencies
|
||||
|
||||
Run the dependency check to ensure all required tools are installed:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh
|
||||
```
|
||||
|
||||
If any `INSTALL_REQUIRED:` lines appear, install the missing dependencies:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh <dep>
|
||||
```
|
||||
|
||||
### Step 4: Decode APK/XAPK
|
||||
|
||||
Decode the APK or XAPK using decode-apk.sh (handles both formats; for XAPKs extracts the base APK automatically):
|
||||
|
||||
```bash
|
||||
# Strip both .apk and .xapk extensions for the output dir name
|
||||
DECODED_DIR="${APK_PATH%.*}-decoded"
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/decode-apk.sh "$APK_PATH" -o "$DECODED_DIR"
|
||||
```
|
||||
|
||||
Verify the decoded directory contains `smali/` and `AndroidManifest.xml` (the script does this automatically and outputs `DECODED_DIR:<path>`).
|
||||
|
||||
### Step 5: Identify targets
|
||||
|
||||
Run entry point detection to find which SDK calls exist in the app code:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Present the results and ask the user what to neutralize:
|
||||
- **Ads only** (`--ads`)
|
||||
- **Trackers only** (`--trackers`)
|
||||
- **Both** (`--all`, recommended)
|
||||
|
||||
### Step 6: Dry-run preview
|
||||
|
||||
Always run a dry-run first so the user can review what will be patched:
|
||||
|
||||
```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).
|
||||
|
||||
### Step 7: Neutralize
|
||||
|
||||
Apply the neutralization:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh "${DECODED_DIR}" --all
|
||||
```
|
||||
|
||||
Parse the `PATCHED:` and `MANIFEST_DISABLED:` output lines for the report.
|
||||
|
||||
### Step 8: Rebuild & sign
|
||||
|
||||
Rebuild the APK with a debug signing key:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh "${DECODED_DIR}" --debug-key
|
||||
```
|
||||
|
||||
### Step 9: Report & next steps
|
||||
|
||||
Generate a neutralization report following the format in `${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/SKILL.md` (Phase 6). **The report must include the "Side Effects & Legal Notice" section.**
|
||||
|
||||
Tell the user what they can do next:
|
||||
- **Test thoroughly**: "Install via `adb install <apk>` and 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"
|
||||
- **Restore**: "Backup `.smali.bak` files were created — I can restore the original methods"
|
||||
- **Legal review**: "Have your legal/compliance team review before distributing the modified APK"
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
---
|
||||
description: Neutralize tracker and ad SDK entry points in Android APKs at the smali bytecode level. Replaces SDK method bodies with stubs (return-void, return null) and disables manifest components. Produces sanitized APKs for enterprise sideloading with telemetry and advertising disabled.
|
||||
trigger: neutralize SDK|neutralize trackers|neutralize ads|remove trackers|disable telemetry|sanitize APK|enterprise APK|strip trackers|strip ads|kill telemetry|patch SDK
|
||||
---
|
||||
|
||||
# SDK Neutralizer
|
||||
|
||||
Neutralize tracker/analytics and advertising SDK entry points in decoded Android APKs. Replaces SDK method bodies with no-op stubs at the smali level and disables manifest components, producing a sanitized APK for enterprise deployment.
|
||||
|
||||
## IMPORTANT — Responsible Use Notice
|
||||
|
||||
**Before starting any neutralization work, you MUST warn the user about the following.** Present this notice clearly and ask the user to confirm they understand and accept before proceeding.
|
||||
|
||||
### Side Effects
|
||||
|
||||
Neutralizing SDK entry points can cause **unexpected app behaviour**:
|
||||
|
||||
- **Crashes**: stubbed `getInstance()` methods return `null`. Any code that calls methods on the result without null-checking will throw `NullPointerException` and crash.
|
||||
- **Broken features**: some app features depend on SDK functionality (e.g., rewarded ads gate premium content, analytics events trigger server-side logic, A/B testing controls UI). Neutralizing the SDK breaks these features.
|
||||
- **Silent data loss**: if the app persists analytics data locally before sending, stubbing the send methods leaves orphan data that may grow indefinitely.
|
||||
- **Startup failures**: SDKs initialized via `ContentProvider` auto-init may cause errors during app startup if their components are disabled in the manifest.
|
||||
- **Native library conflicts**: SDKs with native `.so` components may perform integrity checks that detect the modification and crash or silently disable unrelated functionality.
|
||||
|
||||
**The dry-run step is mandatory** — always show the user what will be patched and get explicit confirmation before applying changes.
|
||||
|
||||
### Legal and EULA Implications
|
||||
|
||||
Modifying an APK may violate:
|
||||
|
||||
- **The app's Terms of Service or EULA** — most app licenses explicitly prohibit reverse engineering and modification.
|
||||
- **SDK provider agreements** — ad/analytics SDK terms typically prohibit tampering with their code.
|
||||
- **Intellectual property laws** — depending on jurisdiction, unauthorized modification may constitute copyright infringement.
|
||||
- **Distribution restrictions** — redistributing modified APKs (even internally) may require legal authorization.
|
||||
|
||||
Legitimate use cases exist (enterprise privacy compliance, authorized security testing, interoperability under EU Directive 2009/24/EC, GDPR data minimisation), but the user **must verify they have proper authorization** for their specific situation.
|
||||
|
||||
**Always remind the user**: "Make sure you have the right to modify this application and that your use complies with applicable laws, the app's EULA, and your organization's policies."
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This skill requires an APK file. It will decode the APK with apktool, neutralize SDK methods in the smali code, and rebuild a signed APK.
|
||||
|
||||
Required tools: `java 17+`, `apktool`, `apksigner` or `jarsigner`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 0: Responsible Use Warning
|
||||
|
||||
**Before any technical step**, present the user with the side effects and legal notice above. Ask the user to explicitly confirm:
|
||||
|
||||
1. They have authorization to modify this APK (e.g., they own the app, have enterprise authorization, or are doing authorized security research).
|
||||
2. They understand that the modified APK may crash, lose features, or behave unexpectedly.
|
||||
3. They understand that the APK signature will be invalidated and Play Integrity will fail.
|
||||
|
||||
**Do not proceed if the user does not confirm.** This is not optional.
|
||||
|
||||
### Phase 1: Verify Dependencies
|
||||
|
||||
Check that all required tools are installed.
|
||||
|
||||
**Action**: Run the dependency check.
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/check-neutralize-deps.sh
|
||||
```
|
||||
|
||||
If any `INSTALL_REQUIRED:` lines appear, install the missing dependencies:
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/install-dep.sh <dep>
|
||||
```
|
||||
|
||||
### Phase 2: Decode APK
|
||||
|
||||
Decode the APK (or XAPK) into smali and resources using decode-apk.sh. This script handles both `.apk` and `.xapk` files — for XAPKs it automatically extracts the base APK, skipping split/config APKs.
|
||||
|
||||
**Action**: Run the decode script.
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/decode-apk.sh <apk-or-xapk-file> -o <decoded-dir>
|
||||
```
|
||||
|
||||
The script verifies the output contains `smali/` and `AndroidManifest.xml` and outputs `DECODED_DIR:<path>`.
|
||||
|
||||
### Phase 3: Identify Targets
|
||||
|
||||
Run the tracker and ad detection scripts on the decoded smali to identify which SDKs are present and which entry points the app uses.
|
||||
|
||||
**Action**: Run entry point detection.
|
||||
|
||||
```bash
|
||||
# Detect ad SDK entry points called from app code
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh <decoded-dir> --entrypoints
|
||||
|
||||
# Detect tracker SDK entry points called from app code
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh <decoded-dir> --entrypoints
|
||||
```
|
||||
|
||||
Present the detected SDKs and entry points to the user. Ask which categories to neutralize:
|
||||
- `--ads` — only ad SDKs
|
||||
- `--trackers` — only tracker/analytics SDKs
|
||||
- `--all` — both (default)
|
||||
|
||||
### Phase 4: Neutralize
|
||||
|
||||
Run the neutralization script. **Always run a dry-run first** to preview changes.
|
||||
|
||||
**Action**: Dry-run, then apply.
|
||||
|
||||
```bash
|
||||
# Preview changes (no files modified)
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/neutralize.sh <decoded-dir> --all --dry-run
|
||||
|
||||
# Apply changes (with backups)
|
||||
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
|
||||
- `--dry-run` — preview only
|
||||
- `--no-backup` — skip `.smali.bak` creation
|
||||
- `--no-manifest` — skip manifest patching
|
||||
- `--targets-file <file>` — additional custom targets
|
||||
- `--replay` — replay patches from a previous `neutralize-manifest.json` (useful after re-decode)
|
||||
- `--no-save-manifest` — skip saving `neutralize-manifest.json`
|
||||
|
||||
After a successful (non-dry-run) neutralization, a `neutralize-manifest.json` is saved in the decoded directory. This file records all patched methods and disabled components. If the APK is re-decoded, use `--replay` to reapply the same patches automatically.
|
||||
|
||||
### Phase 5: Rebuild & Sign
|
||||
|
||||
Rebuild the decoded directory back into a signed APK.
|
||||
|
||||
**Action**: Run the rebuild script.
|
||||
|
||||
```bash
|
||||
bash ${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/scripts/rebuild-apk.sh <decoded-dir> --debug-key
|
||||
```
|
||||
|
||||
Options:
|
||||
- `-o <output.apk>` — custom output path
|
||||
- `--debug-key` — auto-generate debug keystore (default)
|
||||
- `--keystore <file>` — use a custom keystore
|
||||
- `--no-sign` — output unsigned APK
|
||||
- `--zipalign` / `--no-zipalign` — control zipalign step
|
||||
|
||||
### Phase 6: Verify & Report
|
||||
|
||||
Generate a structured neutralization report and suggest next steps.
|
||||
|
||||
**Report format:**
|
||||
|
||||
```markdown
|
||||
# Neutralization Report — <app name>
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | SDKs Targeted | Methods Patched | Manifest Components Disabled |
|
||||
|---|---|---|---|
|
||||
| Ad SDKs | AdMob, Unity, IronSource | 12 | 5 |
|
||||
| Tracker SDKs | Firebase, Adjust, AppsFlyer | 8 | 3 |
|
||||
| **Total** | **6** | **20** | **8** |
|
||||
|
||||
## Patched Methods
|
||||
|
||||
| SDK | Method | File | Stub Type |
|
||||
|---|---|---|---|
|
||||
| AdMob | initialize | smali/com/google/.../MobileAds.smali | return-void |
|
||||
| Firebase | logEvent | smali/com/google/.../FirebaseAnalytics.smali | return-void |
|
||||
| Firebase | getInstance | smali/com/google/.../FirebaseAnalytics.smali | const/4+return-object |
|
||||
| ... | | | |
|
||||
|
||||
## Disabled Manifest Components
|
||||
|
||||
| Component | Type | SDK |
|
||||
|---|---|---|
|
||||
| com.google.android.gms.ads.AdActivity | activity | AdMob |
|
||||
| com.google.android.gms.measurement.AppMeasurementService | service | Firebase |
|
||||
| ... | | |
|
||||
|
||||
## Warnings
|
||||
|
||||
- Play Integrity / SafetyNet will FAIL (expected for enterprise sideloading)
|
||||
- Stubbed getInstance() methods return null — may cause NullPointerException in app code
|
||||
- Features gated behind ad views (e.g., rewarded content) will stop working
|
||||
- [Any SDK-specific warnings, e.g., native .so integrity checks detected]
|
||||
- [Obfuscated classes that could not be matched]
|
||||
|
||||
## Side Effects & Legal Notice
|
||||
|
||||
This APK has been modified at the bytecode level. The original signature is invalidated.
|
||||
|
||||
**Side effects**: The app may crash, lose features, or behave unexpectedly due to
|
||||
neutralized SDK methods. Test thoroughly before deploying.
|
||||
|
||||
**Legal**: Ensure you have proper authorization to modify and distribute this application.
|
||||
Modifying APKs may violate the app's EULA, SDK provider agreements, or intellectual
|
||||
property laws. This tool is intended for authorized enterprise use, security research,
|
||||
and privacy compliance only.
|
||||
|
||||
## Output
|
||||
|
||||
- Sanitized APK: `<path>`
|
||||
- Signed with: debug key / custom keystore
|
||||
- Install via: `adb install <path>`
|
||||
```
|
||||
|
||||
**Next steps to suggest:**
|
||||
- Re-run `find-ads.sh --entrypoints` and `find-trackers.sh --entrypoints` on the rebuilt APK to verify neutralization
|
||||
- **Test the APK thoroughly** on a device/emulator — watch for crashes, broken features, and startup errors
|
||||
- Check for runtime crashes caused by null returns from stubbed `getInstance()` methods
|
||||
- Use `--targets-file` to add custom neutralization targets for obfuscated code
|
||||
- Review the legal implications with your organization's legal/compliance team before distributing
|
||||
|
||||
## References
|
||||
|
||||
- `${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/references/neutralization-guide.md` — Approach overview, stub types, pitfalls, legal disclaimer
|
||||
- `${CLAUDE_PLUGIN_ROOT}/skills/sdk-neutralizer/references/smali-patterns.md` — Complete smali stub catalog per SDK
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# SDK Neutralization Guide
|
||||
|
||||
## Overview
|
||||
|
||||
SDK neutralization replaces the bodies of tracker/ad SDK methods with stub instructions that effectively disable them. This is the safest bytecode-level approach because:
|
||||
|
||||
1. **Stub > Class removal** — Removing classes causes `ClassNotFoundException` at runtime. Stubbing preserves the class structure; methods simply do nothing when called.
|
||||
2. **Stub > Network blocking** — Network-level blocking (iptables, hosts file) doesn't prevent local data collection and may cause SDK retry loops that drain battery.
|
||||
3. **Entry points only** — We patch only the methods called by app code (init, track, show), not internal SDK wiring. This minimizes breakage surface.
|
||||
|
||||
## Smali Stub Reference
|
||||
|
||||
### Return Type to Stub Mapping
|
||||
|
||||
| Return Type | Descriptor | Stub Code | Registers |
|
||||
|---|---|---|---|
|
||||
| void | `V` | `return-void` | 1 |
|
||||
| boolean | `Z` | `const/4 v0, 0x0` + `return v0` | 1 |
|
||||
| int | `I` | `const/4 v0, 0x0` + `return v0` | 1 |
|
||||
| short | `S` | `const/4 v0, 0x0` + `return v0` | 1 |
|
||||
| byte | `B` | `const/4 v0, 0x0` + `return v0` | 1 |
|
||||
| char | `C` | `const/4 v0, 0x0` + `return v0` | 1 |
|
||||
| float | `F` | `const/4 v0, 0x0` + `return v0` | 1 |
|
||||
| long | `J` | `const-wide/16 v0, 0x0` + `return-wide v0` | 2 |
|
||||
| double | `D` | `const-wide/16 v0, 0x0` + `return-wide v0` | 2 |
|
||||
| Object (`L...;`) | `L` | `const/4 v0, 0x0` + `return-object v0` | 1 |
|
||||
| Array (`[...`) | `[` | `const/4 v0, 0x0` + `return-object v0` | 1 |
|
||||
|
||||
### Stub Format
|
||||
|
||||
```smali
|
||||
.method public methodName(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
For object returns (returns null):
|
||||
```smali
|
||||
.method public static getInstance(Landroid/content/Context;)Lcom/example/Sdk;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
```
|
||||
|
||||
## Special Cases
|
||||
|
||||
### Singleton `getInstance()` returning null
|
||||
|
||||
When a stubbed `getInstance()` returns null, any subsequent method call on the result will throw `NullPointerException`. This is generally acceptable because:
|
||||
|
||||
- The NPE is caught by standard try-catch blocks in well-written app code
|
||||
- If the app crashes, it indicates tight coupling that requires manual intervention
|
||||
- Alternative: stub all methods on the singleton class too (neutralize.sh does this)
|
||||
|
||||
### Callback-dependent methods (Rewarded Ads)
|
||||
|
||||
Rewarded ad SDKs call back via listener when a reward is earned. Stubbing `show()` means:
|
||||
- The reward callback is never invoked
|
||||
- If the app gates content behind the reward, the user cannot proceed
|
||||
- Mitigation: the app should handle the "ad not available" case already
|
||||
|
||||
### Firebase coupling
|
||||
|
||||
Firebase Analytics is often initialized via a `ContentProvider` that runs before `Application.onCreate()`. Stubbing `getInstance()` and `logEvent()` is sufficient — the auto-init still runs but collected data goes nowhere when `logEvent` is a no-op.
|
||||
|
||||
`setAnalyticsCollectionEnabled(false)` can also be called in the stub to explicitly disable collection at the API level.
|
||||
|
||||
### ContentProvider auto-init
|
||||
|
||||
Some SDKs (Firebase, Facebook, WorkManager) use `ContentProvider` for auto-initialization. Disabling the provider in the manifest (`android:enabled="false"`) prevents auto-init without removing the class.
|
||||
|
||||
## Manifest Patching
|
||||
|
||||
### `android:enabled="false"` vs Removal
|
||||
|
||||
- **Disable** (`android:enabled="false"`): The component remains declared but the system does not instantiate it. This preserves the XML structure and avoids manifest merge errors during rebuild.
|
||||
- **Remove**: Deleting the XML element entirely. Risk of resource reference errors if other parts of the manifest or code reference the component.
|
||||
|
||||
**Recommendation**: Always disable, never remove.
|
||||
|
||||
### Component Types
|
||||
|
||||
| Type | Effect of Disabling |
|
||||
|---|---|
|
||||
| `<activity>` | Cannot be launched via Intent; ad fullscreen activities won't show |
|
||||
| `<service>` | Service cannot start; background telemetry upload stops |
|
||||
| `<receiver>` | Broadcast receiver does not fire; install referrer, BOOT_COMPLETED handlers disabled |
|
||||
| `<provider>` | ContentProvider not initialized; auto-init SDKs disabled |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
### Multidex
|
||||
|
||||
APKs with multidex have multiple smali directories: `smali/`, `smali_classes2/`, `smali_classes3/`, etc. The neutralization script scans all `smali*` directories to handle this.
|
||||
|
||||
### Native `.so` integrity checks
|
||||
|
||||
Some SDKs (particularly ad SDKs like AdMob, IronSource) include native libraries that perform integrity checks. These cannot be patched at the smali level. If the native code detects tampering:
|
||||
- The SDK may silently disable itself (acceptable)
|
||||
- The SDK may crash (rare, but requires manual `.so` removal or stubbing of the JNI bridge)
|
||||
|
||||
### R8/ProGuard obfuscation
|
||||
|
||||
If the APK was built with R8 or ProGuard, SDK class names are obfuscated (e.g., `Lcom/google/android/gms/ads/MobileAds;` becomes `La/b/c;`). In this case:
|
||||
- The standard target list will not match
|
||||
- Use `find-ads.sh --entrypoints` and `find-trackers.sh --entrypoints` on the decoded smali to identify obfuscated calls
|
||||
- Use `--targets-file` with manually identified class:method pairs
|
||||
- String constants (SDK keys, endpoint URLs) are usually not obfuscated and can help identify the SDK
|
||||
|
||||
### Signature invalidation
|
||||
|
||||
Rebuilding the APK invalidates the original signature. Consequences:
|
||||
- **Play Integrity / SafetyNet**: Will fail. Apps that check integrity at runtime may refuse to run.
|
||||
- **Signature-based permissions**: `android:protectionLevel="signature"` permissions will not be granted.
|
||||
- **App updates**: The neutralized APK cannot update from the Play Store version (different signing key).
|
||||
|
||||
This is acceptable for enterprise sideloading where the modified APK is distributed via MDM.
|
||||
|
||||
## Legal Disclaimer
|
||||
|
||||
SDK neutralization for enterprise deployment is supported by:
|
||||
|
||||
- **EU Software Directive 2009/24/EC** Art. 5-6: Permits decompilation for interoperability and error correction
|
||||
- **GDPR Art. 5(1)(c)** (data minimisation): Supports removing unnecessary data collection from enterprise-distributed apps
|
||||
- **US DMCA §1201(f)**: Permits reverse engineering for interoperability
|
||||
|
||||
**This tool is intended for authorized enterprise use only.** The user is solely responsible for ensuring compliance with:
|
||||
- Software license agreements of the modified app
|
||||
- Terms of service of the SDK providers
|
||||
- Applicable local and international laws
|
||||
- Internal enterprise security and compliance policies
|
||||
|
||||
The authors provide this tool for legitimate security research, privacy compliance, and authorized enterprise deployment. Any use for piracy, circumvention of digital rights management for unauthorized purposes, or distribution of modified apps without authorization is strictly prohibited.
|
||||
|
|
@ -0,0 +1,728 @@
|
|||
# Smali Patterns Catalog
|
||||
|
||||
Complete reference of smali patterns for SDK neutralization. Each section shows the target class, methods, and the exact stub code injected.
|
||||
|
||||
## Ad SDK Patterns
|
||||
|
||||
### AdMob / Google Mobile Ads
|
||||
|
||||
**Class**: `Lcom/google/android/gms/ads/MobileAds;`
|
||||
|
||||
```smali
|
||||
# Original: public static void initialize(Context, OnInitializationCompleteListener)
|
||||
.method public static initialize(Landroid/content/Context;Lcom/google/android/gms/ads/initialization/OnInitializationCompleteListener;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
# Original: public static void setRequestConfiguration(RequestConfiguration)
|
||||
.method public static setRequestConfiguration(Lcom/google/android/gms/ads/RequestConfiguration;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
**Class**: `Lcom/google/android/gms/ads/interstitial/InterstitialAd;`
|
||||
|
||||
```smali
|
||||
# Original: public static void load(Context, String, AdRequest, InterstitialAdLoadCallback)
|
||||
.method public static load(Landroid/content/Context;Ljava/lang/String;Lcom/google/android/gms/ads/AdRequest;Lcom/google/android/gms/ads/interstitial/InterstitialAdLoadCallback;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
**Class**: `Lcom/google/android/gms/ads/rewarded/RewardedAd;`
|
||||
|
||||
```smali
|
||||
# Original: public static void load(Context, String, AdRequest, RewardedAdLoadCallback)
|
||||
.method public static load(Landroid/content/Context;Ljava/lang/String;Lcom/google/android/gms/ads/AdRequest;Lcom/google/android/gms/ads/rewarded/RewardedAdLoadCallback;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
**Class**: `Lcom/google/android/gms/ads/AdView;`
|
||||
|
||||
```smali
|
||||
# Original: public void loadAd(AdRequest)
|
||||
.method public loadAd(Lcom/google/android/gms/ads/AdRequest;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Unity Ads
|
||||
|
||||
**Class**: `Lcom/unity3d/ads/UnityAds;`
|
||||
|
||||
```smali
|
||||
# Original: public static void initialize(Context, String, boolean, IUnityAdsInitializationListener)
|
||||
.method public static initialize(Landroid/content/Context;Ljava/lang/String;ZLcom/unity3d/ads/IUnityAdsInitializationListener;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
# Original: public static void load(String, IUnityAdsLoadListener)
|
||||
.method public static load(Ljava/lang/String;Lcom/unity3d/ads/IUnityAdsLoadListener;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
# Original: public static void show(Activity, String, IUnityAdsShowListener)
|
||||
.method public static show(Landroid/app/Activity;Ljava/lang/String;Lcom/unity3d/ads/IUnityAdsShowListener;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### IronSource / LevelPlay
|
||||
|
||||
**Class**: `Lcom/ironsource/mediationsdk/IronSource;`
|
||||
|
||||
```smali
|
||||
.method public static init(Landroid/app/Activity;Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static loadInterstitial()V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static showInterstitial()V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static showRewardedVideo()V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static loadBanner(Landroid/app/Activity;Lcom/ironsource/mediationsdk/ISBannerSize;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### AppLovin / MAX
|
||||
|
||||
**Class**: `Lcom/applovin/sdk/AppLovinSdk;`
|
||||
|
||||
```smali
|
||||
# Original: public static AppLovinSdk getInstance(Context) — returns null
|
||||
.method public static getInstance(Landroid/content/Context;)Lcom/applovin/sdk/AppLovinSdk;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
# Original: public void initializeSdk()
|
||||
.method public initializeSdk()V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Meta Audience Network
|
||||
|
||||
**Class**: `Lcom/facebook/ads/AudienceNetworkAds;`
|
||||
|
||||
```smali
|
||||
.method public static initialize(Landroid/content/Context;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Vungle / Liftoff
|
||||
|
||||
**Class**: `Lcom/vungle/warren/Vungle;`
|
||||
|
||||
```smali
|
||||
.method public static init(Ljava/lang/String;Landroid/content/Context;Lcom/vungle/warren/InitCallback;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static loadAd(Ljava/lang/String;Lcom/vungle/warren/LoadAdCallback;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static playAd(Ljava/lang/String;Lcom/vungle/warren/model/AdConfig;Lcom/vungle/warren/PlayAdCallback;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### InMobi
|
||||
|
||||
**Class**: `Lcom/inmobi/sdk/InMobiSdk;`
|
||||
|
||||
```smali
|
||||
.method public static init(Landroid/content/Context;Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Chartboost
|
||||
|
||||
**Class**: `Lcom/chartboost/sdk/Chartboost;`
|
||||
|
||||
```smali
|
||||
.method public static startWithAppId(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static showInterstitial(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static showRewardedVideo(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static cacheInterstitial(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Pangle / TikTok
|
||||
|
||||
**Class**: `Lcom/bytedance/sdk/openadsdk/TTAdSdk;`
|
||||
|
||||
```smali
|
||||
.method public static init(Landroid/content/Context;Lcom/bytedance/sdk/openadsdk/TTAdConfig;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
**Class**: `Lcom/pgl/sys/ces/PAGSdk;`
|
||||
|
||||
```smali
|
||||
.method public static init(Lcom/pgl/sys/ces/PAGConfig;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Mintegral
|
||||
|
||||
**Class**: `Lcom/mbridge/msdk/MBridgeSDKFactory;`
|
||||
|
||||
```smali
|
||||
# Returns null instead of SDK instance
|
||||
.method public static getMBridgeSDK()Lcom/mbridge/msdk/MBridgeSDK;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
```
|
||||
|
||||
## Tracker SDK Patterns
|
||||
|
||||
### Firebase Analytics
|
||||
|
||||
**Class**: `Lcom/google/firebase/analytics/FirebaseAnalytics;`
|
||||
|
||||
```smali
|
||||
# Returns null — callers should null-check
|
||||
.method public static getInstance(Landroid/content/Context;)Lcom/google/firebase/analytics/FirebaseAnalytics;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public logEvent(Ljava/lang/String;Landroid/os/Bundle;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public setUserId(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public setUserProperty(Ljava/lang/String;Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public setAnalyticsCollectionEnabled(Z)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Adjust
|
||||
|
||||
**Class**: `Lcom/adjust/sdk/Adjust;`
|
||||
|
||||
```smali
|
||||
.method public static onCreate(Lcom/adjust/sdk/AdjustConfig;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static trackEvent(Lcom/adjust/sdk/AdjustEvent;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static setEnabled(Z)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static addSessionCallbackParameter(Ljava/lang/String;Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### AppsFlyer
|
||||
|
||||
**Class**: `Lcom/appsflyer/AppsFlyerLib;`
|
||||
|
||||
```smali
|
||||
# Returns null
|
||||
.method public static getInstance()Lcom/appsflyer/AppsFlyerLib;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public init(Ljava/lang/String;Lcom/appsflyer/AppsFlyerConversionListener;Landroid/content/Context;)Lcom/appsflyer/AppsFlyerLib;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public start(Landroid/content/Context;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public logEvent(Landroid/content/Context;Ljava/lang/String;Ljava/util/Map;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public setCustomerUserId(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Mixpanel
|
||||
|
||||
**Class**: `Lcom/mixpanel/android/mpmetrics/MixpanelAPI;`
|
||||
|
||||
```smali
|
||||
# Returns null
|
||||
.method public static getInstance(Landroid/content/Context;Ljava/lang/String;)Lcom/mixpanel/android/mpmetrics/MixpanelAPI;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public track(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public identify(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public timeEvent(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public registerSuperProperties(Lorg/json/JSONObject;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Amplitude
|
||||
|
||||
**Class**: `Lcom/amplitude/api/AmplitudeClient;`
|
||||
|
||||
```smali
|
||||
# Returns null
|
||||
.method public static getInstance()Lcom/amplitude/api/AmplitudeClient;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public initialize(Landroid/content/Context;Ljava/lang/String;)Lcom/amplitude/api/AmplitudeClient;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public logEvent(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public setUserId(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public setUserProperties(Lorg/json/JSONObject;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Segment
|
||||
|
||||
**Class**: `Lcom/segment/analytics/Analytics;`
|
||||
|
||||
```smali
|
||||
# Returns null
|
||||
.method public static with(Landroid/content/Context;)Lcom/segment/analytics/Analytics;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public track(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public identify(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public screen(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public group(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public alias(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Braze
|
||||
|
||||
**Class**: `Lcom/braze/Braze;`
|
||||
|
||||
```smali
|
||||
.method public static configure(Landroid/content/Context;Lcom/braze/configuration/BrazeConfig;)Z
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return v0
|
||||
.end method
|
||||
|
||||
.method public logCustomEvent(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public changeUser(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public logPurchase(Ljava/lang/String;Ljava/lang/String;Ljava/math/BigDecimal;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### CleverTap
|
||||
|
||||
**Class**: `Lcom/clevertap/android/sdk/CleverTapAPI;`
|
||||
|
||||
```smali
|
||||
# Returns null
|
||||
.method public static getDefaultInstance(Landroid/content/Context;)Lcom/clevertap/android/sdk/CleverTapAPI;
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return-object v0
|
||||
.end method
|
||||
|
||||
.method public pushEvent(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public onUserLogin(Ljava/util/Map;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public pushProfile(Ljava/util/Map;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public recordEvent(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
### Flurry
|
||||
|
||||
**Class**: `Lcom/flurry/android/FlurryAgent;`
|
||||
|
||||
```smali
|
||||
.method public static logEvent(Ljava/lang/String;)I
|
||||
.registers 1
|
||||
|
||||
const/4 v0, 0x0
|
||||
|
||||
return v0
|
||||
.end method
|
||||
|
||||
.method public static setUserId(Ljava/lang/String;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static onStartSession(Landroid/content/Context;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public static onEndSession(Landroid/content/Context;)V
|
||||
.registers 1
|
||||
|
||||
return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
## Manifest Disable Patterns
|
||||
|
||||
### Ad SDK Components
|
||||
|
||||
```xml
|
||||
<!-- AdMob -->
|
||||
<activity android:name="com.google.android.gms.ads.AdActivity" android:enabled="false" ... />
|
||||
<provider android:name="com.google.android.gms.ads.MobileAdsInitProvider" android:enabled="false" ... />
|
||||
|
||||
<!-- Unity Ads -->
|
||||
<activity android:name="com.unity3d.ads.adunit.AdUnitActivity" android:enabled="false" ... />
|
||||
<activity android:name="com.unity3d.ads.adunit.AdUnitTransparentActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- IronSource -->
|
||||
<activity android:name="com.ironsource.sdk.controller.InterstitialActivity" android:enabled="false" ... />
|
||||
<activity android:name="com.ironsource.sdk.controller.ControllerActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- AppLovin -->
|
||||
<activity android:name="com.applovin.adview.AppLovinFullscreenActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Meta AN -->
|
||||
<activity android:name="com.facebook.ads.AudienceNetworkActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Vungle -->
|
||||
<activity android:name="com.vungle.warren.ui.VungleActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Chartboost -->
|
||||
<activity android:name="com.chartboost.sdk.CBImpressionActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Pangle -->
|
||||
<activity android:name="com.bytedance.sdk.openadsdk.activity.TTFullScreenVideoActivity" android:enabled="false" ... />
|
||||
<activity android:name="com.bytedance.sdk.openadsdk.activity.TTRewardVideoActivity" android:enabled="false" ... />
|
||||
<activity android:name="com.bytedance.sdk.openadsdk.activity.TTInterstitialActivity" android:enabled="false" ... />
|
||||
<activity android:name="com.bytedance.sdk.openadsdk.activity.TTAdActivity" android:enabled="false" ... />
|
||||
<activity android:name="com.bytedance.sdk.openadsdk.activity.TTDelegateActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Vungle (new SDK) -->
|
||||
<activity android:name="com.vungle.ads.internal.ui.VungleActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Meta AN (provider) -->
|
||||
<provider android:name="com.facebook.ads.AudienceNetworkContentProvider" android:enabled="false" ... />
|
||||
|
||||
<!-- AppLovin (init provider) -->
|
||||
<provider android:name="com.applovin.sdk.AppLovinInitProvider" android:enabled="false" ... />
|
||||
|
||||
<!-- BidMachine -->
|
||||
<provider android:name="io.bidmachine.BidMachineInitProvider" android:enabled="false" ... />
|
||||
|
||||
<!-- IronSource (lifecycle) -->
|
||||
<provider android:name="com.ironsource.lifecycle.IronsourceLifecycleProvider" android:enabled="false" ... />
|
||||
|
||||
<!-- Amazon APS -->
|
||||
<activity android:name="com.amazon.device.ads.DTBAdActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Mintegral -->
|
||||
<activity android:name="com.mbridge.msdk.activity.MBCommonActivity" android:enabled="false" ... />
|
||||
<activity android:name="com.mbridge.msdk.reward.player.MBRewardVideoActivity" android:enabled="false" ... />
|
||||
|
||||
<!-- Smaato -->
|
||||
<receiver android:name="com.smaato.sdk.core.SmaatoBroadcastReceiver" android:enabled="false" ... />
|
||||
```
|
||||
|
||||
### Tracker SDK Components
|
||||
|
||||
```xml
|
||||
<!-- Firebase Analytics -->
|
||||
<service android:name="com.google.android.gms.measurement.AppMeasurementService" android:enabled="false" ... />
|
||||
<receiver android:name="com.google.android.gms.measurement.AppMeasurementReceiver" android:enabled="false" ... />
|
||||
<provider android:name="com.google.android.gms.measurement.AppMeasurementContentProvider" android:enabled="false" ... />
|
||||
<receiver android:name="com.google.android.gms.measurement.AppMeasurementInstallReferrerReceiver" android:enabled="false" ... />
|
||||
<service android:name="com.google.android.gms.measurement.AppMeasurementJobService" android:enabled="false" ... />
|
||||
|
||||
<!-- Adjust -->
|
||||
<receiver android:name="com.adjust.sdk.AdjustReferrerReceiver" android:enabled="false" ... />
|
||||
|
||||
<!-- AppsFlyer -->
|
||||
<receiver android:name="com.appsflyer.SingleInstallBroadcastReceiver" android:enabled="false" ... />
|
||||
<receiver android:name="com.appsflyer.MultipleInstallBroadcastReceiver" android:enabled="false" ... />
|
||||
|
||||
<!-- Braze -->
|
||||
<service android:name="com.braze.push.BrazeFirebaseMessagingService" android:enabled="false" ... />
|
||||
|
||||
<!-- CleverTap -->
|
||||
<receiver android:name="com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver" android:enabled="false" ... />
|
||||
<service android:name="com.clevertap.android.sdk.pushnotification.CTNotificationIntentService" android:enabled="false" ... />
|
||||
|
||||
<!-- AppsFlyer (internal receiver) -->
|
||||
<receiver android:name="com.appsflyer.internal.AFSingleInstallBroadcastReceiver" android:enabled="false" ... />
|
||||
```
|
||||
|
||||
## Grep Patterns for Smali Files
|
||||
|
||||
Use these patterns with `grep -rn` on smali directories to find SDK methods:
|
||||
|
||||
```bash
|
||||
# AdMob initialization
|
||||
grep -rn 'Lcom/google/android/gms/ads/MobileAds;->initialize' smali/
|
||||
|
||||
# Firebase Analytics event logging
|
||||
grep -rn 'Lcom/google/firebase/analytics/FirebaseAnalytics;->logEvent' smali/
|
||||
|
||||
# All tracker init methods
|
||||
grep -rn -E '(FirebaseAnalytics;->getInstance|Adjust;->onCreate|AppsFlyerLib;->getInstance|MixpanelAPI;->getInstance|AmplitudeClient;->getInstance|Analytics;->with|Braze;->configure|CleverTapAPI;->getDefaultInstance|FlurryAgent;->onStartSession)' smali/
|
||||
|
||||
# All ad SDK init methods
|
||||
grep -rn -E '(MobileAds;->initialize|UnityAds;->initialize|IronSource;->init|AppLovinSdk;->getInstance|AudienceNetworkAds;->initialize|Vungle;->init|InMobiSdk;->init|Chartboost;->startWithAppId|TTAdSdk;->init|PAGSdk;->init|MBridgeSDKFactory;->getMBridgeSDK)' smali/
|
||||
|
||||
# Find invoke-static and invoke-virtual calls to SDK methods
|
||||
grep -rn 'invoke-\(static\|virtual\).*Lcom/google/android/gms/ads/' smali/
|
||||
grep -rn 'invoke-\(static\|virtual\).*Lcom/google/firebase/analytics/' smali/
|
||||
```
|
||||
|
||||
## Custom Target File Format
|
||||
|
||||
For `--targets-file`, use one entry per line:
|
||||
|
||||
```
|
||||
# Comment lines start with #
|
||||
# Format: smali_class_path:method_name
|
||||
|
||||
# Custom tracker
|
||||
com/example/analytics/CustomTracker:init
|
||||
com/example/analytics/CustomTracker:track
|
||||
com/example/analytics/CustomTracker:setUser
|
||||
|
||||
# Obfuscated class (identified via string analysis)
|
||||
a/b/c:a
|
||||
a/b/c:b
|
||||
```
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env bash
|
||||
# check-neutralize-deps.sh — Verify dependencies for SDK neutralization
|
||||
# Output includes machine-readable INSTALL_REQUIRED: and INSTALL_OPTIONAL: lines.
|
||||
set -euo pipefail
|
||||
|
||||
REQUIRED_JAVA_MAJOR=17
|
||||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
echo "=== SDK Neutralizer: Dependency Check ==="
|
||||
echo
|
||||
|
||||
# --- Java 17+ (required) ---
|
||||
if command -v java &>/dev/null; then
|
||||
java_version_output=$(java -version 2>&1 | head -1)
|
||||
java_version=$(echo "$java_version_output" | sed -n 's/.*"\([0-9]*\)\..*/\1/p')
|
||||
if [[ -z "$java_version" ]]; then
|
||||
java_version=$(echo "$java_version_output" | grep -oP '\d+' | head -1)
|
||||
fi
|
||||
if [[ "$java_version" == "1" ]]; then
|
||||
java_version=$(echo "$java_version_output" | sed -n 's/.*"1\.\([0-9]*\)\..*/\1/p')
|
||||
fi
|
||||
|
||||
if [[ -n "$java_version" ]] && (( java_version >= REQUIRED_JAVA_MAJOR )); then
|
||||
echo "[OK] Java $java_version detected"
|
||||
else
|
||||
echo "[WARN] Java detected but version $java_version is below $REQUIRED_JAVA_MAJOR"
|
||||
missing_required+=("java")
|
||||
fi
|
||||
else
|
||||
echo "[MISSING] Java is not installed or not in PATH"
|
||||
missing_required+=("java")
|
||||
fi
|
||||
|
||||
# --- apktool (required, minimum 2.9.0) ---
|
||||
APKTOOL_MIN_MAJOR=2
|
||||
APKTOOL_MIN_MINOR=9
|
||||
APKTOOL_MIN_PATCH=0
|
||||
|
||||
if command -v apktool &>/dev/null; then
|
||||
apktool_version_raw=$(apktool --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
if [[ -n "$apktool_version_raw" ]]; then
|
||||
IFS='.' read -r at_major at_minor at_patch <<< "$apktool_version_raw"
|
||||
at_major=${at_major:-0}; at_minor=${at_minor:-0}; at_patch=${at_patch:-0}
|
||||
|
||||
version_ok=false
|
||||
if (( at_major > APKTOOL_MIN_MAJOR )); then
|
||||
version_ok=true
|
||||
elif (( at_major == APKTOOL_MIN_MAJOR && at_minor > APKTOOL_MIN_MINOR )); then
|
||||
version_ok=true
|
||||
elif (( at_major == APKTOOL_MIN_MAJOR && at_minor == APKTOOL_MIN_MINOR && at_patch >= APKTOOL_MIN_PATCH )); then
|
||||
version_ok=true
|
||||
fi
|
||||
|
||||
if [[ "$version_ok" == true ]]; then
|
||||
echo "[OK] apktool $apktool_version_raw detected"
|
||||
else
|
||||
echo "[WARN] apktool $apktool_version_raw detected but version >= ${APKTOOL_MIN_MAJOR}.${APKTOOL_MIN_MINOR}.${APKTOOL_MIN_PATCH} is required"
|
||||
echo " Older versions fail on modern APKs (new resource types, targetSdk 34+)."
|
||||
missing_required+=("apktool")
|
||||
fi
|
||||
else
|
||||
echo "[OK] apktool detected (could not parse version — assuming compatible)"
|
||||
fi
|
||||
else
|
||||
echo "[MISSING] apktool is not installed or not in PATH (required for decode/rebuild)"
|
||||
missing_required+=("apktool")
|
||||
fi
|
||||
|
||||
# --- apksigner or jarsigner (at least one required) ---
|
||||
signer_found=false
|
||||
if command -v apksigner &>/dev/null; then
|
||||
echo "[OK] apksigner detected"
|
||||
signer_found=true
|
||||
elif command -v jarsigner &>/dev/null; then
|
||||
echo "[OK] jarsigner detected (fallback signer)"
|
||||
signer_found=true
|
||||
fi
|
||||
if [[ "$signer_found" == false ]]; then
|
||||
echo "[MISSING] Neither apksigner nor jarsigner found (at least one required for signing)"
|
||||
missing_required+=("apksigner")
|
||||
fi
|
||||
|
||||
# --- keytool (required for debug key generation) ---
|
||||
if command -v keytool &>/dev/null; then
|
||||
echo "[OK] keytool detected (part of JDK)"
|
||||
else
|
||||
echo "[MISSING] keytool not found (required for debug key generation, part of JDK)"
|
||||
missing_required+=("java")
|
||||
fi
|
||||
|
||||
# --- zipalign (optional, improves APK performance) ---
|
||||
if command -v zipalign &>/dev/null; then
|
||||
echo "[OK] zipalign detected (optional)"
|
||||
else
|
||||
# Check Android SDK build-tools
|
||||
za_found=false
|
||||
for sdk_dir in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}"; do
|
||||
if [[ -n "$sdk_dir" ]] && [[ -d "$sdk_dir/build-tools" ]]; then
|
||||
latest_bt=$(ls -1 "$sdk_dir/build-tools" 2>/dev/null | sort -V | tail -1)
|
||||
if [[ -n "$latest_bt" ]] && [[ -f "$sdk_dir/build-tools/$latest_bt/zipalign" ]]; then
|
||||
echo "[OK] zipalign found in Android SDK build-tools ($latest_bt)"
|
||||
za_found=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ "$za_found" == false ]]; then
|
||||
echo "[MISSING] zipalign not found (optional — improves APK alignment for performance)"
|
||||
missing_optional+=("zipalign")
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Machine-readable summary ---
|
||||
echo
|
||||
if [[ ${#missing_required[@]} -gt 0 ]]; then
|
||||
for dep in "${missing_required[@]}"; do
|
||||
echo "INSTALL_REQUIRED:$dep"
|
||||
done
|
||||
fi
|
||||
if [[ ${#missing_optional[@]} -gt 0 ]]; then
|
||||
for dep in "${missing_optional[@]}"; do
|
||||
echo "INSTALL_OPTIONAL:$dep"
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Tip: If apktool decode/build fails with framework errors, try:"
|
||||
echo " rm -f ~/.local/share/apktool/framework/1.apk"
|
||||
echo
|
||||
|
||||
if [[ ${#missing_required[@]} -gt 0 ]]; then
|
||||
echo "*** ${#missing_required[@]} required dependency/ies missing. ***"
|
||||
echo "Run install-dep.sh <name> to install, or see references/neutralization-guide.md."
|
||||
exit 1
|
||||
else
|
||||
if [[ ${#missing_optional[@]} -gt 0 ]]; then
|
||||
echo "Required dependencies OK. ${#missing_optional[@]} optional dependency/ies missing."
|
||||
else
|
||||
echo "All dependencies are installed. Ready to neutralize."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
227
plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh
Executable file
227
plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/decode-apk.sh
Executable file
|
|
@ -0,0 +1,227 @@
|
|||
#!/usr/bin/env bash
|
||||
# decode-apk.sh — Decode an APK or XAPK into smali using apktool
|
||||
#
|
||||
# Handles both .apk (direct decode) and .xapk (extract base APK, then decode).
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — success
|
||||
# 1 — error (invalid input, missing tools, decode failed)
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: decode-apk.sh <file> [OPTIONS]
|
||||
|
||||
Decode an APK or XAPK file into smali and resources using apktool.
|
||||
|
||||
For XAPK files, extracts the base APK from the archive and decodes it.
|
||||
Split APKs (config.*.apk) are skipped with a warning.
|
||||
|
||||
Arguments:
|
||||
<file> Path to .apk or .xapk file
|
||||
|
||||
Options:
|
||||
-o, --output <dir> Output directory (default: <basename>-decoded)
|
||||
-f, --force Overwrite output directory if it exists (default)
|
||||
--no-force Do not overwrite existing output directory
|
||||
-h, --help Show this help message
|
||||
|
||||
Output:
|
||||
DECODED_DIR:<path>
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Argument parsing
|
||||
# =====================================================================
|
||||
|
||||
INPUT_FILE=""
|
||||
OUTPUT_DIR=""
|
||||
FORCE=true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o|--output)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then echo "Error: --output requires a directory argument" >&2; exit 1; fi
|
||||
OUTPUT_DIR="$1"; shift ;;
|
||||
-f|--force) FORCE=true; shift ;;
|
||||
--no-force) FORCE=false; shift ;;
|
||||
-h|--help) usage ;;
|
||||
-*) echo "Error: Unknown option $1" >&2; usage ;;
|
||||
*) INPUT_FILE="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$INPUT_FILE" ]]; then
|
||||
echo "Error: No input file specified." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -f "$INPUT_FILE" ]]; then
|
||||
echo "Error: File not found: $INPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check apktool
|
||||
if ! command -v apktool &>/dev/null; then
|
||||
echo "Error: apktool is not installed or not in PATH." >&2
|
||||
echo "Run: install-dep.sh apktool" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine file type
|
||||
ext_lower="${INPUT_FILE##*.}"
|
||||
ext_lower=$(echo "$ext_lower" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
case "$ext_lower" in
|
||||
apk|xapk) ;;
|
||||
*)
|
||||
echo "Error: Unsupported file type '.$ext_lower'. Expected .apk or .xapk" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
BASENAME=$(basename "$INPUT_FILE" ".$ext_lower")
|
||||
INPUT_FILE_ABS=$(realpath "$INPUT_FILE")
|
||||
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
OUTPUT_DIR="${BASENAME}-decoded"
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# XAPK handling — extract base APK
|
||||
# =====================================================================
|
||||
|
||||
APK_TO_DECODE="$INPUT_FILE_ABS"
|
||||
XAPK_TMPDIR=""
|
||||
|
||||
cleanup_xapk() {
|
||||
if [[ -n "$XAPK_TMPDIR" ]] && [[ -d "$XAPK_TMPDIR" ]]; then
|
||||
rm -rf "$XAPK_TMPDIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup_xapk EXIT
|
||||
|
||||
if [[ "$ext_lower" == "xapk" ]]; then
|
||||
echo "=== Extracting XAPK archive ==="
|
||||
XAPK_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/xapk-decode-XXXXXX")
|
||||
unzip -qo "$INPUT_FILE_ABS" -d "$XAPK_TMPDIR"
|
||||
|
||||
# Show manifest.json if present
|
||||
if [[ -f "$XAPK_TMPDIR/manifest.json" ]]; then
|
||||
echo "XAPK manifest found."
|
||||
fi
|
||||
|
||||
# Collect all APK files
|
||||
all_apks=()
|
||||
while IFS= read -r -d '' apk_file; do
|
||||
all_apks+=("$apk_file")
|
||||
done < <(find "$XAPK_TMPDIR" -name "*.apk" -print0 | sort -z)
|
||||
|
||||
if [[ ${#all_apks[@]} -eq 0 ]]; then
|
||||
echo "Error: No APK files found inside XAPK archive." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found ${#all_apks[@]} APK(s) inside XAPK:"
|
||||
for f in "${all_apks[@]}"; do
|
||||
echo " - $(basename "$f")"
|
||||
done
|
||||
|
||||
# Select base APK: prefer "base.apk" by name
|
||||
base_apk=""
|
||||
for f in "${all_apks[@]}"; do
|
||||
if [[ "$(basename "$f")" == "base.apk" ]]; then
|
||||
base_apk="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Fallback: largest APK excluding config.*.apk
|
||||
if [[ -z "$base_apk" ]]; then
|
||||
largest_size=0
|
||||
for f in "${all_apks[@]}"; do
|
||||
fname=$(basename "$f")
|
||||
# Skip config splits
|
||||
if [[ "$fname" == config.* ]]; then
|
||||
continue
|
||||
fi
|
||||
fsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || echo 0)
|
||||
if (( fsize > largest_size )); then
|
||||
largest_size=$fsize
|
||||
base_apk="$f"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$base_apk" ]]; then
|
||||
echo "Error: Could not identify a base APK inside the XAPK." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Selected base APK: $(basename "$base_apk")"
|
||||
|
||||
# Warn about skipped splits
|
||||
skipped=0
|
||||
for f in "${all_apks[@]}"; do
|
||||
if [[ "$f" != "$base_apk" ]]; then
|
||||
echo " [skipped] $(basename "$f")"
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
done
|
||||
if (( skipped > 0 )); then
|
||||
echo "Warning: $skipped split APK(s) skipped. Only the base APK is decoded."
|
||||
echo " Split APKs contain config-specific resources (density, locale, ABI)."
|
||||
fi
|
||||
echo
|
||||
|
||||
APK_TO_DECODE="$base_apk"
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Decode with apktool
|
||||
# =====================================================================
|
||||
|
||||
echo "=== Decoding APK with apktool ==="
|
||||
|
||||
APKTOOL_ARGS=()
|
||||
if [[ "$FORCE" == true ]]; then
|
||||
APKTOOL_ARGS+=("-f")
|
||||
fi
|
||||
APKTOOL_ARGS+=("-o" "$OUTPUT_DIR")
|
||||
APKTOOL_ARGS+=("$APK_TO_DECODE")
|
||||
|
||||
if ! apktool d "${APKTOOL_ARGS[@]}" 2>&1; then
|
||||
echo "Error: apktool decode failed." >&2
|
||||
echo "Tip: If this is a framework error, try: rm -f ~/.local/share/apktool/framework/1.apk" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Verify output
|
||||
# =====================================================================
|
||||
|
||||
has_smali=false
|
||||
for d in "$OUTPUT_DIR"/smali*; do
|
||||
if [[ -d "$d" ]]; then
|
||||
has_smali=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$has_smali" == false ]]; then
|
||||
echo "Error: No smali/ directory found in decoded output." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$OUTPUT_DIR/AndroidManifest.xml" ]]; then
|
||||
echo "Warning: AndroidManifest.xml not found in decoded output." >&2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Decoded successfully: $OUTPUT_DIR"
|
||||
echo "DECODED_DIR:$OUTPUT_DIR"
|
||||
exit 0
|
||||
757
plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh
Executable file
757
plugins/android-reverse-engineering/skills/sdk-neutralizer/scripts/neutralize.sh
Executable file
|
|
@ -0,0 +1,757 @@
|
|||
#!/usr/bin/env bash
|
||||
# neutralize.sh — Neutralize tracker/ad SDK entry points in decoded APK smali
|
||||
#
|
||||
# Replaces SDK method bodies with stubs (return-void, return 0, return-object null)
|
||||
# and optionally disables manifest components.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — success (methods patched)
|
||||
# 1 — error (invalid input, missing files)
|
||||
# 2 — no targets found
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: neutralize.sh <decoded-dir> [OPTIONS]
|
||||
|
||||
Neutralize tracker and ad SDK entry points in a decoded APK directory.
|
||||
|
||||
The decoded directory must be the output of 'apktool d' and contain
|
||||
a smali/ directory and AndroidManifest.xml.
|
||||
|
||||
Arguments:
|
||||
<decoded-dir> Path to the apktool-decoded APK directory
|
||||
|
||||
Options:
|
||||
--ads Neutralize only ad SDK entry points
|
||||
--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
|
||||
--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>)
|
||||
--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
|
||||
-h, --help Show this help message
|
||||
|
||||
Output:
|
||||
Machine-readable lines:
|
||||
PATCHED:<sdk>:<class>:<method>:<stub_type>:<file>
|
||||
MANIFEST_DISABLED:<type>:<name>:<sdk>
|
||||
DRY_RUN:WOULD_PATCH:<sdk>:<class>:<method>:<stub_type>:<file>
|
||||
DRY_RUN:WOULD_DISABLE:<type>:<name>:<sdk>
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Argument parsing
|
||||
# =====================================================================
|
||||
|
||||
DECODED_DIR=""
|
||||
NEUTRALIZE_ADS=false
|
||||
NEUTRALIZE_TRACKERS=false
|
||||
NEUTRALIZE_ALL=true
|
||||
DRY_RUN=false
|
||||
DO_BACKUP=true
|
||||
DO_MANIFEST=true
|
||||
TARGETS_FILE=""
|
||||
DO_REPLAY=false
|
||||
DO_SAVE_MANIFEST=true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ads) NEUTRALIZE_ADS=true; NEUTRALIZE_ALL=false; shift ;;
|
||||
--trackers) NEUTRALIZE_TRACKERS=true; NEUTRALIZE_ALL=false; shift ;;
|
||||
--all) NEUTRALIZE_ALL=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--backup) DO_BACKUP=true; shift ;;
|
||||
--no-backup) DO_BACKUP=false; shift ;;
|
||||
--manifest) DO_MANIFEST=true; shift ;;
|
||||
--no-manifest) DO_MANIFEST=false; shift ;;
|
||||
--targets-file)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Error: --targets-file requires a file argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
TARGETS_FILE="$1"; shift ;;
|
||||
--replay) DO_REPLAY=true; shift ;;
|
||||
--save-manifest) DO_SAVE_MANIFEST=true; shift ;;
|
||||
--no-save-manifest) DO_SAVE_MANIFEST=false; shift ;;
|
||||
-h|--help) usage ;;
|
||||
-*) echo "Error: Unknown option $1" >&2; usage ;;
|
||||
*) DECODED_DIR="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$DECODED_DIR" ]]; then
|
||||
echo "Error: No decoded directory specified." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -d "$DECODED_DIR" ]]; then
|
||||
echo "Error: Directory not found: $DECODED_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find smali directories (multidex support: smali/, smali_classes2/, etc.)
|
||||
SMALI_DIRS=()
|
||||
for d in "$DECODED_DIR"/smali*; do
|
||||
if [[ -d "$d" ]]; then
|
||||
SMALI_DIRS+=("$d")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#SMALI_DIRS[@]} -eq 0 ]]; then
|
||||
echo "Error: No smali/ directory found in $DECODED_DIR" >&2
|
||||
echo "Make sure this is an apktool-decoded APK directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MANIFEST="$DECODED_DIR/AndroidManifest.xml"
|
||||
if [[ ! -f "$MANIFEST" ]]; then
|
||||
echo "Warning: AndroidManifest.xml not found in $DECODED_DIR" >&2
|
||||
DO_MANIFEST=false
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Counters & patch log
|
||||
# =====================================================================
|
||||
|
||||
METHODS_PATCHED=0
|
||||
METHODS_SKIPPED=0
|
||||
COMPONENTS_DISABLED=0
|
||||
|
||||
# Patch log file — captures all PATCHED: and MANIFEST_DISABLED: lines
|
||||
PATCH_LOG_FILE=$(mktemp "${TMPDIR:-/tmp}/neutralize-log-XXXXXX")
|
||||
cleanup_patch_log() {
|
||||
rm -f "$PATCH_LOG_FILE"
|
||||
}
|
||||
trap cleanup_patch_log EXIT
|
||||
|
||||
# =====================================================================
|
||||
# patch_method() — Replace a smali method body with a stub
|
||||
#
|
||||
# Arguments:
|
||||
# $1 — smali file path
|
||||
# $2 — method name (e.g., "initialize", "logEvent")
|
||||
# $3 — SDK name (for reporting, e.g., "AdMob", "Firebase")
|
||||
# $4 — smali class descriptor (e.g., "Lcom/google/android/gms/ads/MobileAds;")
|
||||
#
|
||||
# The function finds .method declarations matching the method name,
|
||||
# determines the return type, and replaces the body with appropriate stubs.
|
||||
# =====================================================================
|
||||
|
||||
patch_method() {
|
||||
local file="$1"
|
||||
local method_name="$2"
|
||||
local sdk_name="$3"
|
||||
local class_desc="$4"
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Use awk to find and patch method bodies
|
||||
local tmp_file
|
||||
tmp_file=$(mktemp)
|
||||
local patched=false
|
||||
|
||||
awk -v method="$method_name" -v sdk="$sdk_name" -v cls="$class_desc" \
|
||||
-v dry_run="$DRY_RUN" -v src_file="$file" '
|
||||
BEGIN {
|
||||
in_target = 0
|
||||
found = 0
|
||||
}
|
||||
|
||||
# Match .method line containing our target method name
|
||||
/^\.method / && $0 ~ "[ ;]" method "\\(" {
|
||||
in_target = 1
|
||||
found = 1
|
||||
|
||||
# Extract return type from method descriptor
|
||||
# The descriptor is the last part: (params)ReturnType
|
||||
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
|
||||
gsub(/[[:space:]]+$/, "", ret_type)
|
||||
} else {
|
||||
ret_type = "V"
|
||||
}
|
||||
|
||||
# Determine stub type
|
||||
if (ret_type == "V") {
|
||||
stub_type = "return-void"
|
||||
stub_body = " .registers 1\n\n return-void"
|
||||
} 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"
|
||||
} 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"
|
||||
} 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"
|
||||
}
|
||||
|
||||
if (dry_run == "true") {
|
||||
printf "DRY_RUN:WOULD_PATCH:%s:%s:%s:%s:%s\n", sdk, cls, method, stub_type, src_file > "/dev/stderr"
|
||||
} else {
|
||||
printf "PATCHED:%s:%s:%s:%s:%s\n", sdk, cls, method, stub_type, src_file > "/dev/stderr"
|
||||
}
|
||||
|
||||
# Print the .method line unchanged
|
||||
print $0
|
||||
next
|
||||
}
|
||||
|
||||
# Inside a target method — skip original body until .end method
|
||||
in_target && /^\.end method/ {
|
||||
# Print the stub body, then .end method
|
||||
printf "%s\n", stub_body
|
||||
print ""
|
||||
print $0
|
||||
in_target = 0
|
||||
next
|
||||
}
|
||||
|
||||
# Inside target method — skip all lines (original body)
|
||||
in_target {
|
||||
next
|
||||
}
|
||||
|
||||
# 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)
|
||||
|
||||
if [[ "$DRY_RUN" == false ]] && [[ "$patched" == true ]]; then
|
||||
if [[ "$DO_BACKUP" == true ]]; then
|
||||
cp "$file" "${file}.bak"
|
||||
fi
|
||||
mv "$tmp_file" "$file"
|
||||
else
|
||||
rm -f "$tmp_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# find_and_patch() — Find smali files for a class and patch methods
|
||||
#
|
||||
# Arguments:
|
||||
# $1 — smali class path (e.g., "com/google/android/gms/ads/MobileAds")
|
||||
# $2 — comma-separated method names
|
||||
# $3 — SDK name
|
||||
# $4 — smali class descriptor
|
||||
# =====================================================================
|
||||
|
||||
find_and_patch() {
|
||||
local class_path="$1"
|
||||
local methods_csv="$2"
|
||||
local sdk_name="$3"
|
||||
local class_desc="$4"
|
||||
|
||||
IFS=',' read -ra methods <<< "$methods_csv"
|
||||
|
||||
for smali_dir in "${SMALI_DIRS[@]}"; do
|
||||
local smali_file="$smali_dir/$class_path.smali"
|
||||
if [[ -f "$smali_file" ]]; then
|
||||
for method in "${methods[@]}"; do
|
||||
patch_method "$smali_file" "$method" "$sdk_name" "$class_desc"
|
||||
done
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# SDK Target Lists
|
||||
# =====================================================================
|
||||
|
||||
patch_ad_targets() {
|
||||
# AdMob / Google Mobile Ads
|
||||
find_and_patch "com/google/android/gms/ads/MobileAds" \
|
||||
"initialize,setRequestConfiguration" \
|
||||
"AdMob" "Lcom/google/android/gms/ads/MobileAds;"
|
||||
find_and_patch "com/google/android/gms/ads/interstitial/InterstitialAd" \
|
||||
"load" \
|
||||
"AdMob" "Lcom/google/android/gms/ads/interstitial/InterstitialAd;"
|
||||
find_and_patch "com/google/android/gms/ads/rewarded/RewardedAd" \
|
||||
"load" \
|
||||
"AdMob" "Lcom/google/android/gms/ads/rewarded/RewardedAd;"
|
||||
find_and_patch "com/google/android/gms/ads/rewarded/RewardedInterstitialAd" \
|
||||
"load" \
|
||||
"AdMob" "Lcom/google/android/gms/ads/rewarded/RewardedInterstitialAd;"
|
||||
find_and_patch "com/google/android/gms/ads/appopen/AppOpenAd" \
|
||||
"load" \
|
||||
"AdMob" "Lcom/google/android/gms/ads/appopen/AppOpenAd;"
|
||||
find_and_patch "com/google/android/gms/ads/AdView" \
|
||||
"loadAd" \
|
||||
"AdMob" "Lcom/google/android/gms/ads/AdView;"
|
||||
find_and_patch "com/google/android/gms/ads/AdLoader" \
|
||||
"loadAd,loadAds" \
|
||||
"AdMob" "Lcom/google/android/gms/ads/AdLoader;"
|
||||
|
||||
# Unity Ads
|
||||
find_and_patch "com/unity3d/ads/UnityAds" \
|
||||
"initialize,load,show" \
|
||||
"UnityAds" "Lcom/unity3d/ads/UnityAds;"
|
||||
|
||||
# IronSource / LevelPlay
|
||||
find_and_patch "com/ironsource/mediationsdk/IronSource" \
|
||||
"init,loadInterstitial,showInterstitial,showRewardedVideo,loadBanner" \
|
||||
"IronSource" "Lcom/ironsource/mediationsdk/IronSource;"
|
||||
|
||||
# AppLovin / MAX
|
||||
find_and_patch "com/applovin/sdk/AppLovinSdk" \
|
||||
"getInstance,initializeSdk" \
|
||||
"AppLovin" "Lcom/applovin/sdk/AppLovinSdk;"
|
||||
|
||||
# Meta Audience Network
|
||||
find_and_patch "com/facebook/ads/AudienceNetworkAds" \
|
||||
"initialize,buildInitSettings" \
|
||||
"MetaAN" "Lcom/facebook/ads/AudienceNetworkAds;"
|
||||
|
||||
# Vungle / Liftoff
|
||||
find_and_patch "com/vungle/warren/Vungle" \
|
||||
"init,loadAd,playAd" \
|
||||
"Vungle" "Lcom/vungle/warren/Vungle;"
|
||||
find_and_patch "com/vungle/ads/VungleInterstitial" \
|
||||
"load,show" \
|
||||
"Vungle" "Lcom/vungle/ads/VungleInterstitial;"
|
||||
find_and_patch "com/vungle/ads/VungleRewarded" \
|
||||
"load,show" \
|
||||
"Vungle" "Lcom/vungle/ads/VungleRewarded;"
|
||||
find_and_patch "com/vungle/ads/VungleBanner" \
|
||||
"load" \
|
||||
"Vungle" "Lcom/vungle/ads/VungleBanner;"
|
||||
|
||||
# InMobi
|
||||
find_and_patch "com/inmobi/sdk/InMobiSdk" \
|
||||
"init" \
|
||||
"InMobi" "Lcom/inmobi/sdk/InMobiSdk;"
|
||||
find_and_patch "com/inmobi/ads/InMobiInterstitial" \
|
||||
"load,show" \
|
||||
"InMobi" "Lcom/inmobi/ads/InMobiInterstitial;"
|
||||
find_and_patch "com/inmobi/ads/InMobiBanner" \
|
||||
"load" \
|
||||
"InMobi" "Lcom/inmobi/ads/InMobiBanner;"
|
||||
|
||||
# Chartboost
|
||||
find_and_patch "com/chartboost/sdk/Chartboost" \
|
||||
"startWithAppId,cacheInterstitial,showInterstitial,cacheRewardedVideo,showRewardedVideo" \
|
||||
"Chartboost" "Lcom/chartboost/sdk/Chartboost;"
|
||||
|
||||
# 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" \
|
||||
"init" \
|
||||
"Pangle" "Lcom/pgl/sys/ces/PAGSdk;"
|
||||
|
||||
# Mintegral
|
||||
find_and_patch "com/mbridge/msdk/MBridgeSDKFactory" \
|
||||
"getMBridgeSDK" \
|
||||
"Mintegral" "Lcom/mbridge/msdk/MBridgeSDKFactory;"
|
||||
}
|
||||
|
||||
patch_tracker_targets() {
|
||||
# Firebase Analytics
|
||||
find_and_patch "com/google/firebase/analytics/FirebaseAnalytics" \
|
||||
"getInstance,logEvent,setUserId,setUserProperty,setAnalyticsCollectionEnabled" \
|
||||
"Firebase" "Lcom/google/firebase/analytics/FirebaseAnalytics;"
|
||||
|
||||
# Adjust
|
||||
find_and_patch "com/adjust/sdk/Adjust" \
|
||||
"onCreate,trackEvent,addSessionCallbackParameter,addSessionPartnerParameter,setEnabled" \
|
||||
"Adjust" "Lcom/adjust/sdk/Adjust;"
|
||||
|
||||
# AppsFlyer
|
||||
find_and_patch "com/appsflyer/AppsFlyerLib" \
|
||||
"getInstance,init,start,logEvent,setCustomerUserId" \
|
||||
"AppsFlyer" "Lcom/appsflyer/AppsFlyerLib;"
|
||||
|
||||
# Mixpanel
|
||||
find_and_patch "com/mixpanel/android/mpmetrics/MixpanelAPI" \
|
||||
"getInstance,track,trackMap,identify,timeEvent,registerSuperProperties" \
|
||||
"Mixpanel" "Lcom/mixpanel/android/mpmetrics/MixpanelAPI;"
|
||||
|
||||
# Amplitude
|
||||
find_and_patch "com/amplitude/api/AmplitudeClient" \
|
||||
"getInstance,initialize,logEvent,setUserId,setUserProperties" \
|
||||
"Amplitude" "Lcom/amplitude/api/AmplitudeClient;"
|
||||
|
||||
# Segment
|
||||
find_and_patch "com/segment/analytics/Analytics" \
|
||||
"with,track,identify,screen,group,alias" \
|
||||
"Segment" "Lcom/segment/analytics/Analytics;"
|
||||
|
||||
# Braze (and legacy Appboy)
|
||||
find_and_patch "com/braze/Braze" \
|
||||
"configure,logCustomEvent,changeUser,logPurchase" \
|
||||
"Braze" "Lcom/braze/Braze;"
|
||||
find_and_patch "com/appboy/Appboy" \
|
||||
"configure,logCustomEvent,changeUser" \
|
||||
"Braze" "Lcom/appboy/Appboy;"
|
||||
|
||||
# CleverTap
|
||||
find_and_patch "com/clevertap/android/sdk/CleverTapAPI" \
|
||||
"getDefaultInstance,pushEvent,onUserLogin,pushProfile,recordEvent" \
|
||||
"CleverTap" "Lcom/clevertap/android/sdk/CleverTapAPI;"
|
||||
|
||||
# Flurry
|
||||
find_and_patch "com/flurry/android/FlurryAgent" \
|
||||
"logEvent,setUserId,onStartSession,onEndSession" \
|
||||
"Flurry" "Lcom/flurry/android/FlurryAgent;"
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# patch_manifest() — Disable SDK components in AndroidManifest.xml
|
||||
# =====================================================================
|
||||
|
||||
patch_manifest() {
|
||||
if [[ "$DO_MANIFEST" == false ]] || [[ ! -f "$MANIFEST" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Known SDK components to disable
|
||||
# Format: "component_substring|sdk_name"
|
||||
local -a AD_COMPONENTS=(
|
||||
"com.google.android.gms.ads.AdActivity|AdMob"
|
||||
"com.google.android.gms.ads.MobileAdsInitProvider|AdMob"
|
||||
"com.google.android.gms.ads.AdService|AdMob"
|
||||
"com.unity3d.ads.adunit.AdUnitActivity|UnityAds"
|
||||
"com.unity3d.ads.adunit.AdUnitTransparentActivity|UnityAds"
|
||||
"com.unity3d.services.ads.adunit.AdUnitActivity|UnityAds"
|
||||
"com.ironsource.sdk.controller.InterstitialActivity|IronSource"
|
||||
"com.ironsource.sdk.controller.ControllerActivity|IronSource"
|
||||
"com.applovin.adview.AppLovinFullscreenActivity|AppLovin"
|
||||
"com.applovin.sdk.AppLovinWebViewActivity|AppLovin"
|
||||
"com.facebook.ads.AudienceNetworkActivity|MetaAN"
|
||||
"com.facebook.ads.InterstitialAdActivity|MetaAN"
|
||||
"com.vungle.warren.ui.VungleActivity|Vungle"
|
||||
"com.chartboost.sdk.CBImpressionActivity|Chartboost"
|
||||
"com.bytedance.sdk.openadsdk.activity.TTFullScreenVideoActivity|Pangle"
|
||||
"com.bytedance.sdk.openadsdk.activity.TTRewardVideoActivity|Pangle"
|
||||
"com.vungle.ads.internal.ui.VungleActivity|Vungle"
|
||||
"com.facebook.ads.AudienceNetworkContentProvider|MetaAN"
|
||||
"com.applovin.sdk.AppLovinInitProvider|AppLovin"
|
||||
"io.bidmachine.BidMachineInitProvider|BidMachine"
|
||||
"com.ironsource.lifecycle.IronsourceLifecycleProvider|IronSource"
|
||||
"com.amazon.device.ads.DTBAdActivity|AmazonAPS"
|
||||
"com.bytedance.sdk.openadsdk.activity.TTInterstitialActivity|Pangle"
|
||||
"com.bytedance.sdk.openadsdk.activity.TTAdActivity|Pangle"
|
||||
"com.bytedance.sdk.openadsdk.activity.TTDelegateActivity|Pangle"
|
||||
"com.mbridge.msdk.activity.MBCommonActivity|Mintegral"
|
||||
"com.mbridge.msdk.reward.player.MBRewardVideoActivity|Mintegral"
|
||||
"com.smaato.sdk.core.SmaatoBroadcastReceiver|Smaato"
|
||||
)
|
||||
|
||||
local -a TRACKER_COMPONENTS=(
|
||||
"com.google.android.gms.measurement.AppMeasurementService|Firebase"
|
||||
"com.google.android.gms.measurement.AppMeasurementReceiver|Firebase"
|
||||
"com.google.android.gms.measurement.AppMeasurementContentProvider|Firebase"
|
||||
"com.google.android.gms.measurement.AppMeasurementInstallReferrerReceiver|Firebase"
|
||||
"com.google.android.gms.measurement.AppMeasurementJobService|Firebase"
|
||||
"com.google.firebase.iid.FirebaseInstanceIdReceiver|Firebase"
|
||||
"com.adjust.sdk.AdjustReferrerReceiver|Adjust"
|
||||
"com.appsflyer.SingleInstallBroadcastReceiver|AppsFlyer"
|
||||
"com.appsflyer.MultipleInstallBroadcastReceiver|AppsFlyer"
|
||||
"com.mixpanel.android.mpmetrics.MixpanelFCMMessagingService|Mixpanel"
|
||||
"com.braze.push.BrazeFirebaseMessagingService|Braze"
|
||||
"com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver|CleverTap"
|
||||
"com.clevertap.android.sdk.pushnotification.CTNotificationIntentService|CleverTap"
|
||||
"com.appsflyer.internal.AFSingleInstallBroadcastReceiver|AppsFlyer"
|
||||
)
|
||||
|
||||
local -a components_to_disable=()
|
||||
|
||||
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
|
||||
|
||||
if [[ "$DO_BACKUP" == true ]] && [[ "$DRY_RUN" == false ]]; then
|
||||
cp "$MANIFEST" "${MANIFEST}.bak"
|
||||
fi
|
||||
|
||||
for entry in "${components_to_disable[@]}"; do
|
||||
local component_name="${entry%%|*}"
|
||||
local sdk_name="${entry##*|}"
|
||||
|
||||
# Check if the component exists in the manifest
|
||||
if grep -q "$component_name" "$MANIFEST"; then
|
||||
# Determine component type
|
||||
local comp_type="unknown"
|
||||
if grep -B1 "$component_name" "$MANIFEST" | grep -q "<activity"; then
|
||||
comp_type="activity"
|
||||
elif grep -B1 "$component_name" "$MANIFEST" | grep -q "<service"; then
|
||||
comp_type="service"
|
||||
elif grep -B1 "$component_name" "$MANIFEST" | grep -q "<receiver"; then
|
||||
comp_type="receiver"
|
||||
elif grep -B1 "$component_name" "$MANIFEST" | grep -q "<provider"; then
|
||||
comp_type="provider"
|
||||
fi
|
||||
|
||||
# Check if already disabled
|
||||
if grep "$component_name" "$MANIFEST" | grep -q 'android:enabled="false"'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "DRY_RUN:WOULD_DISABLE:$comp_type:$component_name:$sdk_name"
|
||||
else
|
||||
if grep "$component_name" "$MANIFEST" | grep -q 'android:enabled='; then
|
||||
# Replace existing android:enabled value (avoids duplicate attribute)
|
||||
sed -i "/$component_name/s|android:enabled=\"[^\"]*\"|android:enabled=\"false\"|g" "$MANIFEST"
|
||||
else
|
||||
# Add android:enabled="false" after android:name
|
||||
sed -i "s|android:name=\"$component_name\"|android:name=\"$component_name\" android:enabled=\"false\"|g" "$MANIFEST"
|
||||
fi
|
||||
echo "MANIFEST_DISABLED:$comp_type:$component_name:$sdk_name"
|
||||
echo "MANIFEST_DISABLED:$comp_type:$component_name:$sdk_name" >> "$PATCH_LOG_FILE"
|
||||
fi
|
||||
COMPONENTS_DISABLED=$((COMPONENTS_DISABLED + 1))
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Load custom targets from file
|
||||
# =====================================================================
|
||||
|
||||
patch_custom_targets() {
|
||||
if [[ -z "$TARGETS_FILE" ]] || [[ ! -f "$TARGETS_FILE" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
# Skip comments and empty lines
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" == \#* ]] && continue
|
||||
|
||||
# Format: Lcom/example/Class;:methodName or com/example/Class:methodName
|
||||
local class_part="${line%%:*}"
|
||||
local method_part="${line##*:}"
|
||||
|
||||
if [[ -z "$class_part" ]] || [[ -z "$method_part" ]]; then
|
||||
echo "Warning: Skipping malformed target line: $line" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Normalize class path: remove L prefix and ; suffix if present
|
||||
class_part="${class_part#L}"
|
||||
class_part="${class_part%;}"
|
||||
|
||||
# Build class descriptor for reporting
|
||||
local class_desc="L${class_part};"
|
||||
|
||||
find_and_patch "$class_part" "$method_part" "Custom" "$class_desc"
|
||||
done < "$TARGETS_FILE"
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Replay from neutralize-manifest.json
|
||||
# =====================================================================
|
||||
|
||||
if [[ "$DO_REPLAY" == true ]]; then
|
||||
MANIFEST_JSON="$DECODED_DIR/neutralize-manifest.json"
|
||||
if [[ ! -f "$MANIFEST_JSON" ]]; then
|
||||
echo "Error: neutralize-manifest.json not found in $DECODED_DIR" >&2
|
||||
echo "Cannot replay without a previous neutralization manifest." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Replaying from neutralize-manifest.json ==="
|
||||
|
||||
# Restore original scope from saved options
|
||||
saved_options=""
|
||||
if command -v jq &>/dev/null; then
|
||||
saved_options=$(jq -r '.options // ""' "$MANIFEST_JSON" 2>/dev/null)
|
||||
else
|
||||
saved_options=$(awk -F'"' '/"options"/ { print $4 }' "$MANIFEST_JSON")
|
||||
fi
|
||||
|
||||
case "$saved_options" in
|
||||
*--all*) NEUTRALIZE_ALL=true ;;
|
||||
*--ads*) NEUTRALIZE_ADS=true; NEUTRALIZE_ALL=false ;;
|
||||
*--trackers*) NEUTRALIZE_TRACKERS=true; NEUTRALIZE_ALL=false ;;
|
||||
esac
|
||||
|
||||
# Extract class:method pairs into a temporary targets file
|
||||
REPLAY_TARGETS=$(mktemp "${TMPDIR:-/tmp}/replay-targets-XXXXXX")
|
||||
|
||||
if command -v jq &>/dev/null; then
|
||||
jq -r '.patched_methods[]? | .class + ":" + .method' "$MANIFEST_JSON" > "$REPLAY_TARGETS" 2>/dev/null
|
||||
else
|
||||
# Fallback: parse JSON with awk (handles the simple array-of-objects format)
|
||||
awk '
|
||||
/"class"/ { gsub(/[",]/, ""); gsub(/^[[:space:]]*class:[[:space:]]*/, ""); class = $2 }
|
||||
/"method"/ { gsub(/[",]/, ""); gsub(/^[[:space:]]*method:[[:space:]]*/, ""); method = $2
|
||||
if (class != "" && method != "") print class ":" method
|
||||
}
|
||||
' "$MANIFEST_JSON" > "$REPLAY_TARGETS" 2>/dev/null
|
||||
fi
|
||||
|
||||
replay_count=$(wc -l < "$REPLAY_TARGETS" | tr -d ' ')
|
||||
echo "Loaded $replay_count method targets from previous manifest."
|
||||
|
||||
if [[ "$replay_count" -gt 0 ]]; then
|
||||
if [[ -n "$TARGETS_FILE" ]]; then
|
||||
# Merge replay targets into existing targets file
|
||||
cat "$REPLAY_TARGETS" >> "$TARGETS_FILE"
|
||||
else
|
||||
TARGETS_FILE="$REPLAY_TARGETS"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Main
|
||||
# =====================================================================
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "=== SDK Neutralizer — DRY RUN ==="
|
||||
else
|
||||
echo "=== SDK Neutralizer ==="
|
||||
fi
|
||||
echo "Decoded directory: $DECODED_DIR"
|
||||
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
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ "$NEUTRALIZE_ALL" == true ]] || [[ "$NEUTRALIZE_TRACKERS" == true ]]; then
|
||||
echo "--- Neutralizing Tracker SDK entry points ---"
|
||||
patch_tracker_targets
|
||||
echo
|
||||
fi
|
||||
|
||||
# Patch custom targets
|
||||
if [[ -n "$TARGETS_FILE" ]]; then
|
||||
echo "--- Neutralizing custom targets from $TARGETS_FILE ---"
|
||||
patch_custom_targets
|
||||
echo
|
||||
fi
|
||||
|
||||
# Patch manifest
|
||||
patch_manifest
|
||||
|
||||
# Summary
|
||||
echo
|
||||
echo "=== Neutralization Summary ==="
|
||||
echo "Methods patched: $METHODS_PATCHED"
|
||||
echo "Manifest components disabled: $COMPONENTS_DISABLED"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo
|
||||
echo "DRY RUN — no files were modified."
|
||||
echo "Remove --dry-run to apply changes."
|
||||
fi
|
||||
|
||||
if [[ "$METHODS_PATCHED" -eq 0 ]] && [[ "$COMPONENTS_DISABLED" -eq 0 ]]; then
|
||||
echo
|
||||
echo "No targets found in the decoded directory."
|
||||
echo "The app may not contain the targeted SDKs, or they may use obfuscated class names."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Save neutralize-manifest.json
|
||||
# =====================================================================
|
||||
|
||||
if [[ "$DO_SAVE_MANIFEST" == true ]] && [[ "$DRY_RUN" == false ]] && \
|
||||
[[ "$METHODS_PATCHED" -gt 0 || "$COMPONENTS_DISABLED" -gt 0 ]]; then
|
||||
|
||||
MANIFEST_JSON="$DECODED_DIR/neutralize-manifest.json"
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
# Build options string
|
||||
OPTIONS_STR=""
|
||||
if [[ "$NEUTRALIZE_ALL" == true ]]; then
|
||||
OPTIONS_STR="--all"
|
||||
else
|
||||
[[ "$NEUTRALIZE_ADS" == true ]] && OPTIONS_STR="${OPTIONS_STR} --ads"
|
||||
[[ "$NEUTRALIZE_TRACKERS" == true ]] && OPTIONS_STR="${OPTIONS_STR} --trackers"
|
||||
OPTIONS_STR="${OPTIONS_STR# }"
|
||||
fi
|
||||
|
||||
# Build JSON from patch log file using printf (no jq dependency for writing)
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "timestamp": "%s",\n' "$TIMESTAMP"
|
||||
printf ' "options": "%s",\n' "$OPTIONS_STR"
|
||||
printf ' "patched_methods": [\n'
|
||||
|
||||
first_method=true
|
||||
while IFS= read -r logline; do
|
||||
if [[ "$logline" == PATCHED:* ]]; then
|
||||
# PATCHED:<sdk>:<class>:<method>:<stub_type>:<file>
|
||||
IFS=':' read -r _ p_sdk p_class p_method p_stub p_file <<< "$logline"
|
||||
if [[ "$first_method" == true ]]; then
|
||||
first_method=false
|
||||
else
|
||||
printf ',\n'
|
||||
fi
|
||||
printf ' {"sdk": "%s", "class": "%s", "method": "%s", "stub": "%s", "file": "%s"}' \
|
||||
"$p_sdk" "$p_class" "$p_method" "$p_stub" "$p_file"
|
||||
fi
|
||||
done < "$PATCH_LOG_FILE"
|
||||
|
||||
printf '\n ],\n'
|
||||
printf ' "disabled_components": [\n'
|
||||
|
||||
first_comp=true
|
||||
while IFS= read -r logline; do
|
||||
if [[ "$logline" == MANIFEST_DISABLED:* ]]; then
|
||||
# MANIFEST_DISABLED:<type>:<name>:<sdk>
|
||||
IFS=':' read -r _ c_type c_name c_sdk <<< "$logline"
|
||||
if [[ "$first_comp" == true ]]; then
|
||||
first_comp=false
|
||||
else
|
||||
printf ',\n'
|
||||
fi
|
||||
printf ' {"type": "%s", "name": "%s", "sdk": "%s"}' \
|
||||
"$c_type" "$c_name" "$c_sdk"
|
||||
fi
|
||||
done < "$PATCH_LOG_FILE"
|
||||
|
||||
printf '\n ]\n'
|
||||
printf '}\n'
|
||||
} > "$MANIFEST_JSON"
|
||||
|
||||
echo
|
||||
echo "Patch manifest saved: $MANIFEST_JSON"
|
||||
echo "Use --replay after re-decode to reapply the same patches."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
#!/usr/bin/env bash
|
||||
# rebuild-apk.sh — Rebuild and sign a decoded APK directory
|
||||
#
|
||||
# Pipeline: apktool b → zipalign (optional) → sign → verify
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — success
|
||||
# 1 — error (build failed, signing failed)
|
||||
# 2 — manual action needed (missing tools)
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: rebuild-apk.sh <decoded-dir> [OPTIONS]
|
||||
|
||||
Rebuild an apktool-decoded APK directory back into a signed APK.
|
||||
|
||||
Arguments:
|
||||
<decoded-dir> Path to the apktool-decoded APK directory
|
||||
|
||||
Options:
|
||||
-o, --output <file> Output APK path (default: <decoded-dir>-neutralized.apk)
|
||||
--debug-key Sign with an auto-generated debug keystore (default)
|
||||
--keystore <file> Path to a custom keystore file
|
||||
--key-alias <alias> Key alias within the keystore (default: key0)
|
||||
--key-pass <password> Key password (default: android)
|
||||
--store-pass <password> Keystore password (default: android)
|
||||
--no-sign Skip signing (output unsigned APK)
|
||||
--no-res Skip resource recompilation (apktool b --no-res)
|
||||
--zipalign Run zipalign before signing (recommended, default if available)
|
||||
--no-zipalign Skip zipalign
|
||||
-h, --help Show this help message
|
||||
|
||||
Output:
|
||||
BUILD_OK:<output-apk>
|
||||
BUILD_WARNING:Resources were not recompiled (--no-res fallback)
|
||||
SIGN_OK:<output-apk>
|
||||
VERIFY_OK:<output-apk>
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# Argument parsing
|
||||
# =====================================================================
|
||||
|
||||
DECODED_DIR=""
|
||||
OUTPUT=""
|
||||
USE_DEBUG_KEY=true
|
||||
KEYSTORE=""
|
||||
KEY_ALIAS="key0"
|
||||
KEY_PASS="android"
|
||||
STORE_PASS="android"
|
||||
DO_SIGN=true
|
||||
DO_ZIPALIGN=true
|
||||
NO_ZIPALIGN=false
|
||||
FORCE_NO_RES=false
|
||||
BUILD_USED_NO_RES=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o|--output)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then echo "Error: --output requires a file argument" >&2; exit 1; fi
|
||||
OUTPUT="$1"; shift ;;
|
||||
--debug-key) USE_DEBUG_KEY=true; shift ;;
|
||||
--keystore)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then echo "Error: --keystore requires a file argument" >&2; exit 1; fi
|
||||
KEYSTORE="$1"; USE_DEBUG_KEY=false; shift ;;
|
||||
--key-alias)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then echo "Error: --key-alias requires an argument" >&2; exit 1; fi
|
||||
KEY_ALIAS="$1"; shift ;;
|
||||
--key-pass)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then echo "Error: --key-pass requires an argument" >&2; exit 1; fi
|
||||
KEY_PASS="$1"; shift ;;
|
||||
--store-pass)
|
||||
shift
|
||||
if [[ $# -eq 0 ]]; then echo "Error: --store-pass requires an argument" >&2; exit 1; fi
|
||||
STORE_PASS="$1"; shift ;;
|
||||
--no-sign) DO_SIGN=false; shift ;;
|
||||
--no-res) FORCE_NO_RES=true; shift ;;
|
||||
--zipalign) DO_ZIPALIGN=true; shift ;;
|
||||
--no-zipalign) NO_ZIPALIGN=true; DO_ZIPALIGN=false; shift ;;
|
||||
-h|--help) usage ;;
|
||||
-*) echo "Error: Unknown option $1" >&2; usage ;;
|
||||
*) DECODED_DIR="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$DECODED_DIR" ]]; then
|
||||
echo "Error: No decoded directory specified." >&2
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -d "$DECODED_DIR" ]]; then
|
||||
echo "Error: Directory not found: $DECODED_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Default output name
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
# Strip trailing slash
|
||||
local_dir="${DECODED_DIR%/}"
|
||||
OUTPUT="${local_dir}-neutralized.apk"
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Tool checks
|
||||
# =====================================================================
|
||||
|
||||
info() { echo "[INFO] $*"; }
|
||||
ok() { echo "[OK] $*"; }
|
||||
fail() { echo "[FAIL] $*" >&2; }
|
||||
|
||||
if ! command -v apktool &>/dev/null; then
|
||||
fail "apktool is not installed. Run: install-dep.sh apktool"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v java &>/dev/null; then
|
||||
fail "java is not installed. Run: install-dep.sh java"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine signing tool
|
||||
SIGNER=""
|
||||
if [[ "$DO_SIGN" == true ]]; then
|
||||
if command -v apksigner &>/dev/null; then
|
||||
SIGNER="apksigner"
|
||||
elif command -v jarsigner &>/dev/null; then
|
||||
SIGNER="jarsigner"
|
||||
else
|
||||
fail "Neither apksigner nor jarsigner found. Install apksigner or use --no-sign."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check zipalign availability
|
||||
ZIPALIGN_CMD=""
|
||||
if [[ "$DO_ZIPALIGN" == true ]] && [[ "$NO_ZIPALIGN" == false ]]; then
|
||||
if command -v zipalign &>/dev/null; then
|
||||
ZIPALIGN_CMD="zipalign"
|
||||
else
|
||||
# Check Android SDK
|
||||
for sdk_dir in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}"; do
|
||||
if [[ -n "$sdk_dir" ]] && [[ -d "$sdk_dir/build-tools" ]]; then
|
||||
latest_bt=$(ls -1 "$sdk_dir/build-tools" 2>/dev/null | sort -V | tail -1)
|
||||
if [[ -n "$latest_bt" ]] && [[ -f "$sdk_dir/build-tools/$latest_bt/zipalign" ]]; then
|
||||
ZIPALIGN_CMD="$sdk_dir/build-tools/$latest_bt/zipalign"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ -z "$ZIPALIGN_CMD" ]]; then
|
||||
info "zipalign not found — skipping alignment (APK will still work)"
|
||||
DO_ZIPALIGN=false
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Step 1: Build APK with apktool
|
||||
# =====================================================================
|
||||
|
||||
echo "=== Rebuilding APK ==="
|
||||
echo "Source: $DECODED_DIR"
|
||||
echo "Output: $OUTPUT"
|
||||
echo
|
||||
|
||||
info "Running apktool build..."
|
||||
BUILT_APK="$DECODED_DIR/dist/$(basename "$DECODED_DIR").apk"
|
||||
|
||||
build_apk() {
|
||||
local no_res_flag="${1:-}"
|
||||
# Clean previous build artifacts
|
||||
rm -rf "$DECODED_DIR/dist/" 2>/dev/null || true
|
||||
if apktool b $no_res_flag "$DECODED_DIR" 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ "$FORCE_NO_RES" == true ]]; then
|
||||
# User explicitly requested --no-res
|
||||
if ! build_apk "--no-res"; then
|
||||
fail "apktool build failed (with --no-res)."
|
||||
echo "Tip: If this is a framework error, try: rm -f ~/.local/share/apktool/framework/1.apk"
|
||||
exit 1
|
||||
fi
|
||||
BUILD_USED_NO_RES=true
|
||||
else
|
||||
# First attempt: normal build
|
||||
if ! build_apk ""; then
|
||||
info "Build failed — retrying with --no-res (skipping resource recompilation)..."
|
||||
if ! build_apk "--no-res"; then
|
||||
fail "apktool build failed (both normal and --no-res)."
|
||||
echo "Tip: If this is a framework error, try: rm -f ~/.local/share/apktool/framework/1.apk"
|
||||
exit 1
|
||||
fi
|
||||
BUILD_USED_NO_RES=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# apktool outputs to dist/ inside the decoded dir
|
||||
if [[ ! -f "$BUILT_APK" ]]; then
|
||||
# Try finding any APK in dist/
|
||||
BUILT_APK=$(find "$DECODED_DIR/dist/" -name "*.apk" -type f | head -1)
|
||||
if [[ -z "$BUILT_APK" ]] || [[ ! -f "$BUILT_APK" ]]; then
|
||||
fail "Built APK not found in $DECODED_DIR/dist/"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
ok "APK built: $BUILT_APK"
|
||||
echo "BUILD_OK:$BUILT_APK"
|
||||
if [[ "$BUILD_USED_NO_RES" == true ]]; then
|
||||
echo "BUILD_WARNING:Resources were not recompiled (--no-res fallback)"
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Step 2: Zipalign (optional)
|
||||
# =====================================================================
|
||||
|
||||
ALIGNED_APK="$BUILT_APK"
|
||||
if [[ "$DO_ZIPALIGN" == true ]] && [[ -n "$ZIPALIGN_CMD" ]]; then
|
||||
info "Running zipalign..."
|
||||
ALIGNED_APK="${BUILT_APK%.apk}-aligned.apk"
|
||||
if "$ZIPALIGN_CMD" -f 4 "$BUILT_APK" "$ALIGNED_APK"; then
|
||||
ok "Zipaligned: $ALIGNED_APK"
|
||||
# Remove unaligned APK
|
||||
rm -f "$BUILT_APK"
|
||||
else
|
||||
info "Zipalign failed — continuing with unaligned APK"
|
||||
ALIGNED_APK="$BUILT_APK"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Step 3: Sign APK
|
||||
# =====================================================================
|
||||
|
||||
if [[ "$DO_SIGN" == false ]]; then
|
||||
cp "$ALIGNED_APK" "$OUTPUT"
|
||||
ok "Unsigned APK saved to: $OUTPUT"
|
||||
echo "BUILD_OK:$OUTPUT"
|
||||
echo
|
||||
echo "WARNING: APK is unsigned and cannot be installed without signing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate debug keystore if needed
|
||||
if [[ "$USE_DEBUG_KEY" == true ]]; then
|
||||
KEYSTORE="$DECODED_DIR/.neutralizer-debug.keystore"
|
||||
if [[ ! -f "$KEYSTORE" ]]; then
|
||||
info "Generating debug keystore..."
|
||||
keytool -genkeypair \
|
||||
-keystore "$KEYSTORE" \
|
||||
-alias "$KEY_ALIAS" \
|
||||
-keyalg RSA \
|
||||
-keysize 2048 \
|
||||
-validity 10000 \
|
||||
-storepass "$STORE_PASS" \
|
||||
-keypass "$KEY_PASS" \
|
||||
-dname "CN=SDK Neutralizer Debug Key, OU=Debug, O=Debug, L=Unknown, ST=Unknown, C=US" \
|
||||
2>/dev/null
|
||||
ok "Debug keystore generated: $KEYSTORE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$KEYSTORE" ]]; then
|
||||
fail "Keystore not found: $KEYSTORE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Signing APK with $SIGNER..."
|
||||
|
||||
if [[ "$SIGNER" == "apksigner" ]]; then
|
||||
apksigner sign \
|
||||
--ks "$KEYSTORE" \
|
||||
--ks-key-alias "$KEY_ALIAS" \
|
||||
--ks-pass "pass:$STORE_PASS" \
|
||||
--key-pass "pass:$KEY_PASS" \
|
||||
--out "$OUTPUT" \
|
||||
"$ALIGNED_APK"
|
||||
elif [[ "$SIGNER" == "jarsigner" ]]; then
|
||||
# jarsigner signs in-place, so copy first
|
||||
cp "$ALIGNED_APK" "$OUTPUT"
|
||||
jarsigner \
|
||||
-keystore "$KEYSTORE" \
|
||||
-storepass "$STORE_PASS" \
|
||||
-keypass "$KEY_PASS" \
|
||||
-signedjar "$OUTPUT" \
|
||||
"$ALIGNED_APK" \
|
||||
"$KEY_ALIAS"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$OUTPUT" ]]; then
|
||||
fail "Signed APK not found at: $OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ok "APK signed: $OUTPUT"
|
||||
echo "SIGN_OK:$OUTPUT"
|
||||
|
||||
# Clean up intermediate files
|
||||
rm -f "$ALIGNED_APK" 2>/dev/null || true
|
||||
|
||||
# =====================================================================
|
||||
# Step 4: Verify signature
|
||||
# =====================================================================
|
||||
|
||||
info "Verifying signature..."
|
||||
|
||||
if [[ "$SIGNER" == "apksigner" ]]; then
|
||||
if apksigner verify "$OUTPUT" 2>/dev/null; then
|
||||
ok "Signature verified (apksigner)"
|
||||
echo "VERIFY_OK:$OUTPUT"
|
||||
else
|
||||
info "Signature verification returned warnings (may still be installable)"
|
||||
fi
|
||||
elif [[ "$SIGNER" == "jarsigner" ]]; then
|
||||
if jarsigner -verify "$OUTPUT" 2>/dev/null; then
|
||||
ok "Signature verified (jarsigner)"
|
||||
echo "VERIFY_OK:$OUTPUT"
|
||||
else
|
||||
info "Signature verification returned warnings (may still be installable)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# Summary
|
||||
# =====================================================================
|
||||
|
||||
echo
|
||||
echo "=== Rebuild Complete ==="
|
||||
echo "Output APK: $OUTPUT"
|
||||
echo "Signed with: $SIGNER ($( [[ "$USE_DEBUG_KEY" == true ]] && echo "debug key" || echo "custom keystore" ))"
|
||||
|
||||
APK_SIZE=$(stat -f%z "$OUTPUT" 2>/dev/null || stat -c%s "$OUTPUT" 2>/dev/null || echo "unknown")
|
||||
echo "APK size: $APK_SIZE bytes"
|
||||
|
||||
if [[ "$BUILD_USED_NO_RES" == true ]]; then
|
||||
echo
|
||||
echo "NOTE: Resources were NOT recompiled (--no-res was used)."
|
||||
echo " The APK uses original resources. Manifest XML changes (e.g., android:enabled)"
|
||||
echo " may not be reflected unless they were applied before decode."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "WARNING: Play Integrity / SafetyNet will FAIL — expected for enterprise sideloading."
|
||||
echo "Install via: adb install $OUTPUT"
|
||||
|
||||
exit 0
|
||||
Loading…
Reference in New Issue