6 Commits

Author SHA1 Message Date
inliver233 a1ada96cc3 Lint clean: adopt the PR's type/style gate, apply it to the current tree, and drop its path filter
PR #3 proposed adding mypy and flake8 to CI, with configurations for both. The idea is right and the
configurations are kept. Three things about the PR itself needed correcting, and all three were found
by running the gate rather than reading the diff:

  1. It was written against an older tree. Its own edits reference code that no longer exists there
     (`unknown, positions, warnings = ...`, a `struct` import at line 857 that is used 600 lines
     later), so merging it as-is would have removed a live import. The fixes were re-derived against
     the current tree instead, and the PR is superseded.
  2. It added a `paths:` filter to the workflow, watching only `*.py` plus the two lint configs. That
     would stop `check_refs.py` and `check_routing.py` -- documentation gates -- from running on a
     documentation-only commit, which is exactly the change they exist to catch. The workflow keeps
     the PR's `actions/*@v7` bump and gains no filter; the reason is written next to the trigger.
  3. `pip install --upgrade pip pytest` was dropped to just the packages, because upgrading pip in CI
     is not a goal and can fail for reasons unrelated to this repository.

What the gate found on the current tree, and what was done about it -- 70 flake8 findings and 3 mypy
errors, none of which were cosmetic-only:

  F401 (23)  unused imports, removed. These were safe to delete because the import statement itself
             cannot have a side effect that matters here.
  F841 (12)  unused bindings, handled three ways rather than one. Dead code was deleted
             (`mt_mcp_probe.print_waiting_help`'s `host`, `repack.py`'s `signer_jar`); a computed
             value that was then forgotten was **wired up** (`rasc_build.py` now names the linker
             target in its build-failure hint instead of discarding it); and a binding that is
             intentional kept its place with `# noqa: F841` and the reason -- `java2c_probe` reads
             dex header fields one by one against the format spec, and deleting three of them to
             please a linter would have made the parser stop lining up with the format it implements.
  E741 (14)  the ambiguous loop name `l`, renamed with an AST pass that resolves each binding to its
             own scope. A regex rename would have been wrong: several scopes bind their own `l`.
             That pass had a defect of its own -- it treated a comprehension target as living in the
             comprehension scope while leaving references to it in the enclosing one -- which
             produced seven `undefined name 'l'`. Found by running flake8, fixed, and the counting
             bug is documented in `check_routing.py`, whose own `tables()` had the same class of
             error in the other direction.
  style      continuation indents, blank lines between top-level definitions, and lines over length.
             The line limit is 160, not 120, and the number is a **measurement**: this repository's
             secret-scanner rule table holds raw regex source of 148 and 156 characters, which cannot
             be split without changing the pattern.
  mypy (3)   two missing annotations on module-level containers, and one optional-dependency rebind
             (`dexutil = None` after `except ImportError`) that is the repository's own idiom and
             now says so with a `type: ignore[assignment]` and the reason. No code was reordered to
             satisfy the checker.

Verified on a venv containing only `mypy` and `flake8` -- the poorest environment the configuration
must work in, and the one CI uses: **flake8 reports 0, mypy reports "Success: no issues found in 55
source files"**. Behaviour is unchanged: 625 passed / 163 skipped / 25 xfailed, the same five gates
green (check_repo 0 problem, check_refs 0 dangling, check_routing coherent, check_budget 0 failure,
check_commands 0 drift), and every key script still prints help.

  .flake8        new, with the line-length and complexity numbers and their reasons
  mypy.ini       new; optional imports declared, nothing excluded from the analysis
  ci.yml         `actions/*@v7`; new `lint` job on one interpreter; no path filter
  30 files       the findings above
2026-09-22 19:56:34 +08:00
inliver233 9b159253c6 Adopt rasc (the Rust ASC) as the indexer, with its measured blind spot written down
The Python `droidasc` indexer works and is one `pip install` away, but the same project has a Rust
re-implementation on its `rust` branch, and this pass answers whether it can be used instead of the
Python one. It can, with one documented caveat. Everything below was measured here; nothing is taken
from the upstream README's numbers.

