diff --git a/README.md b/README.md index b302316..1d6bcb0 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,9 @@ measurements. - 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 that the - trivial-body ratio is *bimodal rather than a threshold*: it reads ~100% for a bare `return-void` - skeleton, but is blind to nop-cleared bodies, throw stubs and partial extraction, which are the - shapes real shells actually use. Then knowing which recovery route applies, including the root-side - dump for when `frida` itself is refused by the target. +- 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 @@ -94,8 +92,7 @@ measurements. - 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`, and the - shapes are separated by a measured native-density difference of roughly 2000x, not by intuition. + 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. @@ -426,14 +423,13 @@ provably out of reach, and **on-device tooling**. A **benchmark pass** then put 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, replacing the trivial-body *threshold* with a measured bimodal result and reopening -a VMP verdict that a hand-written opcode table had got wrong. Unity/IL2CPP logic recovery, +and **kernel-module templates with their version gates** — and it corrected two earlier claims +whose measurements disagreed with them. 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. -Three qualifications that the Coverage section states in full and that belong here too: +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 @@ -454,16 +450,24 @@ Three qualifications that the Coverage section states in full and that belong he ## Repository maintenance -Three tools live at the root and are not part of the installed skill: +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 check_refs.py every cross-reference that names a section of another document reaches a real heading in that document +check_budget.py keep the always-loaded part from creeping: narrative lines in + SKILL.md (index lines counted separately, because one line per + bundled file is the price of discoverability), index-row length, + and conclusions restated outside their legitimate homes 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. + `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 diff --git a/check_budget.py b/check_budget.py new file mode 100644 index 0000000..f2ca940 --- /dev/null +++ b/check_budget.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Budget gate for the apk-reverse skill: keep the always-loaded part from creeping. + +`check_repo.py` proves the repository is *consistent* (paths resolve, indexes are complete). +This proves it is not *bloating*. Those are different failures, and the second has no natural +counter-pressure: every pass adds a reference, an index row and a coverage claim, and nothing in +the repository notices. + +What is measured, and why each measure is shaped the way it is: + + 1. **Content lines vs index lines.** An index row is the price of discoverability: one line per + bundled file, and this skill ships 38 references and 48 scripts, so ~100 of its lines are + structural. Counting them against a prose budget would push the skill to drop files it needs, + so the budget applies to the *narrative* lines — rules, workflow, gates, classification — + which is the part that actually competes for attention. Anthropic's Agent Skills guidance + says "keep SKILL.md under 500 lines"; its own `skill-creator` ships 480 and `mcp-builder` 237. + Here the narrative budget is 460, and the total is reported for context, not as a verdict. + 2. **Index row length.** The index tables are the largest part of SKILL.md and the easiest to + grow by accident: each new entry is written by someone who knows the detail, and the detail + lands in the row. An index row says *when to load the file*; the file says what is in it. + 3. **Restated conclusions** (reported as notes, never as failures). A conclusion legitimately + appears in the benchmark matrix, in the evidence record, and in the coverage statement — + those three exist to state results. The check flags a concept that has spread *beyond* those + homes, because that is when a future correction has to hunt for every copy. It is a note + rather than a failure precisely because the three legitimate homes make a high count normal. + 4. **Reference discoverability.** Every reference must be reachable from SKILL.md or nothing will + ever load it. (`check_repo.py` also enforces this; the duplication is deliberate, because a + dead reference is invisible in a way a broken path is not.) + +Usage: + python check_budget.py # report; exit 1 if a hard limit is exceeded + python check_budget.py --strict # treat every warning as a failure + python check_budget.py --json +""" +import argparse +import glob +import json +import os +import re +import sys + +ROOT = os.path.dirname(os.path.abspath(__file__)) +SKILL_DIR = os.path.join(ROOT, 'skills', 'apk-reverse') +SKILL = os.path.join(SKILL_DIR, 'SKILL.md') +REFS = os.path.join(SKILL_DIR, 'references') + +CONTENT_LINES_OK = 460 # narrative budget +CONTENT_LINES_HARD = 560 # past this, something is structurally wrong +INDEX_ROW_SOFT = 250 # chars of description per row +INDEX_ROW_HARD = 400 # a row longer than this is a paragraph, not an index entry +INDEXES = ('Symptom index', 'Reference index', 'Script index') + +# Concepts that have been restated once already. Reported, not enforced -- see the docstring. +RESTATED = { + 'trivial-body is bimodal': [r'bimodal'], + 'Java2C native density (2000x)': [r'82\.76', r'2,?000x'], + 'Stalker exclusion keeps the process alive': [r'keeps? the (?:target|process) alive'], + 'Dex-VMP coverage fixture (218/224)': [r'218 of 224', r'218/224'], + 'ezAndroid is JNI sinking, not a VMP': [r'JNI sinking'], + 'KernelSU userspace cannot change a syscall': [r'userspace module.{0,60}cannot change'], + 'protobuf proto3 explicit zero': [r'proto3.{0,40}(?:explicit )?zero'], +} +# Documents whose job is to state results; a concept appearing here is not drift. +LEGITIMATE_HOMES = ('tests/benchmark.md', 'docs/tool-verification/') + + +def skill_lines(): + with open(SKILL, encoding='utf-8') as fh: + return fh.read().splitlines() + + +def index_rows(lines): + """(index line count, [(section, first-cell, desc-length)] for over-long rows).""" + n = 0 + long_rows = [] + cur = None + for ln in lines: + if ln.startswith('## '): + cur = ln[3:].strip() + continue + if not (cur and any(cur.startswith(i) for i in INDEXES) and ln.startswith('|')): + continue + if set(ln) <= set('|-: '): + continue + cells = [c.strip() for c in ln.strip().strip('|').split('|')] + if len(cells) < 2: + continue + n += 1 + if cells[0].lower() in ('file', 'script', 'what you observe', 'status'): + continue + desc = cells[-1] + if len(desc) > INDEX_ROW_SOFT: + long_rows.append((cur, cells[0][:44], len(desc))) + return n, long_rows + + +def check_size(lines): + n_idx, _ = index_rows(lines) + content = len(lines) - n_idx + if content <= CONTENT_LINES_OK: + lv, msg = 'ok', 'SKILL.md %d narrative lines + %d index lines = %d total (budget %d narrative)' % ( + content, n_idx, len(lines), CONTENT_LINES_OK) + elif content <= CONTENT_LINES_HARD: + lv, msg = 'warn', ('SKILL.md narrative is %d lines, over the %d budget by %d (%d index lines ' + 'excluded). Move detail into references/ and leave a pointer.' % + (content, CONTENT_LINES_OK, content - CONTENT_LINES_OK, n_idx)) + else: + lv, msg = 'fail', 'SKILL.md narrative is %d lines, past the hard limit of %d' % ( + content, CONTENT_LINES_HARD) + return lv, msg, n_idx, content + + +def check_index_rows(lines): + _, long_rows = index_rows(lines) + out = [] + for section, key, size in long_rows: + hard = size > INDEX_ROW_HARD + out.append(('warn' if hard else 'warn', + '%s: row for %s is %d chars (soft limit %d)%s' % + (section, key, size, INDEX_ROW_SOFT, + ' -- that is a paragraph, not an index entry' if hard else ''))) + return out + + +def check_restated(): + notes = [] + files = (glob.glob(os.path.join(REFS, '*.md')) + + glob.glob(os.path.join(ROOT, 'docs', 'tool-verification', '*.md')) + + [SKILL, os.path.join(ROOT, 'README.md'), os.path.join(ROOT, 'tests', 'benchmark.md')]) + corpus = {} + for f in files: + if os.path.isfile(f): + try: + rel = os.path.relpath(f, ROOT).replace('\\', '/') + corpus[rel] = open(f, encoding='utf-8').read() + except OSError: + pass + for label, pats in RESTATED.items(): + hits = [p for p, t in corpus.items() if any(re.search(x, t, re.I) for x in pats)] + # files whose job is to state results do not count as drift + drift = [h for h in hits if not any(h.startswith(l) for l in LEGITIMATE_HOMES)] + if len(drift) > 3: + notes.append('restated in %d non-record files: %s -- %s' % + (len(drift), label, ', '.join(sorted(d.split('/')[-1] for d in drift)))) + return notes + + +def check_discoverable(): + body = open(SKILL, encoding='utf-8').read() + out = [] + for f in sorted(glob.glob(os.path.join(REFS, '*.md'))): + name = os.path.basename(f) + if name not in body: + out.append(('fail', 'references/%s is never mentioned in SKILL.md -- nothing will ' + 'load it' % name)) + return out + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument('--strict', action='store_true', help='treat warnings as failures') + ap.add_argument('--json', action='store_true') + args = ap.parse_args() + + lines = skill_lines() + lv, msg, n_idx, content = check_size(lines) + findings = [(lv, msg)] + findings += check_index_rows(lines) + findings += check_discoverable() + notes = check_restated() + + if args.json: + print(json.dumps({'findings': [{'level': l, 'message': m} for l, m in findings], + 'notes': notes, 'index_lines': n_idx, 'content_lines': content}, indent=2)) + else: + for l, m in findings: + print('%s %s' % ({'ok': ' ok ', 'warn': ' WARN ', 'fail': ' FAIL '}[l], m)) + for m in notes: + print(' note %s' % m) + n_fail = sum(1 for l, _ in findings if l == 'fail') + n_warn = sum(1 for l, _ in findings if l == 'warn') + print() + print('== budget: %d failure(s), %d warning(s), %d note(s) ==' % (n_fail, n_warn, len(notes))) + + n_bad = sum(1 for l, _ in findings if l == 'fail' or (args.strict and l == 'warn')) + return 1 if n_bad else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/skills/apk-reverse/SKILL.md b/skills/apk-reverse/SKILL.md index 9c17b3b..e76fc90 100644 --- a/skills/apk-reverse/SKILL.md +++ b/skills/apk-reverse/SKILL.md @@ -95,142 +95,57 @@ vocabulary and a set of reassuring numbers. **Covered, by verified mechanisms:** -- Client-side ads, promos and splash/popup/tab configuration — including the server-issued UI config - that has no SDK to find (`ad-removal.md`, `server-config-and-updates.md`). -- Deciding whether a membership, paywall or feature gate is *client-enforceable* at all, and saying - so plainly when it is not (`membership-and-limits.md`, `account-gates.md`). -- dex-level surgical patching: equal-length byte edits and dexlib2 method rewrites, plus the header, - verifier and alignment rules that decide whether the build loads at all (`dex-patching.md`, +- Client-side ads, promos and splash/popup/tab configuration, including server-issued UI config that + has no SDK to find (`ad-removal.md`, `server-config-and-updates.md`). +- Whether a membership, paywall or feature gate is *client-enforceable* at all — and saying so + plainly when it is not (`membership-and-limits.md`, `account-gates.md`). +- dex-level surgical patching: equal-length byte edits, dexlib2 method rewrites, and the header, + verifier and alignment rules that decide whether the build loads (`dex-patching.md`, `byte-level-patching.md`, `patch-audit.md`). - Repacking, signing, installing, and the install refusals that look like a broken build (`repack-and-sign.md`). -- Packers, custom loaders and code virtualization: identifying them, measuring the validation - boundary, and the routes that survive it (`packers.md`, - `code-virtualization-and-custom-linkers.md`). -- **Java2C** — recognising that the code left the dex entirely, and the evidence that separates it - from an extraction shell, a VMP and ordinary JNI sinking (`java2c-and-jni-sinking.md`, - `scripts/java2c_probe.py`). Measured: `native` declaration density was **0.03-0.04%** in three real - JNI samples against **82.76%** in a Java2C-shaped fixture — roughly 2000x — and the `Java_*` symbol - count matched the dex native-method count 1:1 per ABI. **No library produced by a Dex-to-C compiler - was built here** (no NDK, no host clang, no device clang, WSL unavailable), so every Java2C-specific - criterion is `inferred`; the JNI-sinking side and the discrimination itself are `observed`. +- Packers, custom loaders and code virtualization — identification, the validation boundary, and the + routes that survive it (`packers.md`, `code-virtualization-and-custom-linkers.md`). +- **Java2C versus an extraction shell versus JNI sinking** — the misdiagnosis that sends an agent + hunting a decrypted DEX that never exists (`java2c-and-jni-sinking.md`, `scripts/java2c_probe.py`). - The native layer: `.so` hosts, tamper-triggered self-termination, forged ELF structure, and - neutralising a terminate path without freezing the process (`native-and-so.md`, + neutralising a terminate path without making it not return (`native-and-so.md`, `native-tamper-and-suicide.md`). -- Flutter / Dart AOT: analysing and patching `libapp.so` **given a snapshot dump** — pool-reference - counting, disassembly windows, caller indexing, patch-site choice (`dart-aot.md`). Measured on a - real Dart 3.6.0 build: `dart_disasm.py` decoded identically to capstone (32/32 and 96/96), the - caller index re-derived independently with a symmetric difference of 0, and a specific business - logic site was located end to end. **The snapshot dump is a dependency, not a detail** — see the - next section. +- Flutter / Dart AOT — analysing and patching `libapp.so` **given a snapshot dump** (`dart-aot.md`). - Runtime analysis with Frida, server-side API probing, feature-scoped TLS failures, update and forced-upgrade neutralisation, and the verification discipline everything above rests on. -**Covered by documented routes — added in the extension pass, and mostly *inferred* rather than -measured (read the strength note below before relying on any of these):** +**Also covered, by documented routes — mostly *inferred* rather than measured:** module-side +delivery when a repack is refused (`lsposed-and-modules.md`) · extraction shells and the VMP boundary, +including how to *measure* that the bodies are empty (`advanced-unpacking.md`) · calling a routine +instead of reading it — emulation and Frida-RPC (`emulation-and-rpc.md`) · instruction-level tracing +and de-obfuscation (`native-dbi-and-deobfuscation.md`) · protocol reversing beyond REST +(`protocol-reverse.md`) · what to do when userspace hooking provably cannot reach the check +(`kernel-and-environment-hardening.md`) · split APK / App Bundle sets (`split-apk.md`) · schema-free +protobuf decoding (`protocol-reverse.md`) · Dex-VMP differential analysis +(`vmp-differential-analysis.md`) · kernel-module templates and their version gate +(`kernelsu_syscall_mask.py`) · working from the phone itself (`on-device-tooling.md`). -- **Module-side delivery instead of a repack** — what to do when the client-side logic is reachable but - a rebuilt APK is refused: scaffolding, deployment, scope configuration, the verification log path, - and the honest boundary of what a Java-layer module cannot reach (`references/lsposed-and-modules.md`). -- **Extraction shells and the VMP boundary** — the dump landed but the method bodies are empty: how to - *measure* that instead of guessing, how active invocation (FART-style) is supposed to recover the - bodies, why the classic hook points stopped working on Android 12-16, and where recovery honestly - stops (`references/advanced-unpacking.md`, `scripts/dex_dump_validate.py`). -- **Calling the target instead of reading it** — emulated execution (Unidbg/Unicorn) and live Frida-RPC - service-ification, for when reversing an algorithm costs more than invoking it - (`references/emulation-and-rpc.md`). -- **Instruction-level tracing and de-obfuscation** — the OLLVM shapes, Frida-Stalker traces, the - trace-to-CFG route, and a measured failure mode where `follow` installs but no events arrive - (`references/native-dbi-and-deobfuscation.md`). -- **Protocol reversing beyond REST** — protobuf without a schema, gRPC frame capture, the QUIC/HTTP3 - limit, and native-side certificate pinning (self-compiled BoringSSL / Flutter) - (`references/protocol-reverse.md`). -- **What to do when userspace hooking provably cannot reach the check** — raw `svc` syscalls, - `init_array`-early detection, what each root scheme does and does not hide, the kernel-route map with - its version gate, and a decision table for when escalating is the wrong answer - (`references/kernel-and-environment-hardening.md`). -- **Working from the phone itself** — MT Manager edit/repack/sign, its built-in APK MCP surface, LSPosed - Manager, and on-device data inspection (`references/on-device-tooling.md`, `scripts/mt_mcp_probe.py`). +**Dependencies this skill does not ship** — the Dart AOT workflow needs a snapshot dump this skill +cannot produce, and naming which front end you used matters. Details: +`references/coverage-and-limits.md`. -**Dependencies this skill does not ship — name them before the workflow starts:** +**How strong these claims are:** every item above carries one of three labels — `observed` (a command +and its output exist behind it), `inferred` (reasoned from mechanism or a neighbouring measurement), +`unverified` (assumed, or reported elsewhere). Treat the distinction as load-bearing rather than +cosmetic, and label your own results the same way. The evidence behind each label, the extension +pass's own footing (*the tool was measured, the route was not*), and the list of scripts that have +never been run all live in `references/coverage-and-limits.md`. -- **Dart AOT analysis needs a snapshot dump.** `dart-aot.md`'s workflow begins at `pp.txt`; producing - it requires a snapshot container resolver that this skill does not contain and cannot synthesize. - `dart_pool_strings.py` reports **file** offsets while `pp.txt` and `dart_pprefs.py` speak in **pool** - offsets, and the mapping between those two spaces is not a constant: over the 4,241 strings present - in both, a measured run found 4,237 distinct deltas. Ship a pinned front end (aotopsy — pure Go, no - toolchain) or build blutter (~80 s, needs a C++ toolchain). **Say which one you are using and why, - because the two report different Dart version labels for the same binary.** Do not describe the - object pool as something this skill decodes on its own. +**Not covered — say so rather than improvise:** Unity / IL2CPP logic recovery · React Native / +Hermes bytecode and Cordova internals · iOS of any kind · defeating a server-side authority · an +off-the-shelf unpacker, or an anti-detection arms race · building and shipping a kernel module · a +Stalker trace guaranteed to work on every device. Each of these has a reason and, where one exists, +an evidence boundary — both are in `references/coverage-and-limits.md`. -**How strong these claims are:** the Measured mechanisms below were established by running the -scripts against a real target during the verification pass recorded in -`docs/tool-verification/`. The rest are **inferred** — documented from experience, but the -repository carries no fixture, log or sample that reproduces them (its own `long-task-discipline.md` -reserves *observed* for a claim with an exact command and output behind it). Treat the distinction as -load-bearing rather than cosmetic, and label your own results the same way. - -**Where the extension-pass claims come from.** The documents listed under *Covered by documented -routes* were written from public work (Kanxue threads, upstream project documentation) plus whatever -the extension pass could actually exercise, and each carries its own strength note at the top. Their -evidence is recorded in `docs/tool-verification/EXTENSION-*.md`, one file per topic, in the same -three labels this section uses. Read that record before treating any of them as a verified route: the -common shape there is *the script was measured, the route was not* — a dump validator that ran against -a real shell is measured; the FART recovery loop that would have used it is inferred until someone -runs it end to end on a sample that actively resists. - -**Not covered — say so rather than improvise:** - -- **Unity / IL2CPP logic recovery.** `framework-runtimes.md` identifies the runtime and establishes - that the dex is not the battlefield; it does not carry the IL2CPP equivalent of `dart-aot.md`. - There is no verified recipe here for locating a method inside `libil2cpp.so` plus - `global-metadata.dat`. -- **React Native / Hermes bytecode** and Cordova/hybrid internals, beyond runtime identification and - the generic "find the string, then find what references it" approach. -- **iOS / `.ipa` of any kind.** Every device, signing and packaging instruction here is Android. -- **Defeating a server-side authority.** `server-api.md` exists to determine *who owns a gate*, not - to break an authorization the server performs. -- **An off-the-shelf unpacker, and an anti-detection arms race.** - `references/advanced-unpacking.md` routes the problem: measure the stub ratio, name which recovery - mechanism applies, and stop when the target is a real VMP. It does **not** ship a modified ART - runtime, a private-bytecode decompiler, or an opcode-mapping derivation, and it says so instead of - presenting a memory dump as a recovery. `detection-and-anti-analysis.md` decides by cost and often - concludes "switch to static"; it is not a catalogue of evasion for every detector you might meet. -- **Kernel development.** `references/kernel-and-environment-hardening.md` maps the kernel-side route - (eBPF, seccomp-BPF, kernel modules) and names the version gate that decides whether it exists on your - device at all; building and shipping a kernel module is outside this skill. -- **A working Stalker trace on every device.** The extension pass measured a configuration where - `Stalker.follow` installed but delivered no events, and a high-frequency follow/unfollow pattern that - crashed a system process. `references/native-dbi-and-deobfuscation.md` carries that as a boundary, - not as a recipe. - -**Not exercised by the verification pass — do not read silence as support:** the packer, code -virtualization, custom-linker, integrity-check-redirection and tamper-triggered-suicide scenarios -were **not run** against the verification target, because that target has none of those features (no -packer, no integrity checker, ordinary application class) and its unmodified build already fails to -start, which removes the repack-and-regress loop those scenarios need. Nothing in -`docs/tool-verification/` is evidence either way about them. If you use those documents, the -claims are still on the inferred footing described above. - -**Scripts this pass did not run** — so they carry no measurement at all, and any conclusion drawn -from them should be labelled accordingly: `native_crash.py`, `apk_diff.py`, `snap.py`, -`grab_crash.py`, `install_test.py`, `repack.py`, `dex_patch_bytes.py`, `dex_find_insn.py`, -`dex_check_verifier.py`, `dex_classdiff.py`, `dex_strpatch.py`, `patch_smali.py`, `smtool.py`, -`datastore_inject.py`, `probe_api.py`, `run_probe.py`, `tls_check.py`, `usb_net_proxy.py`, -`devsh.py`, and the `dexpatch/` java rewriter. Their absence from the record is not a verdict on -them. Note in particular that `grab_crash.py` claims to recover stacks hidden by a crash-reporter -SDK — the exact situation the verification target presented — and was not tried, so that claim -remains **unverified** and the pass used a purpose-written Frida probe instead. - -**The extension pass shipped its own scripts, and each carries its own measurement status** — read -the matching `docs/tool-verification/EXTENSION-*.md` before relying on one: `dex_dump_validate.py` -(measured against a fixture derived from a real hardened sample, and against the sample's own shell -dex), `lsposed_scaffold.py` (its generated project was built end to end and the toolchain timings are -recorded), `frida_rpc_serve.py` (its `rpc.exports` bridge was exercised on a live device against -unrelated processes), `mt_mcp_probe.py` (the "service is down" path is measured; the connected path -needs the service started by hand), and `stalker_trace.js` / `stalker_report.py` (the two *boundary* -results are measured: a follow that delivered no events, and a crash from following a hot libc -export — no successful trace of a real target was produced). +**What was never exercised, and which scripts have never been run**, is recorded in +`references/coverage-and-limits.md` — read it before treating "this was measured" as covering the +route you are about to take. Silence in the record is not support. **The fallback, as an instruction:** if the target does not match that list, or no symptom-index row matches, **stop and classify before choosing a branch.** Answer the thirteen questions first. If the @@ -241,54 +156,22 @@ builds, runs, and does the wrong thing. ## Hand-off points — where this skill ends and another view begins -Three boundaries that are easy to walk into without noticing. Each names what the other side owns, -rather than restating it, because two copies of the same advice drift apart. +Four boundaries that are easy to walk into without noticing. Each names what the other side owns +rather than restating it, because two copies of the same advice drift apart. The JNI form table and +the per-form verification table live in `references/handoff-boundaries.md`. -**1. JNI — a Java `native` declaration and its implementation are two different views of one function.** -This skill reads the Java side (dex) and the native side (`.so`) with different tools, so the join is -where analyses go wrong. - -| Form | What you see | How to find it | -|---|---|---| -| Static linkage | symbol `Java___` in `.dynsym` | search the dynamic symbol table. Under R8 the class name is a short name, so the symbol deforms with it and a search for the readable original finds nothing | -| Dynamic registration | **nothing** in the symbol table — binding happens at runtime | find `RegisterNatives` call sites, or hook it to read the binding table. Obfuscated targets prefer this, and a symbol search fails **silently** on it | -| Native → Java callbacks | native code pulling data back through Java | follow `FindClass` / `GetMethodID` / `CallObjectMethod` | - -`FindClass`/`RegisterNatives` in a `.so` tell you a JNI boundary exists even when no -`Java_*` symbol does. **Strength note:** the three rows above are documented behaviour, not results -from the verification pass, which did not trace a JNI boundary end to end; `FlutterJNI.loadLibrary` -appearing in a dex is the closest it came. Treat them as a map, not as a measurement. - -**2. Hardening — a dex-side packer observation is a native-side implementation question.** If the dex -turns out to be a shell, the logic is behind a loader, and the analysis moves to the `.so` that -performs the unpacking. `packers.md` owns the dex-side identification; the native deep dive belongs on -the other side of this boundary. **Not exercised by the verification pass** — that target had no -packer, so this pointer carries no measurement. - -**3. The existing native boundaries — read native anomalies from the APK side, not from inside.** -`native-and-so.md` and `native-tamper-and-suicide.md` are deliberately scoped to what you can conclude -*from the APK side*: a repacked build that dies instantly with a null-looking fault, a Java-layer -check that reports success while the process dies, a terminate path you made not-return. That -judgement belongs here, because it is about deciding whether your *patch* caused the death. Deep -native work — restoring a symbol, rebuilding a call graph, reversing an OLLVM function — is a -different activity with a different toolchain. Point across rather than duplicating: if you need the -latter, say so instead of extending these two files into it. - -**4. When the deliverable stops being an APK, the verification question changes with it.** §What -"done" means is written for a rebuilt, installable artifact, and every word of it assumes one. The -other three forms in G1 each move the evidence somewhere else, and the failure mode is quiet: a -privileged result gets reported in the language of a finished build. - -| Form | What "verified" now means | What is *not* evidence | -|---|---|---| -| **LSPosed / Xposed module** | The module was loaded into the target and its hook produced an observable effect **in the target's own log**, on a named build of the target | The module installed; `pm path` returned a path; the package was enabled in the manager. All three are true of a module whose entry class does not exist in its own dex | -| **Local RPC / emulation service** | A call returned the value the app itself would produce, from a named target build and a named device or emulated environment — and the harness survives a reconnect | "The script loaded"; a call that returned *something* without a reference value to compare against | -| **Analysis report with a stated boundary** | The evidence chain (commands, outputs, and the layer each conclusion belongs to) plus the boundary — what was **not** determined and why | Any implication that a route was exhausted when it was only abandoned | - -The privileged-form drift R1 warns about lives here. `references/lsposed-and-modules.md`, -`references/emulation-and-rpc.md` and `references/verification.md` each carry the specific check; this -table is the reminder that changing the deliverable's form is a decision that must be re-stated out -loud, not a quiet downgrade of what counts as done. +1. **JNI** — a Java `native` declaration and its implementation are two views of one function, and + this skill reads each with a different tool. A symbol search fails **silently** on dynamic + registration. Authoritative treatment: `java2c-and-jni-sinking.md` §The JNI boundary — why a + symbol search fails silently. +2. **Hardening** — a dex-side packer observation is a native-side implementation question. + `packers.md` owns the dex-side identification; the deep dive belongs on the other side. +3. **Native anomalies** — read them *from the APK side*, because the judgement is whether your patch + caused the death. Restoring a symbol or reversing an OLLVM function is a different activity with a + different toolchain: point across instead of extending `native-and-so.md` into it. +4. **Deliverable form** — when the artifact stops being an APK, the verification question changes with + it. G1's other three forms each move the evidence somewhere else, and the failure is quiet: a + privileged result reported in the language of a finished build. ## Symptom index — a matching row is a stop signal @@ -302,7 +185,7 @@ unread in this repository. |---|---| | A repackaged/re-signed build **dies before your code runs**; `SIGSEGV`, all registers zero, `pc=0`, `fault addr` near `0x0` | `native-tamper-and-suicide.md` (deliberate crash), then `code-virtualization-and-custom-linkers.md` | | **No packer** (Application is the app's own, dex readable) **and it still dies** | `code-virtualization-and-custom-linkers.md` §a loader is still a possibility; but if the same build also dies on a *second, unrelated* device you are looking at an ordinary startup fault, not a hardened one | -| The app dies at startup on **every** device, packed or not, **and there is no tombstone** while `crash_dump` reports `already traced` and logcat says `exited cleanly (0)` | the target's launch section in the verification record under `docs/tool-verification/` — a bundled crash reporter (Sentry NDK) has taken the signal handlers, so the platform's own evidence trail is gone. Frida spawn-gating is the recovery route; it needs a working frida-server, which a disguised one may not be | +| The app dies at startup on **every** device, packed or not, with **no tombstone** while `crash_dump` says `already traced` and logcat says `exited cleanly (0)` | a bundled crash reporter has taken the signal handlers, so the platform's own trail is gone. Frida spawn-gating is the recovery route | | A `FORTIFY: pthread_mutex_lock called on a destroyed mutex` abort in a Flutter app, on the **main** thread, before the first frame completes | `dart-aot.md` — check `libapp.so` is actually being loaded; Flutter's engine bootstrap is the usual place a native lifecycle fault surfaces | | Log says a **Java-layer** signature/integrity check **passed**, yet the process dies | `code-virtualization-and-custom-linkers.md` §a Java-layer "signature killer" is a decoy | | Deleting a library fixes validation but yields `UnsatisfiedLinkError: dlopen failed: library "X" not found` | `code-virtualization-and-custom-linkers.md` §the deadlock that eats hours | @@ -324,7 +207,7 @@ unread in this repository. | A script will not start, or a tool "is missing" | `scripts/doctor.py`, then `toolchain.md` §"not on PATH" is not "not installed" | | Feature-scoped network failure (login/register/pay) while the rest works | `tls-and-cert.md` — do not assume your patch caused it | | Everything works but **every signed request fails** after repack | `signature-derived-keys.md` | -| A re-signed build **runs fine, renders its whole UI and logs no error — but one feature silently never loads**, and `dumpsys`/DNS/logcat show **no request for it at all** (not a rejected request: *no request*) | `code-virtualization-and-custom-linkers.md` §what the native check actually reads — a client-side integrity gate is refusing **before** the request is built. This is *not* the row above: "sent and rejected" and "never sent" have different owners and different fixes | +| A re-signed build **runs fine, renders its whole UI and logs no error — but one feature silently never loads**, and `dumpsys`/DNS/logcat show **no request for it at all** (not a rejected request: *no request*) | `code-virtualization-and-custom-linkers.md` §what the native check actually reads — refusing **before** the request is built. Not the row above: "sent and rejected" and "never sent" have different owners | | You cannot tell whether a missing feature is **your patch's fault or the target's own behaviour** | `long-task-discipline.md` §single-variable discipline. Run the **zero-change control through the same pipeline**, and the decisive variant: the unmodified original with the patch applied **in memory only**, same device, same network | | Under Frida `spawn`, the UI never appears — `mCurrentFocus` stays `null`, screenshots come back blank, the Activity stack never builds | `dynamic-frida.md` §spawn keeps the Activity stack down: write the patch into memory, **detach**, then start the Activity normally | | `frida-server` keeps disappearing mid-experiment, or the device reboots itself while you are working | `dynamic-frida.md` §when the ROM hunts your instrumentation | @@ -334,9 +217,9 @@ unread in this repository. | 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 | | A dumped dex parses in full, the classes are all there, and most method bodies are `return-void` stubs or nop fills | `references/advanced-unpacking.md` — an extraction shell: measure the `stub%` with `scripts/dex_dump_validate.py` before trusting any of it, and know that recovering the bodies is a different route | -| Your `frida` dump dies mid-write (`script has been destroyed`), or the process you are dumping keeps changing pid | `references/advanced-unpacking.md` §dumping when frida is refused. **Rule out the boring cause first**: a device under memory pressure reclaims and relaunches background processes with no instrumentation involved, and that is enough to kill a dump. Only a *stable* target that still refuses the dump is evidence about the target — reproduction on a second, idle device is what separates the two | +| Your `frida` dump dies mid-write (`script has been destroyed`), or the process you are dumping keeps changing pid | `advanced-unpacking.md` §dumping when frida is refused — rule out memory pressure first; a reclaim-and-relaunch needs no instrumentation | | A repack is refused by several independent checks, or the build has to keep working through store updates | `references/lsposed-and-modules.md` — deliver a module instead of an APK; G1's form table says when | -| A hook module is installed, enabled and scoped, yet its log tag never appears — and you are about to conclude it never ran | `references/lsposed-and-modules.md` §Deploy, enable, and verify — **the verdict from `logcat` alone has already been wrong once here**: on one ROM `logd` is broken and module output reaches only `/data/adb/lspd/log/modules_.log`, where nine successful injections were sitting while three `logcat` queries came back empty | +| A hook module is installed, enabled and scoped, yet its log tag never appears — and you are about to conclude it never ran | `lsposed-and-modules.md` §Deploy, enable, and verify — **a `logcat`-only verdict has already been wrong here**: on one ROM `logd` is broken and output reaches only `/data/adb/lspd/log/modules_.log` | | A module's entry class is missing from its own dex (so it can never load), yet the package installs, enables and looks healthy | `references/lsposed-and-modules.md` — check `assets/xposed_init` against the dex's actual classes; installation is not evidence of anything | | You only need to **call** the target's own routine (sign, token, encrypt) rather than change the app | `references/emulation-and-rpc.md` — emulate it, or service-ify the live function over Frida RPC | | A native function is a many-thousand-line `switch` state machine, or the decompiler's output is meaningless | `references/native-dbi-and-deobfuscation.md` — OLLVM shapes, a Stalker trace, and how far a trace actually gets you | @@ -344,7 +227,7 @@ unread in this repository. | The traffic is protobuf/gRPC/QUIC, or a proxy sees TLS but requests still fail on a Flutter app | `references/protocol-reverse.md` — schema-less protobuf, frame capture, and native-side pinning | | Userspace hooks land and the app still dies: the check reads `/proc/self/status` through a raw `svc`, or runs before `JNI_OnLoad` | `references/kernel-and-environment-hardening.md` — what the next layer up and down can actually do, and when to stop | | You must edit, repack, sign or inspect the APK **from the phone itself** | `references/on-device-tooling.md`, `scripts/mt_mcp_probe.py` | -| A captured body decodes to nothing readable, or you cannot tell whether a length-delimited field is a string, a nested message or a packed array | `protocol-reverse.md` §1. Protobuf on the wire (measured) — run `scripts/protobuf_decode_raw.py`; the candidate list and its `tie:` lines are the answer, and `docs/tool-verification/EXTENSION-protobuf-raw.md` records exactly what a schema-free decode can and cannot settle | +| A captured body decodes to nothing readable, or you cannot tell whether a length-delimited field is a string, a nested message or a packed array | `protocol-reverse.md` §1. Protobuf on the wire (measured) — run `scripts/protobuf_decode_raw.py`; the candidate list and its `tie:` lines are the answer | | Method bodies are present but decode as **private opcodes**, and you need the mapping rather than an explanation of why VMP is hard | `vmp-differential-analysis.md`, then `advanced-unpacking.md` for the shape diagnosis | | A store build arrives as `base.apk` + `split_config.*.apk`, or a rebuilt build is refused **as a set** although every file verifies on its own | `split-apk.md` — one keystore across every member for `pm install-multiple`, and check that a merge is legal before trusting a merged single APK | @@ -566,14 +449,14 @@ Load only what the current step needs. | `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/java2c-and-jni-sinking.md` | **Whole classes are bare `native` declarations and you are about to hunt for a decrypted DEX** — Java2C has none, ever: the code was compiled into a `.so`, so the route is reading that library, not dumping memory. Also the JNI boundary itself — why a `Java_*` symbol search fails silently — and the evidence that separates Java2C from an extraction shell, a VMP and ordinary JNI sinking | +| `references/code-virtualization-and-custom-linkers.md` | **No packer, dex is readable, and a re-signed build still dies** — whole classes turned `native`, a private loader with a mismatched SONAME, a self-decrypting payload, a Java-layer "signature killer", and the keep-it/drop-it deadlock | +| `references/java2c-and-jni-sinking.md` | **Whole classes are bare `native` declarations and you are about to hunt for a decrypted DEX** — Java2C has none, ever; the code is in a `.so`. Also the JNI boundary: why a `Java_*` symbol search fails silently | | `references/vmp-differential-analysis.md` | Method bodies are present but decode as **private opcodes** — a real Dex VMP: the known-plaintext differential, which links can and cannot be automated, how to *prove* a derived opcode table, smali generation, and when the route is closed | | `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/detection-and-anti-analysis.md` | The app fights back: it dies after you attach, refuses to run, detects root/hook/debugger, or your dynamic tool does not work here. **Read the first section before escalating** — the answer is usually to switch to static | | `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. | @@ -592,14 +475,16 @@ Load only what the current step needs. | `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. | -| `references/advanced-unpacking.md` | **The dump landed but the method bodies are empty** (an extraction shell), or bodies decode as private opcodes (a real VMP). Measuring the stub ratio, FART-style active invocation and why its classic hooks died on Android 12-16, code_item splicing, the root-side dump that does not need frida, and where recovery honestly stops | -| `references/lsposed-and-modules.md` | The client-side logic is reachable but **a rebuilt APK is refused** (multi-point checks, shell self-verification), or the result only has to run on rooted devices you control: delivering a system-level hook module, its gradle-free build chain, scope configuration and verification, and the layer a Java module cannot reach | +| `references/coverage-and-limits.md` | You need to weigh a claim before trusting it: the evidence behind each covered item, the dependencies this skill does not ship, and the record of what was never exercised | +| `references/handoff-boundaries.md` | A routing decision is about to cross into another discipline: the JNI form table, the packer-versus-loader split, and what "verified" means for each of G1's four deliverable forms | +| `references/advanced-unpacking.md` | **The dump landed but the bodies are empty** (an extraction shell), or bodies decode as private opcodes (a real VMP): measuring the stub ratio, why FART's hooks died on Android 12-16, the root-side dump, and where recovery stops | +| `references/lsposed-and-modules.md` | The client-side logic is reachable but **a rebuilt APK is refused**: delivering a system-level hook module instead, its gradle-free build chain, scope configuration and verification, and the layer a Java module cannot reach | | `references/emulation-and-rpc.md` | You need to **call** a routine rather than change the app — a signing routine, a token, a cipher: emulated execution (Unidbg/Unicorn) with its environment-filling cost, versus service-ifying the live function over Frida RPC | | `references/native-dbi-and-deobfuscation.md` | A native function is an OLLVM state machine, or you need instruction-level execution evidence: Stalker traces, the trace-to-CFG route, the Stalker/QBDI/emulation decision, and the **measured** zero-event and crash boundaries | | `references/protocol-reverse.md` | The traffic is protobuf without a schema, gRPC, or QUIC/HTTP3; or a proxy sees TLS while the app still fails — schema recovery, frame capture, and native-side certificate pinning (Flutter/BoringSSL) with its boundaries | -| `references/kernel-and-environment-hardening.md` | Userspace hooking provably cannot reach the check — raw `svc` syscalls, `init_array`-early detection, a ROM that hunts instrumentation: what each layer up and down can still do, the kernel-route map and its version gate, and when escalating is the wrong answer | +| `references/kernel-and-environment-hardening.md` | Userspace hooking provably cannot reach the check — raw `svc` syscalls, `init_array`-early detection: what each layer can still do, the kernel-route map with its version gate, and when escalating is wrong | | `references/on-device-tooling.md` | Working **from the phone itself**: MT Manager edit/repack/sign and its built-in APK MCP, LSPosed Manager, Termux+frida, and on-device data inspection | -| `references/split-apk.md` | The target is a **split APK / App Bundle set** (`base.apk` + `split_config.*.apk`), or `pm path ` returned several files: reading a set off a device, when merging into one APK is legal versus when only unified re-signing works, the install refusal each mistake produces, the manifest attributes a merge breaks, and ABI/density matching | +| `references/split-apk.md` | The target is a **split APK / App Bundle set** (`base.apk` + `split_config.*.apk`), or `pm path ` returned several files: reading a set, merge versus unified re-signing, and the install refusal each mistake produces | ## Script index @@ -607,12 +492,12 @@ All scripts are parameterized and path-agnostic; pass paths explicitly. Run `--h | 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/doctor.py` | **Run this first.** Which tools exist (including off-PATH and `java -jar` jars), which scripts can run here, and the environment facts that poison experiments: clock skew, leftover forwards/proxy, a device-side frida already running | +| `scripts/dexutil.py` | **Dependency-free dex reader**: structural walk, exact instruction decode, `fix_dex_header`/`verify_dex_header` in the correct order, branch/operand helpers. Shared by the dex scripts; also dumps one method with offsets standalone | | `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_patch_bytes.py` | Apply **equal-length byte patches** from a JSON spec: semantic match, polarity pin via `expect_next`, equal-length enforcement, verifier legality, dex header recompute, re-decode 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/coldstart.py` | Cold-launch and capture a **timed screenshot burst + logcat signals + installed-build facts + launch timing**, and warn when the foreground activity is not your app | | `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** | @@ -622,7 +507,7 @@ All scripts are parameterized and path-agnostic; pass paths explicitly. Run `--h | `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). Takes a smali tree, a `.dex`, a directory of either, or an `.apk`. Always prints what it scanned, so "the input was unreadable" cannot be mistaken for "nothing references it" | -| `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. Also handles **split APK / App Bundle sets**: inventory one (`--split-dir`/`--split`), sign every member with a single keystore for `pm install-multiple` (`--split-mode resign`), or fold code/native members into one standalone APK (`--split-mode merge` — refused by design when a member carries its own `resources.arsc`). `--signer auto/jar/apksigner` covers hosts without uber-apk-signer; see `references/split-apk.md` | +| `scripts/repack.py` | Rebuild an APK: replace dex, strip only signatures, keep `META-INF/services/`, write a **4-byte-aligned** archive, sign, verify. Also **split sets**: `--split-dir`, `--split-mode resign`/`merge`, `--signer auto/jar/apksigner` | | `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) | @@ -634,25 +519,25 @@ All scripts are parameterized and path-agnostic; pass paths explicitly. Run `--h | `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/elf_plt.py` | Resolve a PLT stub to its imported symbol (x86_64 + aarch64) from the **relocation table**, list a symbol's callers, and **byte-diff two libraries naming the symbol each changed stub belongs to**. Run before patching any stub | +| `scripts/so_constpatch.py` | Same-length rewrite of an isolated string constant, for **redirecting a library load instead of defeating a check**. Enforces equal length, refuses substrings of longer identifiers, reports constant-pool hits, patches inside an APK or a bare `.so` | +| `scripts/apk_diff.py` | Entry-level diff of two APKs by content hash: changed / **added** (injection candidates) / removed. Audits a third-party build and proves your own was surgical | +| `scripts/native_crash.py` | Locate a native death from a log or tombstone: signal, fault address, registers, frames split app vs system, the faulting instruction — plus a 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`. | -| `scripts/spawn_patch_detach.py` | **Spawn under a Frida probe, then detach before driving the UI.** Under spawn mode the Activity stack often never comes up (`mCurrentFocus` stays `null`, screenshots blank); memory writes survive detach while hooks do not, so this ordering is what makes an in-memory patch observable. Use it whenever you need to *see* a build that only runs with a memory fix. | -| `scripts/hook_patch_only.js` | The minimal probe for `spawn_patch_detach.py`: neutralise one native death site by offset and report `PATCHED`. Configure `MODULE_NAME`, `FILE_OFFSET`, `PATCH_BYTES`. The replacement must be an equal-length "recover the frame and return" epilogue, never a NOP in front of live code. | -| `scripts/dex_dump_validate.py` | Dedupe, structurally validate and rank a directory of dumped dex images: sha256 grouping, header integrity (`dexutil.verify_dex_header`), classic-vs-extraction discrimination via the **trivial-body ratio**, package-prefix hits, ranking, `--json`. `--trim` accepts page-aligned captures (a `/proc//mem` export always overruns the dex it holds), while a file *shorter* than its own `file_size` stays rejected | -| `scripts/dex_mem_scan.py` | Search memory captures for embedded dex images (`dex\n03x`) and extract each at the size **its own header declares**: bounded chunked scanning for large regions, magic validation, sha256 dedupe, `--dump DIR`, `--keep-partial`. Use it when a packer keeps a decrypted dex in an anonymous mapping that no `maps` entry names; pair the output with `dex_dump_validate.py` | +| `scripts/sig_probe.py` | Find the exact `signatures[0].toCharsString()` value: offline candidates from an APK (`--apk`), or the authoritative read from a live package (`--live`) | +| `scripts/spawn_patch_detach.py` | **Spawn under a probe, detach, then drive the UI.** Under spawn the Activity stack often never comes up; memory writes survive detach while hooks do not. Ordering matters: resume *before* waiting for `PATCHED` | +| `scripts/hook_patch_only.js` | The minimal probe for `spawn_patch_detach.py`: neutralise one native death site by offset and report `PATCHED`. `MODULE_NAME`, `FILE_OFFSET`, `PATCH_BYTES`; the replacement must be an equal-length "return" epilogue | +| `scripts/dex_dump_validate.py` | Dedupe, validate and rank dumped dex images: sha256 grouping, header integrity, extraction-shell discrimination via the **trivial-body ratio** (bimodal, not a threshold — `advanced-unpacking.md`), ranking, `--json`, `--trim` for page-aligned captures | +| `scripts/dex_mem_scan.py` | Find embedded dex images in memory captures and extract each at the size **its own header declares**: chunked scanning, magic validation, sha256 dedupe, `--dump DIR`, `--keep-partial` | | `scripts/lsposed_scaffold.py` | Generate a minimal LSPosed/Xposed **module project skeleton**: manifest with the xposed meta-data, `assets/xposed_init`, the hook class, and build notes for a gradle-free toolchain (javac → d8 → aapt2 → zipalign → apksigner) | | `scripts/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** instead of reversed | | `scripts/rpc_template.js` | The editable companion to `frida_rpc_serve.py`: an `rpc.exports` skeleton plus a native-function call placeholder | | `scripts/stalker_trace.js` | Instruction-level tracing with Frida Stalker: configurable module/offset targets, trigger selection, the event stream, output-size rules, and `transform` customisation | | `scripts/stalker_report.py` | Reduce a `stalker_trace.js` log into block histograms and call sequences, and print an explicit diagnostic for the measured **zero-event** case | | `scripts/mt_mcp_probe.py` | Probe MT Manager's on-device APK MCP (Streamable HTTP, `127.0.0.1:8787/mcp`): JSON-RPC handshake plus grouped `mt_apk_*` tool inventory; prints start-it-by-hand instructions and exits 2 while the service is down | -| `scripts/java2c_probe.py` | Collect the evidence that separates **Java2C** from an extraction shell, a VMP and ordinary JNI sinking: per-class `native` density, native-dominated classes and stub ratio from the dex, plus `Java_*` / `JNI_OnLoad` / dynamic-registration / toolchain-string evidence from the `.so`. Every item carries a strong/medium/weak label and the weak ones print their own warning — reading a single criterion is how this gets misdiagnosed | -| `scripts/protobuf_decode_raw.py` | **Schema-free protobuf decode**: hex string, binary file or stdin to a JSON tree, with every length-delimited field reported as a candidate set (nested message / UTF-8 / packed array / opaque bytes) and ties labelled rather than guessed. Flags the proto3 explicit-zero and packed-boundary ambiguities; `--max-depth`, `--split`, and `--reencode --check` for a byte-exact round trip. Cross-checked field-for-field against the official runtime | -| `scripts/vmp_diff_harness.py` | Differential hardening for Dex-VMP: build a labelled opcode-coverage fixture (**218 of 224 opcodes, measured**), derive a candidate private-opcode map from an original/hardened dex pair with per-entry confidence and a run-level verdict, forge known-table fixtures to verify the comparison in a closed loop, and render a restored stream as a smali skeleton. The upload-to-a-hardening-service link is **not automatable** — see `references/vmp-differential-analysis.md` §2 | -| `scripts/kernelsu_syscall_mask.py` | Generate a configurable 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. **A KernelSU userspace module cannot change a syscall return value** — the kernel-side templates are the part that could, and they are shipped unbuilt | +| `scripts/java2c_probe.py` | The evidence that separates **Java2C** from an extraction shell, a VMP and JNI sinking: per-class `native` density and stub ratio from the dex, plus `Java_*` / `JNI_OnLoad` / registration evidence from the `.so` | +| `scripts/protobuf_decode_raw.py` | **Schema-free protobuf decode**: hex / file / stdin to a JSON tree, every length-delimited field as a candidate set with ties labelled; `--reencode --check` for a byte-exact round trip | +| `scripts/vmp_diff_harness.py` | Differential hardening for Dex-VMP: build a labelled opcode-coverage fixture (**218 of 224 opcodes, measured**), derive a candidate private-opcode map from an original/hardened dex pair, verify it in a closed loop, render smali | +| `scripts/kernelsu_syscall_mask.py` | Generate a KernelSU/APatch syscall-masking scaffold: an installable userspace module plus KPM/LKM/eBPF kernel-side templates, each with its version gate and an explicit unverified label. **A userspace module cannot change a syscall return value** | diff --git a/skills/apk-reverse/references/coverage-and-limits.md b/skills/apk-reverse/references/coverage-and-limits.md new file mode 100644 index 0000000..8432b06 --- /dev/null +++ b/skills/apk-reverse/references/coverage-and-limits.md @@ -0,0 +1,175 @@ +# Coverage and limits — the evidence behind each claim + +`SKILL.md` §Coverage states **what this skill covers and what it does not**, because the failure +worth preventing is not ignorance but *a confident wrong answer produced by applying the nearest +available procedure to a target it was never written for*. That statement has to stay short: it is +read on every task, before any routing decision. + +This file carries the part that is read **only when you need to weigh a claim** — the evidence +behind each covered item, the exact footing of the unverified ones, and the historical record of +what was and was not exercised. Load it when a claim's strength decides whether you trust it. + +## Strength labels + +- **observed** — a command was run and its output is recorded in `docs/tool-verification/`. +- **inferred** — follows from documented mechanism or from a neighbouring measurement; the step + itself was not executed. +- **unverified** — assumed, or reported by someone else, and not reproduced in this repository. + +Reserve `observed` strictly (`references/long-task-discipline.md` does). "I reasoned it out" is +`inferred`. The distinction is load-bearing, not cosmetic: an `inferred` route may still be right, +but nobody here has paid for the counters yet. + +## Covered, by verified mechanisms — the evidence + +- **Client-side ads, promos and splash/popup/tab configuration**, including the server-issued UI + config that has no SDK to find (`ad-removal.md`, `server-config-and-updates.md`). +- **Deciding whether a membership, paywall or feature gate is client-enforceable at all**, and + saying so plainly when it is not (`membership-and-limits.md`, `account-gates.md`). +- **dex-level surgical patching**: equal-length byte edits and dexlib2 method rewrites, plus the + header, verifier and alignment rules that decide whether the build loads (`dex-patching.md`, + `byte-level-patching.md`, `patch-audit.md`). +- **Repacking, signing, installing**, and the install refusals that look like a broken build + (`repack-and-sign.md`). +- **Packers, custom loaders and code virtualization**: identifying them, measuring the validation + boundary, and the routes that survive it (`packers.md`, + `code-virtualization-and-custom-linkers.md`). +- **The native layer**: `.so` hosts, tamper-triggered self-termination, forged ELF structure, and + neutralising a terminate path without freezing the process (`native-and-so.md`, + `native-tamper-and-suicide.md`). +- **Flutter / Dart AOT**: analysing and patching `libapp.so` **given a snapshot dump** — + pool-reference counting, disassembly windows, caller indexing, patch-site choice (`dart-aot.md`). + Measured on a real Dart 3.6.0 build: `dart_disasm.py` decoded identically to capstone (32/32 and + 96/96), the caller index was re-derived independently with a symmetric difference of 0, and a + specific business logic site was located end to end. **The snapshot dump is a dependency, not a + detail** — see the next section. +- **Runtime analysis with Frida, server-side API probing, feature-scoped TLS failures, update and + forced-upgrade neutralisation**, and the verification discipline everything above rests on. + +### Added by the benchmark pass, and measured against public targets + +Each of these has a row in `tests/benchmark.md` naming its target, its strength and the evidence +file behind it. + +- **Java2C versus JNI sinking versus an extraction shell.** Native-declaration density separates the + shapes by roughly 2000x: **0.03–0.04%** on three real JNI samples against **82.76%** on a + Java2C-shaped fixture, with `Java_*` symbols matching dex native-method counts 1:1 per ABI. A + symbol search fails *silently* on real Java2C — `dcc` emits `-fvisibility=hidden`, and C++ + `jni.h` inlines `RegisterNatives` so it leaves no symbol at all + (`java2c-and-jni-sinking.md`, `scripts/java2c_probe.py`). **No Dex-to-C compiler output was ever + built here** (no NDK, no host clang, no device clang, WSL unavailable), so every Java2C-specific + criterion is `inferred`; the JNI-sinking side and the discrimination itself are `observed`. +- **Split APK / App Bundle sets.** Inventory, unified re-signing for `pm install-multiple`, and a + merge of code/native members into one APK — the merge **refuses by design** when a member carries + its own `resources.arsc` (`split-apk.md`, `scripts/repack.py`). Measured on two real sets pulled + from a device; `adb install-multiple` and launch confirmation were **not** executed. +- **Schema-free protobuf decoding**, cross-checked against the official runtime + (`scripts/protobuf_decode_raw.py`, `protocol-reverse.md`). 26/26 built-in fixtures, 21/21 against + the official runtime, and a real DataStore container round-tripped byte-exact. +- **Differential hardening for a real Dex VMP** — a labelled opcode-coverage fixture, a closed-loop + verification of the derived map, and an explicit cost judgement that the upload link cannot be + automated (`vmp-differential-analysis.md`, `scripts/vmp_diff_harness.py`). +- **Kernel-module templates with their version gates** + (`kernel-and-environment-hardening.md`, `scripts/kernelsu_syscall_mask.py`). The generator is + measured; **no kernel-side artefact was compiled or loaded** — see the record below. + +## Covered by documented routes — inferred rather than measured + +Written from public work and from whatever the extension pass could exercise; each document carries +its own strength note at the top. + +- **Module-side delivery instead of a repack** — what to do when the client-side logic is reachable + but a rebuilt APK is refused (`lsposed-and-modules.md`). +- **Extraction shells and the VMP boundary** — how to *measure* that the bodies are empty instead of + guessing, why the classic active-invocation hooks stopped working on Android 12–16, and where + recovery honestly stops (`advanced-unpacking.md`, `scripts/dex_dump_validate.py`). +- **Calling the target instead of reading it** — emulated execution and live Frida-RPC + service-ification (`emulation-and-rpc.md`). +- **Instruction-level tracing and de-obfuscation** — the OLLVM shapes, a Stalker trace, the + trace-to-CFG route, and the measured boundaries in `native-dbi-and-deobfuscation.md` §6. +- **Protocol reversing beyond REST** — protobuf without a schema, gRPC frame capture, the QUIC/HTTP3 + limit, native-side certificate pinning (`protocol-reverse.md`). +- **What to do when userspace hooking provably cannot reach the check** — raw `svc` syscalls, + `init_array`-early detection, and the kernel-route map with its version gate + (`kernel-and-environment-hardening.md`). +- **Working from the phone itself** — MT Manager, its APK MCP surface, LSPosed Manager, on-device + data inspection (`on-device-tooling.md`, `scripts/mt_mcp_probe.py`). + +## Dependencies this skill does not ship — name them before the workflow starts + +- **Dart AOT analysis needs a snapshot dump.** `dart-aot.md`'s workflow begins at `pp.txt`; + producing it requires a snapshot container resolver this skill does not contain and cannot + synthesize. `dart_pool_strings.py` reports **file** offsets while `pp.txt` and `dart_pprefs.py` + speak in **pool** offsets, and the mapping between the two spaces is not a constant: over the + 4,241 strings present in both, a measured run found 4,237 distinct deltas. Ship a pinned front end + (aotopsy — pure Go, no toolchain) or build blutter (~80 s, needs a C++ toolchain). **Say which one + you are using and why, because the two report different Dart version labels for the same binary.** + Do not describe the object pool as something this skill decodes on its own. + +## Not covered — say so rather than improvise + +- **Unity / IL2CPP logic recovery.** `framework-runtimes.md` identifies the runtime and establishes + that the dex is not the battlefield; it does not carry the IL2CPP equivalent of `dart-aot.md`. + There is no verified recipe here for locating a method inside `libil2cpp.so` plus + `global-metadata.dat`. +- **React Native / Hermes bytecode** and Cordova/hybrid internals, beyond runtime identification and + the generic "find the string, then find what references it" approach. +- **iOS / `.ipa` of any kind.** Every device, signing and packaging instruction here is Android. +- **Defeating a server-side authority.** `server-api.md` determines *who owns a gate*, not how to + break an authorization the server performs. +- **An off-the-shelf unpacker, and an anti-detection arms race.** `advanced-unpacking.md` routes the + problem — measure the ratio, name the recovery mechanism, stop when the target is a real VMP — but + ships no modified ART runtime, no private-bytecode decompiler and no opcode-mapping derivation, + and says so rather than presenting a memory dump as a recovery. `detection-and-anti-analysis.md` + decides by cost and often concludes "switch to static"; it is not a catalogue of evasion. +- **Kernel development.** `kernel-and-environment-hardening.md` maps the kernel-side route and names + the version gate that decides whether it exists on your device; building and shipping a kernel + module is outside this skill. +- **A working Stalker trace on every device.** Two boundaries are measured and one of them is now + better understood: exclusion keeps the target alive but does **not** restore event delivery + (`docs/tool-verification/EXTENSION-stalker-exclude.md`). Treat the zero-event case as a boundary to + identify, not a recipe to follow. + +## Historical verification record + +This is the part that grows every pass, which is why it does not live in `SKILL.md`. Read it before +trusting any "this was measured" statement above. + +**The first verification pass measured, against a real target:** a mid-size Flutter AOT application +with no packer — the Dart work above, library mapping, and the environment facts. It **did not +exercise** the packer, code-virtualization, custom-linker, integrity-check-redirection or +tamper-suicide scenarios: that target has none of those features and its unmodified build already +fails to start, which removes the repack-and-regress loop those scenarios need. Nothing in +`docs/tool-verification/` is evidence either way about them. + +**Scripts the first pass did not run** — they carry no measurement from that pass, and any +conclusion drawn from them should be labelled accordingly: `native_crash.py`, `apk_diff.py`, +`snap.py`, `grab_crash.py`, `install_test.py`, `repack.py`, `dex_patch_bytes.py`, +`dex_find_insn.py`, `dex_check_verifier.py`, `dex_classdiff.py`, `dex_strpatch.py`, +`patch_smali.py`, `smtool.py`, `datastore_inject.py`, `probe_api.py`, `run_probe.py`, +`tls_check.py`, `usb_net_proxy.py`, `devsh.py`, and the `dexpatch/` java rewriter. Absence from +that record is not a verdict on them. Note that `grab_crash.py` claims to recover stacks hidden by +a crash-reporter SDK — the exact situation that target presented — and was not tried, so that claim +remains **unverified**. **The benchmark pass has since run some of these**: `repack.py`, +`dex_patch_bytes.py`, `dex_dump_validate.py`, `dex_mem_scan.py`, `spawn_patch_detach.py` and +`stalker_trace.js` now have measurements, with the failures recorded alongside the successes in +`tests/benchmark.md`. + +**The extension pass shipped its own scripts, each with its own status** — read the matching +`docs/tool-verification/EXTENSION-*.md` before relying on one: `dex_dump_validate.py` (measured +against a fixture derived from a real hardened sample, and against that sample's own shell dex), +`lsposed_scaffold.py` (its generated project was built end to end, toolchain timings recorded), +`frida_rpc_serve.py` (the `rpc.exports` bridge was exercised on a live device), +`mt_mcp_probe.py` (the "service is down" path is measured; the connected path needs the service +started by hand), and `stalker_trace.js` / `stalker_report.py` (two boundary results, no successful +trace of a real target). + +**The extension pass's claims are mostly `inferred`.** Its evidence lives in +`docs/tool-verification/EXTENSION-*.md`, one file per topic, each with its own strength note. The +common shape there is *the tool was measured, the route was not* — read those files before treating +any of the newer documents as a verified path. + +**Where the record lives.** `docs/tool-verification/README.md` indexes it; `tests/benchmark.md` is +the public-target regression matrix. Neither ships inside the installed skill (they sit at the +repository root), so an installed copy of the skill carries the claim but not its evidence file — +which is why the strength label is stated here rather than left to be looked up. diff --git a/skills/apk-reverse/references/handoff-boundaries.md b/skills/apk-reverse/references/handoff-boundaries.md new file mode 100644 index 0000000..daea77e --- /dev/null +++ b/skills/apk-reverse/references/handoff-boundaries.md @@ -0,0 +1,70 @@ +# Hand-off boundaries — where this skill ends and another view begins + +Four boundaries that are easy to walk into without noticing. Each names what the other side owns +rather than restating it, because two copies of the same advice drift apart. `SKILL.md` keeps the +one-line version of each so a routing decision can be made without loading this file; this file +carries the detail. + +## 1. JNI — a Java `native` declaration and its implementation are two different views of one function + +This skill reads the Java side (dex) and the native side (`.so`) with different tools, so the *join* +is where analyses go wrong. + +| Form | What you see | How to find it | +|---|---|---| +| Static linkage | symbol `Java___` in `.dynsym` | search the dynamic symbol table. Under R8 the class name is a short name, so the symbol deforms with it and a search for the readable original finds nothing | +| Dynamic registration | **nothing** in the symbol table — binding happens at runtime | find `RegisterNatives` call sites, or hook it to read the binding table. Obfuscated targets prefer this, and a symbol search fails **silently** on it | +| Native → Java callbacks | native code pulling data back through Java | follow `FindClass` / `GetMethodID` / `CallObjectMethod` | + +`FindClass`/`RegisterNatives` in a `.so` tell you a JNI boundary exists even when no `Java_*` symbol +does. **Strength note:** the three rows above are documented behaviour, not results from the first +verification pass, which did not trace a JNI boundary end to end; `FlutterJNI.loadLibrary` appearing +in a dex is the closest it came. Treat them as a map, not as a measurement. + +**The benchmark pass then measured the failure the middle row predicts:** a real dynamically +registering library contains **zero `RegisterNatives` symbols**, because the C++ `jni.h` inlines the +call — so a symbol-based search fails structurally, not by bad luck. The combination that did fire +on a real library was *"exports `JNI_OnLoad` and zero `Java_*`"*. The authoritative treatment is +`references/java2c-and-jni-sinking.md` §The JNI boundary — why a symbol search fails silently; this +section is the pointer, not the source. + +## 2. Hardening — a dex-side packer observation is a native-side implementation question + +If the dex turns out to be a shell, the logic is behind a loader and the analysis moves to the `.so` +that performs the unpacking. `packers.md` owns the dex-side identification; the native deep dive +belongs on the other side of this boundary. **Not exercised by the first verification pass** — that +target had no packer, so this pointer carries no measurement from it. The benchmark pass measured the +*shape discrimination* on public samples but still did not run a live extraction shell +(`tests/benchmark.md` row B3). + +## 3. The existing native boundaries — read native anomalies from the APK side, not from inside + +`native-and-so.md` and `native-tamper-and-suicide.md` are deliberately scoped to what you can +conclude *from the APK side*: a repacked build that dies instantly with a null-looking fault, a +Java-layer check that reports success while the process dies, a terminate path you made not-return. +That judgement belongs here, because it is about deciding whether your *patch* caused the death. Deep +native work — restoring a symbol, rebuilding a call graph, reversing an OLLVM function — is a +different activity with a different toolchain. Point across rather than duplicating: if you need the +latter, say so instead of extending those two files into it. + +## 4. When the deliverable stops being an APK, the verification question changes with it + +`SKILL.md` §What "done" means is written for a rebuilt, installable artifact, and every word of it +assumes one. The other three forms in G1 each move the evidence somewhere else, and the failure mode +is quiet: **a privileged result gets reported in the language of a finished build.** + +| Form | What "verified" now means | What is *not* evidence | +|---|---|---| +| **LSPosed / Xposed module** | The module was loaded into the target and its hook produced an observable effect **in the target's own log**, on a named build of the target | The module installed; `pm path` returned a path; the package was enabled in the manager. All three are true of a module whose entry class does not exist in its own dex | +| **Local RPC / emulation service** | A call returned the value the app itself would produce, from a named target build and a named device or emulated environment — and the harness survives a reconnect | "The script loaded"; a call that returned *something* without a reference value to compare against | +| **Analysis report with a stated boundary** | The evidence chain (commands, outputs, and the layer each conclusion belongs to) plus the boundary — what was **not** determined and why | Any implication that a route was exhausted when it was only abandoned | + +The privileged-form drift R1 warns about lives here. `references/lsposed-and-modules.md`, +`references/emulation-and-rpc.md` and `references/verification.md` each carry the specific check; this +table is the reminder that changing the deliverable's form is a decision that must be re-stated out +loud, not a quiet downgrade of what counts as done. + +**A fifth boundary, added by the benchmark pass:** when the deliverable is a **tool or a document in +this repository**, "verified" means an independent check exists — a second implementation, an +official disassembler, a byte-exact round trip. A tool's own self-test passing is not that check. +`tests/benchmark.md` records which rows have one and which do not.