Commit Graph
7 Commits
Author SHA1 Message Date
inliver233 ce2e4f98e1 Extension pass: module route, extraction shells, emulation/RPC, DBI, kernel layer, protocols
Closes the six coverage gaps named in an external review of this skill, and moves the
deliverable-form decision out of the single assumption it rested on.

New references (7):
- lsposed-and-modules.md      deliver a hook module when a repack is refused
- advanced-unpacking.md       extraction shells, FART-style recovery, the root-side dump
- emulation-and-rpc.md        call the routine instead of reading it
- native-dbi-and-deobfuscation.md  OLLVM, Stalker traces, two measured boundaries
- protocol-reverse.md         protobuf/gRPC/QUIC and native-side pinning
- kernel-and-environment-hardening.md  when userspace hooking cannot reach the check
- on-device-tooling.md        MT Manager, its APK MCP, working from the phone

New scripts (8): dex_dump_validate.py (+ --trim for page-aligned captures),
dex_mem_scan.py, lsposed_scaffold.py, frida_rpc_serve.py + rpc_template.js,
stalker_trace.js + stalker_report.py, mt_mcp_probe.py

SKILL.md:
- G1 carries a four-form deliverable table (rebuilt APK / module / RPC service / report)
  with the evidence that says when to leave the repack column, plus a matching hand-off
  section on how "verified" moves with the form
- Coverage gains a documented-routes tier carrying its own strength note; Not-covered
  loses the now-inaccurate "general unpacker" line and gains the limits that do apply
- 13 new symptom rows, 7 reference rows, 8 script rows, a fifth tooling obligation
- the pid-drift symptom is stated with its competing causes, not a single attribution

toolchain.md gains measured ASC/ddc cards; README gains the structure, capability and
scope entries for everything above; .gitignore stops tracking the local workspace.

Evidence: docs/tool-verification/EXTENSION-*.md (7 files), same three labels as the rest
of the record. Measured: the gradle-free module build chain (5.27 s end to end), a
successful injection confirmed through the LSPosed file channel (9 rounds) after three
logcat queries came back empty on a ROM with a broken logd, 17 dex images recovered by
root-side /proc/<pid>/mem dumping, the trivial-body metric separating a shell skeleton
from real bodies, ASC/ddc on a hardened APK, and Stalker's zero-event and crash
boundaries. The pid-drift attribution was corrected twice against better measurements.
2026-09-21 20:57:41 +08:00
inliver233 5fa80a5bea find_refs: read .dex directly, and stop reporting unreadable input as zero refs
The docstring promised "a smali tree or a directory of dex files" and delivered neither: iter_files() yielded only .smali, a direct .dex was read as UTF-8 text and regex-matched against smali syntax, and a dex-only directory yielded nothing at all. A real dex, a dex directory and a typo path all printed [total refs] 0 and a note blaming the needle, so the one tool the skill recommends for judging blast radius before patching returned a confident false zero.

A .dex is now decoded through dexutil (no baksmali needed), a directory is walked for .smali and .dex alike, an archive is opened for its classes*.dex, input that cannot be read is refused with exit 2 and a reason, and every run prints [scanned] N file(s) so the two cases can never print identically again. Also corrects the recorded exit code for this defect: it was 1, not 0, which moves it from "no signal at all" to "the signal was there and the message contradicted it".

Verified against a fixture whose call graph is known by construction (docs/tool-verification/fixtures/make_fixture_dex.py, committed as the evidence): the .dex and the equivalent smali tree both return 3/2/2, matching what was written. And against the real target: Ljava/lang/String;->length gives 356 refs in 2.3 s where it previously gave 0; a five-dex directory gives 9,472; a typo path exits 2.