**What was built.** `rasc` publishes no artifact -- no release asset, and `cargo install rasc`
fetches an unrelated maths parser -- so adopting it means building it. rustup (gnu host, rustc 1.98.1)
plus a 64-bit MinGW-w64 gcc 16.1.0 as the linker, `cargo build --release` in **117 s**, producing a
2.13 MB binary from `MG1937/ASC@e809c7b` (rust branch, sha256 `1becdb86…c0264b`). The build traps are
recorded because they cost time here: the host's existing MinGW is **32-bit** and fails with
`sorry, unimplemented: 64-bit mode not compiled in`, E:\vs2022 has no VC tools, and the registered WSL2
distribution cannot start (`HCS_E_SERVICE_NOT_AVAILABLE`) -- so neither the MSVC nor the Linux path
was available.

**What it answers.** Same CLI shape as the Python tool (`classes` / `manifest` / `getclass` /
`findrefs {string,type,method,field}`), and on this repository's archives the answers match:

  MASTG UnCrackable-Level3 (1.4 MB)   classes 5.6x   findrefs string 15.6x   findrefs type 16.2x
  a real 34.8 MB app (30,768 classes) classes 4.0x   -- class sets IDENTICAL, 0 differences either way

`scripts/rasc_build.py` wraps the whole path -- `--check`, `--build`, `--verify <apk>` -- so the
verification travels with the kit instead of living in a note: `--verify` compares the class-definition
set against `droidasc` and fails on any difference. It resolves the repository root by walking up
rather than assuming a layout, prepends a non-default `CARGO_HOME` to PATH, and reports a missing
toolchain as exit 3 with the install step.

**The blind spot, and why the rule is a rule.** The rasc project ships its own JADX-parity harness.
Run here on a fair sample (app-like classes; the harness refuses to judge a class JADX cannot
decompile), 17 of 20 were judged and 16 matched literal-for-literal with 0 reorderings. The one
difference is not cosmetic: `org.bouncycastle.crypto.PasswordConverter` is an enum whose constants
override a method, JADX emits the three anonymous subclasses with their `convert` / `getType` bodies,
and **rasc emits a bare constant list -- the bodies are gone and no warning is emitted**. Clean,
plausible, incomplete output is precisely the failure shape this repository catalogues, so:

  use rasc to locate, and to read ordinary classes; confirm an enum that shows no bodies with a
  second reader; keep droidasc and JADX as the cross-checks the skill already requires.

The automated verdict said "literals rasc lost: 1", which understates it -- counting literals cannot
see a dropped body. That is recorded in the evidence file rather than smoothed over.

**What changed in the kit.** `references/rasc-and-droidsaw.md` (the measurements, the failing class,
the build traps, provenance); `scripts/rasc_build.py`; `toolchain.md` Tier 1 now lists `rasc` first
with `droidasc` as the fallback and the cross-check; README structure and script tables; the evidence
record `docs/tool-verification/EXTENSION-rasc.md`.

**One inconsistency this exposed and fixed.** `check_repo.py` looked for a reference or script name in
`SKILL.md` + the README only, while this repository's reference inventory lives in
`references/routing.md` -- so a correctly registered file was reported as orphaned, and the script
half of that check was not actually running. The registration surface is now SKILL.md + README +
`references/routing.md`, nested reference directories are read too, and a planted unregistered script
was used to confirm the check still fires.

Gates: check_repo 0 problem, check_refs 0 dangling / 0 warning, check_routing coherent, check_budget
0 failure / 0 warning, check_commands 154 checked / 0 drift, pytest 625 passed / 163 skipped /
25 xfailed.
2026-09-22 19:38:50 +08:00
inliver233 2beade67f2 Reconstruction pass: make the gates tell the truth, make the package self-contained, keep the one-hop symptom index
An external audit of this repository landed on four structural findings, and this pass is the
response to them. Everything below was re-measured on this tree before being acted on; two of the
audit's claims needed a correction of my own, and both corrections are noted in place.

**The gates were optimistic, and one of them did not read content at all.**

  doctor.py    the capability table was hard-coded (`repack + sign = bool(java) and python`), so a
               machine with a JDK and no zipalign/apksigner was told it could re-sign; the script
               inventory was a hand-written list of 28 against 51 files on disk, omitting the nine
               most important scripts. It now scans the directory (`registered/checked = 52 / 52`,
               unjudgeable entries counted as `unknown`, never skipped) and resolves capability as a
               real requires-closure, reporting BLOCKED with the missing piece and an installable
               next step. Measured: the old doctor reports `[OK] repack + sign` on a JDK-only PATH
               where keytool, jarsigner, zipalign, apksigner and all eight smali jars are absent.
               The report is allowed to look bad; that is the point.
  check_commands.py (new)
               anchor checks prove a section exists, never that a command runs. This parses each
               script's argparse table with `ast` and checks every documented invocation against it.
               146 commands / 41 interfaces, 0 drift. It caught `sig_probe.py --live --pkg`, which
               does not exist -- the reference had been wrong since it was written.
  check_routing.py (new)
               the symptom index now has one home (`SKILL.md`, because symptom -> file must stay one
               hop) and the inventory has another (`references/routing.md`). This gate proves the
               mirror agrees, that every reference file is named, and that every script is named.

