mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:34:29 +08:00
Every claim carried one of three labels, and the third was doing two jobs. `unverified` meant
both "someone asserted it and nobody here paid for the counters" and "no evidence was gathered
at all" -- different statements with different next actions. This adds `unknown` for the second.
The distinction that pays for itself lives inside a *negative* result, because an empty probe has
three causes and only one of them is a finding:
absent -- coverage reached the question, nothing is there
present but unreadable -- the thing is there and this route cannot read it
unknown -- nothing covered the question
The middle one is the dangerous one: it produces byte-identical output to the first, and it is the
normal case on the input this skill is aimed at. A packed APK returning four classes is not an APK
with four classes, and "the string was not in the dex" is a statement about a probe. The rule now
says to name the probe's coverage and run a control that must hit before writing "not present", and
to record `unknown` about the probe rather than `absent` about the target.
Also in this pass:
* P47 -- two tools disagree about the same artifact, and the disagreement is your parser. One
extraction rule reused across outputs that do not share a format: an unescaped `$` dropped every
descriptor containing one, and dotted names were compared against DEX descriptors so one side
parsed to nothing. The comfortable failure is the one that confirms what you already expected,
which is why it survived the sanity check it should not have.
* dexsql: a cache is second-hand evidence and it ages silently. A stale cache answers for the old
artifact with no error, and `--trusted-input` skips revalidating the recorded source inputs --
so a cached run supports "true of the artifact the cache was built from", not "true of the
artifact in front of me".
* ete-diff-tracing: a divergence is a location, not a cause. The differential method produces a
positional claim, and three readings it does not authorise are written down beside the step that
produces it.
* CONTRIBUTING.md -- the gates, what a green run does and does not establish, the sync rules a
change has to keep (a new reference must be named in the inventory; renaming a heading breaks
every section reference that pointed at it), and the two failure modes to watch when editing the
gates: a limit that cannot fail, and a gate whose answer varies with the runner. README links it
from both language editions.
713 lines
54 KiB
Markdown
713 lines
54 KiB
Markdown
<p align="center">
|
||
<picture>
|
||
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.svg">
|
||
<img src="assets/banner-light.svg" alt="apk-reverse" width="100%">
|
||
</picture>
|
||
</p>
|
||
|
||
<p align="center">
|
||
<b>English</b> · <a href="README.zh-CN.md">简体中文</a>
|
||
</p>
|
||
|
||
<p align="center">
|
||
<a href="https://github.com/newliver666/apk-reverse/stargazers"><img src="https://img.shields.io/github/stars/newliver666/apk-reverse?style=flat-square&label=stars&color=49454F" alt="stars"></a>
|
||
<a href="https://github.com/newliver666/apk-reverse/network/members"><img src="https://img.shields.io/github/forks/newliver666/apk-reverse?style=flat-square&label=forks&color=49454F" alt="forks"></a>
|
||
<a href="LICENSE"><img src="https://img.shields.io/github/license/newliver666/apk-reverse?style=flat-square&color=49454F" alt="license"></a>
|
||
<img src="https://img.shields.io/badge/python-3.9%2B-49454F?style=flat-square&logo=python&logoColor=white" alt="python">
|
||
<img src="https://img.shields.io/badge/platform-android-49454F?style=flat-square&logo=android&logoColor=white" alt="android">
|
||
<a href="https://github.com/newliver666/apk-reverse/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/newliver666/apk-reverse/ci.yml?style=flat-square&label=ci&color=49454F" alt="ci"></a>
|
||
</p>
|
||
|
||
<p align="center">
|
||
<a href="#what-it-is-good-at">Capabilities</a> · <a href="#structure">Structure</a> · <a href="#install">Install</a> · <a href="#requirements">Requirements</a> · <a href="#read-this-first">Failure catalogue</a> · <a href="#scope">Scope</a> · <a href="#repository-maintenance">Maintenance</a> · <a href="#disclaimer">Disclaimer</a>
|
||
</p>
|
||
|
||
# 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 skills/apk-reverse/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.
|
||
- Deciding **what the deliverable should be when an APK is not an option** — a repack refused by
|
||
several independent checks is *blocked*, not expensive, and the fallback ladder is a system-level
|
||
module, a local RPC service, or an honest report with a stated boundary.
|
||
- Telling a **real memory dump from an extraction-shell skeleton**, and knowing which recovery route
|
||
applies — including the root-side dump for when `frida` itself is refused. What the measurement can
|
||
and cannot see is in `skills/apk-reverse/references/advanced-unpacking.md`.
|
||
- **Calling a routine instead of reversing it** when reversing costs more than invoking: emulated
|
||
execution on the host, or a live function service-ified over Frida RPC.
|
||
- Reading **instruction-level execution evidence** when a native function has been flattened into a
|
||
state machine by OLLVM — including the two ways Stalker was measured to bite back on a real device.
|
||
- Reaching for the **hardware layer** when every software tracer modifies what it observes: arm64
|
||
hardware watchpoints (no code change, but an honest `TracerPid` ledger), ETE/TRBE differential
|
||
tracing with its five gates, and kernel-armed injection templates that are labelled unverified
|
||
until a device with the matching gate fires them.
|
||
- Recognising when **userspace hooking cannot reach the check at all** (raw `svc` syscalls,
|
||
`init_array`-early detection), what the next layer up and down can actually do, and when escalating
|
||
is the wrong answer.
|
||
- Working **protocols that are not REST** — protobuf without a schema, gRPC, QUIC/HTTP3 — and
|
||
native-side certificate pinning that ignores the system trust store.
|
||
- Working **from the phone itself**: MT Manager's edit/repack/sign flow and its APK MCP surface,
|
||
LSPosed Manager, and on-device data inspection, alongside the PC toolchain rather than instead of it.
|
||
- Telling **Java2C apart from an extraction shell** before spending hours hunting a decrypted DEX that
|
||
does not exist at any point in the process lifetime — the code was compiled into a `.so`.
|
||
- Handling a build that arrives as a **split APK / App Bundle set**: reading the set off a device,
|
||
signing every member with one keystore for `pm install-multiple`, or merging code/native members
|
||
into a standalone APK when that is legal.
|
||
- Working a **real Dex VMP** with the known-plaintext differential — which links can be automated and
|
||
which cannot, what a compiled fixture can and cannot reach, and how to *prove* a derived
|
||
private-opcode table instead of asserting one.
|
||
- **Publishing what it learns without publishing the target** — a scanner that reports identity
|
||
shapes with their context, an explicit list of what must *not* be redacted (tools, libraries,
|
||
protocol fields, CVEs, hardening products, public crackmes) because redacting those destroys the
|
||
reusable part, and exit codes that gate a commit.
|
||
- Reading a **precedent** before repeating work this repository already converged on: the positive
|
||
half of the record, with the route including its dead ends, a grade on every assertion, and the
|
||
files the case says to write back to.
|
||
|
||
## Structure
|
||
|
||
`SKILL.md`, `references/` and `scripts/` are all inside the skill directory, `skills/apk-reverse/`.
|
||
Everything at the repository root is maintenance tooling shared across skills, not part of an
|
||
installed skill.
|
||
|
||
```
|
||
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
|
||
server-config-and-updates.md
|
||
the most common shape of "ad" and the one usually mis-diagnosed:
|
||
the server supplies UI the client renders (launch screen, popup,
|
||
announcement, tab set). The two-layer fetch that proves it, how to
|
||
find the config DTOs by 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
|
||
byte-level-patching.md equal-length byte edits: why they beat method rebuilding (measured),
|
||
locating an instruction's exact offset without scraping listings,
|
||
the instruction width traps that desynchronise a decode, neutralise
|
||
a branch vs redirect it, dex header integrity field order, and the
|
||
verifier's move-result rule
|
||
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.
|
||
Begins with the snapshot-decoding front end it depends on (aotopsy or
|
||
blutter) because the pool listing is an input, not something this skill
|
||
produces itself
|
||
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,
|
||
**"not on PATH" is not "not installed"**, and which signer to use
|
||
long-task-discipline.md live record, conclusion grading, drift control, timeout and
|
||
wait calibration, deliverable-form drift, captures-you-never-looked-at,
|
||
long-context decay, 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
|
||
desensitization-and-leak-scans.md
|
||
publishing discipline: what must be desensitized and what must stay,
|
||
the do-not-anonymize list, the leak scanner and its exit states, and
|
||
the entry-point file as a prompt surface
|
||
precedents/ the positive case library: route including dead ends, a grade per
|
||
assertion, measured pit-falls, and the write-back checklist
|
||
routing.md the on-demand inventory: every reference with when to load it, every
|
||
script with what it does, and a mirror of the symptom index
|
||
rasc-and-droidsaw.md the Rust re-implementation of the ASC indexer: measured speedup and
|
||
identical class sets, the enum shape where it silently drops bodies,
|
||
and how to build and verify it
|
||
evidence-summary.md the condensation that ships with the skill: capability, one-line
|
||
conclusion, strength, and the evidence you can actually open in an
|
||
installed copy
|
||
|
||
../evals/ NOT a spec directory either, but the location the Agent
|
||
Skills guidance recommends: `evals.json` holds the
|
||
with-skill / without-skill cases this skill has **not** run,
|
||
with the method for running them written into the file
|
||
../evidence/ NOT a spec directory: the machine-readable companions to the evidence
|
||
summary reference above -- `capability-matrix.json` (the same rows with
|
||
more fields), `tested-tool-versions.json` (versions and the probe behind
|
||
each), `known-limitations.md` (the installer-facing limit list). Shipped
|
||
inside the skill so an installed copy can answer "was this verified, and
|
||
how strongly" without the repository
|
||
pitfalls.md the failure catalogue -- read before building
|
||
advanced-unpacking.md the dump landed but the bodies are empty: extraction-shell diagnosis by
|
||
trivial-body ratio, FART-style active invocation and why its classic hooks
|
||
died on Android 12-16, code_item splicing, the root-side dump for when
|
||
frida itself is refused, and the honest VMP boundary
|
||
lsposed-and-modules.md the repack is refused, so deliver a system-level hook module instead:
|
||
module anatomy, a gradle-free build chain, scope configuration and how to
|
||
verify injection, and the layer a Java module cannot reach
|
||
emulation-and-rpc.md call the routine instead of reading it: Unidbg/Unicorn emulation and its
|
||
environment-filling cost, versus service-ifying a live function over Frida RPC
|
||
native-dbi-and-deobfuscation.md
|
||
OLLVM shapes, Frida-Stalker traces, the trace-to-CFG route, the
|
||
Stalker/QBDI/emulation decision, and two measured boundaries (a follow that
|
||
delivers no events, and a crash from following a hot libc export)
|
||
protocol-reverse.md protobuf without a schema, schema recovery from decompiled code, gRPC frame
|
||
capture, the QUIC/HTTP3 limit, and native-side certificate pinning
|
||
kernel-and-environment-hardening.md
|
||
userspace hooking provably cannot reach the check: raw `svc`, init_array-early
|
||
detection, what each root scheme hides, the kernel-route map with its version
|
||
gate, and when to stop escalating
|
||
hardware-watchpoints.md invisible memory watchpoints on arm64: the hardware debug slots, the
|
||
ptrace route with its honest `TracerPid` ledger, the 8-byte-window
|
||
alignment rules, the traps (per-thread slots, signal forwarding, the
|
||
breakpoint step-over dance), and the control run every hit is owed
|
||
ete-diff-tracing.md hardware execution tracing (CoreSight ETE/TRBE) plus the differential
|
||
method: the five gates and how each fails silently, per-thread `cs_etm`
|
||
capture, and two one-bit-different runs turned into the input-dependent
|
||
block set — with the two open flattening families' state machines
|
||
read from source, and the xvmp IR-VM shape
|
||
hardware-sampling-and-crash.md
|
||
data flow without a known address (SPE: PC + data address per
|
||
sample), crash forensics when the stack is not trustworthy (BRBE
|
||
branch records / the ETE tail), and runtime addresses turned into
|
||
symbols and static offsets (`trace_addr2sym.py`, PIE vaddr/file-offset
|
||
rules)
|
||
native-vm-recovery.md recovering a native VM: existence signals, the behavioural table
|
||
that separates stacked CFF/VM dispatchers, relocation-recovered
|
||
dispatch tables, register anchoring with triple verification,
|
||
nested-VM segment stacks, STORE-backward operand slicing with byte
|
||
provenance, and the clean-re-VM validation loop — absorbed from two
|
||
public write-ups
|
||
kernel-event-injection.md the driver as your hands inside the target: watchpoints armed from the
|
||
kernel with no tracer relationship, register rewrites at the trap, the
|
||
primitive map per route (KPM/LKM/eBPF/Zygisk), and the composition
|
||
recipes that stack this layer with the other two
|
||
on-device-tooling.md working from the phone itself: MT Manager edit/repack/sign and its APK MCP,
|
||
LSPosed Manager, Termux+frida, on-device data inspection
|
||
java2c-and-jni-sinking.md Java2C and JNI sinking, the two hardening shapes most easily confused
|
||
with an extraction shell: the table that separates landing shell /
|
||
extraction shell / VMP / Java2C / JNI sinking, why the code is in the
|
||
`.so` and *never* in a dumped dex, and why a `Java_*` symbol search
|
||
comes back empty (dynamic registration, `-fvisibility=hidden`)
|
||
split-apk.md App Bundle / split APK sets: what the set is, pulling it off a device,
|
||
merging into one APK vs signing the set as a unit, the install refusals
|
||
and what each means, and making an installable fixture from a pulled set
|
||
vmp-differential-analysis.md
|
||
the known-plaintext differential for a real Dex VMP: which links can be
|
||
automated and which cannot (the upload is the bottleneck), the coverage a
|
||
compiled fixture can reach, how to *prove* a derived private-opcode
|
||
table, smali generation, and when the route is closed
|
||
coverage-and-limits.md the claim ladder applied to the skill itself: the evidence behind each
|
||
covered item, the dependencies this skill does not ship, and what was
|
||
never exercised
|
||
handoff-boundaries.md where this skill ends and another discipline begins: the JNI form
|
||
table, the packer-versus-loader split, and what "verified" means for
|
||
each of the four deliverable forms
|
||
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)
|
||
dexutil.py dependency-free dex reader: structural walk + exact instruction
|
||
decode, dex header recompute/verify (correct checksum/signature
|
||
order), branch-target and operand helpers. Library shared by the
|
||
dex scripts, also runs standalone to dump one method with offsets
|
||
dex_find_insn.py locate an instruction by decoded semantics and print its exact byte
|
||
offset with context and both sides of any branch -- how you find a
|
||
patch site instead of guessing offsets
|
||
dex_patch_bytes.py equal-length byte patches from a JSON spec: semantic match, polarity
|
||
pin via expect_next, equal-length enforcement, verifier check, dex
|
||
header recompute, re-decode to prove it landed (--dry-run first)
|
||
dex_check_verifier.py tier-3 check: does any conditional branch target a move-result
|
||
(bypassing its producer)? Compares two builds and separates
|
||
pre-existing findings from regressions your patch introduced
|
||
coldstart.py cold-launch capture: timed screenshot burst + logcat signals +
|
||
installed-build facts + launch timing, and warns when the foreground
|
||
activity is not your app
|
||
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 rewriter (for changes that need new instructions)
|
||
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, keep META-INF/services/, write a
|
||
4-byte-aligned archive (resources.arsc STORED+aligned), sign, verify;
|
||
also split APK / App Bundle sets: inventory, sign every member with one
|
||
keystore, or merge code/native members into a standalone APK
|
||
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
|
||
spawn_patch_detach.py spawn under a Frida probe, detach, then launch and capture: under
|
||
spawn mode the Activity stack often never comes up, and memory
|
||
writes survive detach while hooks do not
|
||
hook_patch_only.js the minimal probe for spawn_patch_detach.py — neutralise one native
|
||
death site by offset and report PATCHED
|
||
dex_dump_validate.py dedupe, validate and rank a directory of dumped dex images: sha256
|
||
grouping, header integrity, the trivial-body ratio that separates a real
|
||
dump from an extraction-shell skeleton, and a most-likely-original ranking
|
||
(--trim for page-aligned /proc/<pid>/mem captures)
|
||
dex_mem_scan.py search memory captures for embedded dex images and extract each at the
|
||
size its own header declares -- for a decrypted dex sitting in an
|
||
anonymous mapping no maps entry names
|
||
lsposed_scaffold.py generate a minimal LSPosed/Xposed module project (manifest with the
|
||
xposed meta-data, assets/xposed_init, hook class, gradle-free build notes)
|
||
frida_rpc_serve.py bridge a Frida script's rpc.exports to a local caller with reconnect
|
||
handling, so a live native function can be called rather than reversed
|
||
rpc_template.js the editable companion to frida_rpc_serve.py
|
||
stalker_trace.js instruction-level tracing with Frida Stalker: configurable targets,
|
||
trigger selection, the event stream, and output-size rules
|
||
stalker_report.py reduce a stalker_trace.js log to block histograms and call sequences,
|
||
with an explicit diagnostic for the measured zero-event case
|
||
mt_mcp_probe.py probe MT Manager's on-device APK MCP (Streamable HTTP, port 8787):
|
||
JSON-RPC handshake plus the grouped tool inventory
|
||
java2c_probe.py collect the evidence that separates Java2C from an extraction shell, a
|
||
VMP and ordinary JNI sinking: native density and stub ratio from the dex,
|
||
JNI_OnLoad / dynamic registration / toolchain strings from the `.so`,
|
||
each item labelled strong/medium/weak
|
||
protobuf_decode_raw.py schema-free protobuf decode: hex / file / stdin to a JSON tree, every
|
||
length-delimited field kept as a candidate set with ties labelled rather
|
||
than guessed, plus a byte-exact re-encode to check a round trip
|
||
vmp_diff_harness.py build a labelled opcode-coverage fixture, derive a candidate private-
|
||
opcode map from an original/hardened dex pair, verify the comparison in a
|
||
closed loop, and render a restored stream as a smali skeleton
|
||
kernelsu_syscall_mask.py generate a KernelSU/APatch syscall-masking scaffold: an installable
|
||
userspace module skeleton plus KPM/LKM/eBPF kernel-side templates, each
|
||
with its version gate and an explicit unverified label
|
||
hw_watch.py invisible memory watchpoints / execution breakpoints on arm64: plan
|
||
(WCR/BCR encoders with the 8-byte-window rules), emit (a self-contained
|
||
on-device C helper — compiles clean as a static aarch64 ELF), gates,
|
||
run (build+push+stream hits) and decode (module+offset per hit)
|
||
trace_diff.py differential trace reduction: reduce one trace to runs+histogram, or
|
||
diff two traces into first-divergence / divergent regions / blocks
|
||
unique to each side — producer-agnostic (Stalker logs or any
|
||
address-per-line stream, incl. decoded cs_etm)
|
||
ete_capture.py CoreSight ETE capture with gates first: the five gates (kernel era,
|
||
coresight sysfs, cs_etm PMU, perf-with-CORESIGHT, permission), the
|
||
perf/OpenCSD build recipe, and the per-thread record wrapper whose
|
||
`--control` validation run must pass before any real capture
|
||
kernel_instrument_scaffold.py
|
||
generate the kernel-side instrumentation scaffold: KPM/LKM watchpoint +
|
||
register-rewrite templates and an honest userspace carrier, each with
|
||
its gate and a `NOT COMPILED AND NOT LOADED` banner that `verify`
|
||
enforces
|
||
trace_addr2sym.py runtime addresses -> symbols + static offsets, zero-dependency
|
||
ELF64 (PT_LOAD vaddr/file-off conversion, .dynsym/.symtab, stripped
|
||
fallback): accepts hw_watch hits, trace mod+off keys or raw address
|
||
lists; host-verified against a real cross-compiled aarch64 .so
|
||
vm_handler_slice.py de-virtualize an interpreter trace: dispatcher detection, slicing
|
||
into handler segments at dispatch entries, signature clustering, and
|
||
the guest-opcode trajectory via an optional handler->opcode map —
|
||
calibrated against the open xvmp IR virtualizer, with a nested-loop
|
||
diagnostic for VMs called from VM code
|
||
rasc_build.py build and verify rasc, the Rust ASC re-implementation:
|
||
--check what is present, --build clone plus cargo, --verify an APK
|
||
against droidasc and fail on any class-set difference
|
||
scan_leaks.py scan a repository for target identity before publishing it: bundle ids
|
||
in manifest / `pm` / `ps` contexts, serial-shaped tokens, PATs, inline
|
||
appkey assignments, literal endpoints, host user paths. Exemptions for
|
||
everything that must stay (tools, libraries, CVEs, hardening products,
|
||
public crackmes, placeholders), findings carry their context,
|
||
`--show-exempt` prints why a hit was suppressed, exit 0/1/2
|
||
svc_scan.py name the syscall behind an inline `svc` and the segment it sits in,
|
||
which decides whether a libc-level hook can observe the call at all;
|
||
`--context` shows neighbours because a byte scan also matches data
|
||
anti_detect_probe.js observer-only Frida probe (patches nothing): path/loader/thread/kill
|
||
hooks with caller module + offset, an environment self-report
|
||
(`TracerPid`, frida-named mappings), and live streaming so a sub-second
|
||
self-destructing target still yields evidence
|
||
```
|
||
|
||
The repository also carries an **executable** test layer, which is a different thing from the
|
||
evidence record: `tests/` asserts what the scripts do (unit, CLI contract, no-device
|
||
integration) and `tests/benchmark.md` records what a route did on a real target. `tests/README.md`
|
||
states the split, and `.github/workflows/ci.yml` runs the gates plus the suite.
|
||
|
||
## Install
|
||
|
||
This repository is a **skills repository**: the skill lives at `skills/apk-reverse/`, which is the
|
||
layout the `skills` CLI resolves, and it is installed by name rather than by copying a directory:
|
||
|
||
```
|
||
npx skills add newliver666/apk-reverse # install every skill in the repo
|
||
npx skills add newliver666/apk-reverse --list # list what is here, install nothing
|
||
npx skills add newliver666/apk-reverse --skill apk-reverse -y
|
||
npx skills use newliver666/apk-reverse@apk-reverse # use it once, without installing
|
||
```
|
||
|
||
The CLI symlinks the skill into your agent's skills directory by default (`--copy` makes independent
|
||
copies instead), and `-g` installs for every project rather than the current one. With one skill in
|
||
the repository, `--skill apk-reverse` is redundant today; it is written out here because it is what
|
||
selects a single skill once a second one exists.
|
||
|
||
Once installed, 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, and no build step.
|
||
|
||
## Requirements
|
||
|
||
Nothing is mandatory; each script checks what it needs. `skills/apk-reverse/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=<dir>;<dir> # Windows, e.g. an SDK or project-local tools dir
|
||
export APKREV_TOOLS=<dir>:<dir> # 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 |
|
||
| **`droidasc`** (ASC) (**optional but strongly recommended — install this first**) | whole-APK cross-reference index: `findrefs` / `listclass` / `getclass` / `getmanifest`. One `pip install droidasc`, no JVM, no SDK, no index build. Turns "which of N thousand classes mentions this string" into a sub-second query, and it is the only route to a class whose name R8 mangled. **This is the tool an agent should reach for before any full decompile** — see `skills/apk-reverse/references/toolchain.md` §droidasc (ASC) — ask an APK "who references this?", in one query |
|
||
| `ddc` (optional but strongly recommended) | single-binary dex→Java decompiler with query subcommands (`info`, `findrefs`, `strings --with-locations`, per-class decompile). No JVM. **Reads** what ASC **locates**; also reports package identity reliably — see `skills/apk-reverse/references/toolchain.md` §ddc — dex-to-Java with query subcommands (worth adopting) |
|
||
| **`dexsql`** (optional but strongly recommended) | single-binary **SQL over the whole dex**, plus the APK manifest and `resources.arsc`, plus an MCP server. Its class-definition set matched `rasc`/`droidasc`/`ddc` exactly on four archives, and it answers what the other two cannot express: callers of a callee, field accesses, numeric constants, debug lines, annotations, hiddenapi flags. Reach for it when the question is a **join or an aggregate** — see `skills/apk-reverse/references/dexsql-sql-query-layer.md` |
|
||
| `baksmali` / `smali` + `dexlib2` jars | disassembly, assembly, surgical patching |
|
||
| JDK (`javac`, `java`) | building/running the dexlib2 patcher; also provides `keytool`/`jarsigner` |
|
||
| Android SDK build-tools (`aapt`, `zipalign`, `apksigner`) | manifest info, alignment, signing. **`apksigner` is the signer to use** — `jarsigner` rewrites the archive and breaks the alignment Android R+ requires |
|
||
| `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 |
|
||
|
||
None of these need to be on `PATH`: every script accepts an explicit path for the
|
||
tools it shells out to, and `skills/apk-reverse/references/toolchain.md` covers finding
|
||
an install that `PATH` does not know about (the common case for `apksigner` and
|
||
`keytool`).
|
||
|
||
## Read this first
|
||
|
||
**This project is published for learning, research and authorized security testing only.** It ships
|
||
no exploit payloads, no target data and no third-party binaries — it is a method, a set of scripts
|
||
and an evidence record. You are responsible for having the right to analyze whatever you point it at;
|
||
see **Disclaimer** at the end of this file.
|
||
|
||
`skills/apk-reverse/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.
|
||
|
||
What it covers, and what it deliberately does not, is stated at the top of `SKILL.md`
|
||
under **Coverage**. The short version: Android only (no iOS), and deep on the layers
|
||
that have been worked through for real — dex patching, repacking, packers and custom
|
||
loaders, native tamper response, and Flutter/Dart AOT. An extension pass added a second
|
||
tier of documented routes: **module-side delivery** when a repack is blocked,
|
||
**extraction-shell recovery** and its VMP boundary, **emulation and live RPC** for
|
||
calling rather than reading, **instruction-level tracing** against OLLVM, **protocol
|
||
reversing** beyond REST, the **kernel-side route** map for when userspace hooking is
|
||
provably out of reach, and **on-device tooling**. A **benchmark pass** then put public
|
||
targets under those routes (`tests/benchmark.md`): it added **Java2C discrimination** (the
|
||
misdiagnosis that sends an agent hunting a decrypted DEX that never exists), **split APK /
|
||
App Bundle handling**, **schema-free protobuf decoding**, a **Dex-VMP differential** harness,
|
||
and **kernel-module templates with their version gates** — and it corrected two earlier claims
|
||
whose measurements disagreed with them. A **hardware pass** then added the layer under the
|
||
software tooling: **hardware watchpoints** with their honest `TracerPid` ledger, **ETE/TRBE
|
||
differential tracing** with its five gates, and **kernel-side injection templates** — every
|
||
host-checkable piece measured, every device-bound piece labelled `unverified` until a gate
|
||
opens. Unity/IL2CPP logic recovery,
|
||
React Native/Hermes bytecode internals, and defeating a server-side authority are **not**
|
||
covered, and the skill is written to say so and stop rather than apply the nearest
|
||
documented procedure to a target it was not written for.
|
||
|
||
Four qualifications that the Coverage section states in full and that belong here too:
|
||
|
||
- **Flutter/Dart AOT analysis has a dependency.** The workflow begins at a pool listing
|
||
(`pp.txt`-class output). Producing that needs a snapshot-decoding decompiler — aotopsy (a static
|
||
binary, no toolchain) or blutter (built from source, ~80 s) — and this repository does not contain
|
||
one. It is named as a prerequisite rather than left implicit.
|
||
- **Not every claim in this repository has a run behind it.** `docs/tool-verification/` records
|
||
what was actually measured, on which target, and with which independent cross-check; anything not
|
||
covered there is documented from experience and should be read as *inferred*, per this skill's own
|
||
claim ladder.
|
||
- **The extension pass is recorded separately and is mostly *inferred*.** Its evidence lives in
|
||
`docs/tool-verification/EXTENSION-*.md`, one file per topic, with its own strength note. The
|
||
common shape there is *the tool was measured, the route was not* — so read those files before
|
||
treating any of the newer documents as a verified path.
|
||
- **The benchmark pass is recorded per row, with that row's own strength.** `tests/benchmark.md`
|
||
names each public target, the scripts the row exercises, what actually happened (including the
|
||
rows that failed and the rows nobody ran), and how strong the evidence is. Rows marked
|
||
`unverified` are statements about the evidence in this repository, not about the mechanism.
|
||
|
||
## Repository maintenance
|
||
|
||
**Changing something here? Start with [CONTRIBUTING.md](CONTRIBUTING.md).** It lists the gates, what a
|
||
green run does and does not establish, the sync rules a change has to keep (a new reference must be
|
||
named in the inventory, renaming a heading breaks every section reference that pointed at it), and the
|
||
two failure modes to watch for when editing the gates themselves.
|
||
|
||
Four tools live at the root and are not part of the installed skill:
|
||
|
||
```
|
||
check_repo.py every skill discovered, frontmatter valid, scripts runnable,
|
||
documented paths resolve, README paths explicit and existing,
|
||
and -- on the tracked surface only -- no target identity
|
||
(delegates the rules to skills/apk-reverse/scripts/scan_leaks.py
|
||
so there is one place to argue with the exemption list)
|
||
check_refs.py every cross-reference that names a section of another
|
||
document reaches a real heading in that document
|
||
check_routing.py the on-demand inventory still matches the entry point: the
|
||
symptom mirror agrees with SKILL.md, every reference file is
|
||
named in skills/apk-reverse/references/routing.md, and every
|
||
script is too
|
||
check_commands.py every command a document tells you to run is checked against
|
||
the script's own argparse table -- a documented flag that does
|
||
not exist is a drift the anchor checks cannot see
|
||
check_budget.py keep the always-loaded part from creeping: SKILL.md's whole
|
||
body (index lines included, because they load too) measured in
|
||
lines and tokens, index-row length, long files with no
|
||
navigable head, and hedged rules reported as a trend
|
||
build_scripts.py audit for machine-specific leftovers (absolute paths, credentials)
|
||
```
|
||
|
||
Consistency has a natural counter-pressure -- a broken path fails loudly, and someone fixes it.
|
||
Bloat has none, which is why the third tool exists: every pass adds a reference, an index row and
|
||
a coverage claim, and without a measurement nothing in the repository notices.
|
||
|
||
### What a green gate establishes, and what it does not
|
||
|
||
The checks above prove **consistency**, not truth. `check_repo.py`, `check_refs.py`,
|
||
`check_routing.py`, `check_commands.py` and `check_budget.py` passing means paths resolve, the
|
||
inventory is complete, documented flags exist, the entry point has not crept, and no target
|
||
identity reached the tracked surface. They say nothing about whether a *claim* is right: a
|
||
reference can cite a heading that exists and still describe a mechanism nobody ran, and a budget
|
||
gate cannot tell a load-bearing file from an adjacent one. Those are the two questions a reader
|
||
actually has, and the only things that answer them are the strength labels and the evidence record.
|
||
|
||
### Integrating an outside contribution
|
||
|
||
**Fail closed.** If the contribution's fit with what this repository claims to cover, its
|
||
side effects, or the verification needed to establish either one is **materially unresolved**, do
|
||
not merge it and do not ship it: leave it pending, narrow it, or decline it. Recording the open
|
||
question is a valid outcome; converting it into "none" or "probably fine" is not.
|
||
|
||
Four things that are not approval, however good they look: **a green CI run, a finished
|
||
implementation, upstream popularity, and the absence of an objection.** They establish that code
|
||
runs and that nobody has complained, which is a different statement from *this belongs here and its
|
||
limits are known*.
|
||
|
||
Two consequences that decide most of these cases:
|
||
|
||
- **Labels over volume.** A large contribution whose every claim carries a correct `observed` /
|
||
`inferred` / `unverified` / `unknown` label is easier to accept than a small one that presents
|
||
assumptions as findings, because the first one can be checked. Ask what was actually run, and
|
||
treat "no claim was upgraded to `observed` by reading about it" as a good sign rather than a
|
||
weakness.
|
||
- **Unresolved is `unknown`, never absent.** A capability that has not been exercised here is
|
||
`unknown` -- not excluded, not disqualified, and also not merged on the assumption that it
|
||
probably works. Where a contribution ships a device-bound or toolchain-bound half, the mergeable
|
||
part is the half that was measured plus an explicitly labelled template for the rest, with the
|
||
gate that says what would have to exist for the other half to become `observed`.
|
||
|
||
`tests/benchmark.md` holds the **regression matrix**: dimension -> public target -> the scripts the
|
||
row exercises -> measured result -> strength label. It is the checklist to re-run before trusting
|
||
any claim under `docs/tool-verification/`. Samples are downloaded into `tools/_work/` and are never
|
||
committed, so each row names its public source and records the hash it was run against.
|
||
|
||
`docs/tool-verification/` is not part of the installed skill either. It is the evidence record
|
||
for one measurement pass against a real target: what each script actually did, which independent
|
||
method confirmed it, which defects were found, and which scenarios the target could not exercise.
|
||
It exists so the **Coverage** claims in `SKILL.md` can be checked against runs instead of trusted,
|
||
and so the gaps are written down where the next person will find them.
|
||
|
||
---
|
||
|
||
Proudly supported by the [LINUX DO](https://linux.do) community.
|
||
|
||
## Disclaimer
|
||
|
||
**For learning, research and authorized security testing only.** Every script, reference and
|
||
recorded result in this repository exists to explain *how* Android application analysis works, so
|
||
that practitioners can reason about the tools they already own. Nothing here is a service, a
|
||
product, or an endorsement of any particular use.
|
||
|
||
- **Authorized targets only.** Use this on applications you own or have been explicitly permitted to
|
||
analyze, on public CTF/challenge material, or in a sandbox you control. Analyzing software you have
|
||
no right to analyze may be unlawful where you live, and that determination is yours to make, not
|
||
this repository's.
|
||
- **No warranty, no fitness for any purpose.** The material is provided *as is*, without warranty of
|
||
any kind. Results are recorded as they were measured on one machine at one time; nothing here
|
||
promises that a route will work on your target, your device, your toolchain or today's app version.
|
||
- **Verify before you trust; back up before you act.** Several scripts modify artifacts (dex, APK,
|
||
`.so`, stored app data) and some operate on a rooted device. Keep your own copies, work on
|
||
duplicates, and read `SKILL.md`'s gates before running anything against something you care about.
|
||
- **Your use is your responsibility.** The authors and contributors accept no liability for any loss,
|
||
damage, legal consequence or service interruption arising from the use or misuse of this
|
||
repository, and are not affiliated with, endorsed by, or acting on behalf of any application,
|
||
vendor or platform it may be used to examine.
|
||
- **Test data is not distributed here.** Samples, dumps and device artifacts are deliberately absent
|
||
from the tree (`.gitignore` excludes them) and live only in a local, ignored workspace. Anything you
|
||
obtain to follow along is yours to keep safe and to delete when you are done with it — follow your
|
||
local rules and the terms that came with the sample. What this repository *does* publish is the
|
||
method and the evidence, with all target identity removed.
|
||
- **No affiliation.** Names of tools, libraries, hardening products and public challenge targets
|
||
appear only to make the material reusable; they belong to their respective owners and this project
|
||
is not connected to them.
|