Docs updated with it: F9 records the opcode-table finding the fix exposed, F2/F3 and TOOL-VERDICTS.md are corrected, and dex-patching.md now tells the reader to look at the [scanned] line before the count.
2026-09-21 18:19:23 +08:00
inliver233 03485faa2a dexutil: take opcode names and widths from the format specification
The opcode table was shifted by one from 0x16 to 0x2C, and nine widths were wrong with it: 0x1C was named monitor-enter and given one unit where the specification has const-class at two; 0x17 was const-wide/16 at 2 where the spec has const-wide/32 at 3; 0x18 was 3 where the spec has const-wide at 5; 0x26 was throw at 1 where the spec has fill-array-data at 3. Each wrong entry shifts every instruction after it in the same method, and the decode keeps producing plausible instructions, so nothing looks broken until an operand index walks off its table.

Measured on a real 8.9 MB dex before the fix: 65 of 50,791 method bodies did not decode to their declared boundary, and 32 carried an operand index outside its table. After: 50,791 of 50,791 land exactly on insns_off + insns_size*2, with 0 illegal indices. Cross-checked against androguard 4.1.4, which shares no code with this project: identical per-instruction width sequences for all 50,791 bodies.

Names and widths now come from the format specification table, and width is a lookup (OP_UNITS) instead of a chain of range tests -- the shape that let a one-observation-at-a-time repair look complete. The goto family moves with it in branch_target() and describe(), and in dex_find_insn.py kind map, where a search for a branch was matching throw and goto/32.
2026-09-21 18:19:16 +08:00
inliver233 ab0a5ae0e2 dexutil: fix instruction widths that desynchronised the decode
The decoder could lose sync mid-method and keep producing plausible instructions,
only shifted -- so every offset derived after the fault point was silently wrong.
Found by differential testing against two independent decoders (dexlib2 and
androguard), not by reading the table and trusting it.

Fixed, each verified by asserting the walk ends exactly on
insns_off + insns_size*2 across three real dex images:

- Payload widths. The field after `00 <ident>` is a different quantity per payload
  kind: packed-switch has size(uint16)+first_key(int32), sparse-switch has
  size(uint16)+size*(key,target), fill-array-data has element_width(uint16)+
  size(uint32). All three were read as `1 + <one uint16>`, so a 30-element
  four-byte-wide array payload counted as 5 units instead of 64.
- 0x22 is 22c (2 units); 0x23/0x24 are 35c/3rc (3 units). They sat in each other's
  groups -- the inline comment already said `22c new-array` on a group returning 3.
- 0x20 was grouped as 2 units.

Result: methods that fail to land on the exact end went from 137/113/70 to 44/51/30
across the three images, a 55-68% reduction.

Two further slot-wise edits (shifting the goto family by one width) were made during
the investigation and REVERTED: measurement showed alignment got worse, because the
name table's misalignment had made the wrong slot look authoritative.

Remaining defect, recorded in-file rather than guessed at: OP_NAMES is misaligned with
the opcode slots from roughly 0x1a onward (the slot named `array-length` decodes as
`instance-of`, `goto/16` decodes as `goto`). Widths key on the opcode value, not the
name, so this does not corrupt a decode by itself -- but the names are unsafe as a
lookup key and they misled a reader here. Fixing it means rebuilding the table from an
independent decoder in one pass, not editing it slot by slot.

check_repo.py: 0 problems. check_refs.py: 182 reachable, 0 dangling.
2026-09-20 19:37:19 +08:00
inliver233 206a2f5465 Toolchain guidance, observation ordering, and target-neutral records
Capability-first tooling guidance, plus removal of target identity from the repo.

Toolchain (references/toolchain.md):
- New ASC (droidasc) section + README Requirements row. An agent should reach for an
  indexer before any full decompile. Covers install, query subcommands, the division
  of labour with ddc (ASC locates, ddc reads), and its measured limits -- including the
  unverified-negative trap when a non-ASCII needle goes through a shell, and the
  mangled-class case where ASC is the only route.
- ddc: two measured limits -- a renamed class is unreachable via -c, and a decompiled
  body can be semantically wrong rather than merely ugly.