**so_constpatch.py was corrupting APKs it rebuilt.** The writer copied filename and timestamp onto a
fresh ZipInfo and recompressed the whole archive: 435-entry real package, `extra` lost on 205 entries,
`external_attr` rewritten on all 435, 132 untouched entries recompressed, and `resources.arsc` pushed
to an unaligned offset. Android's own `zipalign -c -v 4` fails the old output and passes the new one;
a 26-case fixture suite pins it, and `extractNativeLibs="false"` is now refused by default with a
pointer at `repack.py`. Fixing it exposed the same defect in `repack.py`, where `^lib/[^/]+\.so$`
cannot match `lib/arm64-v8a/*.so` -- so its alignment handling had never once fired on a real APK.

**The package was not self-contained.** 59 references pointed at the repository root, which `npx
skills add` does not install. They now converge on `references/evidence-summary.md` (capability,
conclusion, strength, evidence that ships) plus `evidence/` with the machine-readable companions: 8
references remain and each is explicitly labelled "repository root, not shipped". A drill in which
the new files were copied to an isolated directory and asked three questions answered all three.

**SKILL.md: 572 lines / 15,733 tokens -> 398 lines / ~10,900 tokens**, without losing the two things
that had to stay. The symptom index remains inline; the reference and script indexes moved to the
on-demand routing file. The freed budget went into making the rules *stronger*, not shorter: the
constraints section is now a table of thirteen non-negotiable constraints each paired with the check
that catches a violation, and the tooling obligations are instructions whose violation is a defect
(hedging language in SKILL.md: 1 line, down from a paragraph of hedged advice). Long references gained
a navigation head so a reader can decide from the top whether to load one.

**New: an executable test layer.** `tests/` (unit, CLI contract, no-device integration) plus
`pytest.ini`, `tests/run_tests.py`, a minimal CI job, and `evals/evals.json` written but never run --
stated as such in the file. 608 passed / 158 skipped / 25 xfailed. The 25 xfails are a work order, not
a pass: thirteen scripts report a missing input by exiting 1 instead of 2, seven let a traceback
escape, four treat `--help` as an error, and `preflight --json` lacks a `status` field. A regression
row for `dex_patch_bytes.py` reproduces a previous artifact byte-for-byte
(`67ae1e58e7a44777e5ac8f9f823ab21e…`) and additionally pins the four bytes that may move.

**Two real defects the new tests found in the tooling itself.** `check_repo.py` decoded subprocess
output with the locale encoding, so on a GBK console a script printing an em dash in its usage line
was reported as crashed; it now decodes UTF-8 explicitly. And a CLI contract test drove a *real
attached device* through `coldstart.py` -- logcat, screenshots, five orphan `adb logcat` processes --
which is fixed twice over: child processes run in a private temporary cwd, and the fifteen
device-bound scripts are never executed beyond `--help`/argument parsing.

**Quoting on the device side is now one implementation.** `devsh.su` escaped only double quotes, so
`;`, `$( )` and backticks in a caller-supplied value still reached the device shell while
`coldstart.Device.shell` quoted correctly. Both use `scripts/device_shell.py` now: POSIX single-quote
escaping, argv arrays by default, and identifier validation that refuses a malformed package, path or
component with exit 2 instead of a traceback. The integration test that used to pin the *defect* (it
asserted `su -c "…"` with `\"` escapes) now asserts the property, across five metacharacter payloads.

Gates: check_repo 0 problems (including the content-reading identity scan over git ls-files),
check_refs 0 dangling / 0 warning, check_routing coherent (53 symptom / 45 reference / 54 script rows),
check_budget 0 failure / 0 warning with the always-loaded body now measured in tokens rather than in
"narrative lines with the index excluded", check_commands 146 checked / 0 drift, pytest 608 passed.

Not done, and stated rather than implied: CI has never run on a runner; the evals have never been
executed; the 25 xfails are open; no device-side install/launch was performed in this pass.
2026-09-22 01:37:51 +08:00
inliver233 6cffc3d2ac Absorption pass: take the mechanics from four external skill repositories, keep our discipline
Four projects were cloned for comparison (reverse-skill, awesome-game-security,
android-reverse-engineering-claude-skill, MobileRE-Skill) and read for what they do that this
repository does not. None is vendored and none becomes a runtime dependency: what crosses over is a
phase order, a checklist or a command pattern, with the source URL and the retrieval date recorded in
the file that absorbed it.

