Add the four DESIGN 3.3 commit-7b entries (Q1): detection-only, no
targets, manifest_components or protected_patterns, so registry-scan and
neutralize.sh ignore them and the audit (commit 8) gains visibility on
5-6 corpus apps each.
- tiktok-business (attribution, com.tiktok): marker TikTokBusinessSdk,
verified in af1945, blockcraft, bulldozer, ttom2 (pristine decodes)
- bigo-ads (ads, sg.bigo.ads): marker BigoAdSdk (af1945, bulldozer),
namespace sg.bigo
- apm-insight (crash_reporting, com.apm.insight): no detection facts
- kidoz (ads, net.kidoz): marker net.kidoz.sdk.Kidoz (subway), domain
kidoz.net; com.kpadplayer stays out of packages (Q2) so the Subway
com.kpadplayer.sdk wrapper candidate is kept
Gate: detect-adwrapper (sh with and without --registry, ps1) and
registry-scan goldens are byte-identical on the corpus, so all four
entries stay in this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
Add the two detection-only entries of DESIGN 3.3 (no targets, no
manifest_components, no protected_patterns), so find-trackers keeps its
--segment and --flurry flags once the audit reads the registry (commit 8),
and the two legacy_flags rows that point at them, in the historical flag
order of find-trackers.sh (L13 stays green).
flurry carries the corpus-verified marker com.flurry.android.FlurryAgent
(blockcraft), the Builder init and the event/identity APIs, the flurry.com
domain, and also FlurryAgent#init / #onStartSession, the legacy init
tokens that R5 now confirms (corpus copy and vendor API docs; DEVIATIONS
7a.1). segment is not in the corpus: its facts come from the design.
registry-scan skips both entries (I4 on real data): the registry-scan
golden, the consumed snapshot of the 48 entries and the detect-adwrapper
corpus golden (sh and ps1) are unchanged. On the synthetic adwrapper
fixture com.segment.analytics is now an excluded registry package, so its
candidate line leaves the expected output. t_reader now expects the new
legacy_flags rows; t_adwrapper compares the frozen scratch registry copies
with their own reference (DEVIATIONS 7a.2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
Add the audit-only `detection` object (DESIGN 3.2 ads/consent table, 3.5)
to 17 ads, mediation and consent entries: marker classes verified on the
corpus (MobileAds, UnityAds, AppLovinSdk, AudienceNetworkAds,
UserMessagingPlatform), the AdMob gms.ads.identifier and Appodeal
explorestack.protobuf excludes, vendor namespaces and mediation adapter
packages, extra identity packages seen in the corpus (Unity scar and
gatewayprotocol, Pangle pgl/bykv/adsdk, InMobi media/commons, ...),
role-tagged APIs (consent setters, format-specific loaders, legacy
Chartboost/Pangle/Vungle calls), the ca-app-pub- id prefix and the
applovin.sdk.key meta-data. The neutralizer reads none of it: the
consumed snapshot of the 48 entries is unchanged and the registry-scan /
detect-adwrapper goldens stay byte-identical.
T-FORMAT now covers the new ads apis: 14 pairs no format rule maps
(config/consent/identity setters, init calls, AdLoader.Builder) join
format_none, and its schema description names those reasons.
The AdMob meta-data names (prefix-matched by the identity root) and the
ogury io.presage namespace (already an identity package) are left out;
see DEVIATIONS 6b.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
Add the audit-only `detection` object (DESIGN 3.2 tracker table, 3.5) to
the 16 tracker entries: marker classes verified on the corpus, the
firebase-analytics connector exclude and measurement namespaces, the
facebook exclude of com.facebook.ads (clears the L11 warning), role-tagged
APIs, manifest names and endpoint domains. The neutralizer reads none of
it: the consumed snapshot of the 48 entries is unchanged and registry-scan
/ detect-adwrapper goldens stay byte-identical.
Facts that R5 cannot confirm are left out (FirebaseAnalytics.newInstance,
Adjust.create, MixpanelAPI.init), as is the appsflyer manifest name that
its identity root already matches by prefix; see DEVIATIONS 6a.
Add tests/corpus/marker-verify.sh (T-MARKER): every marker_classes item
must exist in the smali of at least one corpus app (pristine decode first)
where the ground truth has that SDK, or be in the reviewed
marker-allowlist.tsv (empty).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
_schema.json gains the optional `detection` object (audit-only facts:
extra/exclude/namespace/adapter package lists, marker_classes, apis,
domains, manifest_names, id_prefixes, note) and the definitions
package_list, top_level_class, host and api_fact (DESIGN 2.1/2.2).
No entry uses `detection` yet; registry-scan, neutralize.sh and
detect-adwrapper never read it, so the golden outputs and the consumed
snapshot are unchanged.
tests/lint/lint-registry.sh implements L5-L15 (DESIGN 4.6): allowed keys
and syntax read from _schema.json, case-colliding keys at any depth,
extra/exclude consistency, the awk-fallback text contract of packages /
sdk_id / in_house_wrapper, in-house disjointness, nested roots without
exclusion (WARN), shared namespaces, _audit-shared.json checks (L13 is
green: legacy_flags has no segment/flurry rows yet), an INFO report of
the FROZEN detect-adwrapper roots without an owning entry, and
library_roots vs identity roots with a commented allowlist. New INFO
level and --no-schema option. t_lint covers every rule with mutations
and checks that the DESIGN 3.3 segment/flurry entries lint clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
- install-dep.sh: new `jq` target. A jq >= 1.6 already on PATH (same feature
probe as registry-query) -> [OK]; Homebrew; apt/dnf/pacman only with working
sudo (then probe again: a distro jq 1.5 goes on); otherwise the pinned jq
1.8.2 static binary (linux/macos x amd64/arm64, SHA-256 verified) into
~/.local/bin/jq. MINGW/MSYS and download failures -> instructions, exit 2.
neutralize-all installs jq as an optional extra in a subshell: a failure
prints "[WARN] jq not installed (optional)" and never changes the exit code.
- check-deps.sh / check-neutralize-deps.sh: jq is optional (INSTALL_OPTIONAL:jq
when missing or older than 1.6), never required; exit codes unchanged.
- Windows: install-dep.ps1 jq, check-deps.ps1 and check-neutralize-deps.ps1 say
jq is not required (the .ps1 scripts use ConvertFrom-Json); no INSTALL_*:jq.
check-deps.ps1 no longer dies on `java -version` stderr under 'Stop' (PS 5.1).
- registry-query.sh prepends ~/.local/bin to PATH when it is missing, like the installer
and the checkers, so all of them resolve the same jq (no install-and-retry loop after a
static install). The jq-shim test runs use HOME=<missing dir> so a real ~/.local/bin/jq
cannot beat the shim.
- install-dep.sh jq checks the jq the caller's scripts will run (the caller's PATH, with
~/.local/bin prepended when missing), not its own PATH. If an older or broken jq comes
before ~/.local/bin, it does not report [OK] and does not download again on every run:
it prints the PATH fix and exits 2 (manual action). jq stays a soft-fail in
neutralize-all.
- tests/unit/t_deps.sh covers the install paths (no sudo, jq15, fake
sudo/apt/brew/uname/curl), neutralize-all vs the 4c166fe script, registry-query check-jq
after a static install with ~/.local/bin off PATH, an old jq before ~/.local/bin
(installer, reader, checkers, neutralize-all), the checkers'
exit codes and INSTALL_* names, and the ps1 checkers via interop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
detect-adwrapper.sh/.ps1 no longer grep every dotted string of each registry
file. They read only the "packages" arrays: the sh twin through
`registry-query.sh packages --lenient` (jq >= 1.6, or the POSIX awk fallback),
the ps1 twin through registry-lib.ps1 Get-RegistryPackages -Lenient
(dot-sourced; no param block, so it is safe in-process under decode-apk.ps1).
A malformed entry is skipped with a WARN. An unreadable registry prints
"[WARN] registry unreadable: registry exclusions skipped", keeps the static
tables and still exits 0 with ADWRAPPER_SUMMARY (fixes bug #7). The ps1
in-house map is now an ordered list (sdk_id ordinal, then file order),
matching sh. The new --dump-exclusions / -DumpExclusions prints EXCL and
INHOUSE for the I11 test. NETWORK_ROOTS/COMMON_ROOTS stay frozen and marked.
Reader fix (registry-query.sh / registry-lib.ps1, DEVIATIONS 3.4): an
unreadable entry file is now a per-file integrity problem ("unreadable file":
WARN + skip in --lenient, ERROR + exit 1 in strict) in the jq path, the awk
fallback (which used to abort and drop the whole registry) and the ps1 twin
(which used to throw), so jq, nojq and ps1 give the same exclusions. sh now
skips non-regular *.json files like ps1.
Corpus output is byte-identical (golden, sh with and without --registry, ps1).
In the fixture, tv.superawesome.lib, yads.core.impl and
backtraceio.library.sub disappear as planned.
Tests: t_adwrapper (fixture, jq/nojq/jq15, regD/regE, chmod-000 entry,
unreadable registry, dump, decode-apk.sh exit code), t_reader (unreadable
entry and shared file), t_network_roots (T-NETROOTS),
corpus/exclusions-superset.sh (I11), t_parity sections registry-query
(chmod-000 entry) and adwrapper (fixture incl. chmod-000 entry,
decode-apk.ps1 in-process, 8 apps). run.sh exports ARE_FULL=1 under `full`.
Harness (DEVIATIONS 3.5): `# requires:` and `# awk-matrix` are read from the
whole leading comment block of a test (header_block in tests/lib/common.sh),
not from a fixed 20-line window that silently dropped the directives of the
longer t_adwrapper/t_parity headers. t_runner checks deep directives and
fails on any directive outside a leading comment block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
registry-query becomes the single reader of the SDK registry (DESIGN 4):
- registry-query.sh + registry-query.jq: rows (EAV rows per entry, ordinal
sdk_id order), shared (_audit-shared.json as kind/key/value rows), packages
(only .packages[], what detect-adwrapper reads), check-jq, version. One jq
process on the fast path; per-file diagnosis names the culprit. Strict mode
fails on BOM, empty file, invalid JSON, several documents, non-object root,
case-colliding keys, missing/duplicate sdk_id, targets: null; --lenient
skips such files with a WARN. Without jq (or jq < 1.6) rows/shared/check-jq
exit 2 + INSTALL_REQUIRED:jq, packages falls back to a POSIX awk parser.
bash 3.2 compatible, no regex in the jq program.
- registry-lib.ps1 (function-only library, no param()/global state) and the
thin registry-query.ps1 CLI: same bytes, messages and exit codes as the sh
twin; output on the pipeline as one string (capturable in-process);
Get-LongPath for \\?\ and \\?\UNC\ roots.
- _audit-shared.json (+ schema): category/tool groups, legacy CLI flags
(segment/flurry rows arrive with their entries in 7a), role labels, ad
format tables + format_none allowlist, library_roots, hints. The leading
'_' keeps registry-scan.py, neutralize.sh and detect-adwrapper away from it.
Tests: t_reader (fixtures registry-mini/edge/bad + expected outputs),
t_pkg_parity (awk == jq under gawk --posix/mawk/busybox, nojq, jq15),
t_categories (T-CAT), t_libroots (T-LIBROOTS), t_format (T-FORMAT),
parity/t_parity.sh --only registry-query (PowerShell 5.1, -File and
in-process). Golden and snapshot unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
A registry entry without `targets` is now a detection-only entry (DESIGN 2.1, D2):
- _schema.json: `targets` dropped from `required` (still type object, so null is
invalid); a root if/then forbids manifest_components/protected_patterns on an
entry without targets (otherwise neutralize.sh would disable those components);
display_name gets the pattern ^[^:|\n\r\t]+$ (':' and '|' are separators);
descriptions of targets and sdk_id updated. The `detection` object comes in a
later commit.
- registry-scan.py: one-line guard in load_registry() right after json.load
(invariant I23), so detection-only entries never MATCH, never change the exit
code and never add manifest lines (I4). The 48 existing entries all have
targets: output is byte-identical (golden, corpus + synth48).
- tests: unit/t_scan_filter.sh (I4 on synthetic trees with the contract fixtures
tol/regA1 and tol/regA2 copied in tests/fixtures/scan-filter, plus a control on
the 4c166fe scanner), t_lint.sh schema cases for if/then, the display_name
pattern and targets null. lint-registry.sh now prints the failing schema path
in SCHEMA findings (`<loc> [<schema path>]: <message>`) and t_lint matches on it:
jsonschema 3.x and 4.x word the `not` message differently and in 4.x the long
instance repr pushes the distinguishing text past the 200-char cut, while the
path (`then/not`, `properties/display_name/pattern`, ...) is the same in both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
Dev-only harness for the registry-single-source work; no product change.
- run.sh: quick|full|<suite>|<test>, PASS/FAIL/SKIP/SKIP(requires) per test,
`# requires:` preconditions so later-commit suites stay green, and
--expect-pass (a SKIP of those names, or a suite with no test script,
counts as FAIL); corpus discovery follows the DESIGN script names
(groundtruth.sh, marker-verify.sh, ...); optional awk matrix; run.sh itself
is covered by unit/t_runner.
- lib/common.sh: paths, env (ARE_CORPUS, ARE_JADX, ARE_GOLD, ARE_PS, BASH32,
ARE_PRISTINE), assertions, normalize(), mktemp_dir (newline-separated
cleanup list, safe with spaces/globs in TMPDIR; self-test unit/t_common).
- lint/lint-registry.sh: rules L1-L4 (+ JSON-schema check when jsonschema exists).
- golden/: capture.sh + compare.sh (72 registry-scan runs, detect-adwrapper sh
with and without --registry, ps1 when PowerShell is available, 9 synth48 scans)
against the frozen baseline; G-synth captured with the 4c166fe product in
golden/synth48.before; snapshot.sh + snapshot.before.jsonl (T-SNAPSHOT).
- fixtures/: synth48 generator, adwrapper fixture + expected, pitfall fixtures
(t_pitfalls is SKIP(requires) until the sdk-audit engine exists).
- shims/: nojq, jq15, nosudo, awk-{gawk-posix,mawk,busybox}; parity/ps.sh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZEKWAFaRKUy4hJ4rjFCzc
Distinct new capability: instead of only replacing method bodies with a
return, the patcher can now INJECT working code. Used to defeat the IAB TCF
consent dialog at the root: in Subway Surfers 28 classes across 13 SDKs read
the IABTCF_* SharedPreferences to decide whether to track, so writing a
reject-all state there makes every TCF-honoring SDK self-limit and removes
the nag.
Mechanism (documented as its own feature in references/consent-neutralization.md):
- targets-file lines gain an optional 3rd field <class>:<method>:<inject-kind>
(back-compatible: 2-field lines and wildcards unchanged; verified no
regression on the other 47 entries). _schema.json adds category `consent`
and an `inject` field (stub optional). registry-scan.py emits the 3rd field.
neutralize.sh gains a guarded code-injection branch (SKIP_INJECT on
signature mismatch), bash 3.2/BSD-safe, no new GNU-isms.
- registry/consent-umptcf.json (google-ump-consent): writes reject-all
IABTCF_* (consent AND legitimate-interest all-zero, gdprApplies=1 always,
AddtlConsent="1~", plus a decode-verified reject-all TCString) to the
default SharedPreferences on BOTH requestConsentInfoUpdate (the near-
universal first UMP call) and loadAndShowConsentFormIfRequired, then fires
the success/dismiss callbacks; canRequestAds=true and getConsentStatus=
OBTAINED keep the app from re-showing the form. If neither hook is present
it is a safe no-op.
Complements, does not replace, SDK neutralization: non-TCF trackers (Firebase,
Singular) ignore IABTCF and still need direct stubbing. Reviewed twice
(build + adversarial), assembled and rebuilt on Subway; on-device behavior
still to be confirmed. Docs updated to 48 SDKs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
SYBO (Subway Surfers' publisher) ships com.sybo.analytics.AnalyticsBridge,
the game-facing layer that schedules WorkManager jobs to flush/upload event
data over HTTPS — including while the app is closed. Per-SDK neutralization
(Singular, Firebase) already drops the events, but the SYBO scheduler was
untouched. Stub SetupWorkerForAppClosed/SetupWorkerForAppRunning/
StopWorkerForAppRunning (all void) so no upload worker is ever scheduled.
Fire-and-forget: the game does not await the flush, so the void stubs can't
hang it. GetCountryISO3CodeUpper and the FlushWhileRunningCallback bridge
(GetData/HasData/OnDataSendComplete) are protected. com.sybo.consent
(age-gate) and com.sybo.native_sharing are intentionally left alone. Verified
by dry-run on subway-decoded (3 methods). Docs updated to 47 SDKs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
Rovio games (Bad Piggies, Angry Birds…) drive ads through their own
com.rovio.beacon layer, not the networks directly, so per-network
neutralization left an interstitial running. This entry stubs the game-facing
choke point AdsSdk.load/show/hide/poke (one lever kills every adapter and the
WebView house-ad path: WebViewAd/WebViewAdFactory/MRAIDController) plus the
fire-and-forget analytics/tracking beacons (RovioTrackingInterface,
WebViewWrapper.onRovioTrackEvent, VideoPlayer track events).
Callback-aware: the onAd* bridge back to the game and the attribution/ID
surface (Attribution.checkLicense/getInstallReferrer, DeviceInfo async IDs,
Globals.getActivity) are protected so a stub cannot hang or NPE the game. The
com.rovio.fusion.VideoPlayerActivity manifest component was intentionally NOT
disabled (it is engine cutscene glue, not an ad component).
Verified on device: Bad Piggies HD 2.4.3379 runs and the interstitial is
gone. Detected automatically by detect-adwrapper (in_house_wrapper:true).
Docs updated to 46 SDKs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
Both meta-mediators matched but stubbed almost nothing, so on mediated apps
(Subway Surfers, Bad Piggies, Alto, 1945…) the ad waterfall stayed live —
the app calls ONE mediator and its adapters drive every other network, so
this is the highest-impact gap.
- applovin: the MAX ad targets used the wrong package
(com.applovin.mediation.MaxInterstitialAd). Real classes are under
com.applovin.mediation.ads.* — added MaxInterstitialAd/MaxRewardedAd/
MaxAppOpenAd load/show/isReady/destroy (+ MaxAdView), kept AppLovinSdk.init
and the legacy .mediation.* targets as harmless fallbacks. Subway: 1→41
methods matched; Bad Piggies: 4→32.
- ironsource: the legacy com.ironsource.mediationsdk.IronSource methods are
gone in 9.x (0 patched). Extended with the modern init
com.unity3d.ironsourceads.IronSourceAds and com.unity3d.mediation.LevelPlay,
the LevelPlay ad loaders/ads, and the Unity bridge
com.ironsource.unity.androidbridge.LevelPlayBridge (what IL2CPP games call);
legacy + ISDemandOnly targets kept. Subway: 0→68 methods matched.
Extend-not-replace: legacy targets stay for the many APKs still shipping old
SDKs; absent classes/methods are skipped by the patcher. Callback bridges and
object getters protected so a stub can't strand the app on a missing callback.
Verified by neutralize.sh --dry-run on the real subway/bad-piggies decodes.
Docs updated to 362 entry points / 337 ad operations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
From research on the most-played children's Android apps of 2026: the ad
SDKs and (equally important) the trackers/telemetry that these apps ship and
the registry did not yet cover. Each entry was built from the official Maven
AARs, inspected across versions with javap/jadx, every target method's real
return type confirmed by a neutralize.sh dry-run on the real bytecode, and
validated against _schema.json.
Ads:
- superawesome (tv.superawesome) — the kids-focused ad SDK (Talking Tom/Angela, Subway Surfers)
- appodeal (com.appodeal.ads/com.explorestack, mediation; explorestack kept detection-only, not deep-stubbed, to avoid nulling its shaded-protobuf/IAB getters)
- mytarget (com.my.target), hyprmx (com.hyprmx.android), maio (jp.maio, v1+v2)
Trackers / analytics / attribution:
- singular (com.singular.sdk), amplitude (com.amplitude, v1+v2 — v2 is a fluent
builder so only flush() is stubbed, chained methods protected),
google-analytics (Universal/legacy, distinct from the Firebase Analytics/GA4 entry)
Crash / telemetry (Exodus classifies these as trackers):
- sentry (io.sentry), bugsnag (com.bugsnag), backtrace (backtraceio)
Init + capture/send stubbed and auto-init providers disabled, while
object/scope getters and singletons the app chains on are protected so a
stub cannot itself crash the app (e.g. Bugsnag's static facade throws before
start(), so its delegators are stubbed to no-op).
Verified on real decodes where present: Appodeal on 1945 Air Force, Singular
and Google Analytics on Block Craft; the rest are AAR-verified pending a
decoded app. Docs updated to 45 SDKs / 337 entry points / 281 ad operations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
Per-network neutralization misses a publisher's own mediation layer, which
calls the networks (or serves house/WebView ads) on the app's behalf — e.g.
Rovio Beacon in Bad Piggies kept serving an interstitial with every network
SDK stubbed; Guru Fusion is the same shape.
New detect-adwrapper.{sh,ps1} score a candidate namespace under the app's
own vendor root (excluding known-SDK and common-lib roots) by: references to
>=2 ad-network packages, its own WebView+MRAID ad path, and analytics/
attribution classes. Emit ADWRAPPER_DETECTED:<pkg>:<confidence>:<evidence>
and ADWRAPPER_SUMMARY, always exit 0; decode-apk runs it after decode and
points the user at the discovery workflow. Registry entries flagged
in_house_wrapper:true (new optional schema property; guru-fusion marked) are
NOT excluded — a matched candidate is annotated registry=<sdk_id>.
Detection and reporting only — nothing removes, disables or bypasses a
wrapper. True positive on Rovio (com.rovio.beacon) and The Fun Games
(com.tfg.libs, Block Craft); no false positives on Alto/HeartStar/1945/
Bulldozer. sh/ps1 output is byte-identical (verified via the PowerShell 5.1
interop).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
Real apps still ship the old package names of SDKs that later renamed, and
AdColony was not covered at all — so a full-registry pass left ads running
(seen in Bad Piggies HD 2.4.3379, which ships com.vungle.warren and
com.adcolony).
- adcolony (new entry): com.adcolony.sdk.AdColony configure/request*/show/
isExpired (all boolean -> return-false/0) + the two AdColony activities;
built from com.adcolony:sdk 4.8.2/4.6.0 and the version in Bad Piggies.
- vungle: add legacy com.vungle.warren (6.x Vungle facade: init/loadAd/
playAd/canPlayAd, Banners, NativeAd) and com.vungle.publisher (4.x/5.x
VunglePub), plus their manifest components incl. the warren auto-init
ContentProvider; new com.vungle.ads targets kept. From publisher-sdk-android
6.12.1/6.9.1/5.3.2/5.3.0 and the warren classes in Bad Piggies.
- ogury: add legacy io.presage (Presage/PresageSdk/Ads init, interstitial
and optin-video ops) and 14 io.presage manifest components; new com.ogury.*
targets kept. From co.ogury:ogury-sdk 4.9.1/4.3.13.
Every target and return type verified against real bytecode; entries
validate against _schema.json and match on Bad Piggies (registry-scan +
neutralize dry-run). Docs updated to 34 SDKs / 208 entry points /
241 ad operations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
A rebuilt and re-signed APK of a protected app will not run (signature
check, VM-encrypted code, or a license gate), so warn the user up front
instead of spending time on a rebuild that can't be installed.
New detect-protection.{sh,ps1} operate on a decoded dir and emit
PROTECTION_DETECTED:<id>:<category>:<confidence>:<evidence> plus a final
PROTECTION_SUMMARY (none|integrity|license|hardener|signature-vm), always
exiting 0 (informational; the caller decides). PairIP is split into the
hard blocker (signature-vm: SignatureCheck/VMRunner/VmDecryptor/
libpairipcore.so) and the softer license-only variant. Also covers Google
LVL, Play Integrity/SafetyNet (low/informational) and common commercial
hardeners (DexGuard, Promon, AppDome, Verimatrix, Arxan, SecNeo/Bangcle,
Jiagu/360, Tencent Legu, iJiami, Baidu, Alibaba, NetEase Yidun,
DexProtector, AppSealing, LIAPP). Non-PairIP signatures were verified
against APKiD's YARA rules; only file/class/manifest signals a decoded
tree can express are used, with confidence marked accordingly.
decode-apk.{sh,ps1} run the check after a successful decode and print a
prominent warning for a hard blocker; the decode exit code is unchanged.
This is detection and warning only — no rule, code path or doc removes,
disables or bypasses any protector.
Wired into SKILL.md (Phase 2b), commands/neutralize.md (Step 4b),
CLAUDE.md, README and the neutralization guide. Verified on real apps:
1945 Air Force 15.82 -> signature-vm, Bulldozer Master 11.1.0 -> license,
Block Craft / Alto's Adventure / HeartStar -> none; the Facebook
CrashShield false positive is ruled out. sh/ps1 output parity checked
through the Windows PowerShell 5.1 interop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
Decoding only the base APK of an XAPK made apktool write @null for every
reference to a resource living in a split (e.g. density drawables), and no
post-decode merge could repair that. Splits (XAPK/APKM/APKS or a directory)
are now merged with APKEditor (REAndroid, Apache-2.0, pinned 1.4.9 with
SHA-256 verification) before `apktool d`, so the decoded tree is complete.
- decode-apk: bundle input is merged first; old behaviour kept as the
deprecated --keep-splits / -KeepSplits mode
- rebuild-apk: always a single APK by default; stored native libs are
page-aligned (zipalign -P 16, -p fallback) and verified after signing
(ALIGN_OK / ALIGN_WARNING / ALIGN_FAIL); ABI_WARNING for 32-bit-only
output; stable user-level debug key shared by --auto-keystore and the
default mode; apksigner looked up in SDK build-tools, v1-only jarsigner
refused for targetSdk >= 30; --no-res removed (no-op on apktool 2.10)
- install-dep: new `apkeditor` and `build-tools` (pinned Build-Tools 36.0.0,
per-platform SHA-256, explicit Android SDK license acceptance required);
per-user tools are no longer installed under sudo
- check-neutralize-deps: reports APKEditor, zipalign page-alignment support,
unzip and build-tools
- new PowerShell 5.1 twins: decode-apk.ps1, rebuild-apk.ps1,
check-neutralize-deps.ps1 (non-ASCII and cmd-metacharacter paths staged,
UTF-8 output restored on exit, ZIP64-aware alignment check)
- merge-splits.sh removed: obsolete with the pre-decode merge
- docs: SKILL.md, neutralize command, CLAUDE.md, setup guide, README
(XAPK output deprecation note)
Tested end to end on Linux and Windows PowerShell 5.1 with Block Craft 3D,
Bulldozer Master, Water Sort Puzzle, HeartStar, Alto's Adventure and
1945 Air Force; reviewed twice independently.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
Four new registry entries built from the official SDK artifacts (Maven),
inspected across several versions each (javap/jadx) so that every target
method, return type and manifest component is backed by real bytecode:
- tapjoy: Tapjoy Offerwall 11.x-14.x (connect entry points, TJPlacement ops)
- yandex-ads: Yandex Mobile Ads 5.x-8.x (MobileAds/YandexAds init, loaders)
- google-tag-manager: GTM v3/v4/v5 (void-only targets; object-returning APIs protected)
- appmetrica: io.appmetrica.analytics and legacy com.yandex.metrica
Validated against _schema.json and exercised with registry-scan.py and
neutralize.sh on real APKs (Bulldozer Master 11.1.0, Block Craft 3D 4.5.1).
Docs updated to 33 SDKs / 201 entry points / 210 ad operations; examples
now use neutral placeholder packages instead of a specific vendor.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tae7iP5Zh8tax1Sn9p3A5
The hardening patch widened STRICT_URL to recover IPv4 literals, apex
2-label domains and internal hosts that the PR's strict-only regex
discarded as collateral while killing Kotlin-stdlib dictionary noise.
Widening alone reopened a narrow noise class: 'word.word' fragments such
as "www.this" / "this.introduction" pass as apex domains.
Keep extraction permissive and add a small awk pass that decides per host:
- IPv4 literal: always keep (dict fragments are words, never dotted-quads)
- >=3 labels: always keep (any TLD; same tolerance as the original regex)
- any host with a :port or /path: always keep (structured = high signal)
- bare 2-label apex: keep only when the TLD is a real one, matched as a
whole field (so "introduction" != "in" — the prefix-match bug a single
mega-regex would have)
Trade-off documented inline: a first-party host referenced bare with an
uncommon TLD (e.g. https://foo.store with no path) is dropped; a path or
port keeps it. awk is POSIX (sub/split/~/print) — more portable than the
bash>=4 'declare -A' already used in the summary header.
Verified: dictionary noise dropped; IPs, apex, internal and subdomain
hosts kept; --all on a zero-match tree still exits 0; host list and
full-URL list stay consistent (no orphan hosts).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PR #16 additions were wired into SKILL.md and references/ but the
human-facing README was never updated. Surface them, with prominent
emphasis on first-class Kotlin support:
- Top blurb: callout for R8 Kotlin name recovery + Ktor/Apollo/Koin
- "What it does" table: Phase 0 fingerprint, Kotlin name recovery,
modern Kotlin/KMP stacks (Ktor, Apollo, Koin, HMAC)
- Usage: fingerprint.sh example, --ktor/--apollo/--paths flags, and a
dedicated "Kotlin name recovery (R8 deobfuscation)" subsection
- Repository Structure: add the three new scripts + two new references
- Acknowledgments: credit @tajchert (#16)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- find-api-calls.sh: add missing '|| true' on the --paths inventory and
--urls extraction pipelines; with set -euo pipefail a zero-match grep
aborted the whole script (including the default --all run) with exit 1.
- find-api-calls.sh: widen STRICT_URL to also match IPv4 literals, apex
2-label domains and bare single-label hosts followed by :port or /path
(localhost, internal backends) while still rejecting dictionary-fragment
noise from the Kotlin stdlib.
- recover-kotlin-names.sh: sanitize the by_package/ filename with
os.path.basename; a crafted absolute path in untrusted @DebugMetadata
package names could otherwise escape the output directory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two small changes that together meaningfully reduce wasted effort:
1. Phase 3 now explicitly tells the agent to read every BuildConfig.java.
These files are almost never obfuscated and routinely contain the
single highest-signal constants in the APK — base URLs, flavor names,
build types, third-party API keys, feature flags. They were not
mentioned in the previous workflow despite being the cheapest possible
high-value target. One grep, finds them all.
2. The Phase 5 documentation template was a single per-endpoint block
asking for path params, query params, request body, response type,
and call chain. On apps with 100+ endpoints that easily becomes hours
of work for output the consumer will not read.
Replace it with two tiers:
* Tier 1 — flat table covering every endpoint (host, method, path,
auth required, source file). Always produced. Takes ~5 minutes
from the --paths output.
* Tier 2 — the existing detailed block, but explicitly reserved for
high-value endpoints: the entire auth flow, payment/checkout, and
anything the user specifically asked about. Default cap of ~10
Tier-2 entries unless asked for more.
This matches the natural shape of how analysts actually use this work
(one inventory table to know the surface area, plus a deep dive on
auth and a couple of flows) and prevents over-investment in detail
for endpoints nobody will read about.
Without an overview the script dumps thousands of file:line: matches
across many sections, leaving the reader to figure out which framework
even applies. A short summary at the top makes the rest of the output
actionable.
The summary counts hits per framework / DI / auth-signal category in a
single grep pass over the source tree (8 separate greps would have
roughly octupled the runtime on a large decompile). Output is a 3-line
table:
HTTP framework: Retrofit=N OkHttp=N Ktor=N Apollo=N Volley=N
DI framework: Hilt/Dagger=N Koin=N
Auth signals: Bearer=N HMAC/Sign=N
A reader can immediately see which framework the app actually uses,
whether auth is bearer-token or signed, and whether to spend time on a
section or skip it. The summary is suppressed when a single section flag
(--retrofit, --ktor, --paths, ...) is given, so the existing single-section
workflows are unchanged.
A reminder of the available section flags is printed below the counts
so the agent does not have to consult --help.
Two gaps in the previous coverage:
1. Koin was not mentioned anywhere — Hilt/Dagger got a full section in
call-flow-analysis.md but Koin (the dominant DI in KMP and a large
share of Kotlin-only Android apps) had zero patterns. Add a Koin
subsection with the runtime-DSL patterns (module {}, single<>,
factory<>, viewModel<>, by inject, by viewModel) plus the practical
trick for resolving an interface to its impl after R8 obfuscation:
intersect "files that import org.koin.core.module" with "files that
reference the interface name".
2. The --auth mode caught Bearer / API-key / OAuth header patterns but
missed HMAC and other request-signing schemes. A hardcoded HMAC
secret embedded in an APK is a security finding worth surfacing —
the same kind of authority the user gets is the same authority a
decompiler grants to anyone. Add patterns for:
* JCA primitives: HmacSHA{1,256,512}, Mac.getInstance(...),
SecretKeySpec(...), Signature.getInstance(...)
* Header conventions: X-Signature, X-Hmac, X-Amz-Signature,
X-Client-Authorization, AWS4-HMAC, signRequest(), signaturev2/3
* Likely secret-bearing identifiers: app_secret, client_secret,
signing_key, hmac_secret, consumer_secret, private_key
* Ktor BearerTokens / loadTokens / refreshTokens DSL
These survive R8 because the JCA and Ktor APIs are public and not
shrunk. On a real-world app with a homegrown HMAC scheme they pinpoint
the signing class and its hardcoded key directly.
The previous --urls mode was a plain grep for "https?://..." which on a
real APK produced thousands of lines, half of them junk strings extracted
from Kotlin stdlib's compression dictionary ("http://An Introduction to..."
fragments) and the other half SDK URLs (Google, Firebase, AppsFlyer,
Datadog, Sentry, ...) that the analyst is not looking for. The signal —
first-party backend hosts — was buried.
Two changes:
1. Strict URL regex: hostname must have at least one dot and end in a 2+
letter TLD, with no whitespace / angle brackets / non-printables in the
path. This eliminates the dictionary-fragment noise.
2. Bucket the surviving URLs into "likely first-party" vs "third-party"
using references/third_party_hosts.txt — a curated denylist of
~80 patterns covering Google/Firebase/Apple/Microsoft/Adobe, attribution
and observability vendors (AppsFlyer, Datadog, Sentry, Bugsnag, ...),
payments (Stripe, PayU, Adyen, ...), support/chat SDKs, CAs, and
standards namespaces (w3.org, etc.).
The new output starts with a frequency-sorted list of likely first-party
hosts — which is the artifact every reverse-engineer wants on the first
page — followed by the collapsed third-party list and the full URL set
for first-party hosts only.
The denylist is a sidecar text file (one regex per line) so users can
extend or override it without editing the script.
When R8 inlines call sites — client.get("/api/users") becomes
a.b(c, "/api/users") — the existing framework-specific patterns find
nothing, but the path string literal itself is never obfuscated. This
single observation is the most useful endpoint-extraction technique on
heavily shrunk apps; the existing --urls mode only catches full
"https://..." URLs, missing every relative path.
Add a --paths mode that greps for quoted strings matching either:
* an absolute path with at least two slash-separated segments, or
* a relative path beginning with a known API root keyword
(api, v1/v2/v3, graphql, users, auth, profile, cart, order, ...)
with a {0,8}-segment cap and a small denylist for MIME types and system
paths (image/png, /proc/, /sys/, /dev/, etc.) which would otherwise pollute
results.
The output is a deduplicated inventory followed by the full call-site
list. On a real-world Kotlin/Ktor app this produced ~240 distinct API
paths in one shot — paths that the Retrofit/OkHttp/Ktor patterns missed
entirely because every call was inlined. This is the recommended first
extraction step on any obfuscated app.
Document the regex and rationale in references/api-extraction-patterns.md.
The previous find-api-calls.sh covered only Retrofit, OkHttp, and Volley.
Modern Kotlin and KMP apps increasingly ship Ktor as their HTTP client
(used by ~25 % of new Kotlin apps as of 2025), and many product apps use
Apollo Kotlin for GraphQL. Both produced zero hits with the old patterns.
Add two new modes to find-api-calls.sh:
--ktor Ktor client calls (client.get/post/...), HttpRequestBuilder,
defaultRequest blocks, and the Auth bearer DSL
(BearerTokens / loadTokens / refreshTokens)
--apollo ApolloClient, .serverUrl(), HttpNetworkTransport, and
.query/.mutation/.subscription operation calls
Document both in references/api-extraction-patterns.md with example
post-decompile snippets and a note on R8 obfuscation: Ktor call sites
get inlined to obfuscated method calls, but the path string literals
and Ktor library symbols (BearerTokens, URLProtocol, etc.) survive,
so library-internal patterns still work as anchors.
R8 obfuscates JVM symbols but cannot strip the Kotlin metadata strings —
the Kotlin runtime needs them at runtime for reflection, coroutines, and
data-class features. The original FQNs leak through:
* @DebugMetadata(c = "<real.fqn>") emitted for every coroutine
SuspendLambda (~ every suspend function in modern apps)
* @Metadata(d2 = {"L<real/fqn>;"}) on every Kotlin class
Add scripts/recover-kotlin-names.sh that walks decompiled sources, mines
both annotations, and writes an obf -> real mapping (TSV + JSON + per-real-
package index). On a real-world Kotlin app this recovers ~100 % of
*Repository / *ViewModel / *UseCase / *Impl classes — exactly the classes
worth reading.
Add scripts/lookup-name.sh as a CLI over the mapping with four modes:
search by real-name substring, resolve obf -> real, list a real package,
and an annotated `--grep` that suffixes every hit with the owning real
class. This is a strict upgrade over plain grep against decompiled sources.
Replace the misleading 'use --deobf' tip in call-flow-analysis.md with a
pointer to this technique. --deobf only renames symbols with synthetic
placeholders; metadata recovery returns actual developer-written names.
Document the technique, expected recovery rates, and limitations in
references/kotlin-name-recovery.md, and reference it from SKILL.md as
optional Phase 3.5 (only when Phase 0 reports an obfuscated Kotlin app).
Decompiling Java is wasted effort for Flutter, React Native, Cordova/
Capacitor, and Xamarin apps — their code lives in libapp.so, the JS bundle,
assets/www/, or .NET DLLs respectively. The previous workflow jumped
straight to Phase 1 (install deps) and Phase 2 (decompile), so the agent
had no way to know which path to take until after a full jadx run.
The new fingerprint.sh inspects an APK/XAPK in seconds and reports:
* Detected mobile framework with the file marker that triggered it
* HTTP stack hints (Retrofit, OkHttp, Ktor, Apollo, Volley) via DEX
string scanning — survives R8 obfuscation
* DI and serialization libraries
* Obfuscation level estimate
* Notable third-party SDKs found in assets/ and DEX
* Consolidated native libraries across base + split APKs (split bundles
often place .so files only in config.<abi>.apk)
* A framework-specific recommendation for the next step
SKILL.md documents this as Phase 0 and explicitly tells the agent to
stop and switch tooling if the app is non-native.
PowerShell port (fingerprint.ps1) intentionally not included — happy to
add if needed; behavior is straightforward to mirror.
Mirrors the bash counterpart updated in #12. Switches the GitHub repo,
the fallback tag (v2.4 -> 2.4.35), and the URL pattern order so that the
canonical ThexXTURBOXx naming (dex-tools-2.4.35.zip, no leading 'v') is
tried first, with the pre-2.4.30 naming as fallback.
Closes drift items 9-11 from post-merge-followup-2026-04. Functional bugs
in decompile.ps1 and PR #10 drift items remain pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a top-level note flagging PS1 scripts as experimental and pointing
issues to this repo. Add an Acknowledgments section crediting the four
external contributors of the recent PR wave (#4, #8, #10, #12).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add Windows/PowerShell support
Add PowerShell equivalents of all bash scripts for Windows users:
- check-deps.ps1: Dependency verification with PATH refresh
- install-dep.ps1: Install via winget/scoop/choco or direct download
- decompile.ps1: APK/XAPK/JAR/AAR decompilation with split APK detection
- find-api-calls.ps1: API endpoint extraction (Retrofit, URLs, auth)
Update SKILL.md with Windows-specific instructions and notes for
each workflow phase.
PowerShell scripts support the same options as their bash counterparts
and automatically refresh PATH after installations.
* fix: check-deps.ps1 jadx fallback path version check, decompile.md lint fixes
Replaces hardcoded smali patterns with a data-driven SDK registry: 29 JSON
definitions covering ad networks, mediation, attribution, and analytics SDKs
(123 entry points, 156 ad operations, 30 deep patterns, 64 manifest components).
- Add plugins/.../sdk-neutralizer/registry/ with _schema.json + 29 SDK files
(Adjust, Braze, CleverTap, Guru Fusion, Mintegral, Mixpanel, MobileFuse,
Moloco, PubMatic, TradPlus, plus the prior 19)
- Add registry-scan.py to consume the registry and emit targets-file +
manifest-components-file consumed by neutralize.sh
- Extend neutralize.sh with --no-builtin-targets, --targets-file,
--manifest-components-file, --package, --cleanup-backups
- Extend find-ads.sh and find-trackers.sh with --summary and --json output
- Mark python3 as INSTALL_OPTIONAL in check-neutralize-deps.sh (fallback
to builtin hardcoded targets when unavailable)
- Bump plugin and marketplace to 1.3.1
- Add .gitignore for __pycache__/, *.pyc, *.pyo
- Update CLAUDE.md, SKILL.md, neutralize.md to reflect registry-driven flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New merge-splits.sh script merges XAPK split APK contents (native libs,
resources, manifest cleanup) into the decoded base directory, enabling
single APK output installable via standard adb install. rebuild-apk.sh
gains --single-apk flag with auto-detection via .merged marker. SKILL.md
updated with Phase 5a/5b/5c workflow for XAPK rebuild choice.
Fixed plugin.json version from 1.0.0 to 1.2.0 to match marketplace.json.
Added versioning guidance to CLAUDE.md to prevent future mismatches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- decode-apk.sh preserves XAPK structure (.xapk-origin/) with splits, manifest, and metadata for automatic reassembly
- rebuild-apk.sh detects .xapk-origin/, re-signs all split APKs, and assembles final .xapk output
- Add --auto-keystore flag: prioritizes ~/.android/debug.keystore → previous debug key → generated key
- Add neutralize-all compound target to install-dep.sh (java + apktool + apksigner + zip in one command)
- Add apktool version check (>= 2.9.0) with automatic upgrade from GitHub releases
- Add install_zip() and zip dependency check for XAPK rebuild
- All scripts now prepend ~/.local/bin to PATH for user-local tool installs
- Update SKILL.md Phase 3 with built-in catalog detection workflow and custom SDK discovery using Claude Code tools
- Update neutralize.md, CLAUDE.md, and README.md with XAPK and auto-keystore documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bump marketplace/plugin version to 1.2.0 with sdk-neutralizer keywords
- Add smali/baksmali and apksigner installers to install-dep.sh
- Add --entrypoints flag to find-trackers.sh for app-code-only SDK call detection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>