feat: apk-reverse Agent Skill
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.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# 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 named `Advertisement*`, `Advert*`, `Banner*`, `Promotion*`.
|
||||
- The client renders whatever the list contains. If the list is empty there is nothing to show — but see `references/pitfalls.md` P5 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):
|
||||
|
||||
1. **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.
|
||||
2. **No-op the show/preload methods** — preserving callbacks.
|
||||
3. **Neutralize the gate** — `isAdReady`/`canShow` return false.
|
||||
4. **Filter server-issued ad data at the consumption layer** — drop entries for the target position before they reach UI state.
|
||||
5. *(avoid)* **Renderer-level suppression** — only if the component is genuinely ad-exclusive. Check `references/pitfalls.md` P6 first.
|
||||
6. *(avoid)* **Transport-level blocking** — see `references/pitfalls.md` P5.
|
||||
|
||||
## Step 4: verify — and verify it properly
|
||||
|
||||
Logcat silence alone is weak evidence. Use several independent signals:
|
||||
|
||||
1. **Process stays alive** through startup and across a few screens; no `FATAL EXCEPTION`, no `VerifyError`, no `uncaughtException`.
|
||||
2. **SDK log keywords absent** — `anythink`, `ATSDK`, `Pangle`, `TTAd`, `GDT`, etc. should not appear at all.
|
||||
3. **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 `InetAddress` hook 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".
|
||||
4. **Exercise the screens that had ads** — splash, home banner, detail page, player, reward button.
|
||||
5. **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.
|
||||
Reference in New Issue
Block a user