Worked end-to-end on a real unobfuscated commercial app (single-dex, no packer,
server-driven launch screen) and folded back everything that cost time. Net +17
files, no new dependencies, no sample-specific identifiers.
New references
byte-level-patching.md
The technique that was missing entirely. Equal-length in-place edits beat
method rebuilding (measured on a real sample: method rewrite grew debug_info
924 B -> 22.8 KB and the dex 4.32 MB -> 7.73 MB), but four failure modes are
silent: same-length is not enough, branch polarity, verifier legality, and
stale dex header fields. Covers locating an instruction's exact offset (why
listings and byte-pattern guesses both fail), the instruction width table and
the two traps that desynchronise a decode, neutralise-a-branch vs redirect-it,
and the header's signature-then-checksum order.
server-config-and-updates.md
The most common shape of "ad" in a modern app, and the one usually
mis-diagnosed: the server supplies UI the client renders. The two-layer fetch
(local fallback then remote override) that proves it, finding the config DTOs
through the field names data classes keep, why you patch the decision and not
the data, deciding the scope of "remove", and remote re-enable / cached config
durability.
New scripts
dexutil.py dependency-free dex reader: structural walk, exact
instruction decode, dex header recompute/verify in the
correct order, branch-target and operand helpers
dex_find_insn.py locate an instruction by decoded semantics, print its exact
byte offset with context and BOTH sides of any branch
dex_patch_bytes.py equal-length byte patches from a JSON spec: semantic match,
polarity pin via expect_next, equal-length enforcement,
verifier check, header recompute, re-decode to prove it landed
dex_check_verifier.py does any conditional branch target a move-result (bypassing
its producer)? Compares builds and separates pre-existing
findings from regressions the patch introduced
coldstart.py cold-launch capture: timed screenshot burst + logcat signals
+ installed-build facts + launch timing, and a warning when
the foreground activity is not the app
check_repo.py maintenance check: every script parses and answers --help,
every referenced path resolves, every file is documented
Fixes found while running the above
repack.py wrote archives with a plain zipfile writer, which cannot satisfy
the Android R+ install gate: resources.arsc must be STORED *and*
4-byte aligned, and a zip extra record cannot express a 1-3 byte
pad (a stored filler entry is inserted instead). Also fixes the
central-directory pointer and adds an alignment gate to the output.
usb_net_proxy.py, dex_strpatch.py
crashed on --help with a traceback instead of printing usage.
Updated
toolchain.md ddc added (single-binary dex->Java with query subcommands,
with measured timings and its real limits); "not on PATH is
not not-installed" with the search commands; which signer
to use and why jarsigner breaks alignment
repack-and-sign.md alignment requirement and its arithmetic; dex header
recompute; jarsigner hazard; vendor install interception
([N] with no symbolic constant -> root pm install); the
installer can keep owning the screen after an install
recon.md resolve package identity from a tool, not from hand-read
AXML strings (a wrong package sends all later device queries
nowhere); snapshot the config surface during recon
long-task-discipline.md captures you never looked at are not evidence (incl. the
consistent-but-wrong-window variant); long-context decay and
the record shape that survives it
dynamic-frida.md when live attach cannot work at all: static instrumentation,
static read, and self-recorded evidence, with the control
build caveat each one carries
ad-removal.md server-driven config is usually the EASIEST case, not the
hardest; a three-way zero result is a positive finding;
scope note on local brand splash vs promotional content
pitfalls.md P37 stale dex header checksum, P38 [-124] alignment,
P39 bare-numeric install failure, P40 wrong-window evidence,
P41 inverted branch polarity, P42 decode desynchronisation
SKILL.md, README.md symptom index, reference/script indexes and requirements
updated for all of the above
41 KiB
name: apk-reverse description: 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.
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
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 |
| 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 |
| 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. 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, and you have seen the
environment warnings — clock skew, leftover adb forward/proxy, a device-side frida process already
running, a tool installed off-PATH. Fail: you are about to attribute to the target a failure caused
by your own setup.
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.mdRead the manifest'sapplication 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
- Ad SDK (Pangle/GDT/AnyThink/Kuaishou/Baidu/Sigmob…) → usually client-side and removable →
-
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. Seereferences/recon.md§code-location andreferences/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.mdandreferences/server-api.md. -
What is your device situation? →
references/environment.mdRooted real device (best), emulator with root, or no device (static only). Also: this determines whether Frida is usable. Runscripts/preflight.pybefore 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.mdP9). -
Which architecture is actually executing? →
references/native-and-so.md§Cross-architecturegetpropreports what the device claims andprimaryCpuAbireports 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.mdnow 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 fortoCharsString()/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 callskill. 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.mdIf 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.pyis 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, leftoveradb forward/proxy, a device-side frida process already running, a tool installed off-PATH). Thenscripts/preflight.pybefore anything else if a device is involved (it takes seconds and prevents a whole class of false conclusions), thenreferences/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 layerreferences/code-virtualization-and-custom-linkers.mdcovers — 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.mdandreferences/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 withscripts/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_info924 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 wholeMETA-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, applyreferences/account-gates.mdand 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.pymust 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). ALong.valueOfwrapper 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 a self-branch does not suppress the check — it freezes the caller, holding whatever lock it had, and unrelated threads wedge behind it. The symptom (a hang, an external kill, a restart loop) looks nothing like the cause, and there is no crash record to explain it. Return a benign value, and prefer success (0) over failure (-1). Never touch the ordinary-path symbols (
pthread_exit,exit,abort,snprintf,closedir).references/native-tamper-and-suicide.md - Look before you conclude — and look while you wait. Execute, capture, and inspect; do not drive and sleep blind. A screen that is actually looked at answers in one step what coordinate-guessing cannot answer in five: the layout moved, a different dialog is up, a countdown is frozen, the text on screen says exactly why. Where the thing you are waiting on is visible, a sample you can inspect beats a duration you hoped was right, and byte-identical samples mean nothing is going to change.
scripts/snap.py;references/environment.md§look at the screen. - Put a timeout on every command, and calibrate it from measurement. An unbounded call turns a stall into "the task stopped making progress", which is indistinguishable from slow work and costs hours silently. Time the operation once, record it, then derive the bound from it — that is what makes slow and hung distinguishable. A deadline that passes is a measurement, not a verdict.
references/long-task-discipline.md§bound every wait. - Every claim needs evidence. "Probably", "should be", "in theory" are not findings. Either you observed it, or you label it unverified.
- "Done" means the user-visible outcome, not an internal signal. A blocking dialog still on screen means the task is not done, however many errors disappeared from the log. Absence of a log line is absence of evidence, never evidence of success.
- Do not discard a route on compound evidence. If a failure followed two simultaneous changes, the attribution is a hypothesis, not a finding. Re-run it single-variable before writing the route off -- mis-attributed failures have removed viable approaches for a long time.
- Prove the device changed before measuring. Install success describes the request, not the app on disk. Confirm the artifact actually advanced, or every following observation describes the previous build.
- Attribute a failure to the right layer before patching again. When something stops working after a rebuild, first check whether the unmodified original fails the same way on the same device and network. Feature-scoped network failures in particular are frequently the app's own TLS/certificate problem (
references/tls-and-cert.md); chasing a signature check that does not exist burns hours.
Reference index
Load only what the current step needs.
| File | Load when |
|---|---|
references/recon.md |
Starting any new sample; identifying packer, SDKs, code location, ABI |
references/server-config-and-updates.md |
The launch screen, a popup or the tab set is server-sent; no ad SDK was found; a removed promo came back; anything controlled by a *Config/*Popup DTO with an enabled flag |
references/byte-level-patching.md |
You want to change behaviour by editing a few bytes rather than rebuilding a method — equal-length patches, locating an instruction's exact offset, dex header integrity fields, branch polarity, verifier legality |
references/packers.md |
The app is packed/hardened, or an edit makes it die before your code runs. Also load before discarding any route as "blocked by the shell" |
references/code-virtualization-and-custom-linkers.md |
No packer, dex is readable, and a re-signed build still dies — whole classes turned into native declarations, a private loader with a mismatched SONAME, an embedded self-decrypting payload, or a Java-layer "signature killer" that logs success while a native check kills you. Covers the keep-it/drop-it deadlock and the string-redirect escape |
references/framework-runtimes.md |
The UI is not native (Flutter / React Native / Unity / Cordova), or Java-layer hooks fire zero times while the UI clearly works |
references/dart-aot.md |
The logic lives in a Dart AOT snapshot (libapp.so): pinning the Dart version, building a matching decompiler, the object pool and reference indexing, register/boolean conventions, locating and patching Dart code |
references/native-and-so.md |
Patching in a .so, needing code to run before the app's own code, hand-built native payloads that crash inside the linker, or deciding which library/ABI is actually loaded and executing |
references/native-tamper-and-suicide.md |
The process dies on its own (no Java stack, or a native crash that looks like a bug); you are about to neutralise a kill/exit/abort path; or a hardened library's sections/function boundaries look wrong |
references/detection-and-anti-analysis.md |
The app fights back: it dies after you attach, refuses to run on your device, detects root/hook/debugger, or your dynamic tool simply does not work in this environment. Read the first section before escalating — the right answer is usually to switch to static, not to fight the detector |
references/toolchain.md |
Choosing or invoking tools, something is not installed (including "not on PATH but present on disk"), a tool's output smells wrong, or you need to know which tools exist only as a GUI |
references/ad-removal.md |
Task involves ads, trackers, sponsored cards, splash/interstitial/reward |
references/updates-and-forced-upgrade.md |
The patched build must keep working over time; the app has any version check, forced-upgrade dialog, self-update installer, or hot-update/resource channel. Load this for essentially every build you intend to ship. |
references/account-gates.md |
Task mentions "no login required", "don't force sign-in", "skip phone binding", "guest mode"; or a screen/feature is unreachable signed-out. Also load before promising that an account-scoped screen will show anything |
references/membership-and-limits.md |
Task involves VIP, subscription, paid content, unlock, "fully cracked" |
references/server-api.md |
The behavior is decided by a response, or you need to know if a patch can even matter |
references/dex-patching.md |
Any actual editing of dex/smali, choosing a patch layer, choosing a tool |
references/patch-audit.md |
Proving a patch landed, or that it is legal: length-vs-bytes comparison, the equal-length blind spot, verifier-level legality (move-result* adjacency), text-matching patch traps, and how to report a missing patch |
references/repack-and-sign.md |
Rebuilding, signing, installing, or a repacked app misbehaves |
references/signature-derived-keys.md |
The app reads signatures[0]/toCharsString(), or a rebuilt APK installs and runs but every signed request fails (sign/_p/uth empty or -1) |
references/runtime-data.md |
Local state matters: DataStore, SharedPreferences, SQLite, protobuf caches, tokens — or your data edit keeps being reverted, or a stored value looks encrypted |
references/dynamic-frida.md |
Frida setup, hooking strategy, tracing caller chains, finding the real call site |
references/environment.md |
Device/emulator setup, root, ADB, networking, offline devices, emulator console control and recovery, the preflight check to run before every experiment block |
references/verification.md |
Defining what "done" means; building the evidence chain |
references/tls-and-cert.md |
One feature fails at runtime (login, registration, payment, an API-backed screen) while the rest of the app works |
references/third-party-builds.md |
The input is a "cracked"/"modded" build you did not produce — audit it before trusting it |
references/long-task-discipline.md |
The task will run long, or you are resuming one. Live record, conclusion grading, drift checkpoints, deliverable-form drift (rooted-only vs shippable), bound-your-waits, captures-you-never-looked-at, long-context decay, handover |
references/pitfalls.md |
Always worth a skim before building. This is the failure catalogue. |
Script index
All scripts are parameterized and path-agnostic; pass paths explicitly. Run --help or read the header of each.
| Script | Purpose |
|---|---|
scripts/doctor.py |
Run this first. Capability report and per-script runnability: which tools exist (including ones installed off-PATH or as java -jar jars), which scripts can actually run here, and the environment facts that silently poison experiments — clock skew, leftover adb forward/proxy, a device-side frida process already running |
scripts/dexutil.py |
Dependency-free dex reader: structural walk + exact instruction decode with a verified format table, fix_dex_header/verify_dex_header (correct checksum/signature order), branch-target and operand helpers. The library the dex scripts below share; also runs standalone to dump one method with offsets |
scripts/dex_find_insn.py |
Locate an instruction by decoded semantics and get its exact byte offset, with context and both sides of any branch. This is how you find a patch site without guessing offsets or scraping listings |
scripts/dex_patch_bytes.py |
Apply equal-length byte patches from a JSON spec: matches by semantics, pins branch polarity via expect_next, enforces equal length, checks verifier legality, recomputes the dex header in the correct order, and re-decodes to prove the edit landed. --dry-run first |
scripts/dex_check_verifier.py |
Tier-3 check: does any conditional branch target a move-result* (bypassing its producer, so the class fails to load)? Compares two builds and distinguishes pre-existing findings from regressions your patch introduced |
scripts/coldstart.py |
Cold-launch an app and capture a timed screenshot burst + logcat signals + installed-build facts + launch timing, and warn when the foreground activity is not your app (a vendor installer left on screen is the classic cause of "evidence" that shows something else) |
scripts/smtool.py |
baksmali/smali wrapper with a bundled classpath (assemble/disassemble dex) |
scripts/patch_smali.py |
Method-body replacement in a smali tree, matched by signature |
scripts/dex_strpatch.py |
Byte-level string constant patch with string_ids ordering guard |
scripts/dex_classdiff.py |
Compare two dex class tables (set + access flags) to prove a patch was surgical |
scripts/dex_strings.py |
Dump/extract strings and endpoints from dex without a decompiler |
scripts/dart_pprefs.py |
Build/query the object-pool offset -> code-site index for a Dart AOT snapshot (arithmetic decode; seconds, not minutes) |
scripts/dart_pool_strings.py |
Recover string literals from a Dart AOT snapshot: framed entries, the one-byte vs UTF-16 split, file offsets, and a run-length noise filter |
scripts/dart_disasm.py |
Annotated windowed disassembly of Dart AOT code (pool + boolean annotations) plus a B/BL caller index |
scripts/find_refs.py |
Count and list callers of a method/field (blast-radius check) |
scripts/repack.py |
Rebuild an APK with replaced dex, strip only signatures, keep META-INF/services/, write a 4-byte-aligned archive (resources.arsc STORED+aligned, which Android R+ refuses to install without), sign, and verify |
scripts/dexpatch/ |
dexlib2 method-level rewriter (for changes that genuinely need new instructions) + build notes |
scripts/devsh.py |
Quoting-safe ADB shell helper for rooted devices |
scripts/usb_net_proxy.py |
Give an offline device network over USB (adb reverse + local proxy) |
scripts/datastore_inject.py |
Encode/inject AndroidX DataStore preferences (protobuf) safely |
scripts/probe_api.py |
Probe an app's HTTP API with correct headers, report status/shape |
scripts/install_test.py |
Install a build and run a launch/health check with logcat signal extraction |
scripts/frida_probe.js |
Four-layer runtime probe: app network layer + OkHttp + java.net + swallowed exception messages |
scripts/run_probe.py |
Inject a probe, stream it to a timestamped log file, stay resident while you operate the app |
scripts/tls_check.py |
Strict certificate check for one or more hosts (expired / wrong host / untrusted CA) |
scripts/preflight.py |
Read-only environment check before every experiment block: device, root, ABI/translation, clock skew, leftover proxy/forwards, dead device server. Run this before blaming a patch. |
scripts/lib_map.py |
What is actually mapped into a live process: per-library path, base, architecture (ELF e_machine), and classification (system / from-APK / runtime-materialized). Answers "which library and which ABI is really executing". |
scripts/elf_plt.py |
Resolve a PLT stub to its imported symbol on x86_64 and aarch64 (from the relocation table, not from position or a comment), list a symbol's callers, and byte-diff two libraries naming the symbol each changed stub belongs to. Run this before patching any stub, and to audit a patch set you inherited. |
scripts/so_constpatch.py |
Same-length in-place rewrite of an isolated string constant, for redirecting a library load instead of defeating a check (System.loadLibrary("checker") -> a library that is already mapped). Enforces equal length, refuses to touch a substring of a longer identifier, reports whether each hit sits in a constant pool, and patches inside an APK or a bare .so. See references/code-virtualization-and-custom-linkers.md. |
scripts/apk_diff.py |
Entry-level diff of two APKs: what changed, what was added (injection candidates), what was removed — by content hash, so same-size replacements are caught. Use it to audit a third-party build and to prove your own build was surgical. |
scripts/native_crash.py |
Locate a native death from a logcat capture or tombstone: signal, fault address, register state, backtrace split into your libraries vs system, the faulting instruction — plus an explicit flag when the fault looks arranged rather than accidental. |
scripts/grab_crash.py |
Recover a stack that a crash-reporter SDK swallowed, when the log shows the app died but prints no backtrace of its own. |
scripts/blob_decode.py |
Decode an opaque stored value by searching the parameter space (base64/base64url/hex × rotation × deflate/zlib/gzip) instead of guessing, then re-encode an edited payload with the same parameters. |
scripts/snap.py |
Bounded burst screenshot + control-tree capture, with a stall detector and an explicit verdict on whether the accessibility tree is usable at all. Use it so you look at the screen instead of driving blind. |
scripts/sig_probe.py |
Find the exact signatures[0].toCharsString() value: offline candidate enumeration from an APK (--apk), or the authoritative value read from a live package (--live). Feed the result into the hardcoded constant described in references/signature-derived-keys.md. |