Close the byte-patching gap, add the server-config path, fix two script crashes
Worked end-to-end on a real unobfuscated commercial app (single-dex, no packer,
server-driven launch screen) and folded back everything that cost time. Net +17
files, no new dependencies, no sample-specific identifiers.
New references
byte-level-patching.md
The technique that was missing entirely. Equal-length in-place edits beat
method rebuilding (measured on a real sample: method rewrite grew debug_info
924 B -> 22.8 KB and the dex 4.32 MB -> 7.73 MB), but four failure modes are
silent: same-length is not enough, branch polarity, verifier legality, and
stale dex header fields. Covers locating an instruction's exact offset (why
listings and byte-pattern guesses both fail), the instruction width table and
the two traps that desynchronise a decode, neutralise-a-branch vs redirect-it,
and the header's signature-then-checksum order.
server-config-and-updates.md
The most common shape of "ad" in a modern app, and the one usually
mis-diagnosed: the server supplies UI the client renders. The two-layer fetch
(local fallback then remote override) that proves it, finding the config DTOs
through the field names data classes keep, why you patch the decision and not
the data, deciding the scope of "remove", and remote re-enable / cached config
durability.
New scripts
dexutil.py dependency-free dex reader: structural walk, exact
instruction decode, dex header recompute/verify in the
correct order, branch-target and operand helpers
dex_find_insn.py locate an instruction by decoded semantics, print its exact
byte offset with context and BOTH sides of any branch
dex_patch_bytes.py equal-length byte patches from a JSON spec: semantic match,
polarity pin via expect_next, equal-length enforcement,
verifier check, header recompute, re-decode to prove it landed
dex_check_verifier.py does any conditional branch target a move-result (bypassing
its producer)? Compares builds and separates pre-existing
findings from regressions the patch introduced
coldstart.py cold-launch capture: timed screenshot burst + logcat signals
+ installed-build facts + launch timing, and a warning when
the foreground activity is not the app
check_repo.py maintenance check: every script parses and answers --help,
every referenced path resolves, every file is documented
Fixes found while running the above
repack.py wrote archives with a plain zipfile writer, which cannot satisfy
the Android R+ install gate: resources.arsc must be STORED *and*
4-byte aligned, and a zip extra record cannot express a 1-3 byte
pad (a stored filler entry is inserted instead). Also fixes the
central-directory pointer and adds an alignment gate to the output.
usb_net_proxy.py, dex_strpatch.py
crashed on --help with a traceback instead of printing usage.
Updated
toolchain.md ddc added (single-binary dex->Java with query subcommands,
with measured timings and its real limits); "not on PATH is
not not-installed" with the search commands; which signer
to use and why jarsigner breaks alignment
repack-and-sign.md alignment requirement and its arithmetic; dex header
recompute; jarsigner hazard; vendor install interception
([N] with no symbolic constant -> root pm install); the
installer can keep owning the screen after an install
recon.md resolve package identity from a tool, not from hand-read
AXML strings (a wrong package sends all later device queries
nowhere); snapshot the config surface during recon
long-task-discipline.md captures you never looked at are not evidence (incl. the
consistent-but-wrong-window variant); long-context decay and
the record shape that survives it
dynamic-frida.md when live attach cannot work at all: static instrumentation,
static read, and self-recorded evidence, with the control
build caveat each one carries
ad-removal.md server-driven config is usually the EASIEST case, not the
hardest; a three-way zero result is a positive finding;
scope note on local brand splash vs promotional content
pitfalls.md P37 stale dex header checksum, P38 [-124] alignment,
P39 bare-numeric install failure, P40 wrong-window evidence,
P41 inverted branch polarity, P42 decode desynchronisation
SKILL.md, README.md symptom index, reference/script indexes and requirements
updated for all of the above
This commit is contained in:
@@ -23,9 +23,27 @@ Do not assume "ads" is one thing. Enumerate first.
|
||||
- The plugin may also be bundled. Two copies still means one init gate; patch that, not the payload.
|
||||
|
||||
**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 `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.
|
||||
- **This is the most common shape in a modern app, and it is usually the EASIEST to
|
||||
remove — not the hardest.** Go to `server-config-and-updates.md` for the full
|
||||
procedure. The short version: the client keeps a complete "do not show it" branch
|
||||
so the operator can turn the slot off, so neutralising that one branch is a
|
||||
small, local edit.
|
||||
- Look for endpoints like `/adverts`, `/adv`, `/banner`, `/config`, and DTOs named
|
||||
`Advertisement*`, `Advert*`, `Banner*`, `Promotion*`, or — more common in practice
|
||||
— a generic `*Config` payload carrying per-feature blocks (`splash`, `noticePopup`,
|
||||
`updatePopup`, `tabbar`, `banner`).
|
||||
- The client renders whatever the list contains. If the list is empty there is nothing
|
||||
to show, but see `pitfalls.md` P5 on **how not** to empty it: never make a shared
|
||||
request fail.
|
||||
- Sponsored cards that look like content (a VPN promo, a network-accelerator card, a
|
||||
third-party product) are usually exactly this.
|
||||
|
||||
**A note on what "no SDK found" means.** If you have grepped the dex, enumerated the
|
||||
loaded classes at runtime, and counted the SDK log tags in a capture, and all three
|
||||
are zero (see Step 1), then there is no SDK, and continuing to search for one is a
|
||||
dead end rather than a thorough approach. Re-classify against this list instead. A
|
||||
zero result on all three signals is a strong positive finding about where the
|
||||
behaviour lives, and it should move you to the server-config path immediately.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -138,9 +156,22 @@ Frame the result at the right strength: *"the ad was hidden"* vs *"the ad subsys
|
||||
|
||||
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. Verify before shipping; a working app with one residual ad beats a broken app.
|
||||
- **Server-issued entitlements gated behind watching an ad.** The gate can be opened client-side, the entitlement cannot be created client-side.
|
||||
- **Content promos embedded deep in a feature's own data payload**, where the screen's
|
||||
load depends on the same request. Distinguish this from a *dedicated config block*:
|
||||
if the promo arrives in its own `splash` / `banner` / `popup` field with its own
|
||||
`enabled` flag, it is removable (Step 1, server-driven). It is only genuinely hard
|
||||
when the same list is both the content and the ad, with no marker separating them.
|
||||
- **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. Verify before shipping; a working app with
|
||||
one residual ad beats a broken app.
|
||||
- **Server-issued entitlements gated behind watching an ad.** The gate can be opened
|
||||
client-side, the entitlement cannot be created client-side.
|
||||
- **A purely local brand launch screen.** If all you see is the app's own logo, no
|
||||
third-party image and no network-sourced content, that is a splash screen, not an ad.
|
||||
Removing it is a judgement call about the user's stated goal — say which one you
|
||||
removed rather than silently treating "startup screen" as "ad".
|
||||
|
||||
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.
|
||||
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