A decision file for when the app fights back or the tool cannot run here: separating detection from a broken environment, choosing between workaround / change-route / accept by cost instead of escalating, recognising an environment where dynamic analysis genuinely cannot work (and switching to static rather than proving it impossible), and keeping 'blocks my analysis' separate from 'blocks the deliverable'.
199 lines
13 KiB
Markdown
199 lines
13 KiB
Markdown
# 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.
|
|
|
|
## 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 decision tree, workflow, hard 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
|
|
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
|
|
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.
|
|
|
|
| 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.
|