Files
apk-reverse/README.md
T
inliver233 d1349b3cfa Restructure SKILL.md as a gated procedure, not advice
Why
  The reference material was good and mostly went unread in practice. The failure mode was
  not ignorance - it was reading the file, agreeing with it, and then reasoning from first
  principles anyway, because everything in it was phrased as guidance. Guidance can be
  acknowledged and set aside; a gate cannot be cleared without doing the thing.

SKILL.md is now a procedure
  New top matter, in the order a reader meets it:
  - "How to use this file" states plainly that this is a procedure with gates, that this
    file wins over intuition until evidence overrides it, and why (every rule is residue
    of a failure that cost hours).
  - Four override rules R1-R4, each with a one-line claim, the concrete trap, and a
    pointer. R1 deliverable-as-a-testable-sentence, R2 one variable plus a control build,
    R3 never ship or claim an unverified artifact, R4 identify the owning layer and
    re-classify when reality disagrees.
  - The symptom index is retitled "a matching row is a stop signal" and now carries an
    explicit instruction: load the file before your next command, not after your next
    three attempts.
  - Four gates G1-G4, each an action with a pass criterion and a fail description:
    deliverable form, environment truth (doctor.py + preflight), code location (including
    the virtualization check even when recon says "no packer"), and baseline/control with
    recorded time-to-death.
  - The workflow gains a skip-condition line per step ("it seems unnecessary" is not a
    condition) and a two-strike rule: two failures of the same shape mean the model is
    wrong, not the parameters, so re-classify instead of running a third variant.
  - New "What done means": six items, all required, with an explicit rule that a clean log
    is not evidence and that anything short of six is a checkpoint to be labelled as one.
  - New "Stop conditions": the six moments where pushing forward is the wrong move -
    repeated same-shape failure, a no-effect patch about to be retried, an unexplained
    failure, an unproven "blocked" verdict, a success claimed from absence of errors, and
    a measurement that disagrees with a settled conclusion.

README
  Documents the gated design and why it exists, and tells an agent reading it what to act
  on rather than read: the four rules, the symptom index as a stop signal, the gates, the
  two-strike rule, and the definition of done. Points at scripts/doctor.py as the cheapest
  first command.

Not changed
  Three section references (`ad-removal.md` server-driven, `recon.md` code-location,
  `dex-patching.md` finding-the-call-site) do not match a `##` heading textually. Each
  target does exist - two are bolded inline subheadings and one is "## Finding the call
  site" - so they resolve for a reader. Left alone deliberately: rewriting them would add
  churn without changing behaviour.
2026-09-20 01:00:11 +08:00

17 KiB
Raw Blame History

apk-reverse

An Agent Skill for Android APK reverse engineering, debloating, ad removal, surgical dex patching, repacking, and runtime/server analysis.

It is a skill, not a tutorial: it is written to be loaded by an agent (Claude Code, Codex, or any harness that supports the Agent Skills format) while it works, so it is organized for progressive disclosure — a short decision-oriented SKILL.md, detailed references loaded only when a step needs them, and parameterized scripts you can run directly.

How an agent is expected to consume this

SKILL.md is deliberately written as a procedure with gates rather than as advice, because the observed failure mode is not ignorance — it is a model reading the whole thing, agreeing with it, and then reasoning from first principles anyway.

So there are four things in the body that are meant to be acted on, not read:

  • Four override rules (R1–R4). Where they conflict with the current plan, they win until evidence overrides them.
  • A symptom index. Each row is a failure that has already been paid for. A matching row is a stop signal: load that file before running another command, rather than after a few more attempts. Reasoning past a known symptom is how the same hours get spent twice.
  • Four gates (G1–G4), each an action with a pass criterion. "I understand the idea" does not clear a gate. They exist so that classification, environment truth and a control build happen before the first patch, not after the third failure.
  • A two-strike rule and stop conditions. Two failures of the same shape mean the model is wrong, not the parameters. The third variant of a hypothesis that already failed twice is where rounds go to die.