The rule applied to every candidate: does this make a judgement easier or a verification harder? The
repository is a decision-and-verification layer, and the four things kept are all of that kind.

  new  scripts/scan_leaks.py            target identity scanned before publishing, with an explicit
                                        do-not-anonymize list and four RESULT=/exit states
  new  references/precedents/           the positive half of the record: route including dead ends, a
                                        grade per assertion, measured pits, and a write-back checklist
  new  scripts/svc_scan.py              does a libc-level hook structurally see this call at all
  new  scripts/anti_detect_probe.js     observer-only: name the check, and report the environment it reads
  new  references/desensitization-and-leak-scans.md
  new  references/dart-aot.md sections   the 32-bit snapshot's real string-record format

  check_repo.py now delegates the identity scan to scripts/scan_leaks.py and runs it over
  git ls-files, so the gate reads content instead of layout for the first time. An explicit file list
  cannot be narrowed by the default work-area exemptions -- that defect was caught by the gate itself.

Two claims that this repository had already written down turned out to be wrong, and both are
recorded as refuted rather than quietly rewritten:

  dart-aot.md   the armv7 string table was documented as [len*2 u32le][UTF-16LE]. It is not. The same
                extractor keeps 4,980 chained entries on arm64 and zero on armv7, and the byte layer
                shows the 32-bit record is [header u32][byte-count u32le][UTF-8 payload]: two adjacent
                literals carry 0x46 (70) and 0x48 (72), exactly their byte lengths. The zero was a
                format mismatch, not an empty table. The engine-version banner on the same target is
                66 bytes of "0.0.1" with a blank build id, so the documented regex returns a
                version-shaped lie.
  leak scan     the pass that added the scanner found 26 strong hits inside its own evidence file on
                the day it was written -- the fixture's literal match values, quoted to prove the
                rules fire. The corpus may contain values; the published surface may not.

Measured, not asserted. The check that fires was named on a public MASTG target: three arms with one
variable, the control alive at the end and the probe arm dead in about 300 ms, with strstr("frida") at
t=303 ms and no tombstone. Reproducibility is bounded honestly -- one full sequence in three arms, on
a target whose whole window is sub-second. svc_scan.py's two independent decoders agree on an
identical 214-site set, and ptrace-free dumping gained a negative boundary: a target whose dex is
deflated inside its APK has no whole-image dex in memory to export, for reasons that say nothing about
protection.

Also in this pass: the detector-locating pipeline (detection-and-anti-analysis.md Step 3) with its
branch conditions; the layered Java-to-svc descent and the dump's honesty boundary in
advanced-unpacking.md; a sourced capability-gap list in toolchain.md where every row is graded
URL-verified or measured; the RESULT=-token convention; and SKILL.md opening with an immediate-action
block, because the symptom index and the gates were sitting in the part of a long file nobody reads.

Gates: check_repo.py 0 problem, check_refs.py 0 dangling / 0 warning, check_budget.py 0 failure /
0 warning / 1 pre-existing note. The scan reports RESULT=leaks_found_strong_only, exit 0: four
endpoint/weak hits, all of them addresses from the RFC 5737 documentation range in usage examples.
2026-09-21 22:51:09 +08:00
inliver233 7176b5e1c9 Restructure to the skills/<name>/ layout the skills CLI resolves
The skill lived at the repository root, which the CLI reads as a single-skill repository and which makes a second skill impossible to add. SKILL.md, references/ and scripts/ move under skills/apk-reverse/ with git mv, so history follows and no in-skill reference needs changing (they are relative to SKILL.md).

