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:
Simone Avogadro 2026-03-02 19:03:40 +01:00
parent f216ec0914
commit e06f794112
10 changed files with 2782 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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