APK Reverse Engineering & Patching
Goal: reach a verified, installable, still-working artifact fast — and avoid the whole class of
mistakes that destroy an APK while looking completely healthy.
How to use this file
This file is a procedure with gates, not background reading. Three things are mandatory:
- Before you patch anything, clear the four gates in §Gates. They are actions with pass criteria,
not attitudes.
- When anything fails in a way your current plan does not explain, stop and check the symptom
index. If a row matches, load that file before running another command.
- When this file and your own reasoning disagree, this file wins until you have evidence that
overrides it. Every rule here is the residue of a failure that cost hours; your current intuition is
the intuition of someone who has not hit it yet.
Four rules that override everything else
R1 — Write the deliverable as a testable sentence before you touch the target.
"It works" is not the goal; "it works under the stated constraint" is. Root-assisted, live-
instrumentation, host-proxy and patched-device results frequently do not satisfy a request for an
installable artifact that works on a normal phone — and it is easy to present such a result as
finished. Write the sentence, re-read it at every checkpoint, and if you cannot meet it, say so
plainly and label the privileged workaround a fallback, never the deliverable.
→ references/long-task-discipline.md §the most expensive drift
R2 — Change one variable at a time, and keep a control build.
An experiment that flips two things teaches nothing when it fails, and a failure you cannot attribute
will be attributed to the wrong cause. Every "the app rejects X" claim needs its own run, and every
patch needs a same-pipeline control that still fails the old way.
→ references/long-task-discipline.md §single-variable discipline
R3 — Never ship or claim an unverified artifact.
"It assembles" is not "it works"; "the process started" is not "the feature works"; "no error in the
log" is not "the check is gone". Install it, launch it, exercise the exact feature you changed, and
look at the screen. Prove the device is running the build you made — hash it, do not trust the
filename.
→ references/verification.md
R4 — Identify the owning layer before patching, and re-classify when reality disagrees.
Ads, paywalls, feature gates, integrity checks and update gates live in different layers (Java, dex,
native, Dart/Unity, server). Patching the wrong layer either does nothing or breaks the app. If a
patch "had no effect", the diagnosis was wrong — go back to classification instead of patching harder.
→ references/recon.md, then the layer-specific file the symptom index points at
Tooling — what to reach for, in order, and how to notice what you are missing
Most wasted rounds in this domain are not bad reasoning about the target. They are the right question
asked of a tool too weak to answer it: an hour of grep over a hand-exported smali tree where one
indexed query would do, or a full manual ELF walk where a decompiler was one pip install away. The
failure is invisible from the inside, because the weak route still produces output.
Four obligations. These are instructions, not preferences:
- Orient with an indexer, not with an export. Before reading code, build the ability to ask the
artifact questions —
droidasc findrefs (string/type/method → every reference site, sub-second) or
ddc findrefs. A full decompile is for reading a class you have already located; it is not the
way you locate it. Treat jadx as a readable viewer of last resort, never as the source of truth, and
never as the entry point of a recon. → references/toolchain.md §Tier 1 — dex and Java
- When the hot layer has no working tool here, installing one is part of the task. A missing arm64
decompiler is not a constraint to route around; it is the next step. Route around it and you pay in
hours for a result a decompiler gives in minutes. Ask a human only when installation is genuinely
impossible. →
references/toolchain.md §Closing a capability gap
- Name the gap before you spend against it. State the capability the current blocker requires, and
whether this machine has it. This is a G2 item, not a note to yourself.
- Reach for a script here before writing a new one. The kit exists precisely so that parsing,
hashing, alignment and hot-plug probes are not re-derived per task; a bespoke script written in place
of
scripts/dex_find_insn.py is how offsets get guessed instead of computed.
→ references/toolchain.md §Using the kit's scripts instead of writing your own
A capability you have not checked for is not a capability you lack. Run
python skills/apk-reverse/scripts/doctor.py and read what it finds off-PATH before concluding
that anything is unavailable.
Coverage — what this skill claims, and what it does not
The failure this section prevents is not ignorance. It is a confident wrong answer produced by
applying the nearest available procedure to a target it was never written for. A documented method
that almost fits is more dangerous than no method at all, because it arrives with a plan, a
vocabulary and a set of reassuring numbers.
Covered, by verified mechanisms:
- Client-side ads, promos and splash/popup/tab configuration — including the server-issued UI config
that has no SDK to find (
ad-removal.md, server-config-and-updates.md).
- Deciding whether a membership, paywall or feature gate is client-enforceable at all, and saying
so plainly when it is not (
membership-and-limits.md, account-gates.md).
- dex-level surgical patching: equal-length byte edits and dexlib2 method rewrites, plus the header,
verifier and alignment rules that decide whether the build loads at all (
dex-patching.md,
byte-level-patching.md, patch-audit.md).
- Repacking, signing, installing, and the install refusals that look like a broken build
(
repack-and-sign.md).
- Packers, custom loaders and code virtualization: identifying them, measuring the validation
boundary, and the routes that survive it (
packers.md,
code-virtualization-and-custom-linkers.md).
- The native layer:
.so hosts, tamper-triggered self-termination, forged ELF structure, and
neutralising a terminate path without freezing the process (native-and-so.md,
native-tamper-and-suicide.md).
- Flutter / Dart AOT: analysing and patching
libapp.so given a snapshot dump — pool-reference
counting, disassembly windows, caller indexing, patch-site choice (dart-aot.md). Measured on a
real Dart 3.6.0 build: dart_disasm.py decoded identically to capstone (32/32 and 96/96), the
caller index re-derived independently with a symmetric difference of 0, and a specific business
logic site was located end to end. The snapshot dump is a dependency, not a detail — see the
next section.
- Runtime analysis with Frida, server-side API probing, feature-scoped TLS failures, update and
forced-upgrade neutralisation, and the verification discipline everything above rests on.
Dependencies this skill does not ship — name them before the workflow starts:
- Dart AOT analysis needs a snapshot dump.
dart-aot.md's workflow begins at pp.txt; producing
it requires a snapshot container resolver that this skill does not contain and cannot synthesize.
dart_pool_strings.py reports file offsets while pp.txt and dart_pprefs.py speak in pool
offsets, and the mapping between those two spaces is not a constant: over the 4,241 strings present
in both, a measured run found 4,237 distinct deltas. Ship a pinned front end (aotopsy — pure Go, no
toolchain) or build blutter (~80 s, needs a C++ toolchain). Say which one you are using and why,
because the two report different Dart version labels for the same binary. Do not describe the
object pool as something this skill decodes on its own.
How strong these claims are: the Measured mechanisms below were established by running the
scripts against a real target during the verification pass recorded in
docs/tool-verification/. The rest are inferred — documented from experience, but the
repository carries no fixture, log or sample that reproduces them (its own long-task-discipline.md
reserves observed for a claim with an exact command and output behind it). Treat the distinction as
load-bearing rather than cosmetic, and label your own results the same way.
Not covered — say so rather than improvise:
- Unity / IL2CPP logic recovery.
framework-runtimes.md identifies the runtime and establishes
that the dex is not the battlefield; it does not carry the IL2CPP equivalent of dart-aot.md.
There is no verified recipe here for locating a method inside libil2cpp.so plus
global-metadata.dat.
- React Native / Hermes bytecode and Cordova/hybrid internals, beyond runtime identification and
the generic "find the string, then find what references it" approach.
- iOS /
.ipa of any kind. Every device, signing and packaging instruction here is Android.
- Defeating a server-side authority.
server-api.md exists to determine who owns a gate, not
to break an authorization the server performs.
- A general unpacker, or an anti-detection arms race.
detection-and-anti-analysis.md decides by
cost and often concludes "switch to static"; it is not a catalogue of evasion for every detector
you might meet.
Not exercised by the verification pass — do not read silence as support: the packer, code
virtualization, custom-linker, integrity-check-redirection and tamper-triggered-suicide scenarios
were not run against the verification target, because that target has none of those features (no
packer, no integrity checker, ordinary application class) and its unmodified build already fails to
start, which removes the repack-and-regress loop those scenarios need. Nothing in
docs/tool-verification/ is evidence either way about them. If you use those documents, the
claims are still on the inferred footing described above.
Scripts this pass did not run — so they carry no measurement at all, and any conclusion drawn
from them should be labelled accordingly: native_crash.py, apk_diff.py, snap.py,
grab_crash.py, install_test.py, repack.py, dex_patch_bytes.py, dex_find_insn.py,
dex_check_verifier.py, dex_classdiff.py, dex_strpatch.py, patch_smali.py, smtool.py,
datastore_inject.py, probe_api.py, run_probe.py, tls_check.py, usb_net_proxy.py,
devsh.py, and the dexpatch/ java rewriter. Their absence from the record is not a verdict on
them. Note in particular that grab_crash.py claims to recover stacks hidden by a crash-reporter
SDK — the exact situation the verification target presented — and was not tried, so that claim
remains unverified and the pass used a purpose-written Frida probe instead.
The fallback, as an instruction: if the target does not match that list, or no symptom-index row
matches, stop and classify before choosing a branch. Answer the thirteen questions first. If the
shape still does not fit — an unknown runtime, a mechanism you cannot name — say exactly that, and
propose the cheapest experiment that would identify it, rather than taking the closest documented
route and applying it anyway. A wrong branch here does not fail loudly: it produces an artifact that
builds, runs, and does the wrong thing.
Hand-off points — where this skill ends and another view begins
Three boundaries that are easy to walk into without noticing. Each names what the other side owns,
rather than restating it, because two copies of the same advice drift apart.
1. JNI — a Java native declaration and its implementation are two different views of one function.
This skill reads the Java side (dex) and the native side (.so) with different tools, so the join is
where analyses go wrong.
| Form |
What you see |
How to find it |
| Static linkage |
symbol Java_<pkg>_<Class>_<method> in .dynsym |
search the dynamic symbol table. Under R8 the class name is a short name, so the symbol deforms with it and a search for the readable original finds nothing |
| Dynamic registration |
nothing in the symbol table — binding happens at runtime |
find RegisterNatives call sites, or hook it to read the binding table. Obfuscated targets prefer this, and a symbol search fails silently on it |
| Native → Java callbacks |
native code pulling data back through Java |
follow FindClass / GetMethodID / CallObjectMethod |
FindClass/RegisterNatives in a .so tell you a JNI boundary exists even when no
Java_* symbol does. Strength note: the three rows above are documented behaviour, not results
from the verification pass, which did not trace a JNI boundary end to end; FlutterJNI.loadLibrary
appearing in a dex is the closest it came. Treat them as a map, not as a measurement.
2. Hardening — a dex-side packer observation is a native-side implementation question. If the dex
turns out to be a shell, the logic is behind a loader, and the analysis moves to the .so that
performs the unpacking. packers.md owns the dex-side identification; the native deep dive belongs on
the other side of this boundary. Not exercised by the verification pass — that target had no
packer, so this pointer carries no measurement.
3. The existing native boundaries — read native anomalies from the APK side, not from inside.
native-and-so.md and native-tamper-and-suicide.md are deliberately scoped to what you can conclude
from the APK side: a repacked build that dies instantly with a null-looking fault, a Java-layer
check that reports success while the process dies, a terminate path you made not-return. That
judgement belongs here, because it is about deciding whether your patch caused the death. Deep
native work — restoring a symbol, rebuilding a call graph, reversing an OLLVM function — is a
different activity with a different toolchain. Point across rather than duplicating: if you need the
latter, say so instead of extending these two files into it.
Symptom index — a matching row is a stop signal
You arrive at a symptom, not at a file name. Each row below is a failure that has already been paid
for. If any row matches what you are observing, load the file before your next command — not after
your next three attempts. Reasoning from first principles at this point is how the same hours get
spent twice; more than one entry here is a lesson that was re-derived by hand while the answer sat
unread in this repository.
| What you observe |
Load first |
A repackaged/re-signed build dies before your code runs; SIGSEGV, all registers zero, pc=0, fault addr near 0x0 |
native-tamper-and-suicide.md (deliberate crash), then code-virtualization-and-custom-linkers.md |
| No packer (Application is the app's own, dex readable) and it still dies |
code-virtualization-and-custom-linkers.md §a loader is still a possibility; but if the same build also dies on a second, unrelated device you are looking at an ordinary startup fault, not a hardened one |
The app dies at startup on every device, packed or not, and there is no tombstone while crash_dump reports already traced and logcat says exited cleanly (0) |
the target's launch section in the verification record under docs/tool-verification/ — a bundled crash reporter (Sentry NDK) has taken the signal handlers, so the platform's own evidence trail is gone. Frida spawn-gating is the recovery route; it needs a working frida-server, which a disguised one may not be |
A FORTIFY: pthread_mutex_lock called on a destroyed mutex abort in a Flutter app, on the main thread, before the first frame completes |
dart-aot.md — check libapp.so is actually being loaded; Flutter's engine bootstrap is the usual place a native lifecycle fault surfaces |
| Log says a Java-layer signature/integrity check passed, yet the process dies |
code-virtualization-and-custom-linkers.md §a Java-layer "signature killer" is a decoy |
Deleting a library fixes validation but yields UnsatisfiedLinkError: dlopen failed: library "X" not found |
code-virtualization-and-custom-linkers.md §the deadlock that eats hours |
Whole classes appear as bare native declarations with no body |
code-virtualization-and-custom-linkers.md |
| A library's SONAME does not match its filename |
code-virtualization-and-custom-linkers.md, native-and-so.md |
| Your edit had no effect at all, with no error |
server-config-and-updates.md §3 (the value may be server-sent), then packers.md §map the validation boundary |
Process hangs with no crash record, or dies to a uid 0 killer |
native-tamper-and-suicide.md §the rule (you probably made a terminate path not return) |
| Death looks like an ordinary null dereference in a hardened library |
native-tamper-and-suicide.md §deliberate-crash stubs |
| The app dies only while you are attached/rooted |
detection-and-anti-analysis.md; run the unmodified original under identical conditions first |
Install fails with [-124] and mentions resources.arsc / alignment |
repack-and-sign.md §2a — STORED and 4-byte aligned, both required |
Install fails with a bare numeric code (e.g. [-99]) and no INSTALL_FAILED_* |
repack-and-sign.md §vendor install interception — a device-side interceptor, not your build. Use the root pm install path |
After an install, am start does nothing / screenshots show another app / am start -W hangs |
repack-and-sign.md §the installer may still own the screen |
Log shows Failure to verify dex file ...: Bad checksum and a startup ClassNotFoundException for an ordinary class |
byte-level-patching.md §the dex header has two integrity fields — order matters |
| An install "succeeded" but nothing changed, or the version did not move |
long-task-discipline.md §keep the observation window clean |
| Evidence contradicts itself, or a capture looks like two states mixed |
long-task-discipline.md §keep the observation window clean |
| You took screenshots but drew the conclusion from logs or from the patch itself |
long-task-discipline.md §captures you never looked at are not evidence |
| You are about to re-run an experiment whose result you already recorded |
long-task-discipline.md §long-context decay |
| A script will not start, or a tool "is missing" |
scripts/doctor.py, then toolchain.md §"not on PATH" is not "not installed" |
| Feature-scoped network failure (login/register/pay) while the rest works |
tls-and-cert.md — do not assume your patch caused it |
| Everything works but every signed request fails after repack |
signature-derived-keys.md |
A re-signed build runs fine, renders its whole UI and logs no error — but one feature silently never loads, and dumpsys/DNS/logcat show no request for it at all (not a rejected request: no request) |
code-virtualization-and-custom-linkers.md §what the native check actually reads — a client-side integrity gate is refusing before the request is built. This is not the row above: "sent and rejected" and "never sent" have different owners and different fixes |
| You cannot tell whether a missing feature is your patch's fault or the target's own behaviour |
long-task-discipline.md §single-variable discipline. Run the zero-change control through the same pipeline, and the decisive variant: the unmodified original with the patch applied in memory only, same device, same network |
Under Frida spawn, the UI never appears — mCurrentFocus stays null, screenshots come back blank, the Activity stack never builds |
dynamic-frida.md §spawn keeps the Activity stack down: write the patch into memory, detach, then start the Activity normally |
frida-server keeps disappearing mid-experiment, or the device reboots itself while you are working |
dynamic-frida.md §when the ROM hunts your instrumentation |
| Ads still appear after a patch that should have killed them |
server-config-and-updates.md §6 (cached config / remote re-enable), then ad-removal.md §step 4 (count the SDK's own log lines; n -> 0, not "I did not see it") |
| A forced-update or "must update" gate blocks the build |
updates-and-forced-upgrade.md §step 6 |
| The dialog is gone but the feature is still locked |
membership-and-limits.md / account-gates.md — decide server vs client authority before patching again |
| You are about to discard a route as "blocked" |
packers.md — re-read it before writing any route off; mis-attributed failures have removed viable routes for hours |
| The task has run long and you are unsure what is already proven |
long-task-discipline.md §keep a live record |
Gates — clear these before you patch, in order
Each gate is an action with a pass criterion. Do not proceed past a gate you have not cleared, and
do not treat "I understand the idea" as clearing it. Skipping a gate is not a shortcut; it is how the
work gets redone.
G1 · Deliverable form. State, in one sentence you could hand to someone else, what artifact must
exist at the end and under what constraints (rooted or not, installable on a stock device or not, must
survive updates or not, online or offline). Pass: the sentence names a testable constraint, not an
activity. Fail: you are solving a problem in an environment the deliverable will never see.
G2 · Environment truth and capability inventory. Run scripts/doctor.py (and scripts/preflight.py
if a device is in play). Pass: you know which toolchains and scripts can actually run here, you have
seen the environment warnings — clock skew, leftover adb forward/proxy, a device-side frida process
already running, a tool installed off-PATH — and you have written down the capability this target will
demand against the capability this machine has. Name the two or three layers the task will almost
certainly reach (for example "arm64 native decompilation", "Dart AOT snapshot dumping", "device-side
TLS inspection", "dex-wide cross-referencing") and mark each available / missing-but-installable /
genuinely out of reach. Fail: you are about to attribute to the target a failure caused by your own
setup — or to spend a day routing around a tool that installs in ten minutes. A layer whose tool is
missing is a task item, not a constraint to design around. → references/toolchain.md §Closing a
capability gap
G3 · Code location. From the manifest and dex, answer: is there a packer, where does the app's own
code live (dex / native / Dart / Unity / server), and is any of it virtualized to native. Pass: you
can name the class that owns the behaviour you intend to change, or you have an explicit plan to find
it. Fail: you are about to patch a layer you have not located. If recon says "no packer", still
check the virtualization shape — see the index rows above.
G4 · Baseline and control. Pass: you have a control run — the unmodified original, or a
zero-change repack through the same pipeline — and you have recorded the observed failure (including
time-to-death, if it dies). Fail: when the patched build misbehaves you will have nothing to
compare against, and every later measurement is unfalsifiable.
Start here: classify the target in thirteen questions
Answer these before touching a tool. Every one of them changes the whole plan.
Is the app packed/hardened? → references/recon.md
Read the manifest's application android:name. If it is a third-party shell class rather than the app's own Application, you have a packer and must handle it first.
Where does the behavior you want to change actually live?
- Ad SDK (Pangle/GDT/AnyThink/Kuaishou/Baidu/Sigmob…) → usually client-side and removable →
references/ad-removal.md
- Server-issued config for UI the client renders (launch screen, popup, announcement, tab set, sponsored card on a home feed) → the client decides, the server supplies the data →
references/server-config-and-updates.md (this is the most common shape of "ad" in a modern app, and there is no SDK to find — decide this question early, because hunting an SDK that does not exist costs hours)
- Membership / VIP / paid content → usually server-authorized, client patch is cosmetic →
references/membership-and-limits.md (read this before spending hours)
- Feature flag, UI gate, debug switch → usually client-side
- Anything decided by an API response → server-side →
references/server-api.md
Is the app's own code in plain dex, or moved to native/Flutter/Unity?
Plain dex → you can patch. Flutter (libflutter.so + libapp.so) / Unity (libil2cpp.so) / pure native → different toolchain entirely. See references/recon.md §Where does the app's own code live and references/framework-runtimes.md.
Runtime check (cheap -- do it before committing to a layer): hook the obvious Java classes for the UI you care about, then reproduce that UI. If those hooks fire, the behavior is Java-owned. If they fire zero times while the UI is plainly on screen, the behavior is drawn by the runtime or by native code, and a dex-only plan will stall. Do not keep hunting in dex after a zero-hit probe -- that is the most expensive wrong turn in this skill's history.
What must the deliverable be able to do? Write the answer as a testable sentence before
planning anything, then re-read it at every checkpoint. This is the drift guard, and the drift it
guards against is the most expensive one in this skill: a runtime-only result (a data edit, a live
hook, a blocked hostname, a host proxy) can look like success while failing the actual requirement.
The axes that decide it: privilege (unrooted?), modification form (a rebuilt, installable
artifact, or is live instrumentation acceptable?), ABI/device class, network (must it work
online?), persistence (survives restart / upgrade / fresh install?), distribution (must the
shipped file be self-contained?). → references/long-task-discipline.md §the most expensive drift.
Does the app verify its own signature, or does the server?
App-side → you must bypass it. Server-side → re-signing silently breaks the app later. See references/repack-and-sign.md and references/server-api.md.
What is your device situation? → references/environment.md
Rooted real device (best), emulator with root, or no device (static only). Also: this determines whether Frida is usable. Run scripts/preflight.py before your first experiment, and again whenever a failure surprises you — device state, a dead device server, a leftover proxy, and clock drift all masquerade as a broken patch (pitfalls.md P9).
Which architecture is actually executing? → references/native-and-so.md §Cross-architecture
getprop reports what the device claims and primaryCpuAbi reports what the package manager chose — neither is what is running. Only the live mapping is ground truth (scripts/lib_map.py). If the library you meant to patch is not mapped, a translator is in play, or the ABI differs from your assumption, that changes the plan more than any patch will.
Is one specific feature failing at runtime — login, registration, payment, an API-backed screen — while the rest of the app works?
→ references/tls-and-cert.md. A feature-scoped network failure is very often a TLS/certificate problem on one code path, not a consequence of your patch. The app can even carry two independent trust chains, so "other requests work" proves nothing. Rule this out in minutes before hunting for a signature check.
Was the input a build you did not produce (a "cracked"/"modded" APK circulating online)?
→ references/third-party-builds.md. Audit it before adopting it: such builds are frequently re-protected (sometimes with more layers than the original) and may carry injected components or endpoints. Never use one as a patching workbench.
Long-task rule: if this is likely to run long, open references/long-task-discipline.md now
and keep its record updated as you go. Re-read the refuted-conclusions and dead-routes sections
before starting any new experiment. Losing earlier findings is the most expensive failure in this
skill, and it is entirely preventable.
Does the client sign its requests with its own signing certificate?
→ references/signature-derived-keys.md. Grep for toCharsString() / signatures[0] /
getPackageInfo(..., 64) before the first repack. If that value feeds a native HMAC/DES
routine, the rebuilt APK must hardcode the original certificate value at every read site, or
every signed request fails while the app still launches and looks healthy. This is the single
most expensive silent failure in a repack, and 15 minutes of grep prevents it.
Does the app die on its own after a while — with no Java stack trace, or with a native
crash that looks like a bug?
→ references/native-tamper-and-suicide.md. A hardened library that decides the build is
tampered rarely calls kill. It more often arranges a fault (load a small constant, use it
as a pointer) so the death looks like an ordinary defect, and the system then reports it as an
app "crash" or "abnormal" dialog. Two rules before you touch anything: enumerate which
mechanism actually fires (the signal and the tombstone split them apart), and neutralise by
returning, never by making it not return — a spinning stub freezes the process and produces a
symptom that looks nothing like the cause.
Will this build still be usable in a week? → references/updates-and-forced-upgrade.md
If the app has any version check, upgrade prompt, or self-update path, an unpatched build can be
turned off remotely or replaced by the official package. This is one or two edits and it decides
whether the work is durable — do it as part of the build, not as a follow-up. Also check for a
hot-update / remote-config channel, which can restore behaviour you removed without any version
change at all.
Does the request touch sign-in or phone binding — "no login required", "skip binding", "guest ok"?
→ references/account-gates.md. The whole difficulty here is separating a client-side gate
(patchable) from an account-scoped resource (the screen is empty because the server has no
account to answer for — not patchable). Classify first; and never fabricate a session to satisfy a
gate, which produces a state worse than being signed out.
The workflow, end to end
Steps are ordered. Skip a step only when its stated skip condition is met — "it seems
unnecessary" is not a condition, and it is the reason most of the failures in pitfalls.md happened.
Two-strike rule. If the same kind of attempt fails twice, stop and go back to classification.
Do not run a third variation of a hypothesis that has already failed twice. Two failures of one shape
means the model is wrong, not that the parameters need tuning — and the third attempt is where an
entire round gets spent confirming what the first two already said. Re-read the symptom index at that
point; it exists for exactly this moment.
- Preflight, then Recon —
scripts/doctor.py is the cheapest possible first command: it reports which toolchains and scripts can actually run here, and surfaces the environment facts that poison experiments (clock skew, leftover adb forward/proxy, a device-side frida process already running, a tool installed off-PATH). Then scripts/preflight.py before anything else if a device is involved (it takes seconds and prevents a whole class of false conclusions), then references/recon.md. Manifest, package name, version, ABI, dex count, packer, embedded SDKs, where the app's own code lives. Ten minutes here saves hours. If it is packed, unpack before anything else (references/recon.md §unpacking): you cannot patch code you cannot read, the encrypted payload lengths tell you which dumped dex is the original, and a memory dump must be de-duplicated by hash and structurally validated before any of it is trusted.
If recon says there is no packer but a re-signed build still dies, you are in the layer references/code-virtualization-and-custom-linkers.md covers — do not proceed on the assumption that "no packer" means "editable".
If the app already dies on its own — especially at a roughly constant time after launch, or with a native crash — locate the mechanism before planning any patch (references/native-tamper-and-suicide.md, scripts/native_crash.py). Record the observed time-to-death: it is the baseline every later attempt is measured against, and without it a surviving run cannot be told from a changed schedule.
Skip condition: never skipped. G2/G3 in §Gates are cleared here or not at all.
- Extract strings and endpoints — build a picture of the app's API surface and SDK inventory from the dex string tables. No decompiler needed for this, and it is fast. Scripts:
scripts/dex_strings.py.
- Trace to the owning class — find the class that wraps the behavior (the app almost always wraps third-party SDKs in one helper). Reverse-lookup instructions:
references/dex-patching.md §finding-the-call-site.
- Decide the patch layer — client SDK call / client rendering / client data consumption / server contract. See the table in
references/ad-removal.md.
- Patch surgically —
references/dex-patching.md and references/byte-level-patching.md.
Two techniques, and picking the right one is a decision, not a preference:
equal-length byte edits (scripts/dex_patch_bytes.py, located with
scripts/dex_find_insn.py) when the change fits in an existing instruction slot
or constant — nothing moves, so no offset, try/catch block or debug pointer can
be invalidated. dexlib2 method rewriting (scripts/dexpatch/) only when the
change genuinely needs new instructions. Whole-tree smali round-trip damages
R8-optimized dex in ways that only show up at runtime; a method rebuild also
inflates the file (measured: debug_info 924 B -> 22.8 KB, dex 4.32 MB ->
7.73 MB on one sample). Whichever you use, recompute the dex header integrity
fields (signature first, checksum last) — references/byte-level-patching.md
§the dex header has two integrity fields.
- Repack and sign —
references/repack-and-sign.md. Do not strip the whole META-INF/. This single mistake destroys otherwise-correct builds.
6b. Neutralise the update path — before you call the build done. If the app checks for updates at all, add the two-layer patch (references/updates-and-forced-upgrade.md): no-op the update routine's entry, and force the version comparison to its "no update" side. A build that can be switched off or replaced remotely is not a deliverable, and this costs minutes here versus a rebuild later. Do the same for any remote-config or hot-update channel that could restore the behaviour you removed.
6c. Handle account gates only after classifying them — if the request mentions sign-in or binding, apply references/account-gates.md and state plainly which guarded screens become usable and which stay empty because their content is account-scoped.
- Verify on device —
references/environment.md + references/verification.md. Check: launches, the changed behavior actually changed, nothing unrelated broke, and the app reaches its normal UI with no blocking dialog. First prove the artifact actually changed on the device -- a package manager reporting success does not prove an interposed confirmation was accepted (P18). Capture continuously for the first ~20 seconds after launch, and look at the captures — sampling gaps are how a blocking modal goes unseen (P20), and a burst of images that were never inspected is not evidence. If the accessibility tree is empty, the image is the primary evidence rather than a fallback.
- Log what you learned — if a failure cost you more than thirty minutes, add it to
references/pitfalls.md. That file is the most valuable artifact in this skill.
What "done" means — do not claim it earlier
Every item below must be true before you report completion. Anything less is a checkpoint and must
be labelled as one, out loud, with what remains. Premature "done" is the most damaging thing you can
report, because it ends the investigation while the user believes the problem is solved.
- The artifact exists and its identity is recorded — path plus hash, not a filename.
- It was installed and launched on the environment the deliverable sentence names (G1/R1). If
that environment was not available to you, say so and label the result accordingly.
- The behaviour you changed is verified changed — by direct observation of the feature, not by
the absence of an error message. "The log is clean" is not evidence; "the screen shows X" is.
- The features it touches still work. You exercised them. A build that starts but whose affected
feature is dead is not a result.
- The original limitation is stated if any survives — with the coupling that causes it, so the
next person can decide whether to accept it.
- Nothing you did leaves the target or the device in a broken state unless that was the goal, and
any privileged workaround is labelled a fallback rather than the deliverable.
If items 1–4 hold but the environment was wrong, you have a prototype, not a deliverable. Say
"prototype" and name the gap.
Stop conditions — halt and re-classify, do not retry
These are moments where continuing to push forward is the wrong move. Each has cost hours somewhere.
- The same shape of attempt failed twice. See the two-strike rule above.
- A patch had no effect and you were about to try a third variant of it. No effect means the
diagnosis was wrong, not that the patch was unlucky. Re-classify the layer.
- A new failure has no place in your current model. That is the symptom index's trigger condition.
- You are about to write off a route as "blocked" without a control build proving the block is
the app's doing rather than your pipeline's. Mis-attributed blocks have removed viable routes.
- You are about to claim success on absence of errors. See §What "done" means.
- A measurement disagrees with a conclusion you already recorded as settled. Re-open the
conclusion; do not explain the measurement away.
Non-negotiable constraints
- Read-only inputs. Keep the original APK/dex untouched; work on copies. Always keep a known-good baseline to diff against.
- One variable at a time. If you change two things and it breaks, you learn nothing. Build a control (same pipeline, zero patches) and compare.
- Verify structure after every dex edit.
scripts/dex_classdiff.py must report zero differences in class set and access flags for classes you did not intend to change.
- Do not patch a method that is widely shared. Before patching any helper, count its callers (
scripts/find_refs.py). A Long.valueOf wrapper with 30 callers is not an ad-specific hook.
- Do not make an API fail to suppress a UI element. A 404/400 on an endpoint that other features depend on takes the whole screen down with it. Suppress at the data-consumption or render layer instead.
- Neutralise a native terminate path by returning, never by making it not return. A stub, stub patch, or function entry replaced with a spin or
…(truncated)
1---2name: apk-reverse3description: Reverse engineer, debloat, de-ad, patch, or re-sign Android APKs, and analyze their runtime and server-side behavior. Use when a task involves an .apk/.aab/.dex/.so sample, smali or dex patching, Frida/objection runtime hooking, repacking and re-signing, removing ads or SDK trackers, probing a mobile app's HTTP API, or deciding whether a client-side patch is even capable of achieving the goal. Covers recon, anti-tamper, ad removal, membership/paywall limits, dex-level surgical patching, repack pitfalls, device and emulator setup, and a hard-won failure catalogue. Load the body before planning any patch work: it opens with a symptom index and four gates that must be cleared first.4---56# APK Reverse Engineering & Patching78Goal: reach a **verified, installable, still-working artifact** fast — and avoid the whole class of9mistakes that destroy an APK while looking completely healthy.1011## How to use this file1213This file is a **procedure with gates**, not background reading. Three things are mandatory:1415- Before you patch anything, clear the **four gates** in §Gates. They are actions with pass criteria,16 not attitudes.17- When anything fails in a way your current plan does not explain, **stop and check the symptom18 index**. If a row matches, load that file before running another command.19- When this file and your own reasoning disagree, **this file wins** until you have evidence that20 overrides it. Every rule here is the residue of a failure that cost hours; your current intuition is21 the intuition of someone who has not hit it yet.2223## Four rules that override everything else2425**R1 — Write the deliverable as a testable sentence before you touch the target.**26"It works" is not the goal; "it works under the stated constraint" is. Root-assisted, live-27instrumentation, host-proxy and patched-device results frequently do **not** satisfy a request for an28installable artifact that works on a normal phone — and it is easy to present such a result as29finished. Write the sentence, re-read it at every checkpoint, and if you cannot meet it, say so30plainly and label the privileged workaround a **fallback**, never the deliverable.31→ `references/long-task-discipline.md` §the most expensive drift3233**R2 — Change one variable at a time, and keep a control build.**34An experiment that flips two things teaches nothing when it fails, and a failure you cannot attribute35will be attributed to the wrong cause. Every "the app rejects X" claim needs its own run, and every36patch needs a same-pipeline control that still fails the old way.37→ `references/long-task-discipline.md` §single-variable discipline3839**R3 — Never ship or claim an unverified artifact.**40"It assembles" is not "it works"; "the process started" is not "the feature works"; "no error in the41log" is not "the check is gone". Install it, launch it, exercise the exact feature you changed, and42look at the screen. Prove the device is running the build you made — hash it, do not trust the43filename.44→ `references/verification.md`4546**R4 — Identify the owning layer before patching, and re-classify when reality disagrees.**47Ads, paywalls, feature gates, integrity checks and update gates live in different layers (Java, dex,48native, Dart/Unity, server). Patching the wrong layer either does nothing or breaks the app. If a49patch "had no effect", the diagnosis was wrong — go back to classification instead of patching harder.50→ `references/recon.md`, then the layer-specific file the symptom index points at5152## Tooling — what to reach for, in order, and how to notice what you are missing5354Most wasted rounds in this domain are not bad reasoning about the target. They are **the right question55asked of a tool too weak to answer it**: an hour of `grep` over a hand-exported smali tree where one56indexed query would do, or a full manual ELF walk where a decompiler was one `pip install` away. The57failure is invisible from the inside, because the weak route still produces output.5859Four obligations. These are instructions, not preferences:6061- **Orient with an indexer, not with an export.** Before reading code, build the ability to *ask the62 artifact questions* — `droidasc findrefs` (string/type/method → every reference site, sub-second) or63 `ddc findrefs`. A full decompile is for reading a class you have **already located**; it is not the64 way you locate it. Treat `jadx` as a readable viewer of last resort, never as the source of truth, and65 never as the entry point of a recon. → `references/toolchain.md` §Tier 1 — dex and Java66- **When the hot layer has no working tool here, installing one is part of the task.** A missing arm6467 decompiler is not a constraint to route around; it is the next step. Route around it and you pay in68 hours for a result a decompiler gives in minutes. Ask a human only when installation is genuinely69 impossible. → `references/toolchain.md` §Closing a capability gap70- **Name the gap before you spend against it.** State the capability the current blocker requires, and71 whether this machine has it. This is a G2 item, not a note to yourself.72- **Reach for a script here before writing a new one.** The kit exists precisely so that parsing,73 hashing, alignment and hot-plug probes are not re-derived per task; a bespoke script written in place74 of `scripts/dex_find_insn.py` is how offsets get guessed instead of computed.75 → `references/toolchain.md` §Using the kit's scripts instead of writing your own7677A capability you have not checked for is not a capability you lack. Run78`python skills/apk-reverse/scripts/doctor.py` and read what it finds **off-PATH** before concluding79that anything is unavailable.8081## Coverage — what this skill claims, and what it does not8283The failure this section prevents is not ignorance. It is **a confident wrong answer produced by84applying the nearest available procedure to a target it was never written for.** A documented method85that almost fits is more dangerous than no method at all, because it arrives with a plan, a86vocabulary and a set of reassuring numbers.8788**Covered, by verified mechanisms:**8990- Client-side ads, promos and splash/popup/tab configuration — including the server-issued UI config91 that has no SDK to find (`ad-removal.md`, `server-config-and-updates.md`).92- Deciding whether a membership, paywall or feature gate is *client-enforceable* at all, and saying93 so plainly when it is not (`membership-and-limits.md`, `account-gates.md`).94- dex-level surgical patching: equal-length byte edits and dexlib2 method rewrites, plus the header,95 verifier and alignment rules that decide whether the build loads at all (`dex-patching.md`,96 `byte-level-patching.md`, `patch-audit.md`).97- Repacking, signing, installing, and the install refusals that look like a broken build98 (`repack-and-sign.md`).99- Packers, custom loaders and code virtualization: identifying them, measuring the validation100 boundary, and the routes that survive it (`packers.md`,101 `code-virtualization-and-custom-linkers.md`).102- The native layer: `.so` hosts, tamper-triggered self-termination, forged ELF structure, and103 neutralising a terminate path without freezing the process (`native-and-so.md`,104 `native-tamper-and-suicide.md`).105- Flutter / Dart AOT: analysing and patching `libapp.so` **given a snapshot dump** — pool-reference106 counting, disassembly windows, caller indexing, patch-site choice (`dart-aot.md`). Measured on a107 real Dart 3.6.0 build: `dart_disasm.py` decoded identically to capstone (32/32 and 96/96), the108 caller index re-derived independently with a symmetric difference of 0, and a specific business109 logic site was located end to end. **The snapshot dump is a dependency, not a detail** — see the110 next section.111- Runtime analysis with Frida, server-side API probing, feature-scoped TLS failures, update and112 forced-upgrade neutralisation, and the verification discipline everything above rests on.113114**Dependencies this skill does not ship — name them before the workflow starts:**115116- **Dart AOT analysis needs a snapshot dump.** `dart-aot.md`'s workflow begins at `pp.txt`; producing117 it requires a snapshot container resolver that this skill does not contain and cannot synthesize.118 `dart_pool_strings.py` reports **file** offsets while `pp.txt` and `dart_pprefs.py` speak in **pool**119 offsets, and the mapping between those two spaces is not a constant: over the 4,241 strings present120 in both, a measured run found 4,237 distinct deltas. Ship a pinned front end (aotopsy — pure Go, no121 toolchain) or build blutter (~80 s, needs a C++ toolchain). **Say which one you are using and why,122 because the two report different Dart version labels for the same binary.** Do not describe the123 object pool as something this skill decodes on its own.124125**How strong these claims are:** the Measured mechanisms below were established by running the126scripts against a real target during the verification pass recorded in127`docs/tool-verification/`. The rest are **inferred** — documented from experience, but the128repository carries no fixture, log or sample that reproduces them (its own `long-task-discipline.md`129reserves *observed* for a claim with an exact command and output behind it). Treat the distinction as130load-bearing rather than cosmetic, and label your own results the same way.131132**Not covered — say so rather than improvise:**133134- **Unity / IL2CPP logic recovery.** `framework-runtimes.md` identifies the runtime and establishes135 that the dex is not the battlefield; it does not carry the IL2CPP equivalent of `dart-aot.md`.136 There is no verified recipe here for locating a method inside `libil2cpp.so` plus137 `global-metadata.dat`.138- **React Native / Hermes bytecode** and Cordova/hybrid internals, beyond runtime identification and139 the generic "find the string, then find what references it" approach.140- **iOS / `.ipa` of any kind.** Every device, signing and packaging instruction here is Android.141- **Defeating a server-side authority.** `server-api.md` exists to determine *who owns a gate*, not142 to break an authorization the server performs.143- **A general unpacker, or an anti-detection arms race.** `detection-and-anti-analysis.md` decides by144 cost and often concludes "switch to static"; it is not a catalogue of evasion for every detector145 you might meet.146147**Not exercised by the verification pass — do not read silence as support:** the packer, code148virtualization, custom-linker, integrity-check-redirection and tamper-triggered-suicide scenarios149were **not run** against the verification target, because that target has none of those features (no150packer, no integrity checker, ordinary application class) and its unmodified build already fails to151start, which removes the repack-and-regress loop those scenarios need. Nothing in152`docs/tool-verification/` is evidence either way about them. If you use those documents, the153claims are still on the inferred footing described above.154155**Scripts this pass did not run** — so they carry no measurement at all, and any conclusion drawn156from them should be labelled accordingly: `native_crash.py`, `apk_diff.py`, `snap.py`,157`grab_crash.py`, `install_test.py`, `repack.py`, `dex_patch_bytes.py`, `dex_find_insn.py`,158`dex_check_verifier.py`, `dex_classdiff.py`, `dex_strpatch.py`, `patch_smali.py`, `smtool.py`,159`datastore_inject.py`, `probe_api.py`, `run_probe.py`, `tls_check.py`, `usb_net_proxy.py`,160`devsh.py`, and the `dexpatch/` java rewriter. Their absence from the record is not a verdict on161them. Note in particular that `grab_crash.py` claims to recover stacks hidden by a crash-reporter162SDK — the exact situation the verification target presented — and was not tried, so that claim163remains **unverified** and the pass used a purpose-written Frida probe instead.164165**The fallback, as an instruction:** if the target does not match that list, or no symptom-index row166matches, **stop and classify before choosing a branch.** Answer the thirteen questions first. If the167shape still does not fit — an unknown runtime, a mechanism you cannot name — say exactly that, and168propose the cheapest experiment that would identify it, rather than taking the closest documented169route and applying it anyway. A wrong branch here does not fail loudly: it produces an artifact that170builds, runs, and does the wrong thing.171172## Hand-off points — where this skill ends and another view begins173174Three boundaries that are easy to walk into without noticing. Each names what the other side owns,175rather than restating it, because two copies of the same advice drift apart.176177**1. JNI — a Java `native` declaration and its implementation are two different views of one function.**178This skill reads the Java side (dex) and the native side (`.so`) with different tools, so the join is179where analyses go wrong.180181| Form | What you see | How to find it |182|---|---|---|183| Static linkage | symbol `Java_<pkg>_<Class>_<method>` in `.dynsym` | search the dynamic symbol table. Under R8 the class name is a short name, so the symbol deforms with it and a search for the readable original finds nothing |184| Dynamic registration | **nothing** in the symbol table — binding happens at runtime | find `RegisterNatives` call sites, or hook it to read the binding table. Obfuscated targets prefer this, and a symbol search fails **silently** on it |185| Native → Java callbacks | native code pulling data back through Java | follow `FindClass` / `GetMethodID` / `CallObjectMethod` |186187`FindClass`/`RegisterNatives` in a `.so` tell you a JNI boundary exists even when no188`Java_*` symbol does. **Strength note:** the three rows above are documented behaviour, not results189from the verification pass, which did not trace a JNI boundary end to end; `FlutterJNI.loadLibrary`190appearing in a dex is the closest it came. Treat them as a map, not as a measurement.191192**2. Hardening — a dex-side packer observation is a native-side implementation question.** If the dex193turns out to be a shell, the logic is behind a loader, and the analysis moves to the `.so` that194performs the unpacking. `packers.md` owns the dex-side identification; the native deep dive belongs on195the other side of this boundary. **Not exercised by the verification pass** — that target had no196packer, so this pointer carries no measurement.197198**3. The existing native boundaries — read native anomalies from the APK side, not from inside.** 199`native-and-so.md` and `native-tamper-and-suicide.md` are deliberately scoped to what you can conclude200*from the APK side*: a repacked build that dies instantly with a null-looking fault, a Java-layer201check that reports success while the process dies, a terminate path you made not-return. That202judgement belongs here, because it is about deciding whether your *patch* caused the death. Deep203native work — restoring a symbol, rebuilding a call graph, reversing an OLLVM function — is a204different activity with a different toolchain. Point across rather than duplicating: if you need the205latter, say so instead of extending these two files into it.206207## Symptom index — a matching row is a stop signal208209You arrive at a symptom, not at a file name. Each row below is a failure that has already been paid210for. **If any row matches what you are observing, load the file before your next command** — not after211your next three attempts. Reasoning from first principles at this point is how the same hours get212spent twice; more than one entry here is a lesson that was re-derived by hand while the answer sat213unread in this repository.214215| What you observe | Load first |216|---|---|217| A repackaged/re-signed build **dies before your code runs**; `SIGSEGV`, all registers zero, `pc=0`, `fault addr` near `0x0` | `native-tamper-and-suicide.md` (deliberate crash), then `code-virtualization-and-custom-linkers.md` |218| **No packer** (Application is the app's own, dex readable) **and it still dies** | `code-virtualization-and-custom-linkers.md` §a loader is still a possibility; but if the same build also dies on a *second, unrelated* device you are looking at an ordinary startup fault, not a hardened one |219| The app dies at startup on **every** device, packed or not, **and there is no tombstone** while `crash_dump` reports `already traced` and logcat says `exited cleanly (0)` | the target's launch section in the verification record under `docs/tool-verification/` — a bundled crash reporter (Sentry NDK) has taken the signal handlers, so the platform's own evidence trail is gone. Frida spawn-gating is the recovery route; it needs a working frida-server, which a disguised one may not be |220| A `FORTIFY: pthread_mutex_lock called on a destroyed mutex` abort in a Flutter app, on the **main** thread, before the first frame completes | `dart-aot.md` — check `libapp.so` is actually being loaded; Flutter's engine bootstrap is the usual place a native lifecycle fault surfaces |221| Log says a **Java-layer** signature/integrity check **passed**, yet the process dies | `code-virtualization-and-custom-linkers.md` §a Java-layer "signature killer" is a decoy |222| Deleting a library fixes validation but yields `UnsatisfiedLinkError: dlopen failed: library "X" not found` | `code-virtualization-and-custom-linkers.md` §the deadlock that eats hours |223| Whole classes appear as bare `native` declarations with no body | `code-virtualization-and-custom-linkers.md` |224| A library's **SONAME does not match its filename** | `code-virtualization-and-custom-linkers.md`, `native-and-so.md` |225| Your edit had **no effect at all**, with no error | `server-config-and-updates.md` §3 (the value may be server-sent), then `packers.md` §map the validation boundary |226| Process **hangs** with no crash record, or dies to a `uid 0` killer | `native-tamper-and-suicide.md` §the rule (you probably made a terminate path *not return*) |227| Death looks like an ordinary null dereference in a hardened library | `native-tamper-and-suicide.md` §deliberate-crash stubs |228| The app dies **only while you are attached/rooted** | `detection-and-anti-analysis.md`; run the unmodified original under identical conditions first |229| **Install fails with `[-124]` and mentions `resources.arsc` / alignment** | `repack-and-sign.md` §2a — STORED **and** 4-byte aligned, both required |230| **Install fails with a bare numeric code (e.g. `[-99]`) and no `INSTALL_FAILED_*`** | `repack-and-sign.md` §vendor install interception — a device-side interceptor, not your build. Use the root `pm install` path |231| **After an install, `am start` does nothing / screenshots show another app / `am start -W` hangs** | `repack-and-sign.md` §the installer may still own the screen |232| Log shows `Failure to verify dex file ...: Bad checksum` and a startup `ClassNotFoundException` for an ordinary class | `byte-level-patching.md` §the dex header has two integrity fields — order matters |233| An install "succeeded" but nothing changed, or the version did not move | `long-task-discipline.md` §keep the observation window clean |234| Evidence contradicts itself, or a capture looks like two states mixed | `long-task-discipline.md` §keep the observation window clean |235| You took screenshots but drew the conclusion from logs or from the patch itself | `long-task-discipline.md` §captures you never looked at are not evidence |236| You are about to re-run an experiment whose result you already recorded | `long-task-discipline.md` §long-context decay |237| A script will not start, or a tool "is missing" | `scripts/doctor.py`, then `toolchain.md` §"not on PATH" is not "not installed" |238| Feature-scoped network failure (login/register/pay) while the rest works | `tls-and-cert.md` — do not assume your patch caused it |239| Everything works but **every signed request fails** after repack | `signature-derived-keys.md` |240| A re-signed build **runs fine, renders its whole UI and logs no error — but one feature silently never loads**, and `dumpsys`/DNS/logcat show **no request for it at all** (not a rejected request: *no request*) | `code-virtualization-and-custom-linkers.md` §what the native check actually reads — a client-side integrity gate is refusing **before** the request is built. This is *not* the row above: "sent and rejected" and "never sent" have different owners and different fixes |241| You cannot tell whether a missing feature is **your patch's fault or the target's own behaviour** | `long-task-discipline.md` §single-variable discipline. Run the **zero-change control through the same pipeline**, and the decisive variant: the unmodified original with the patch applied **in memory only**, same device, same network |242| Under Frida `spawn`, the UI never appears — `mCurrentFocus` stays `null`, screenshots come back blank, the Activity stack never builds | `dynamic-frida.md` §spawn keeps the Activity stack down: write the patch into memory, **detach**, then start the Activity normally |243| `frida-server` keeps disappearing mid-experiment, or the device reboots itself while you are working | `dynamic-frida.md` §when the ROM hunts your instrumentation |244| Ads still appear after a patch that should have killed them | `server-config-and-updates.md` §6 (cached config / remote re-enable), then `ad-removal.md` §step 4 (count the SDK's own log lines; n -> 0, not "I did not see it") |245| A forced-update or "must update" gate blocks the build | `updates-and-forced-upgrade.md` §step 6 |246| The dialog is gone but the feature is still locked | `membership-and-limits.md` / `account-gates.md` — decide server vs client authority before patching again |247| You are about to discard a route as "blocked" | `packers.md` — re-read it before writing any route off; mis-attributed failures have removed viable routes for hours |248| The task has run long and you are unsure what is already proven | `long-task-discipline.md` §keep a live record |249250## Gates — clear these before you patch, in order251252Each gate is an **action with a pass criterion**. Do not proceed past a gate you have not cleared, and253do not treat "I understand the idea" as clearing it. Skipping a gate is not a shortcut; it is how the254work gets redone.255256**G1 · Deliverable form.** State, in one sentence you could hand to someone else, what artifact must257exist at the end and under what constraints (rooted or not, installable on a stock device or not, must258survive updates or not, online or offline). *Pass:* the sentence names a testable constraint, not an259activity. *Fail:* you are solving a problem in an environment the deliverable will never see.260261**G2 · Environment truth and capability inventory.** Run `scripts/doctor.py` (and `scripts/preflight.py`262if a device is in play). *Pass:* you know which toolchains and scripts can actually run here, you have263seen the environment warnings — clock skew, leftover `adb forward`/proxy, a device-side frida process264already running, a tool installed off-PATH — **and you have written down the capability this target will265demand against the capability this machine has.** Name the two or three layers the task will almost266certainly reach (for example "arm64 native decompilation", "Dart AOT snapshot dumping", "device-side267TLS inspection", "dex-wide cross-referencing") and mark each available / missing-but-installable /268genuinely out of reach. *Fail:* you are about to attribute to the target a failure caused by your own269setup — or to spend a day routing around a tool that installs in ten minutes. A layer whose tool is270missing is a **task item**, not a constraint to design around. → `references/toolchain.md` §Closing a271capability gap272273**G3 · Code location.** From the manifest and dex, answer: is there a packer, where does the app's own274code live (dex / native / Dart / Unity / server), and is any of it virtualized to native. *Pass:* you275can name the class that owns the behaviour you intend to change, or you have an explicit plan to find276it. *Fail:* you are about to patch a layer you have not located. If recon says "no packer", still277check the virtualization shape — see the index rows above.278279**G4 · Baseline and control.** *Pass:* you have a control run — the unmodified original, or a280zero-change repack through the same pipeline — and you have recorded the observed failure (including281**time-to-death**, if it dies). *Fail:* when the patched build misbehaves you will have nothing to282compare against, and every later measurement is unfalsifiable.283284285## Start here: classify the target in thirteen questions286287Answer these before touching a tool. Every one of them changes the whole plan.2882891. **Is the app packed/hardened?** → `references/recon.md`290 Read the manifest's `application android:name`. If it is a third-party shell class rather than the app's own Application, you have a packer and must handle it first.2912. **Where does the behavior you want to change actually live?**292 - Ad SDK (Pangle/GDT/AnyThink/Kuaishou/Baidu/Sigmob…) → usually **client-side and removable** → `references/ad-removal.md`293 - **Server-issued config for UI the client renders** (launch screen, popup, announcement, tab set, sponsored card on a home feed) → **the client decides, the server supplies the data** → `references/server-config-and-updates.md` (this is the most common shape of "ad" in a modern app, and there is no SDK to find — decide this question early, because hunting an SDK that does not exist costs hours)294 - Membership / VIP / paid content → **usually server-authorized, client patch is cosmetic** → `references/membership-and-limits.md` (read this *before* spending hours)295 - Feature flag, UI gate, debug switch → usually client-side296 - Anything decided by an API response → server-side → `references/server-api.md`2973. **Is the app's own code in plain dex, or moved to native/Flutter/Unity?**298 Plain dex → you can patch. Flutter (`libflutter.so` + `libapp.so`) / Unity (`libil2cpp.so`) / pure native → different toolchain entirely. See `references/recon.md` §Where does the app's own code live and `references/framework-runtimes.md`.299 **Runtime check (cheap -- do it before committing to a layer):** hook the obvious Java classes for the UI you care about, then reproduce that UI. If those hooks fire, the behavior is Java-owned. If they fire **zero times** while the UI is plainly on screen, the behavior is drawn by the runtime or by native code, and a dex-only plan will stall. Do not keep hunting in dex after a zero-hit probe -- that is the most expensive wrong turn in this skill's history.3004. **What must the deliverable be able to do?** Write the answer as a testable sentence before301 planning anything, then re-read it at every checkpoint. This is the drift guard, and the drift it302 guards against is the most expensive one in this skill: a runtime-only result (a data edit, a live303 hook, a blocked hostname, a host proxy) can look like success while failing the actual requirement.304 The axes that decide it: **privilege** (unrooted?), **modification form** (a rebuilt, installable305 artifact, or is live instrumentation acceptable?), **ABI/device class**, **network** (must it work306 online?), **persistence** (survives restart / upgrade / fresh install?), **distribution** (must the307 shipped file be self-contained?). → `references/long-task-discipline.md` §the most expensive drift.3085. **Does the app verify its own signature, or does the server?**309 App-side → you must bypass it. Server-side → re-signing silently breaks the app later. See `references/repack-and-sign.md` and `references/server-api.md`.3106. **What is your device situation?** → `references/environment.md`311 Rooted real device (best), emulator with root, or no device (static only). Also: this determines whether Frida is usable. **Run `scripts/preflight.py` before your first experiment**, and again whenever a failure surprises you — device state, a dead device server, a leftover proxy, and clock drift all masquerade as a broken patch (`pitfalls.md` P9).3127. **Which architecture is actually executing?** → `references/native-and-so.md` §Cross-architecture313 `getprop` reports what the device claims and `primaryCpuAbi` reports what the package manager chose — neither is what is running. Only the live mapping is ground truth (`scripts/lib_map.py`). If the library you meant to patch is not mapped, a translator is in play, or the ABI differs from your assumption, that changes the plan more than any patch will.3148. **Is one *specific feature* failing at runtime — login, registration, payment, an API-backed screen — while the rest of the app works?**315 → `references/tls-and-cert.md`. A feature-scoped network failure is very often a **TLS/certificate problem on one code path**, not a consequence of your patch. The app can even carry two independent trust chains, so "other requests work" proves nothing. Rule this out in minutes before hunting for a signature check.3169. **Was the input a build you did not produce** (a "cracked"/"modded" APK circulating online)?317 → `references/third-party-builds.md`. Audit it before adopting it: such builds are frequently re-protected (sometimes with *more* layers than the original) and may carry injected components or endpoints. Never use one as a patching workbench.318319 **Long-task rule:** if this is likely to run long, open `references/long-task-discipline.md` now320 and keep its record updated as you go. Re-read the refuted-conclusions and dead-routes sections321 before starting any new experiment. Losing earlier findings is the most expensive failure in this322 skill, and it is entirely preventable.32310. **Does the client sign its requests with its own signing certificate?**324 → `references/signature-derived-keys.md`. Grep for `toCharsString()` / `signatures[0]` /325 `getPackageInfo(..., 64)` **before the first repack**. If that value feeds a native HMAC/DES326 routine, the rebuilt APK must hardcode the *original* certificate value at every read site, or327 every signed request fails while the app still launches and looks healthy. This is the single328 most expensive silent failure in a repack, and 15 minutes of grep prevents it.32911. **Does the app die on its own after a while — with no Java stack trace, or with a native330 crash that looks like a bug?**331 → `references/native-tamper-and-suicide.md`. A hardened library that decides the build is332 tampered rarely calls `kill`. It more often **arranges a fault** (load a small constant, use it333 as a pointer) so the death looks like an ordinary defect, and the system then reports it as an334 app "crash" or "abnormal" dialog. Two rules before you touch anything: **enumerate which335 mechanism actually fires** (the signal and the tombstone split them apart), and **neutralise by336 returning, never by making it not return** — a spinning stub freezes the process and produces a337 symptom that looks nothing like the cause.33812. **Will this build still be usable in a week?** → `references/updates-and-forced-upgrade.md`339 If the app has any version check, upgrade prompt, or self-update path, an unpatched build can be340 turned off remotely or replaced by the official package. This is one or two edits and it decides341 whether the work is durable — do it as part of the build, not as a follow-up. Also check for a342 **hot-update / remote-config** channel, which can restore behaviour you removed without any version343 change at all.34413. **Does the request touch sign-in or phone binding — "no login required", "skip binding", "guest ok"?**345 → `references/account-gates.md`. The whole difficulty here is separating a **client-side gate**346 (patchable) from an **account-scoped resource** (the screen is empty because the server has no347 account to answer for — not patchable). Classify first; and never fabricate a session to satisfy a348 gate, which produces a state worse than being signed out.349350## The workflow, end to end351352Steps are ordered. **Skip a step only when its stated skip condition is met** — "it seems353unnecessary" is not a condition, and it is the reason most of the failures in `pitfalls.md` happened.354355**Two-strike rule.** If the *same kind* of attempt fails twice, stop and go back to classification.356Do not run a third variation of a hypothesis that has already failed twice. Two failures of one shape357means the model is wrong, not that the parameters need tuning — and the third attempt is where an358entire round gets spent confirming what the first two already said. Re-read the symptom index at that359point; it exists for exactly this moment.3603611. **Preflight, then Recon** — `scripts/doctor.py` is the cheapest possible first command: it reports which toolchains and scripts can actually run here, and surfaces the environment facts that poison experiments (clock skew, leftover `adb forward`/proxy, a device-side frida process already running, a tool installed off-PATH). Then `scripts/preflight.py` before anything else if a device is involved (it takes seconds and prevents a whole class of false conclusions), then `references/recon.md`. Manifest, package name, version, ABI, dex count, packer, embedded SDKs, where the app's own code lives. Ten minutes here saves hours. **If it is packed, unpack before anything else** (`references/recon.md` §unpacking): you cannot patch code you cannot read, the encrypted payload lengths tell you which dumped dex is the original, and a memory dump must be de-duplicated by hash and structurally validated before any of it is trusted.362 **If recon says there is no packer but a re-signed build still dies**, you are in the layer `references/code-virtualization-and-custom-linkers.md` covers — do not proceed on the assumption that "no packer" means "editable".363 **If the app already dies on its own** — especially at a roughly constant time after launch, or with a native crash — locate the mechanism *before* planning any patch (`references/native-tamper-and-suicide.md`, `scripts/native_crash.py`). Record the observed time-to-death: it is the baseline every later attempt is measured against, and without it a surviving run cannot be told from a changed schedule.364 *Skip condition:* never skipped. G2/G3 in §Gates are cleared here or not at all.3652. **Extract strings and endpoints** — build a picture of the app's API surface and SDK inventory from the dex string tables. No decompiler needed for this, and it is fast. Scripts: `scripts/dex_strings.py`.3663. **Trace to the owning class** — find the class that wraps the behavior (the app almost always wraps third-party SDKs in one helper). Reverse-lookup instructions: `references/dex-patching.md` §finding-the-call-site.3674. **Decide the patch layer** — client SDK call / client rendering / client data consumption / server contract. See the table in `references/ad-removal.md`.3685. **Patch surgically** — `references/dex-patching.md` and `references/byte-level-patching.md`.369 Two techniques, and picking the right one is a decision, not a preference:370 **equal-length byte edits** (`scripts/dex_patch_bytes.py`, located with371 `scripts/dex_find_insn.py`) when the change fits in an existing instruction slot372 or constant — nothing moves, so no offset, try/catch block or debug pointer can373 be invalidated. **dexlib2 method rewriting** (`scripts/dexpatch/`) only when the374 change genuinely needs new instructions. Whole-tree smali round-trip damages375 R8-optimized dex in ways that only show up at runtime; a method rebuild also376 inflates the file (measured: `debug_info` 924 B -> 22.8 KB, dex 4.32 MB ->377 7.73 MB on one sample). Whichever you use, recompute the dex header integrity378 fields (**signature first, checksum last**) — `references/byte-level-patching.md`379 §the dex header has two integrity fields.3806. **Repack and sign** — `references/repack-and-sign.md`. **Do not strip the whole `META-INF/`.** This single mistake destroys otherwise-correct builds.3816b. **Neutralise the update path — before you call the build done.** If the app checks for updates at all, add the two-layer patch (`references/updates-and-forced-upgrade.md`): no-op the update routine's entry, and force the version comparison to its "no update" side. A build that can be switched off or replaced remotely is not a deliverable, and this costs minutes here versus a rebuild later. Do the same for any **remote-config or hot-update** channel that could restore the behaviour you removed.3826c. **Handle account gates only after classifying them** — if the request mentions sign-in or binding, apply `references/account-gates.md` and state plainly which guarded screens become usable and which stay empty because their content is account-scoped.3837. **Verify on device** — `references/environment.md` + `references/verification.md`. Check: launches, the changed behavior actually changed, nothing unrelated broke, and **the app reaches its normal UI with no blocking dialog**. First prove the artifact actually changed on the device -- a package manager reporting success does not prove an interposed confirmation was accepted (P18). Capture continuously for the first ~20 seconds after launch, **and look at the captures** — sampling gaps are how a blocking modal goes unseen (P20), and a burst of images that were never inspected is not evidence. If the accessibility tree is empty, the image is the primary evidence rather than a fallback.3848. **Log what you learned** — if a failure cost you more than thirty minutes, add it to `references/pitfalls.md`. That file is the most valuable artifact in this skill.385386## What "done" means — do not claim it earlier387388Every item below must be true before you report completion. Anything less is a **checkpoint** and must389be labelled as one, out loud, with what remains. Premature "done" is the most damaging thing you can390report, because it ends the investigation while the user believes the problem is solved.3913921. **The artifact exists and its identity is recorded** — path plus hash, not a filename.3932. **It was installed and launched on the environment the deliverable sentence names** (G1/R1). If394 that environment was not available to you, say so and label the result accordingly.3953. **The behaviour you changed is verified changed** — by direct observation of the feature, not by396 the absence of an error message. "The log is clean" is not evidence; "the screen shows X" is.3974. **The features it touches still work.** You exercised them. A build that starts but whose affected398 feature is dead is not a result.3995. **The original limitation is stated if any survives** — with the coupling that causes it, so the400 next person can decide whether to accept it.4016. **Nothing you did leaves the target or the device in a broken state** unless that was the goal, and402 any privileged workaround is labelled a fallback rather than the deliverable.403404If items 1–4 hold but the environment was wrong, you have a **prototype**, not a deliverable. Say405"prototype" and name the gap.406407## Stop conditions — halt and re-classify, do not retry408409These are moments where continuing to push forward is the wrong move. Each has cost hours somewhere.410411- **The same shape of attempt failed twice.** See the two-strike rule above.412- **A patch had no effect and you were about to try a third variant of it.** No effect means the413 diagnosis was wrong, not that the patch was unlucky. Re-classify the layer.414- **A new failure has no place in your current model.** That is the symptom index's trigger condition.415- **You are about to write off a route as "blocked"** without a control build proving the block is416 the app's doing rather than your pipeline's. Mis-attributed blocks have removed viable routes.417- **You are about to claim success on absence of errors.** See §What "done" means.418- **A measurement disagrees with a conclusion you already recorded as settled.** Re-open the419 conclusion; do not explain the measurement away.420421## Non-negotiable constraints422423- **Read-only inputs.** Keep the original APK/dex untouched; work on copies. Always keep a known-good baseline to diff against.424- **One variable at a time.** If you change two things and it breaks, you learn nothing. Build a control (same pipeline, zero patches) and compare.425- **Verify structure after every dex edit.** `scripts/dex_classdiff.py` must report zero differences in class set and access flags for classes you did not intend to change.426- **Do not patch a method that is widely shared.** Before patching any helper, count its callers (`scripts/find_refs.py`). A `Long.valueOf` wrapper with 30 callers is not an ad-specific hook.427- **Do not make an API fail to suppress a UI element.** A 404/400 on an endpoint that other features depend on takes the whole screen down with it. Suppress at the data-consumption or render layer instead.428- **Neutralise a native terminate path by returning, never by making it not return.** A stub, stub patch, or function entry replaced with a spin or 429430…(truncated)