And one thing at the end that is meant to be withheld: "done" has a definition (six items). A clean log is not one of them. Anything short of all six is a checkpoint, and should be reported as a checkpoint with what remains.

If you are an agent reading this: the cheapest possible first command is python scripts/doctor.py. It tells you which of these tools exist here, which scripts can actually run, and whether something in the environment is already poisoning your measurements.

What it is good at

  • Deciding fast whether a request is even achievable client-side, instead of burning hours on a paywall that is enforced by a server.
  • Deciding what form the deliverable must take before any work starts — an unrooted, self-contained artifact is a different problem from "make it work on this machine", and confusing the two is the most expensive drift in this domain.
  • Choosing the safest patch layer for a given change, and avoiding the layers that break the app.
  • Catching the repack failure that looks like success: an app that installs, launches and renders perfectly while every signed request is rejected, because the client derives its request-signing key from its own signing certificate.
  • Separating your own mistakes from the app's or the server's problems — a feature-scoped failure (login, registration, payment) is often a TLS/certificate issue on one code path, not a consequence of the patch you just built. Device state, a dead device server and clock drift masquerade the same way.
  • Establishing which architecture and which library are actually executing, rather than trusting what the manifest ships or what the device claims.
  • Working through packed/hardened targets: identifying the packer, unpacking, and turning a memory dump back into a patched, installable APK.
  • Handling a hardened library that terminates the process on purpose — including the deliberate-crash shape (fault addr 0x4) that looks exactly like an ordinary null-dereference bug, and the "neutralise it, but never by making it not return" rule that decides whether the fix works or freezes the whole app in a way that looks nothing like the cause.
  • Knowing which tools to reach for and where each one lies — including the ones that only exist as a GUI, so you ask for a human instead of silently substituting a weaker method.
  • Making a patched build stay patched: neutralising version checks, forced-upgrade dialogs and self-update installers so the work cannot be switched off remotely — and recognising the hot-update/remote-config channel that can quietly undo it without any version change.
  • Separating a client-side sign-in gate (patchable) from an account-scoped resource (empty because the server has nothing to answer with), and knowing that forging a session produces a state worse than being signed out.
  • Keeping a long task honest: a live record, graded conclusions, calibrated timeouts, and bounded waits, so progress is not lost and the same mistake is not made twice.
  • Avoiding the specific mistakes that produce an APK that builds perfectly and dies at runtime.

Structure

SKILL.md                  a procedure with gates, not background reading:
                          how-to-use  -> four override rules (R1-R4)
                          symptom index (a matching row is a stop signal)
                          four gates (G1-G4, actions with pass criteria)
                          thirteen classification questions
                          the workflow, with a per-step skip condition and a two-strike rule
                          what "done" means  ->  stop conditions  ->  constraints  ->  indexes
