Commit Graph
70 Commits
Author SHA1 Message Date
dabao1955 2a6d972a0f [temp] Use codeberg machine 2026-10-20 04:20:08 +02:00
root caa981b7e7 Absorb the jsvmp series: JS VMP recovery, and a round-trip-tested loader decoder
A new documented route for logic protected as a private JS opcode stream (jsvmp):
recognition signals, the call-it-first route, stack- and register-machine reading,
AST instruction extraction, the four JS evaluation traps, constant folding and
control-flow recovery. Every procedure is inferred (absorbed from three public
write-ups, anonymized; none run on a real target) and labelled so.

The one shipped piece is the loader decoder (base64 -> varint -> ZigZag), measured
against its own round trip (2,016 values exact, canonical varint/ZigZag layouts
pinned, truncation raises) and it decodes the write-up's own sample literal end to
end. Wired into the symptom index, routing tables, coverage ledgers and the
capability matrix; RN/Hermes still not covered, with the jsvmp carve-out stated.

Also clears two warnings: an E302 in the new script, and the long-standing
check_commands warning where a tool-verification transcript quoted check_budget.py
with its result inline (annotations moved to a continuation line, content intact).

Ran: check_repo/check_refs/check_routing/check_commands/check_budget (all exit 0),
flake8 clean, pytest 763 passed / 199 skipped / 25 xfailed.
2026-10-19 22:33:01 +08:00
dabao1955 eca9c19ee4 Fourth audit pass: seccomp as anti-instrumentation, and the anti-debug sweep
Two focused audits: the seccomp article under the lens of seccomp as the thing
that blocks your tools (not just a sandbox fact), and a corpus-wide sweep for
anti-debug material against the detection file that owns the topic.

