758 lines
26 KiB
Bash
Executable File
758 lines
26 KiB
Bash
Executable File
#!/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
|