- New "Closing a capability gap": installing the missing tool IS the task, with measured
  install costs set against the cost of the workaround.
- New "Using the kit's scripts instead of writing your own", with two-tools-disagreeing
  as the bug detector it is.
- Ghidra section for the aarch64 case: headless invocation, the flattening caveat, and
  the "a decompiler cannot go where the data is not" boundary.
- HaE placed in Tier 3 as traffic triage, naming the three situations where it is the
  wrong next step.

Observation ordering:
- SKILL.md: new Tooling section with four obligations; G2 now requires a written
  capability inventory; symptom index gains four rows (silent feature loss with no
  request at all, telling your patch apart from the target's own behaviour, spawn keeps
  the Activity stack down, the device server being killed by the ROM).
- dynamic-frida.md: the patch-then-detach ordering, and device-server survival notes.
- New scripts/spawn_patch_detach.py and scripts/hook_patch_only.js.

Neutrality:
- docs/verification-jiongnew/ -> docs/tool-verification/. Target-specific transcripts
  dropped; all target identity removed. The record keeps the measurements and loses
  the specimen.
- No target identity remains anywhere in the tree.

check_repo.py: 0 problems. check_refs.py: 182 reachable, 0 dangling, 0 warnings.
2026-09-20 19:14:36 +08:00
inliver233 e193f425ba Measure the toolkit on a real Flutter target and correct what it claimed
Ran the scripts against a real Flutter AOT / multi-dex / 36-.so APK (Dart 3.6.0,
com.tingfeng.tool) and recorded what each one actually did. Two of them were wrong
in the direction that reads as a target finding, one Coverage claim overstated what
ships, and four documentation statements were false.

Fixed:

  dart_pprefs.py decoded only the general-purpose register file, so `ldr dN,[x27,#imm]`
  (0xFD400000) was never matched and every double constant read from the object pool
  was dropped -- 302 pool offsets and 1802 sites on this sample, one of them with 150
  references that would have been reported as zero. The documented advice is to count
  references before editing a shared constant, so a one-sided quiet gap here produces
  exactly the wrong decision. Now covers both register files; a purpose-written
  arbiter agrees with it to 0/0 across .text, against 0/302 before.

Documented but deliberately not fixed, because a fix would not have been run:

  elf_plt.py misses the last PLT stub slot (`off < fsz - 16`), which on libloader.so
  turned a real, called import (dl_iterate_phdr) into "not a resolved stub".
  find_refs.py advertises dex-directory input but iter_files() accepts only .smali,
  so it answers "no references" with exit 0 for input it never read.
  so_constpatch.py's rebuild drops the zip `extra` field and destroys 4 KB alignment.
  Each has its root cause and fix sketch recorded.

Documentation corrections in dart-aot.md:

  blutter's asm/ does contain full instruction listings with pool annotations -- the
  file said to ignore it, and it is the richest artifact in the chain.
  The first build costs ~78 s, not "tens of minutes"; MSVC 19.34 suffices, not VS 2022.
  The file-PP offset mapping is not recoverable from the binary (4241 shared strings,
  4237 distinct deltas), so a snapshot-decoding front end is a dependency, now stated.
  Documented aotopsy as the no-toolchain route, and the trap that its version label is
  a structural profile name (it says 3.6.2 for a 3.6.0 snapshot).

Coverage now separates what was measured from what is inferred, names blutter/aotopsy
as a prerequisite instead of implying the pool decodes itself, lists the scripts this
pass did not run, and says plainly that the packer / VMP / integrity-check scenarios
were not exercised at all.

Added hand-off points for JNI, dex-observed hardening, and the existing native boundary
documents, plus docs/verification-jiongnew/ -- the evidence record, including the four
gate outputs verbatim. No second skill: the gap is one front-end dependency, not a
subject area, and relocating Flutter docs would not have supplied it.
2026-09-20 04:59:38 +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