references/               loaded on demand, one topic each
  recon.md                    identify packer, SDKs, code location, tamper checks; unpacking
  packers.md                  hardened targets: rejection signals, measuring the validation
                              boundary with single-variable tests, choosing a native host
  code-virtualization-and-custom-linkers.md
                              the layer between "packed" and "clean": whole classes turned into
                              `native` declarations, a private loader whose SONAME does not match
                              its filename, an embedded self-decrypting payload, a Java-layer
                              "signature killer" that logs success while a native check kills you.
                              The keep-it/drop-it deadlock, how to separate the *checker* from the
                              *implementation*, and the string-redirect technique that ends it
                              without neutralizing anything
  framework-runtimes.md       Flutter / React Native / Unity: which layer owns the UI, and how to
                              find logic when there are no symbols (string encoding traps)
  dart-aot.md                 Dart AOT in depth: version pinning and building a matching decompiler,
                              the object pool and reference indexes, register/boolean conventions,
                              the three signatures that identify business logic, locating, patching
  native-and-so.md            .so hosts, DT_NEEDED vs JNI_OnLoad, relocation limits,
                              relocation-free bootstrapping, replacing Java methods natively,
                              and which ABI/library is *actually loaded and executing*
  native-tamper-and-suicide.md  how a hardened library kills its own process: the visible
                              mechanisms, how to tell which one actually fires, how to find the
                              site, forged section headers, function boundaries from
                              PT_GNU_EH_FRAME, scanner traps, and neutralising safely
  detection-and-anti-analysis.md  when the app fights back or the tool cannot run here: telling
                              detection apart from a broken environment, deciding by cost instead
                              of escalating, recognising an environment where dynamic analysis
                              simply does not work, and keeping the "blocks my analysis" question
                              separate from "blocks the deliverable"
  toolchain.md                what to install, how to invoke it non-interactively, which tools
                              are GUI-only, version-alignment traps, working offline
  long-task-discipline.md     live record, conclusion grading, drift control, timeout and
                              wait calibration, deliverable-form drift, handover
  ad-removal.md               ad taxonomy, wrapper mapping, callback trap, global gates, verification
  updates-and-forced-upgrade.md  keeping a patched build alive: locating the version check, the
                              two-layer patch (no-op the routine, neutralise the comparison), what not
                              to touch (manifest version, installer permission, host blocking),
                              self-update and hot-update/remote-config channels, verifying that no
                              version request is issued at all
  account-gates.md            sign-in walls, forced phone binding, guest mode: telling a client-side
                              gate (patchable) apart from an account-scoped resource (not), why
                              fabricating a session is worse than staying signed out, and the
                              unavoidable session loss after a reinstall
  signature-derived-keys.md   when the app's own signing certificate is used as key material:
                              detection greps, why offline extraction is unreliable, the
                              hardcode-then-verify procedure
  membership-and-limits.md    server vs client authority; what is and is not patchable
  server-api.md               probe an app's API; prove who owns the gate
  tls-and-cert.md             feature-scoped network failures: expired certs, dual trust chains
  third-party-builds.md       auditing a "cracked"/"modded" APK before trusting it
  dex-patching.md             patch-layer table + dexlib2 technique in depth
  patch-audit.md              proving a patch *landed* and is *legal*: length-vs-bytes
                              comparison, the equal-length-replacement blind spot, verifier-level
                              legality (move-result adjacency) checked statically, text-matching
                              patch traps, and reporting a missing patch
  repack-and-sign.md          repack rules, unpack-and-repack, signing, post-install hazards
  runtime-data.md             DataStore / SharedPreferences / SQLite / protobuf; when the app
                              rewrites your edit, and decoding a value that looks encrypted
  dynamic-frida.md            Frida setup, version pinning, the four-layer probe, hook strategy
  environment.md              device/emulator setup, root, ADB, offline devices, log signals,
                              emulator console control and recovery, preflight, look-at-the-screen
  verification.md             the claim ladder; what "done" means
  pitfalls.md                 the failure catalogue -- read before building