check_repo.py now discovers every skills/*/SKILL.md, validates frontmatter by parsing it instead of pattern-matching it, and requires the README to name skills through explicit skills/<name>/... paths.

check_refs.py is new: it audits file.md section cross-references against the real headings in the target file. It found six dangling ones, all fixed here.

SKILL.md: the description is now quoted. Unquoted, it contained a colon-space, which is invalid YAML, so the CLI skipped the skill and reported No skills found. Adds a Coverage section stating what is out of scope and what to do when nothing matches. toolchain.md: IDA is not GUI-only (idalib-mcp is a headless MCP server), plus a section on MCP servers as external dependencies and the deobfuscate-before-LLM boundary. README.md: install via npx skills add, the new layout, coverage and maintenance tools.

Verified: check_repo.py and check_refs.py both clean, build_scripts.py audit 0 findings, npx skills add . --list reports apk-reverse, and an install into a scratch directory lands SKILL.md plus 28 references and 38 scripts.
2026-09-20 03:37:46 +08:00
inliver233 a552157f1b Close the byte-patching gap, add the server-config path, fix two script crashes
Worked end-to-end on a real unobfuscated commercial app (single-dex, no packer,
server-driven launch screen) and folded back everything that cost time. Net +17
files, no new dependencies, no sample-specific identifiers.

New references
  byte-level-patching.md
    The technique that was missing entirely. Equal-length in-place edits beat
    method rebuilding (measured on a real sample: method rewrite grew debug_info
    924 B -> 22.8 KB and the dex 4.32 MB -> 7.73 MB), but four failure modes are
    silent: same-length is not enough, branch polarity, verifier legality, and
    stale dex header fields. Covers locating an instruction's exact offset (why
    listings and byte-pattern guesses both fail), the instruction width table and
    the two traps that desynchronise a decode, neutralise-a-branch vs redirect-it,
    and the header's signature-then-checksum order.
  server-config-and-updates.md
    The most common shape of "ad" in a modern app, and the one usually
    mis-diagnosed: the server supplies UI the client renders. The two-layer fetch
    (local fallback then remote override) that proves it, finding the config DTOs
    through the field names data classes keep, why you patch the decision and not
    the data, deciding the scope of "remove", and remote re-enable / cached config
    durability.

New scripts
  dexutil.py             dependency-free dex reader: structural walk, exact
                         instruction decode, dex header recompute/verify in the
                         correct order, branch-target and operand helpers
  dex_find_insn.py       locate an instruction by decoded semantics, print its exact
                         byte offset with context and BOTH sides of any branch
  dex_patch_bytes.py     equal-length byte patches from a JSON spec: semantic match,
                         polarity pin via expect_next, equal-length enforcement,
                         verifier check, header recompute, re-decode to prove it landed
  dex_check_verifier.py  does any conditional branch target a move-result (bypassing
                         its producer)? Compares builds and separates pre-existing
                         findings from regressions the patch introduced
  coldstart.py           cold-launch capture: timed screenshot burst + logcat signals
                         + installed-build facts + launch timing, and a warning when
                         the foreground activity is not the app
  check_repo.py          maintenance check: every script parses and answers --help,
                         every referenced path resolves, every file is documented

Fixes found while running the above
  repack.py       wrote archives with a plain zipfile writer, which cannot satisfy
                  the Android R+ install gate: resources.arsc must be STORED *and*
                  4-byte aligned, and a zip extra record cannot express a 1-3 byte
                  pad (a stored filler entry is inserted instead). Also fixes the
                  central-directory pointer and adds an alignment gate to the output.
  usb_net_proxy.py, dex_strpatch.py
                  crashed on --help with a traceback instead of printing usage.

Updated
  toolchain.md            ddc added (single-binary dex->Java with query subcommands,
                          with measured timings and its real limits); "not on PATH is
                          not not-installed" with the search commands; which signer
                          to use and why jarsigner breaks alignment
  repack-and-sign.md      alignment requirement and its arithmetic; dex header
                          recompute; jarsigner hazard; vendor install interception
                          ([N] with no symbolic constant -> root pm install); the
                          installer can keep owning the screen after an install
  recon.md                resolve package identity from a tool, not from hand-read
                          AXML strings (a wrong package sends all later device queries
                          nowhere); snapshot the config surface during recon
  long-task-discipline.md captures you never looked at are not evidence (incl. the
                          consistent-but-wrong-window variant); long-context decay and
                          the record shape that survives it
  dynamic-frida.md        when live attach cannot work at all: static instrumentation,
                          static read, and self-recorded evidence, with the control
                          build caveat each one carries
  ad-removal.md           server-driven config is usually the EASIEST case, not the
                          hardest; a three-way zero result is a positive finding;
                          scope note on local brand splash vs promotional content
  pitfalls.md             P37 stale dex header checksum, P38 [-124] alignment,
                          P39 bare-numeric install failure, P40 wrong-window evidence,
                          P41 inverted branch polarity, P42 decode desynchronisation
  SKILL.md, README.md     symptom index, reference/script indexes and requirements
                          updated for all of the above
2026-09-20 02:05:41 +08:00