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>
- 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>
* 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>
Six improvements from real-world testing on Water Sort Puzzle XAPK:
- Fix manifest patching creating duplicate android:enabled attributes when
the component already has android:enabled="true" (3-way check logic)
- Add decode-apk.sh with XAPK support (auto-extracts base APK, skips splits)
- Add apktool >= 2.9.0 minimum version check in check-neutralize-deps.sh
- Add --no-res fallback in rebuild-apk.sh when apktool build fails on resources
- Add 13 missing manifest components (Vungle new SDK, Meta AN provider,
AppLovin/BidMachine/IronSource init providers, Amazon APS, Pangle/Mintegral
activities, Smaato, AppsFlyer internal receiver)
- Add patch persistence via neutralize-manifest.json and --replay flag
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces two new skills for privacy auditing of decompiled Android apps:
- tracker-analysis: 4-phase workflow detecting Firebase, Adjust, AppsFlyer,
Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry with init/events/
consent/data exfiltration analysis
- ad-analysis: 3-phase workflow detecting AdMob, Unity, IronSource, AppLovin,
Meta AN, Vungle, InMobi, Chartboost, Pangle, Mintegral with ad format
mapping, mediation analysis, and consent framework detection
Key addition: --entrypoints flag in find-ads.sh distinguishes SDKs actively
called by app code from passive mediation adapter dependencies, enabling
accurate "Ad Architecture" classification (single mediator / multiple direct /
hybrid).
Also improves install-dep.sh sudo detection for non-interactive environments
(Claude Code, CI, pipes) and adds CLAUDE.md project instructions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add keywords, skills and commands paths to plugin.json
- Add argument-hint to decompile command for better UX
- Add description to SKILL.md frontmatter for skill auto-matching
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>