Progressive-disclosure skill for Android APK reverse engineering: recon, ad removal, membership limits, dex surgical patching, repack/sign, runtime data, Frida, environment setup, and a hard-won failure catalogue.
6.3 KiB
Ad removal
Goal: ads stop appearing, and nothing else breaks. Read references/pitfalls.md P5 and P6 before patching anything here.
Step 1: identify which ads exist and who owns them
Do not assume "ads" is one thing. Enumerate first.
SDK-integrated ads (the app calls an SDK):
- Search dex strings for SDK markers:
openadsdk,TTAdSdk,TTAdNative,Pangle,pangolin,com.qq.e,GDTAd,gdt_plugin,anythink,ATSDK,ATRewardVideoAd,bdxadsdk,sigmob,ksad,mobads,beizi. - Aggregators (AnyThink, TopOn, and similar) are wrappers: the visible ad networks underneath are their adapters, not app code. Patching an individual network's class is usually pointless — the aggregator still runs and still reaches the network.
- The app almost always funnels every call through one wrapper class. Find it; that is your patch surface.
Server-driven ads / sponsored content (the server returns the ad, the client renders it):
- Look for endpoints like
/adverts,/adv,/banner,/config, and DTOs namedAdvertisement*,Advert*,Banner*,Promotion*. - The client renders whatever the list contains. If the list is empty there is nothing to show — but see
references/pitfalls.mdP5 on how not to empty it. - Sponsored cards that look like content (a VPN promo, a network-accelerator card, a third-party product) are usually exactly this.
Legit content that looks like an ad. Verify before acting. A /adverts?position=banner response containing anime titles and poster images is the home-page carousel, not an advertisement. Removing it removes real functionality.
Step 2: map the wrapper
Find the app's own helper and enumerate its public surface. Typical shape:
helper.init(Application) -> Sdk.init(appId, appKey); Sdk.start()
helper.showSplash(Activity, ViewGroup, onClose: () -> Unit)
helper.showInterstitial(Activity)
helper.showReward(Activity, onStart: () -> Unit, onFinish: (Boolean) -> Unit)
helper.preload*(Activity)
helper.canShow*(Activity): Boolean
Read each method body and classify:
| Method | Contains | Safe patch |
|---|---|---|
| Init | Sdk.init(...) + Sdk.start() |
Replace body with return-void |
| Show splash/interstitial | constructs an ad object, loadAd() / show() |
Replace body, but must still invoke the completion callback |
| Show reward | same, plus success/failure lambdas | Replace body; decide deliberately whether to grant the reward |
| Preload/warm-up | RewardVideoAutoAd.init(...) |
return-void |
| Gate check | isAdReady, canShow |
return false / 0 |
The callback trap (this one breaks startup)
A splash/loading ad usually takes a completion lambda as a parameter. The app's startup state machine waits for that lambda before it dismisses the splash screen.
If you replace the method with a bare return-void, the lambda never fires, and the app hangs forever on the splash screen.
Correct shape — skip the ad, still complete:
.method public final showSplash(Landroid/app/Activity;Landroid/view/ViewGroup;Lkotlin/jvm/functions/Function0;)V
.registers N
invoke-interface {p3}, Lkotlin/jvm/functions/Function0;->invoke()Ljava/lang/Object;
return-void
.end method
Before patch, prove the contract. Read the listener class the method constructs and find which callback triggers the completion lambda (onAdDismiss? onAdError? onAdLoadTimeout?). Only that tells you whether to call it on success, failure, or unconditionally.
For a reward ad whose lambdas are (onStart, (Boolean) -> Unit), granting the reward client-side is a choice: onFinish(true) will make the app treat the reward as earned. Check whether the reward is validated server-side before promising it.
Step 3: choose the patch layer
From safest to riskiest (see references/dex-patching.md for the full table):
- Kill SDK init — the SDK never starts; its networks are never contacted. Highest-value single patch. Verify nothing else depends on the SDK being initialized.
- No-op the show/preload methods — preserving callbacks.
- Neutralize the gate —
isAdReady/canShowreturn false. - Filter server-issued ad data at the consumption layer — drop entries for the target position before they reach UI state.
- (avoid) Renderer-level suppression — only if the component is genuinely ad-exclusive. Check
references/pitfalls.mdP6 first. - (avoid) Transport-level blocking — see
references/pitfalls.mdP5.
Step 4: verify — and verify it properly
Logcat silence alone is weak evidence. Use several independent signals:
- Process stays alive through startup and across a few screens; no
FATAL EXCEPTION, noVerifyError, nouncaughtException. - SDK log keywords absent —
anythink,ATSDK,Pangle,TTAd,GDT, etc. should not appear at all. - DNS / socket level (strongest, and cheap) — hook or observe name resolution. If the ad SDK's network stack is initialized it will resolve its own domains. Many ad SDKs bypass the system proxy, so proxy logs may miss them; a runtime
InetAddresshook does not.- Before: a burst of ad/tracker domains.
- After: none at all — meaning the init path never ran, not merely "shown but not rendered".
- Exercise the screens that had ads — splash, home banner, detail page, player, reward button.
- Confirm nothing unrelated regressed — images load, video plays, lists scroll, login works.
A useful framing for the report: distinguish "the ad was hidden" from "the ad subsystem never started". The second is a much stronger claim, and DNS/socket evidence can prove it.
Step 5: what is usually NOT removable
Be honest about residual ads rather than breaking the app to chase them:
- Server-driven content promos deep in a feature's own data payload, where the screen's load depends on the same request. Suppressing them requires a data-consumption patch, not transport blocking.
- Ads delivered as content (a sponsored "article" or a native card with no SDK marker) — indistinguishable from real content without runtime tracing.
- Ads whose SDK init also enables other features. Removing init can break functionality that silently depended on it.
If a residual ad cannot be removed without breaking something, say so, and say exactly which coupling caused it. That is a better deliverable than a broken APK.