The seccomp audit's strongest finding was repo-internal: advanced-unpacking
claimed seccomp filters were among the 5.10-gated layers "closed" on the 4.14
reference device -- while the repo's own extension notes observed Seccomp: 2
active on that very device, and the hardening file says "any modern kernel".
The contradiction is resolved in favour of the measurement: seccomp is now
named as the one syscall-layer mechanism NOT closed there. The hardening row
gains the full decision set (TRACE hands a syscall to your tracer; KILL_PROCESS
is the action that impersonates a detector), the bionic-installs-at-init timing
with its unverified-network marker (the cnb.cool mirror carries frameworks/base,
libcore and art but not system/seccomp or bionic -- the URLs to retry are
recorded at the claim), the external-tracer-exempt mechanism note (a filter
evaluates the installing process's own syscalls, so "they seccomp-blocked my
attach" is structurally wrong), and Seccomp_filters>1 as the target-installed-
filter signal.

The detection funnel gains the fourth cause its environment table lacked: the
process's own filter denying the agent's syscall -- Seccomp: 2 is the normal
state of every zygote-forked app, and the discriminator is a SIGSYS/SYS_SECCOMP
tombstone naming YOUR syscall, vanishing in the control run. This also finally
interprets the Seccomp field anti_detect_probe.js has been capturing all along.
Snowblind (2024) lands as a named pattern only -- recognition strings in the
third-party-builds marker table and a Step-2C rationale row; the filter+handler
implementation stays out on the stated line: recovery changes the analyst's
observation channel, weaponization makes the target's policy stop applying.
svc_scan.py learns syscall 277; seccomp-tools joins the toolchain table as an
offline filter reader with unverified grades.

The anti-debug sweep's verdict was the opposite shape, stated plainly: on this
topic the corpus is the shallow party and the repo is the owner -- TracerPid,
loader discipline, Frida-detection specifics are all already covered deeper,
and no corpus-driven table was manufactured. What it did add: the
explain-the-target half of SELinux policy reading (wildcard grants, attribute
memberships, missing type transitions -- why the app reaches what you thought
was closed, with the read/inject boundary drawn), the magiskpolicy one-liner
for the measured frida-server denial, and the three kernel-observation gate
knobs (kptr_restrict, dmesg_restrict, perf_event_paranoid) in the ladder rule.

Gates re-run: routing coherent, 414 section references zero dangling, repo
clean (after check_repo caught a broken string literal this pass introduced --
the gate did its job), pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 22:08:14 +08:00
dabao1955 ce227a13c5 Third audit pass: WebView as a probing channel, source-verified against AOSP
Three re-audits under new lenses -- the WebView/JS-bridge as an active probe for
hidden APIs and core logic (not just a config risk surface), environment blind
spots, and jvm-art internals -- each instructed to verify claims against the
cnb.cool AOSP mirror. The subagents' network was denied, so the mirror checks
were run in the main session: WebView/WebView.java and WebSettings.java javadocs,
platform.xml, art/runtime/hidden_api.cc, ActivityThread.java, and
libcore's DexPathList.java were all fetched and cited.

The probing lens rewrote system-ipc-audit.md section 3 around what the audit
found: the bridge-inventory hook over-reported (it printed every declared method;
since API 17 only @JavascriptInterface-annotated public methods are exposed --
javadoc-verified, with the targetSdk<17 legacy caveat), the risk table gained a
defaults column so absence stops being read as off (setAllowFileAccess defaults
true through targetSdk 29; the FileURLs pair has been false-and-deprecated since
API 16), and the DevTools entry was split into its two channels (chrome://inspect
self-discovers; the forward is for /json, socket found via /proc/net/unix) with
the process-wide and debuggable-auto-enable gates stated. New: the five-step
probing playbook -- global diff, dictionary probe (enumeration of the injected
object returns empty on modern WebView), arity/type oracle with the parallel
logcat watch (a Java exception stack can name the handler class without any
hook), return-oracle calibration, and the origin-gate read -- each routed back to
the Java class for an ordinary hook.

jvm-art: the hidden-API boundary sentence in dynamic-frida.md was directionally
right and mechanically wrong; it now states the real policy (targetSdk>=28
gating; greylist warn-only; blocklist and greylist-max-X deny -- verified against
hidden_api.cc), the caller-domain mechanism that explains why Frida's bridge
passes where app reflection fails, and the debuggable/policy exemptions. The
classloader walk (pathList -> dexElements -> Element.dexFile, field names
verified against current main) joins the frida-dexdump route as the hand-rolled
fallback that also says which loader owns a dex.

Environment: wireless debugging/pairing (Android 11+, pairing vs connect ports,
plaintext-TCP caveat), the -d downgrade gate (INSTALL_FAILED_VERSION_DOWNGRADE is
a build-type fact), --user N multi-user handling, and run-as as the cheapest
non-root inspection route. One auditor correction was REJECTED by verification:
current-main platform.xml assigns the shell uid only INTERNET, so the proposed
"shell holds INSTALL_PACKAGES/INJECT_EVENTS" rewrite was itself wrong; the
existing measured claim stays, now with the platform fact recorded beside it.

Gates re-run: routing coherent, 412 section references zero dangling, repo clean,
pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 21:52:42 +08:00
dabao1955 2bb54a72fb Second audit pass: fill gaps and correct drift, with verification where possible
Five read-only re-audits of the source material under a different lens -- not "is
it covered" but gap-filling, drift correction, and a user-assisted framing in
which the AI names the hook point or command and the human builds, runs, and
pastes back. The lens change earned its keep: the audits found drift that the
coverage lens could not, including two errors in what this repository had itself
absorbed.

Drift corrected, worst first:

- native-and-so.md: the absorbed syscall example loaded #0x47 (71, sendfile) --
  the source article's "real libc fragment" mislabels its own bytes, and the
  absorption inherited it. Now #0x40 with the mislabel named, the bionic wrapper
  epilogue (cmn x0,#0x1,lsl#12; b.cs) added as the discriminator between libc's
  own svc sites and a protector's inline ones, and the "no X8 load means data"
  claim softened to match svc_scan.py's nr=? semantics.
- system-ipc-audit.md: the generated-proxy example named installPackage, dead on
  modern IPackageManager -- swapped for getPackageInfo (already this skill's own
  hook) and IPackageInstallerSession.commit. Transaction-code guidance now says
  read the TRANSACTION_* constants, do not count the .aidl. getCallingUid gains
  its app-process reading semantics (own uid; uid 1000 on relayed callbacks --
  the "the system is calling my app" misread). service call gains the full type
  tags, the automatic interface token, and the default-binder-context boundary.
  The binderfs path variant and binder_calls_stats enable are recorded.
- packers.md: "dlopen not found = your bug" was one cause wearing three faces --
  now the namespace/DT_NEEDED/ABI triage with the classloader-namespace logcat
  discriminator and the 7.0+/8.0 gates; plus the extractNativeLibs="false"
  caveat on add-a-file tests, matching what so_constpatch.py already refuses.
- lsposed-and-modules.md: LSPatch paragraph re-labeled per evidence -- the
  delivery-class reasoning stays repo reasoning, the absorbed facts now carry a
  network-verified marker (upstream README: dex+so insertion, rootless, min
  Android 9; LSPosed active as of 2026-10-14). The api:100 parenthetical no
  longer implies a rewrite requirement.
- dynamic-frida.md: the LD_PRELOAD row gains its version gates (7.0+, Treble 8.0).

Gaps filled: signed-vs-unsigned branch suffixes and PAC/BTI recognition with the
SIGILL-on-patch consequence (native-and-so); the on-disk oat/vdex artifacts
check with the InMemory and quickened-opcodes gates, cross-linked from the
"decoder you did not write" rule (advanced-unpacking); the hook-point map --
funnel vs hop, ATMS/install/session gates stated (system-ipc-audit); the
interactive gdbserver escape hatch in detection Step 2, with the
frida-specific-vs-tracer-generic discrimination, and the user-session command
core in the toolchain row; the Hook vocabulary table translating instructions
into MethodHookParam calls, javadoc-verified, closing the one point where the
user-assisted loop forced a repo exit (lsposed-and-modules); cross-links from
the neutralization and TLS hook-body tables to their delivery vehicles.

Gates re-run: routing coherent, section references zero dangling and zero
warnings, repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 21:32:41 +08:00
dabao1955 8c00c88035 Add the AArch64 reading layer the native file assumed
native-and-so.md taught how to find and hook JNI calls in a stripped library but
never said what the listing itself means: registers were named only incidentally
(pitfalls P43 assumes "the AArch64 argument registers" as known), the frame
pattern appeared in examples without explanation, and the addressing shapes were
nowhere. The source notes' ARM64 essentials 00-02 are exactly that missing layer,
and the original scoping pass had filed the whole series under "language teaching"
after reading only its debugging chapter -- same silent-truncation failure mode
the article audit already caught twice.

New section "Reading AArch64 -- registers, addressing, frames", placed after the
call-site recipe it underlies: the register role/saved-by table with the Wn-
zeroes-the-high-half truncation trap, the AAPCS64 argument rules including the
separate floating-point channel and the JNI consequence (env X0, thiz X1, user
args X2 -- the reason the vtable shape above loads from X0), the canonical
prologue/epilogue with the X29 chain identified as what every backtrace walks,
the six addressing shapes on sight (including SXTW-vs-UXTW and the adrp+add PIC
pattern the source notes omit but every string xref wears), movz/movk immediates
with the byte-patching consequence, and the mov-x8-plus-svc syscall shape tied
back to what svc_scan.py actually locates -- including the data-not-code reading
of an svc hit with no X8 load nearby.

03 (GCC inline-asm syntax) and 05-07 (ROP, shellcode, overflow exploitation)
stay out by design: development and exploitation material, not lookup value for
reading a library during a reversing task. Provenance and strength labels at the
point of use, retrieval date included. Gates re-run: routing coherent, 409
section references zero dangling, repo clean, pytest 751 passed / 194 skipped /
25 xfailed.
2026-10-19 21:09:36 +08:00
dabao1955 2f6b26f4dd Close the gaps an independent audit of the source articles found
The milksu integration was audited a second time, independently: four read-only
passes, each taking a group of the source articles and verifying the coverage
claims against the whole repository (references and scripts), with instructions
to contradict rather than confirm. Their findings corrected the prior pass in
both directions and drove every change here.

The audit's most useful correction was against the integration itself: the
ActivityThread.currentApplication() Context idiom had been reported missing
while it ships working in scripts/rpc_template.js and scripts/sig_probe.py --
the pass had audited only references/. The fix is a cross-link, not absorption.
Two more claims were narrowed (advanced-unpacking covers the classloader lab,
not the dynamic-proxy lab; readelf coverage lives in toolchain.md, not the
files first proposed), one attribution was repaired, and one provenance note
was added: the absorbed Binder hook is not the article's script -- the source
hooks Binder.transact with server-side getCallingUid() semantics inside; the
form kept here is corrected and now says so.

What the audit found genuinely absent, now integrated (all labelled unverified
or inferred with the retrieval date at the point of use):

- system-ipc-audit.md: generated-proxy hooking (decoded arguments beside the
  raw transact), the debugfs binder node/ref view for "why does my hook never
  fire", two new Intent signature rows (intent-scheme handoff, ClipData URI
  grants), am start/broadcast action-data-extras driving forms, the Intent
  monitor with live nested-Intent detection, the deep-link getData triage,
  three config rows the earlier table silently truncated (JavaScript, DOM
  storage, mixed content), the loadUrl dynamic hook, the logcat Console
  fallback channel, the four-surface static WebView pass, and the attestation
  KeyDescription/OID reading with the teeEnforced extraction discriminator.
- lsposed-and-modules.md: LSPatch as the fourth route -- module-class
  authoring, repack-class delivery, runs on unrooted phones; plus existence
  proofs for the resource-hook and constructor/field/hook-all API surface.
- dynamic-frida.md: the scripts' own Context idiom cross-linked with the
  Android 9+ hidden-API reflection boundary; dynamic proxy as an interception
  primitive; the LD_PRELOAD-closed-on-Android row in the non-live table.
- toolchain.md: the lldb-server remote recipe, with the attach-is-detection
  pointer.
- java2c-and-jni-sinking.md: the __<proto> mangling decode route step 2
  silently depended on.
- advanced-unpacking.md: the local-reference cap hazard on active-invocation
  loops.
- native-and-so.md: the JNI width/type map beside the vtable shape.
- recon.md: the MODE_WORLD_READABLE/WRITABLE row the earlier table dropped.

Gates re-run: routing coherent, 409 section references with zero dangling,
repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 21:05:08 +08:00
dabao1955 526d487f1a Fill the reading-layer gaps the IPC reference exposed
The first milksu integration only took the system-boundary material and dismissed the
workflow content as overlapping deeper files. That dismissal was asserted, not audited;
auditing it found five places where the repository owns the procedure but not the
reference material the procedure leans on:

- dex-patching.md: a reading-level smali cheat sheet (type descriptors, register
  conventions and the .registers coupling, the five instruction families that carry
  control flow, obfuscation signatures). The file taught how to edit a method you can
  already read; nothing taught the reading.
- recon.md: the sensitive-behavior grep table (shell-outs, JS bridges, mutable
  PendingIntents, WebView width, dynamic dex loading, intent redirection) -- the
  signature-check section had one pattern, the behaviour sweep had none.
- detection-and-anti-analysis.md: the named Java-level root checks and their one-line
  neutralizations, as the concrete form of Step 2A's "simple, localised check" --
  scoped to the named detector, never a blind stack.
- tls-and-cert.md: the two OkHttp-path client sites (CertificatePinner.check,
  SSLContext.init) that the OkHttp section previously pointed at without naming, each
  annotated with what it honestly buys.
- native-and-so.md: the JNIEnv vtable call shape for reading JNI calls in a stripped
  library, with the offset-calibration caveat.

All absorbed material is marked unverified with the retrieval date (milksu.org,
2026-10-19) at the point of use; syntax and layout facts that follow from documented
platform behaviour are marked inferred. Gates re-run: routing coherent, 409 section
references with zero dangling, repo clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 20:29:03 +08:00
dabao1955 60224a26b9 Add the system-IPC audit reference (Binder, Intent, WebView, SELinux, attestation)
The reference set owned the app's own wire (protocol-reverse.md) and the in-process
runtime (dynamic-frida.md), but not the platform boundary between them: what the app
asks system_server to do over Binder, which Intent/JS-bridge inputs an unmodified
build exposes, why an avc: denied is blocking instrumentation, and what a Keystore
attestation check means for a rooted-device workflow. That gap is where several
long detours start -- a hook that could never fire because SELinux says the domain
cannot reach the target, or a "detection" that is actually a server-verified
attestation no client patch can satisfy.

Everything here is absorbed from an external notes source (milksu.org, Android
internals notes, retrieved 2026-10-19) and is labelled accordingly: the commands and
hook scripts are unverified in the coverage-and-limits.md sense -- reported by someone
else, never run by this repository -- while the platform mechanisms they rest on
(AIDL codegen arithmetic, kernel-filled caller identity, FLAG_IMMUTABLE since
Android 12) are inferred from documented AOSP behaviour, with a note that each fails
loudly if wrong. The strength note at the top of the file states this before any
content, and each script carries its own unverified marker, so a reader cannot
mistake a starting point for a measured procedure.

Routing gains one row next to server-api.md. Gates re-run: check_routing.py
coherent (53 rows / 51 files), check_refs.py 0 dangling over 408 section
references, check_repo.py clean, pytest 751 passed / 194 skipped / 25 xfailed.
2026-10-19 20:25:12 +08:00
inliver233 1b94651b1e Fix the bare path that turned CI red
The README named `references/routing.md` without the `skills/apk-reverse/` prefix, and
`check_repo.py` requires that form in the README. CI was right; the commit that introduced it was
pushed without re-running the gates, which is the one step CONTRIBUTING.md asks a contributor to run
and I skipped.

No behaviour changes: the path is now spelled the way the gate requires in both language editions.
The local reproduction is the same as CI's, so this was a process failure rather than a
platform-dependent gate — worth stating plainly, because "it passed locally" is exactly the excuse
the gate cannot hear.
2026-10-16 19:29:14 +08:00
inliver233 7d6b4c7fe9 Do not overstate what the progressive split buys
SKILL.md loads in full; a reference loads when the symptom index or the routing table sends you to
one. That is usually described as cheaper context, and only half of it has been measured here: the
always-loaded body has a measured size and a gate that fails when it grows.

The other half has not been measured -- how many references one real question pulls in, and whether
fetching them one at a time costs more than the alternative would have. A question that needs four
references is not obviously cheaper than one longer file, and each reference also carries the index
row you have to read to find it.

The split's economy is therefore `unknown` and the body budget is `observed`. Saying so is the point:
the label discipline is worth nothing if the repository exempts its own design choices from it.
2026-10-16 19:25:02 +08:00
inliver233 905544ea89 Name the three path namespaces, and where a crossing is legal
A path in this skill lives in one of three places -- the host filesystem, the device filesystem, or an
APK entry name -- and the same string means something different in each. Two properties decide most
of the failures that follow:

  * a host path does not exist on the device and vice versa; `adb push`/`adb pull` is the only place
    a translation happens, and it is the caller's to make;
  * an entry name is always `/`-separated and case-sensitive whatever the host OS is, so a path built
    with `os.path.join` matches nothing on Windows.

The symptom is a "no such file" naming a path you can see exists -- on the other side of the boundary.
`environment.md` now says to ask which namespace a string is in before asking whether the file is
there, and points at `validate_device_path`, which refuses a host-shaped path before it reaches the
device shell where it would fail far from the caller.

The leak scan caught the first draft: the example in the table was a real-looking POSIX home path and
`scan_leaks.py` flagged it as target identity. The scanner was right and the example was wrong; it now
uses a placeholder.
2026-10-16 19:23:59 +08:00
inliver233 5c3ba9cd2e Stop reporting a tool's resource failure as a finding about the target
A JVM reader that dies on a large or deeply nested input -- OutOfMemoryError, an OOM kill, a
container limit, a timeout -- prints an empty result that looks exactly like a reader that read the
file and found nothing. That is the `present but unreadable` case SKILL.md §Coverage names, and it is
where a wrong "that class is not in there" gets recorded.

The new section in toolchain.md says what a failed analysis actually supports (a claim about the
route, not about the artifact) and the three things to check before a negative from a heavy tool
becomes a finding: the exit code and stderr rather than stdout alone, a re-run with room to work, and
the indexed-query route that answers most location questions without materialising anything. A
deadline you set yourself gets the same reading -- it reported nothing about the target.

Also: the README claimed the scripts "work identically on Windows, macOS and Linux", while every gate
run, every test result and every figure in the evidence record comes from one Windows host. The claim
now carries the strength it actually has -- measured on Windows, `unverified` elsewhere -- and names
the usual suspects (path separators, `os.pathsep` in `APKREV_TOOLS`, console encodings, ADB shell
quoting) so a first run on another platform has somewhere to look.
2026-10-16 19:20:45 +08:00
inliver233 6ab905b2c8 Four-state labels, and the negatives that used to look alike
Every claim carried one of three labels, and the third was doing two jobs. `unverified` meant
both "someone asserted it and nobody here paid for the counters" and "no evidence was gathered
at all" -- different statements with different next actions. This adds `unknown` for the second.

The distinction that pays for itself lives inside a *negative* result, because an empty probe has
three causes and only one of them is a finding:

  absent                  -- coverage reached the question, nothing is there
  present but unreadable  -- the thing is there and this route cannot read it
  unknown                 -- nothing covered the question

The middle one is the dangerous one: it produces byte-identical output to the first, and it is the
normal case on the input this skill is aimed at. A packed APK returning four classes is not an APK
with four classes, and "the string was not in the dex" is a statement about a probe. The rule now
says to name the probe's coverage and run a control that must hit before writing "not present", and
to record `unknown` about the probe rather than `absent` about the target.

Also in this pass:

  * P47 -- two tools disagree about the same artifact, and the disagreement is your parser. One
    extraction rule reused across outputs that do not share a format: an unescaped `$` dropped every
    descriptor containing one, and dotted names were compared against DEX descriptors so one side
    parsed to nothing. The comfortable failure is the one that confirms what you already expected,
    which is why it survived the sanity check it should not have.
  * dexsql: a cache is second-hand evidence and it ages silently. A stale cache answers for the old
    artifact with no error, and `--trusted-input` skips revalidating the recorded source inputs --
    so a cached run supports "true of the artifact the cache was built from", not "true of the
    artifact in front of me".
  * ete-diff-tracing: a divergence is a location, not a cause. The differential method produces a
    positional claim, and three readings it does not authorise are written down beside the step that
    produces it.
  * CONTRIBUTING.md -- the gates, what a green run does and does not establish, the sync rules a
    change has to keep (a new reference must be named in the inventory; renaming a heading breaks
    every section reference that pointed at it), and the two failure modes to watch when editing the
    gates: a limit that cannot fail, and a gate whose answer varies with the runner. README links it
    from both language editions.
2026-10-16 19:17:31 +08:00
inliver233 b8aa0a529b Make the budget gate's hard index-row limit actually fail
`check_index_rows` read `'warn' if hard else 'warn'` -- both branches identical -- so
INDEX_ROW_HARD was defined, computed, and unable to fail anything: an index row could grow
without bound while every gate stayed green.

Found by injecting an over-long row into SKILL.md and requiring the gate to fail. That is the
only way a dead threshold shows itself; a gate that has only ever been observed passing is not
evidence that it can see anything.

A test now pins both sides -- a row past the soft limit warns, a row past the hard limit fails
-- and it was checked against the pre-fix behaviour, where it fails.
2026-10-16 19:17:21 +08:00
inliver233 ca38fa393d Merge PR #7 "Pass stage2" (dabao1955/stage2)
Absorbs four public reverse-engineering write-ups into the skill, and completes
the xvmp IR-virtualizer work started in the hardware pass. 20 files, +1151/-151.

What it adds:

  * references/native-vm-recovery.md -- the recovery procedure for a native VM:
    VM-existence signals, separating stacked CFF/VM dispatchers, register
    anchoring with triple verification, nested-VM segment stacks, STORE-backward
    operand slicing with byte provenance, and the clean-re-VM validation loop.
    Absorbed, so every step is labelled inferred in the file itself.
  * pitfalls P43-P46, including the Go stack-ABI one (a watchpoint hit whose
    argument is not in x0-x7 because the frame holds it) and the observer whose
    crash silently disarms every observer installed after it.
  * vm_handler_slice.py: a nested-dispatch diagnostic (a block repeating 4+ times
    inside one segment is a second dispatcher running under the first), plus the
    fix for a latent bug this pass's own review found -- diagnostics() had been
    measuring len() of the (run, closed) tuples since the open-tail change, which
    is always 2, quietly disabling the spin-loop note on the real code path.
  * evidence for a completed workbench toolchain and a measured end-to-end run,
    recorded in EXTENSION-sampling-crash.md SS5-SS6 and EXTENSION-absorption-kx.md.

Verified on the merged tree, not on the PR branch alone:

  * check_repo.py, check_refs.py, check_commands.py, check_routing.py and
    check_budget.py pass with zero problems; flake8 and mypy are clean;
  * pytest: 772 passed, 172 skipped, 25 xfailed;
  * the code change was reproduced independently of the PR's own tests:
    nested_loop_segments() flags the repeat-inside-one-segment shape in both the
    (run, closed) and the legacy bare-run form, and diagnostics() now reports
    avg_segment_len over the run -- 5.0 on a fixture whose tuples are length 2,
    where the pre-fix code reported 2.0.

One defect fixed during the merge: the PR's own record says the local copies of
the absorbed write-ups under `.ref/` are "git-ignored in effect", but .gitignore
had no such rule -- it rested on nobody having run `git add -A` yet. The rule is
now explicit, with the reason stated beside it, the same way this file handles
tools/ and the user's own input material.
2026-10-16 18:34:59 +08:00
dabao1955 cd1df50a2a Complete the xvmp IR virtualizer end to end and measure the de-virtualization chain on a real fixture
The public xvmp tree ships a compiler half that was never compiled (no TU
includes the virtualizer headers; on dev trees they do not build), a weak
empty vm_exec stub, silent skips for unsupported IR, and a companion article
describing an earlier prototype that is not in the tree. This change completes
and measures the chain:

- Completed toolchain, merged into all four omp branches of that tree: gated
  new-PM pass wiring (-mllvm -irobf-irvm, section .irvm), FORMAT v3 bytecode
  (8-byte aligned slots, explicit load/store widths, stable CC_ cast kinds,
  per-index GEP byte scales, branch condition = operand 0), a strong-symbol
  vm_exec interpreter with a 23-check unit suite, and a VMP.h user header.
  Virtualized vs native output identical at -O0/-O1/-O2 on all four
  branches. One branch's ungated IRVM call in Obfuscation::runOnModule
  (rewrote .irvm functions in plain native builds) removed; IRVM is
  strictly flag-gated.

- Fixture measured end to end (fco+bcfobf+subobf+IRVM build): native-identical
  output; 17,961-step guest trace via VMP_TRACE; 24.6M-instruction host trace
  via a ptrace single-stepper; vm_handler_slice yields 95,184 segments / 18
  unique signatures; the fla outer state machine separates as a shuttle class,
  round-boundary segments split the trajectory, positional alignment names
  every handler segment (59/59 in a round; counts match the guest histograms
  exactly), zero unmapped with --opcode-map.

- Tooling hardening measured on the real fixture: opcode maps key by
  signature (first-block keys collide under a shared dispatch successor);
  apply_map keeps first-block keys as fallback, regression tests added.

- Environment boundary recorded: frida stalker does not work in these
  containers (measured past the syscall layer: primitives pass, agent loads,
  zero denied syscalls; the failure is inside the injected agent's hook
  machinery), an outer ptrace tracer conflicts with frida spawn, and the
  ptrace stepper is the working host capture path. Design lesson: a pure
  virtualized call can still be LICM-hoisted by the compiling optimizer.

Records: EXTENSION-sampling-crash.md SS5-SS6 (final-state rewrite),
known-limitations.md, ete-diff-tracing.md SS5, vm_handler_slice.py header,
capability matrix.
2026-10-16 14:45:03 +08:00
inliver233 4aaa586725 Restore the trailing newline PR #6 dropped from evals.json
Every other JSON file in the tree ends with a newline; the hardware pass
removed this one, so the file diffed as 'no newline at end of file'. Content
unchanged -- the eval entry it adds (id 4, the hardware-layer route) parses.
2026-10-16 02:07:50 +08:00
inliver233 e151d7d605 Merge PR #6 "Hardware pass (stage 1)" (dabao1955/feat/ete)
Merges the hardware-layer pass onto the dexsql commit: arm64 hardware
watchpoints, ETE differential tracing, kernel-side event injection, SPE/BRBE
sampling and crash forensics, address symbolization and interpreter-trace
de-slicing. 33 files, 6 new scripts, 6 new test modules, 4 new reference
documents.

The only conflict was in evidence/capability-matrix.json: both sides appended
to the same array tail. Resolved by keeping both -- the PR's seven entries
(44 rows total now) -- and re-validated as JSON before staging.

Verified on the merged tree, not on the PR branch alone:

  * check_repo.py, check_refs.py, check_commands.py, check_routing.py and
    check_budget.py all pass with zero problems;
  * pytest: 766 passed, 172 skipped, 25 xfailed -- the 141 tests this PR adds
    all pass alongside the 625 that were already there;
  * the PR's host-side claims were reproduced by hand, not taken on trust:
    `hw_watch.py plan --addr 0x1004 --len 4 --access w` returns
    `cr: 7701` (WCR 0x1E15) exactly as the record says, both refusals exit 2
    with the documented reason strings, and the kernel scaffold's
    generate/verify round trip returns RESULT=ok.

One documentation defect was found and fixed in the merge: EXTENSION-kernel-instrument.md
recorded `verified 3 kernel-side file(s)` while the same page's own generate
output lists four (kpm/src/kwatch.c, kpm/Makefile, lkm/kwatch.c, lkm/Makefile)
and the tool reports 4. The page now says 4.

Everything the PR ships that needs a device or a kernel toolchain stays
labelled unverified in the files themselves, which is the property that made
it mergeable.
2026-10-16 02:05:59 +08:00
inliver233 cbbeb58343 Adopt dexsql as a third indexer, parity-checked against rasc, droidasc and ddc
LSPosed/dexsql exposes the whole dex as SQL tables (callers, field accesses,
numeric constants, debug lines, annotations, hiddenapi flags) plus the APK
manifest and resources.arsc, plus an MCP server. It is a third indexer at the
same stage as ASC, so it is registered the same way:

  * scripts/capabilities.py gains tool:dexsql and the dex_query_sql capability,
    so doctor.py reports it instead of guessing. INSTALL_HINTS replaces the
    rasc-only hardcoded path list, so a release download or a from-source build
    that landed outside PATH is not reported BLOCKED.
  * references/dexsql-sql-query-layer.md carries the table model, the recipes,
    the MCP tool list and the boundaries; references/toolchain.md gains the
    Tier-1 row and a section; routing.md, SKILL.md, both READMEs and the
    evidence record are updated in step.

What was measured, on four archives (1.4 MB / 1,396 classes, 4.3 MB / 647,
38.7 MB / 4 hardened, 34.8 MB / 30,768):

  * class-definition sets identical to rasc, droidasc and ddc on all four,
    zero differences in either direction;
  * string cross-reference agrees with ASC site for site -- and the match
    semantics differ, which is recorded as a trap (`=` is not `findrefs`);
  * decode emits the same instruction sequence as ddc disasm, line for line;
  * the persistent cache is NOT a speed switch: 0.121 s -> 0.098 s on a 34.8 MB
    archive, a net loss on a small one. Recorded as measured, not as sold.

Full class listing, best of 3: dexsql 0.136 s against droidasc 0.200 s and
rasc 0.055 s on the 34.8 MB archive, so it does not replace rasc for a class
list -- it answers the joins and aggregates the other two cannot express.
2026-10-16 02:01:36 +08:00
root ab22b225ff Hardware pass: hardware watchpoints, ETE differential tracing, kernel-side event injection
Add the layer under the software tooling, in the repository's shape: what runs
on a host is measured, and every device-bound or kernel-bound piece is a gated,
labelled template.

- scripts/hw_watch.py: arm64 hardware watchpoints/execution breakpoints. WCR/
  BCR encoders with the 8-byte-window and alignment rules (unit-tested to the
  canonical 0x1FFD/0x3F05), a self-contained on-device ptrace helper that
  compiles clean as a static aarch64 ELF (observed: cross gcc 16.2), adb
  orchestration, and hit decoding to module+offset. The stealth ledger is
  stated three times on purpose: the ptrace route leaves TracerPid visible.
- scripts/ete_capture.py: CoreSight ETE capture with the five gates (kernel
  era, coresight sysfs, cs_etm PMU, perf-with-CORESIGHT, permission), the
  perf/OpenCSD build recipe, and the per-thread record wrapper whose
  --control one-instruction run must pass before any real capture.
- scripts/trace_diff.py: differential reduction of two execution traces into
  first-divergence / divergent regions / blocks-unique-to-each-side.
  Producer-agnostic: Stalker BLK logs (raw or run_probe-wrapped) and decoded
  cs_etm address streams alike, host-tested on synthetic flattened-dispatcher
  fixtures.
- scripts/kernel_instrument_scaffold.py: KPM/LKM watchpoint + register-rewrite
  templates plus an honest userspace carrier; every kernel file carries a
  NOT COMPILED AND NOT LOADED banner that verify enforces, and the
  register-rewrite action is fenced as experimental in the template itself.
- references/hardware-watchpoints.md, references/ete-diff-tracing.md,
  references/kernel-event-injection.md: the mechanism, the gates, the traps,
  and the composition recipes (watch the key buffer; diff two runs; flip the
  decision at the trap), with hand-offs instead of duplication.
- Registration: 3 symptom rows (SKILL.md + routing mirror), 3 reference rows,
  4 script rows, coverage/route-inventory/evidence rows, capability-matrix
  entries, known-limitations entry, eval case 4, README trees and coverage
  paragraphs in both languages, and three EXTENSION evidence records stating
  exactly what was measured on this host (encoders, compile, fixtures,
  generate/verify) and what was not (no watchpoint armed, no cs_etm stream,
  nothing kernel-side compiled or loaded anywhere).

Hardware pass 2: SPE/BRBE crash forensics, address symbolization, VM de-virtualization

Calibrated against the two public obfuscator implementations the user supplied
(dpkg123/ollvm-aosp-toolchain: Hikari-family and Pluto-family flattening, the
xvmp IR virtualizer at 63fec41f/9b7cf75e; fetched read-only into
tools/_work/refs, git-ignored). Source facts observed, runtime inferred.

- scripts/trace_addr2sym.py: runtime addresses -> symbol + static offset,
  zero-dependency ELF64 (PT_LOAD vaddr<->file-off conversion, .dynsym/.symtab,
  stripped section+off fallback). Host-verified against a real cross-compiled
  aarch64 .so with readelf as the independent cross-check, plus a hand-built
  minimal ELF covering vaddr!=offset; accepts hw_watch hits, trace mod+off
  keys, or raw address lists. Fixes on the way: --base int coercion, and
  nearest-below symbol semantics for PCs in inter-symbol gaps.
- scripts/vm_handler_slice.py: slice an interpreter trace into handler
  segments (dispatcher detection = histogram top + predecessor diversity; the
  first 4x-margin rule refused the calibrated fixture itself and was replaced),
  signature clustering, open-tail handling, and the guest-opcode trajectory via
  an optional handler->opcode map. Calibrated against xvmp's documented shape:
  vm_exec(code,size,vm_args,num) stubs, boxed arguments observable before
  interpretation, 11 opcodes + 19 arithmetic sub-ops, const-materialization
  dispatch as a distinct segment.
- references/hardware-sampling-and-crash.md: SPE (data flow without a known
  address; PC + data address per sample), BRBE (branch records when a crash
  stack cannot be trusted), the crash loop (tombstone -> branch records ->
  symbolize), and what each adds to the OLLVM/VMP routes. All
  documented-and-gated: no SPE sample or BRBE record was captured here.
- Flattening-recipe corrections from source: Hikari keeps a frame-resident
  switchVar written through an indirect pointer with scramble32-permuted cases;
  Pluto stores switchVar/switchXor rolling-XOR so memory only ever holds noise
  -- hardware-watchpoints.md's state-variable recipe now distinguishes the
  families, and ete-diff-tracing.md carries the family table plus the xvmp
  shape. AntiDebugging injects raw svc inline asm with randomized fields, so
  byte signatures cannot survive it; semantic site-location can.
- Registration: 3 symptom rows (untrustworthy crash stack; runtime addresses
  needing symbols; interpreter loop reading as noise), reference/script rows,
  coverage/evidence/capability-matrix entries, README trees in both languages,
  and EXTENSION-sampling-crash.md recording exactly what was measured (real .so
  symbolization, slicer fixtures, the slicer's self-found dispatcher-rule bug)
  and what was not (no SPE/BRBE capture, no hardened binary ever traced).

Fix all PR #1 review findings by severity; un-entrench the tests that pinned them

Major (device-breaking / wrong results):
- hw_watch: match the arm64 user_hwdebug_state slot ABI (address register
  first, control second -- the swap armed nothing correctly)
- hw_watch: every post-seize error path now clears slots and detaches;
  PTRACE_O_EXITKILL turned an analysis failure into a SIGKILL of the target
- hw_watch: forward genuine target SIGTRAPs (raise/tgkill/int3); only ptrace
  event stops are consumed -- the reference's signal-preservation promise
- hw_watch: run checks push/build/chmod and the helper's exit status; a dead
  capture is RESULT=blocked, never ok-no-hits
- hw_watch: decode offsets from the module base, not each mapping's start
- ete_capture: --control now requires the control record to DECODE
  (perf script event lines), the documented gate, not just a zero exit
- ete_capture: -o before the -- workload separator in --cmd mode; all
  values shell-quoted into su -c; stale remote perf.data removed first;
  perf exit status checked (timeout's 124 is the normal window end)
- kernel scaffold: --set-reg restricted to x0..x30; the rewrite is gated by
  its own breakpoint event so a watchpoint hit cannot run it; LKM pins the
  task (get/put_task_struct) across registration and calls the four-argument
  register_user_hw_breakpoint; --break rows initialize .addr/.len/.bp_type
  (there is no .pc member); --id validated against the module-id grammar
- trace_addr2sym: exact module matching (path or basename, never substring);
  load bias aligned against the PT_LOAD (bias = start - (foff - p_offset)
  - p_vaddr); PCs outside the ELF span are refused instead of landing on the
  last symbol; --json stdout is a pure document (RESULT= in text mode only)
- trace_diff: block keys need 0x or >=8 hex digits (PIDs/counts no longer
  parse); vm_handler_slice: dispatcher needs predecessor diversity, no
  frequency-margin fallback; both JSON outputs pure (check_commands' rule)

Minor/nitpick:
- capability-matrix + evidence-summary: trace reduction status ok->partial
  (no real-target producer ran it; the file's own definition of ok)
- hardware-sampling-and-crash.md and the script examples carry --elf
- kernel-event-injection.md no longer claims the callback's first statement
  is a filter (it prints unfiltered; the filter is a required change)
- Elf64 caches symbol addresses for per-row bisect
- docstring coverage of the touched surface: 16.67% -> 100%

Tests: every json.loads workaround stripping RESULT= is gone; new regression
tests cover the slot ABI, release-on-error, SIGTRAP forwarding, non-first
mapping offsets, -o placement and su -c quoting, the control decode gate,
x31/--id refusals, the guarded rewrite, outside-ELF PCs and bias math,
short-token refusal and the dispatcher margin rule. The minimal-ELF fixture
now packs a full 8-field Elf64_Phdr (its 7-field pack silently shifted
p_memsz onto p_align). 739 passed; flake8/mypy and all five gates clean.

Address the follow-up review on 0bd8dfe (6 findings)

- ete_capture: two-layer quoting for the control-record, decode and cleanup
  commands -- shlex.quote spliced between literal single quotes broke out of
  su -c for any spaced output name; --seconds must be positive (timeout 0
  disables the timer), refused before any adb check
- kernel scaffold: --set-reg now refuses sp/pc as well (the template has no
  rewrite path for them; a generate that silently drops the action is the
  misleading kind of success); both templates arm with a sampling period
  (LKM via hw_breakpoint_init, KPM spells out sample_period=1) -- a zero
  period is a non-sampling event whose callback never fires: armed, silent
- trace_addr2sym: contains_vaddr tests each PT_LOAD's loaded span instead
  of the whole-file extent; a PC in the unmapped gap between segments is
  refused rather than named after the nearest symbol below
- trace_diff: a bare all-digit token is indistinguishable from a date or
  count, so block keys need 0x or a non-decimal hex digit; a date-only log
  stays refused as a capture failure

Tests: two-layer quoting with a spaced control name, --seconds refusal,
sp/pc in the rejection list, sampling-period assertions for both templates,
a two-segment gap fixture, and a date-only parser case. 743 passed; flake8,
mypy and all five gates clean.
2026-10-15 15:33:28 +08:00
newliver666 7b6c6932a9 README: add a theme-aware header, live badges, and a Simplified Chinese edition 2026-09-24 17:22:21 +08:00
inliver233 971a961217 Register the ASC indexers as capabilities so G2 reports them instead of guessing
The kit documented rasc and told the reader to reach for it first, but nothing knew whether it was
present: doctor.py had no entry for either indexer, so an environment gate could not answer "is the
fastest indexer available here", and the reference file had no way to say where a built binary lives.

  capabilities.py  two new capabilities, dex_index_rust and dex_index_python, plus the two tool atoms
                   they depend on. rasc's install line is its build command, because that is what
                   acquiring it means; its cost is marked measured (a clean clone built in 117 s here)
                   rather than estimated. The probe also accepts the work-area path that
                   rasc_build.py builds into, so a machine that followed the kit's own instructions
                   is not reported as BLOCKED.
  doctor.py        reports both, with the next action on the blocked one. Verified: OK with the built
                   binary present, BLOCKED with the build command when it is moved away.
  toolchain.md     says the capability is registered, so G2 does not have to guess.

Also fixed, found while verifying this: `doctor.py --json` printed the RESULT= token after the JSON
document, so its output was not parseable by the tool the flag exists for. Same defect that was fixed
in check_commands.py; the status stays a field in the document and the exit code carries the meaning.

Gates: check_repo 0, check_refs 0 dangling, check_routing coherent, check_budget 0, check_commands 0
drift, pytest 625 passed, flake8 0, mypy clean over 55 files.
2026-09-22 20:57:10 +08:00
inliver233 f928b5889f Correct the rasc enum finding: the constant bodies are one query away, not lost
The first write-up said 
asc getclass loses the method bodies of an enum whose constants override an
abstract method. Querying the classes JADX itself names in its comment settles it: they exist as
PasswordConverter/2/3 and both tools decompile them fully (404-407 B, containing convert/getType).

What is real, and narrower: 
asc's outer-class view does not inline them and says nothing about it,
and neither does the Python tool -- so this is a missing convenience step relative to JADX, not a
regression against droidasc, and not unrecoverable code. droidasc's outer listing is richer
(2,170 B vs 314 B, with \, \() and the constructors explicit).

The episode is kept in the record because of how it went wrong: the harness counts literals and pointed
at a real difference while misdescribing it, and the hand check turned that into an overstated claim by
stopping at the first comparison instead of querying the subclass the other tool had named.
2026-09-22 20:50:01 +08:00
inliver233 481089600d CI: do not self-upgrade pip in the test job 2026-09-22 20:01:04 +08:00
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 b098904228 README: add a disclaimer for learning-only, authorized use 2026-09-22 18:44:03 +08:00
inliver233 5c57553186 README: acknowledge the LINUX DO community 2026-09-22 18:41:00 +08:00
inliver233 5aa4c97050 Merge PR #2 (dabao1955): expand the CI matrix, and finish the dcc.py declaration
The PR diagnosed the same CI failure this branch had just fixed, from the other end, and two of its
three changes are kept.

  .github/workflows/ci.yml   KEPT. The matrix was ["3.11", "3.13"] while the frontmatter claims
                             Python 3.9+; it is now 3.11-3.14. Both new versions were checked here
                             before taking it: the gates and the full suite pass on 3.12 and 3.14.
  check_commands.py          KEPT. dcc.py added to EXTERNAL_SCRIPTS. This branch reached the same
                             conclusion structurally -- recognition by filename needs the gitignored
                             tools/ tree to exist -- and the explicit entry is the better belt for
                             the same braces, because it names the situation instead of inferring it.
                             Both spellings are declared: the bare dcc.py the java2c record uses
                             and the ./dcc.py this branch''s record uses.
  its new test               KEPT, with one assertion corrected. It pinned workbench-after-cd when
                             the workbench was present and xternal when it was not, so it asserted
                             one mechanism at a time -- and would have failed on any machine with
                             tools/ present once the declaration above exists, i.e. it was itself
                             machine-dependent, which is the defect the test guards. It now asserts
                             the property both mechanisms produce.

Verified on the merged tree, in both shapes: with tools/ present, 619 passed / 158 skipped / 25
xfailed and all five gates exit 0; with tools/ hidden to imitate a clean checkout, 596 passed / 181
skipped / 25 xfailed and all five gates still exit 0. The second shape is the one CI runs, and it is
the check that was missing when the previous commit was pushed.
2026-09-22 17:12:45 +08:00
inliver233 dd8057cfcb Make the command gate machine-independent, and calibrate the budget fallback
CI went red on `1dc1d9b` in both jobs while the identical command passed locally, and the reason is
worth keeping: the checks depended on state that only exists on a developer's machine.

check_commands.py -- the gate's verdict depended on a gitignored directory
  The previous commit's fix recognised a workbench script by matching its name against a walk of
  `tools/`. That tree is gitignored, so a clean checkout has none of it: `dcc.py` matched locally and
  skipped, and on CI nothing matched, it fell through to `missing-script`, and the gate reported
  drift for three quoted lines. A check whose verdict changes with a directory CI does not have is
  not a check. Two changes remove the dependency:

    * `qualified_basenames()` collects every `.py` the *document itself* writes with a path, so a
      bare name the document has already located elsewhere is a skip, computed from the text.
    * `./dcc.py` is a declared upstream entry point, checked before the workbench-name test.
    * a bare name after a `cd` that resolves nowhere is `after-cd-unresolved` -- unverifiable, which
      is not the same claim as broken, and is now reported as such instead of as drift.

  Verified by running the whole CI sequence against a tree with `tools/` hidden: five gates exit 0,
  the suite passes. That experiment is the one that should have been run before the first push.

check_budget.py -- the measurement was installation-dependent
  The gate measures the always-loaded body in tokens, preferring `tiktoken` and falling back to
  `characters / N`. `N` had been guessed at 3.6 rather than calibrated, so a machine with tiktoken
  saw ~11.1k tokens and passed while a machine without it saw ~14.2k and warned -- and CI's 3.11 job
  is the one without it. `N` is now the measured 4.285 characters per token (47,629 chars -> 11,114
  tokens on SKILL.md), both paths agree within one token, and `tests/unit/test_budget_measurement.py`
  forces both paths and asserts they do not straddle the budget.

  CI now installs `tiktoken` so the comparison exercises the real tokenizer instead of skipping.

Also: two `.py` files are formatted for consistency, and the reconstruction record documents the
CI-only failure, since "it passed on my machine" is the lesson rather than an aside.

Gates: check_repo 0 problem, check_refs 0 dangling / 0 warning, check_routing coherent,
check_budget 0 failure / 0 warning (both measurement paths), check_commands 154 checked / 0 drift,
pytest 617 passed on 3.14 and 609 on 3.11.
2026-09-22 17:09:22 +08:00
dabao1955 b0ff2605fd Fix failing ci tests 2026-09-22 11:20:22 +08:00
inliver233 1dc1d9b707 Fix the command gate's blind spot, and make --json actually be JSON
A reviewer reported seven drifting commands in the evidence record. Reproduced, the report split
three ways, and the middle category was the useful one: the gate had been scoring them clean.

Not reproducible, with the measurement:

  tests/run_tests.py -q   runs. The wrapper forwards its arguments to pytest
                          (`args = list(argv[1:]) or ["-q"]`), and documents that in its own usage.
  dcc.py                  already written as `cd tools/_work/bench/repos/dcc` followed by
                          `python dcc.py ...` -- an external checkout, not a shipped script.

Real, and fixed here: seven commands in two evidence records named a **workbench** script with no
path at all (`analyze_pair.py`, `make_skeleton.py`, `ttd_attach.py`, `b2_measure.py`). The files
exist under `tools/`, which .gitignore excludes, so a reader copying one from the repository root
gets "No such file" -- and two of those documents wrote the correct path elsewhere, so they
disagreed with themselves.

**The actual defect was in `check_commands.py`, not in the seven lines.** `resolve_script()` tried
the document's own directory first, so a bare `analyze_pair.py` inside `docs/tool-verification/`
resolved to `tools/_work/bench/unpack/b3/analyze_pair.py` -- a real file -- and passed. Resolution
now follows the order a reader would try (repository root, then the skill's scripts, then the
document's own directory, which is what `../x.py` in a nested README legitimately means); a bare
name that only resolves under `tools/` is `unqualified-workbench`; and that class counts as **drift,
not as a skip**, because it is a one-token fix that a reader cannot run. The first attempt classified
it as a low-confidence warning, which would have left all seven in place.

One exception is honoured and pinned: a code block opening with `cd tools/_work/bench/repos/dcc` is
telling the reader where to stand, so a bare `python dcc.py` after it is correct.
`documented_commands()` now reports whether a `cd` preceded the command in the same block, and that
case is skipped as `workbench-after-cd`.

Two defects of this pass's own, found while fixing it:

  --json emitted the RESULT= token after the JSON document, so `--json | jq` failed with "Extra
  data". The token is gone from that path; the status is a field in the document, and the exit code
  carries the same meaning.
  The fix's own substitution left four literal backslashes before `$` in the two files it edited --
  a character a reader would copy by mistake. Removed, and verified absent.

`tests/integration/test_check_commands_gate.py` pins the resolution order, the drift classification,
the `cd` exception, and a clean pass over the committed tree, so this cannot come back silently.

Gates: check_commands 153 checked / 0 drift, check_repo 0 problem, check_refs 0 dangling / 0 warning,
check_routing coherent, check_budget 0 failure / 0 warning, pytest 613 passed / 158 skipped / 25
xfailed.
2026-09-22 02:18:40 +08:00
inliver233 fe5bdee087 Merge: keep the maintenance pass and the reconstruction pass, both
Two passes met on this branch and neither is discarded. `d0fe0ea` (inliver233) hardened
`build_scripts.py` -- 37 findings that were mostly noise, down to 0, with two defects found in its
own first implementation by an 18-case table -- generalised three absolute paths out of the evidence
record, and added a corpus-size note to `check_budget.py`. `2beade6` (this pass) rewrote
`check_budget.py`'s measurement, added four gates, rewrote `so_constpatch.py`'s zip writer,
replaced 59 out-of-package references, and shrank the always-loaded body.

Resolution, file by file:

  check_budget.py   the only real conflict. Their corpus note and this pass's rewritten measurement
                    are kept as separate functions and separate notes, because they measure different
                    quantities: what a task loads versus what the repository carries. Collapsing them
                    would let a growing corpus hide behind a green always-loaded count -- which is
                    the defect their note exists to prevent.
  build_scripts.py  theirs, taken as-is, including the case table.
  REPO-DECISIONS.md both sections: their maintenance-pass record follows this pass's decision record.
  three extensions  their absolute-path generalisations, on top of this pass's convergence edits.
  everything else   this pass's versions, which supersede (SKILL.md, the routing file, tests/, the
                    skill's evidence/ and references/).

One regression this merge introduced and this commit fixes: `EXTENSION-test-harness.md` carried a
device serial in a quoted `adb devices` transcript. It is `<DEVICE>` now, and `check_repo.py`'s
content scan -- the gate added in the same pass -- is what caught it.

Gates on the merged tree: check_repo 0 problem, check_refs 0 dangling / 0 warning, check_routing
coherent, check_budget 0 failure / 0 warning (6 notes, including both corpus thresholds),
check_commands 146 checked / 0 drift, pytest 608 passed / 158 skipped / 25 xfailed.
2026-09-22 01:43:07 +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 d0fe0ea337 Harden the path audit, generalise three absolute paths, and gate the corpus size
An external review found the evidence record honest and the gates real, but raised three
concrete defects. Each is fixed at its cause rather than at its symptom, and the reasoning
is recorded in docs/tool-verification/REPO-DECISIONS.md.

build_scripts.py -- the audit reported 37 findings, most of them noise
  It matched any `X:\...` literal, so `C:\Program Files\...`, `C:\Python314\...` and literal
  placeholders all counted as machine-specific leftovers. A gate whose output is mostly noise
  stops being read -- the same failure scan_leaks.py names when it refuses to exempt the
  RFC 5737 ranges. Two explicit exemption families are added instead of a looser regex:
  GENERIC_PATH_PREFIXES (install locations that exist on every host of that platform) and
  `_leaks_nothing()` (also exempts an already-redacted path, since a placeholder or an
  ellipsis means a normalisation pass already ran).

  Two traps surfaced only because the change was tested rather than merely run. BARE_ABS stops
  at whitespace, so `C:\Program Files\...` arrives truncated as `C:\Program` and a one-way
  `startswith` misses the exemption; the comparison is now bidirectional under a minimum
  length. And with a lookahead of only `[a-z0-9]` the optional `\\?` backtracks to empty, the
  next character is the separator, it passes, and a real personal path is swallowed -- the
  lookahead has to exclude the separator as well. Both were caught by the case table, not by
  the audit run.

  Result: 37 -> 0 findings, exit 2 -> exit 0. The real hits (`E:\apk-reverse\...`,
  `E:\agb_apktool.jar`) were fixed in the documents, not by widening the list.

Three absolute paths on the published surface
  EXTENSION-desensitization.md, EXTENSION-extraction-shell-bench.md and EXTENSION-vmp-diff.md
  quoted a workbench root, a tool jar and an SDK location absolutely.
  docs/tool-verification/ already states the rule -- a record may say a run happened at a
  `tools/...` path, written relatively -- so the three files were made to match the rule they
  were already under. The tool-jar and SDK cases are rephrased rather than placeholder-ised,
  so no reader has to learn a new token.

check_budget.py -- blind to the half of the problem it was not measuring
  The gate budgets the always-loaded part and could not see the corpus, where a new reference
  costs exactly one index line: the corpus can double while the narrative count stays green.
  Two note-level thresholds now report it. Notes, not failures -- a large corpus is a choice,
  a growth nobody measured is the defect. The first run reports 47 references + 55 scripts =
  102 files, 31,423 lines.

A cross-tool inconsistency this pass ran into, recorded rather than papered over
  build_scripts.py now exempts any `<...>` placeholder; scan_leaks.py recognises a fixed token
  list. An invented placeholder in a source comment passed the first and was read by the
  second as the very personal name its `user_path_windows` rule exists to catch, turning
  check_repo.py red. That is by design -- a strict list is what stops a real name hiding
  behind a plausible-looking placeholder -- but the two tools disagree about what "already
  redacted" means, and the disagreement stays invisible until someone writes a new token.
  Rephrased here; a future pass may want one shared list.

Verification
  - build_scripts.py: 37 -> 0 findings on a clean clone, exit 0.
  - An 18-case table pins both directions, 0 failures: generic and already-redacted paths are
    exempt, while a real workbench path, a real personal name after `C:\Users\`, a real POSIX
    home and a real project path still report. It found two defects in the first
    implementation.
  - All four gates pass: check_repo 0 problem, check_refs 0 dangling / 0 warning,
    check_budget 0 failure / 0 warning / 3 notes, build_scripts 0 findings.
2026-09-21 23:18:17 +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 fd2a1150be Structure: sink the evidence record out of SKILL.md, and gate the budget
SKILL.md was 658 lines against the Anthropic Agent Skills guidance of keeping the body under 500, and the growth sat where it costs most: the Coverage section had accumulated the evidence for every claim plus a per-pass verification history (153 lines / 11.8 KB), and Hand-off had grown two knowledge tables. Both now load on demand, with SKILL.md keeping the decision-level statement and a pointer.

  Coverage   153 -> 68 lines   new references/coverage-and-limits.md
  Hand-off    51 -> 19 lines   new references/handoff-boundaries.md
  SKILL.md   658 -> 543 lines  (404 narrative + 139 index)
  index rows 24.0 KB -> ~19 KB  descriptions now say when to load, not what is inside

check_budget.py is the substance of this pass. check_repo.py proves consistency, and consistency has a natural counter-pressure: a broken path fails loudly and someone fixes it. Bloat has none -- every pass adds a reference, an index row and a coverage claim, and nothing in the repository notices. The new gate measures narrative lines separately from index lines, because 139 of SKILL.md's lines are the discoverability tax for 86 bundled files and counting them against a prose budget would push the skill to drop files it needs. Conclusions restated beyond the benchmark matrix, the evidence record and the coverage statement are reported as notes.

No measured content changed in this pass: every claim, strength label and negative result is where it was, and the two new references carry the moved text verbatim plus the corrections the benchmark pass produced.
2026-09-21 22:12:58 +08:00
inliver233 fa2f62107f docs: stop citing workbench paths that are not shipped
Eight citations in the reference documents pointed at tools/ paths, and .gitignore excludes tools/ -- so neither an installed skill nor a clone of this repository can open them. Each is now either an inline statement of the fact or a pointer to the evidence file under docs/tool-verification/ that actually holds it. One of the eight was introduced by the benchmark pass itself (protocol-reverse.md cited a workbench script).

The evidence records keep quoting tools/_work/... deliberately: there they are operation records of where a run happened. docs/tool-verification/README.md now states that distinction and the rule that follows from it -- a reference document never cites a tools/ path as its evidence, even when the work happened there.
2026-09-21 22:01:28 +08:00
inliver233 6221a072ca Benchmark pass: measure the documented routes, close five tool gaps, correct four claims
Public targets under the existing routes, recorded per row in tests/benchmark.md with each row's own strength label: the MASTG crackmes (equal-length patch -> repack -> re-sign with a zero-change control, and the anti-debug race with its time-to-death baseline), DexPatcher's ezAndroid sample, two real split APK sets, and a self-built extraction-skeleton set. Hardening-shape discrimination now separates Java2C from an extraction shell, a VMP and JNI sinking.

New: java2c_probe.py and java2c-and-jni-sinking.md; split-apk.md with repack.py growing set inventory, unified re-signing and merge; protobuf_decode_raw.py cross-checked against the official runtime; vmp_diff_harness.py and its cost table; kernelsu_syscall_mask.py with three kernel-side templates; stalker_trace.js gains Stalker.exclude() and a zero-event warning.

Corrections, each because a measurement contradicted a recorded conclusion: the trivial-body ratio is bimodal rather than a threshold and the old threshold claim was deleted; a Dex-VMP verdict was withdrawn after dexdump contradicted a hand-written opcode table; the assumption that Stalker.exclude() fixes zero-event delivery is disproved (it keeps the process alive, nothing more); and a repack.py signing-path claim expired mid-pass when a second writer added one.

Emulated and kernel-side routes are labelled unverified where no toolchain or device could run them; each evidence file states what was not run.
2026-09-21 21:57:23 +08:00
inliver233 bea9955eb6 docs: normalize the last target identifiers left in the verification record
The previous pass renamed the record directory but left two concrete identifiers in
the transcripts: a package name plus device serial in TOOL-VERDICTS.md, and a
logcat-truncated tag in FINDINGS.md. Both are now <PKG> / <DEVICE>, matching the
convention the extension-pass records already use. No claim, command or conclusion
changes -- only the identifiers.
2026-09-21 21:00:13 +08:00
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 90f7a58615 Record the selective-install measurement and the external checks
The brief asked for the shared-tooling question to be settled by the install product
rather than by preference. Measured with --skill apk-reverse -y --copy in an empty
directory:

  the skill is copied whole and self-contained into each detected agent directory
  (six here), every file under <agent>/skills/apk-reverse/ -- SKILL.md, all 28
  references, all 34 scripts including dexpatch/. Nothing outside the skill directory
  is installed, and there is no cross-skill sharing mechanism.

So a root-level shared/ can never work: it is not part of the product. With one skill
there is nothing to share, and had the skill been split the halves would have needed
duplicated scripts or a path that does not survive installation.

VERIFY-OUTPUT.md gains a section recording which premises were re-checked against
primary sources and which did not hold: the blutter build budget and compiler floor,
aotopsy as the no-toolchain route, the idalib-mcp shift away from the GUI plugin, and
that "GNU readelf is on PATH" is false here (it is pyelftools' readelf.py, and MinGW
objdump/nm reject aarch64 outright). Also records one intermittent check_repo.py exit 1
that printed "0 problem(s)" and did not reproduce in five later runs, with the line
numbers of the encoding path that produces it.
2026-09-20 05:01:48 +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 46812ec9a4 Reword the check_refs.py description so it is not read as a reference
The maintenance table illustrated the check with a literal example of the shape it looks for, and the check then reported that example as a dangling reference to a file that does not exist.
2026-09-20 03:39:06 +08:00
inliver233 dc0afeb40c Follow the layout change in .gitignore
The negation rule pointed at scripts/dexpatch/, which no longer exists at that path. Also ignore the scratch directory build_scripts.py audits into and the agent skill directories that installing this repo into itself would create.
2026-09-20 03:38:28 +08:00