scripts/                  parameterized, path-agnostic
  doctor.py                   run this first: capability report + per-script runnability, finds
                              tools installed off-PATH or as runnable jars, and surfaces the
                              environment facts that poison experiments (clock skew, leftover
                              adb forward / proxy, a device-side frida process already running)
  so_constpatch.py            same-length in-place rewrite of an isolated string constant, for
                              redirecting a library load instead of defeating a check
  smtool.py                   baksmali/smali wrapper with a configurable classpath
  dexpatch/                   dexlib2 method-level surgical rewriter (preferred tool)
  patch_smali.py              method-body replacement in a smali tree
  dex_strpatch.py             byte-level string patch with a string_ids ordering guard
  dex_classdiff.py            prove a dex edit was surgical
  dex_strings.py              strings/URLs/SDK markers without a decompiler
  dart_pool_strings.py        recover literals from a Dart AOT snapshot (framed entries, the
                              one-byte vs UTF-16 split, file offsets, run-length noise filter)
  dart_pprefs.py              build/query the object-pool -> code-site index for a Dart snapshot
  dart_disasm.py              annotated windowed disassembly of Dart AOT code + B/BL caller index
  find_refs.py                count callers of a method before patching it
  repack.py                   rebuild APK, strip only signatures, sign, verify
  devsh.py                    quoting-safe ADB shell helper
  usb_net_proxy.py            give an offline device network over USB
  datastore_inject.py         encode/inject AndroidX DataStore preferences safely
  probe_api.py                probe an HTTP API with the right headers
  grab_crash.py               recover stacks hidden by a crash-reporter SDK
  install_test.py             install + launch health check with logcat signal scan
  frida_probe.js              four-layer runtime probe (app net layer + OkHttp + java.net + exceptions)
  run_probe.py                inject the probe, stream it to a log file, stay resident
  tls_check.py                strict certificate check for one or more hosts
  preflight.py                environment check before every experiment block (device, root,
                              ABI/translation, clock skew, leftover proxy/forwards, dead server)
  lib_map.py                  what is *actually mapped* into a live process: per-library path,
                              base, architecture, and whether it came from the APK or was
                              materialized at runtime
  elf_plt.py                  resolve a PLT stub to its imported symbol (x86_64 + aarch64) from
                              the relocation table; list a symbol's callers; byte-diff two
                              libraries and name the symbol each changed stub belongs to
  apk_diff.py                 entry-level diff of two builds: changed / added / removed, by
                              content hash so same-size replacements are caught
  native_crash.py             locate a native death from a log or tombstone: signal, fault
                              address, registers, frames split app vs system, the faulting
                              instruction, and a flag when the fault looks *arranged*
  blob_decode.py              search, don't guess, the framing of a stored value
                              (base64/hex x rotation x deflate); re-encode the edited payload
  snap.py                     bounded burst screenshots + control-tree capture with a stall
                              detector, and a verdict on whether the tree is usable at all
  sig_probe.py                find the exact signatures[0].toCharsString() value — offline
                              candidates from an APK, or the authoritative read from a device

Install

Drop this directory where your agent finds skills, e.g.:

<skills-dir>/apk-reverse/SKILL.md

The agent loads SKILL.md when a task matches its description, and pulls in references/* only as needed. No global state, no machine-specific paths.

Requirements

Nothing is mandatory; each script checks what it needs. scripts/doctor.py reports which of these are present here, which scripts can therefore run, and — usefully — which tools exist somewhere other than PATH.

If your toolchain lives outside PATH (a project-local tools/ directory, a versioned SDK folder, a runnable .jar instead of a command), set APKREV_TOOLS to one or more directories and doctor.py will find them:

set APKREV_TOOLS=D:\tools;D:\android\build-tools\35.0.0     # Windows
export APKREV_TOOLS=/opt/tools:/opt/android/build-tools     # POSIX

The scripts themselves are plain python3 and are intended to work identically on Windows, macOS and Linux; where a snippet is POSIX-only it is labelled. Nothing here assumes a Unix shell.

Tool Used for
Python 3.9+ all scripts
baksmali / smali + dexlib2 jars disassembly, assembly, surgical patching
JDK (javac, java) building/running the dexlib2 patcher
Android SDK build-tools (aapt, zipalign, apksigner) manifest info, alignment, signing
uber-apk-signer (optional) one-step align + sign
ADB device work
Frida (host package + matching on-device server) dynamic analysis
a rooted device or emulator anything beyond static analysis

Read this first

references/pitfalls.md. It is the most valuable file here — every entry is a failure that produced a broken artifact while looking completely healthy.

The four that hurt most:

  1. Stripping the whole META-INF/ during a repack deletes ServiceLoader registrations and the app dies at startup with an error that names an unrelated library.
  2. Patching a byte-level string without preserving string_ids ordering gets the whole dex rejected, while checksums and signatures verify perfectly.
  3. Rebuilding a dex with a whole-tree smali round-trip damages R8 output invisibly — class tables compare clean, and it only blows up at runtime.
  4. Neutralising a native terminate path by making it not return. A spinning stub does not suppress the check; it freezes the caller and every thread behind it. The app hangs with no crash record at all, and the eventual death gets blamed on whatever killed the frozen process.

Scope

Built for working on your own applications, on samples you are authorized to analyze, and in CTF/competition sandboxes. It contains no vendored third-party binaries and no target-specific data.