mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:14:26 +08:00
caa981b7e7d68daaff407dbc30683d701c404b2e
69
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
7b6c6932a9 | README: add a theme-aware header, live badges, and a Simplified Chinese edition | ||
|
|
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.
|
||
|
|
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. |
||
|
|
481089600d | CI: do not self-upgrade pip in the test job | ||
|
|
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 |
||
|
|
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.
|
||
|
|
b098904228 | README: add a disclaimer for learning-only, authorized use | ||
|
|
5c57553186 | README: acknowledge the LINUX DO community | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
b0ff2605fd | Fix failing ci tests | ||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
7176b5e1c9 |
Restructure to the skills/<name>/ layout the skills CLI resolves
The skill lived at the repository root, which the CLI reads as a single-skill repository and which makes a second skill impossible to add. SKILL.md, references/ and scripts/ move under skills/apk-reverse/ with git mv, so history follows and no in-skill reference needs changing (they are relative to SKILL.md). check_repo.py now discovers every skills/*/SKILL.md, validates frontmatter by parsing it instead of pattern-matching it, and requires the README to name skills through explicit skills/<name>/... paths. check_refs.py is new: it audits file.md section cross-references against the real headings in the target file. It found six dangling ones, all fixed here. SKILL.md: the description is now quoted. Unquoted, it contained a colon-space, which is invalid YAML, so the CLI skipped the skill and reported No skills found. Adds a Coverage section stating what is out of scope and what to do when nothing matches. toolchain.md: IDA is not GUI-only (idalib-mcp is a headless MCP server), plus a section on MCP servers as external dependencies and the deobfuscate-before-LLM boundary. README.md: install via npx skills add, the new layout, coverage and maintenance tools. Verified: check_repo.py and check_refs.py both clean, build_scripts.py audit 0 findings, npx skills add . --list reports apk-reverse, and an install into a scratch directory lands SKILL.md plus 28 references and 38 scripts. |