mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 21:24:21 +08:00
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.
180 lines
12 KiB
Markdown
180 lines
12 KiB
Markdown
# Findings about the skill, from measuring it
|
||
|
||
Everything here is a result about **the skill**, not about the target. Each item names the evidence
|
||
that produced it and its strength.
|
||
|
||
---
|
||
|
||
## F1 — The Coverage claim was partly aspirational, and the pass identified exactly which part
|
||
|
||
`SKILL.md`'s Coverage section lists as *covered by verified mechanisms*:
|
||
|
||
> Flutter / Dart AOT: pinning the engine version, decoding the object pool, locating and patching
|
||
> business logic inside `libapp.so` (`dart-aot.md`).
|
||
|
||
Measured against a real Dart 3.6.0 `libapp.so`, that sentence splits into three claims of very
|
||
different standing:
|
||
|
||
| Sub-claim | Standing after this pass |
|
||
|---|---|
|
||
| pinning the engine version | **verified** — the banner route (§1) is correct and 3.6.0 is confirmed three ways |
|
||
| decoding the object pool | **false as written** — the kit cannot do it; there is no resolver |
|
||
| locating business logic inside `libapp.so` | **true, but conditional** — it works, on top of an external snapshot dumper that the skill neither ships nor installs |
|
||
|
||
The worked example in `TOOL-VERDICTS.md` shows both halves at once: the chain
|
||
`"showRewardAdProvider"` → `pp+0x23548` → ref site `0x7a9464` → `ShowRewardAdFamily.call_2f28ac
|
||
@ 0x7a93ec` succeeded, and step 1 was performed by blutter, not by the skill.
|
||
|
||
The reason is structural, not a shortage of effort: `dart_pool_strings.py` reports **file offsets**
|
||
while `pp.txt` and `dart_pprefs.py` work in **pool offsets**. Over the 4,241 strings present in both
|
||
spaces there are **4,237 distinct deltas** (min −1,361,429, max +277,654). The mapping is a property
|
||
of the reconstructed pool and is not a constant, so no amount of care inside the script recovers it.
|
||
|
||
Capabilities the kit does not have, stated plainly: snapshot/object-pool deserialisation; symbol and
|
||
name recovery; a real call graph (the `--build-index` index holds raw `B`/`BL` targets only); a
|
||
decompiler; a function boundary/size table.
|
||
|
||
---
|
||
|
||
## F2 — Two scripts fail in the way that reads as a target finding (the P26 class)
|
||
|
||
The handover singled out `pitfalls.md` P26: *"a self-built analysis script fails in a way that reads
|
||
as a target finding."* This pass produced two live instances of exactly that, which is why the P26
|
||
warning earns its place.
|
||
|
||
**`elf_plt.py` returned a confident false negative.** It reports 3,437 stubs where `.rela.plt` holds
|
||
3,438, and answers `0x587060 -> not a resolved stub` for an entry that is a legitimate stub for
|
||
`dl_iterate_phdr`, with a real caller at `0x576a2c`. Nothing in the output indicates a problem. For
|
||
the script's stated purpose — deciding whether a hardening symbol exists in a library — the wrong
|
||
answer is indistinguishable from the right one.
|
||
|
||
**`find_refs.py` returns a clean empty result for input it cannot read.** Its docstring advertises
|
||
"a smali tree or a directory of dex files"; `iter_files()` accepts only `.smali`. Given the dex
|
||
directory it printed `[total refs] 0`, exited **0**, and emitted no warning. This is worse than a
|
||
crash: it is the exact input to "nothing references this, safe to patch", produced by the very tool
|
||
recommended for judging blast radius before patching.
|
||
|
||
Both, plus `dart_pprefs.py`'s silently dropped double constants, share one property: **the failure is
|
||
one-sided and quiet.** None of the three reports reduced confidence in its own output.
|
||
|
||
---
|
||
|
||
## F3 — Defects found, ranked by consequence
|
||
|
||
| # | Script | Defect | Consequence | Status |
|
||
|---|---|---|---|---|
|
||
| 1 | `find_refs.py` | `iter_files()` matches only `.smali`; dex dirs yield 0 results, no error, exit 0 | silent false "no references" on the tool whose job is blast-radius | **cause identified, not fixed** |
|
||
| 2 | `elf_plt.py` | loop bound `off < fsz - 16` misses the final stub slot | false negative for one real symbol per library, conditionally | **fixed on a copy, re-verified** |
|
||
| 3 | `dart_pprefs.py` | only the GP register file is decoded; `ldr dD,[x27,#imm]` (`0xFD400000`) and all double constants lost | under-reports pool references (302 displacements / 1,802 sites here), which breaks the "count refs to judge importance" advice in `dart-aot.md` §9/§10 — one site went `[0 refs]` → `[79 refs]` | **fixed on a copy; agrees with capstone 0/0** |
|
||
| 4 | `so_constpatch.py` | rebuild drops the zip `extra` field, destroying 4 KB alignment | for `extractNativeLibs=false` APKs the output will not load; masked here because this target is DEFLATED + `extractNativeLibs=true` | **cause identified, proven by fixture; not fixed** |
|
||
| 5 | `so_constpatch.py` | `--diff --name-regions` compares file offset with vaddr | feature completely inert on libraries where `p_vaddr != p_offset` (2 of 36 here), with no warning | cause identified |
|
||
| 6 | `so_constpatch.py` | `external_attr` rewritten on 1982/1982 entries | metadata-based integrity comparison detects the rebuild | cause identified |
|
||
| 7 | `dexutil.py` | switch payloads decoded as instructions; `decode()` does not resolve operands | caused a spurious "desync likely" verdict on `saveToLocal` | cause identified |
|
||
| 8 | `so_constpatch.py` | "N byte(s) changed" is the overwrite window, not bytes changed | overstates change count (13 reported vs 7 actual) | cause identified |
|
||
| 9 | `lib_map.py` | `?` means both "read failed" and "not probed" | a real read failure is invisible | cause identified |
|
||
| 10 | `dart_pool_strings.py` | TSV escaping is lossy for LF/CR/TAB | 7 of 18,363 rows ambiguous; 19 entries genuinely contain those bytes | cause identified |
|
||
| 11 | `elf_plt.py` | `size = 16 if ... else 16` | dead code | cause identified |
|
||
| 12 | `dex_strings.py` | `--per-file` prints only files with hits | can be misread as "only this dex was scanned" | cause identified |
|
||
|
||
Items 1-3 are the ones that changed a conclusion during this pass rather than merely being noticed.
|
||
|
||
---
|
||
|
||
## F4 — Documentation contradictions
|
||
|
||
Independently found while mapping the skill and while using it. The executable cross-reference
|
||
checkers (`check_refs.py`, `check_repo.py`) pass, so these are invisible to the repo's own gates.
|
||
|
||
| # | Location | Problem |
|
||
|---|---|---|
|
||
| 1 | `dart-aot.md` §2 | Claims blutter's `asm/` has *no instructions* — "Do not plan around it." **Wrong.** One plugin file under `asm/<plugin>/src/ads/reward/` is 1,327 lines carrying class layouts, signatures, a full instruction listing, pool annotations and resolved call targets. It is the richest artifact in the chain and the doc says to ignore it. |
|
||
| 2 | `dart-aot.md` §2 | Budgets "tens of minutes" for the first blutter build; measured **≈78 s** (~30× pessimistic). Also demands a very recent C++ compiler; **MSVC 19.34 (Nov 2022) suffices**. |
|
||
| 3 | `dart-aot.md` §7 | Presents two-byte hit chaining as an acceptance test. On this sample, 9,840 two-byte CJK candidates yield 2,081 chained runs, all garbage. |
|
||
| 4 | `signature-derived-keys.md:61` | Documents `python scripts/sig_probe.py --live --pkg <pkg>` — the script has no `--pkg`; it takes `--live PKG`. Line 54 and `pitfalls.md` P27 give the correct form. |
|
||
| 5 | `dart-aot.md:204` | `dart_disasm.py ... --index callers.json --build-index` omits the required argument value; argparse errors out. |
|
||
| 6 | `byte-level-patching.md:186` | Says different-length strings "need `dex_strpatch`-class tooling", while `dex_strpatch.py` requires `len(new) == len(old)` — it is precisely the tool that cannot do this. Contradicts `dex-patching.md:123`. |
|
||
| 7 | Coverage vs `toolchain.md` | Coverage lists Unity/IL2CPP logic recovery as **not covered**, but `toolchain.md` Tier 4 names concrete tooling without pointing back at that disclaimer, so a reader of `toolchain.md` alone would take IL2CPP for a supported route. |
|
||
| 8 | Script count | `SKILL.md`/`README.md` index 35 rows, which is 34 scripts plus the `scripts/dexpatch/` directory row. |
|
||
| 9 | `dart_pool_strings.py` docstring | Says two-byte auto-detection is unreliable (confirmed here); `dart-aot.md` §7 treats UTF-16LE hits as a reliable rule. |
|
||
| 10 | `verification.md` rung 7 | Re-downloading a distributed file to compare hashes has no landing place in `SKILL.md`'s six "done" criteria, so the two sections disagree on strength. |
|
||
|
||
Item 1 is the most damaging because it steers a reader away from the best available evidence.
|
||
|
||
---
|
||
|
||
## F5 — "verified mechanisms" left no reproducible trace in the repo
|
||
|
||
The Coverage preamble frames its list as *verified mechanisms*, and `references/long-task-discipline.md`
|
||
defines **Observed** as *"reproduced it, with the exact command and output"*. The repository contains
|
||
no sample, no fixture, no captured log or screenshot, no patch record, no test and no CI, and its
|
||
`README.md` states it holds no target-specific data.
|
||
|
||
So the strongest support the repository itself can exhibit for those claims is *Inferred*. This is
|
||
**not** an accusation that the claims are false — several were confirmed here on the first attempt,
|
||
and `dart-aot.md` §1 proved exactly right. It is a statement about evidentiary standing: the repo
|
||
asks its readers to label their own claims and does not leave itself a way to meet its own bar.
|
||
|
||
The concrete measurement figures that *are* reproducible are the 14 sample-scoped numbers listed in
|
||
the skill map (e.g. dex 4.32 MB → 7.73 MB; `.RSA` 1199 B vs 777 B at runtime; a log tag going `8 → 0`).
|
||
|
||
---
|
||
|
||
## F6 — Boundary evidence: where the skill's method held, and where it ran out
|
||
|
||
This is the observation the pass was built to produce, so it is stated as a sequence rather than a
|
||
verdict.
|
||
|
||
**Held, unchanged:** manifest and packer triage; multi-dex string/class recon at 27.6 MB across four
|
||
dex files (`dex_strings.py`, `dexutil.py`); native ELF import mapping at scale (36,974 relocations);
|
||
live-process library mapping; equal-length `.so` constant patching; the launch-timeline methodology
|
||
(`coldstart.py`); the P26 discipline itself — checking whether a tool was ever able to answer before
|
||
reading its silence as a finding, which is what turned the blutter crash from a false "unusable" into
|
||
a corrected "works after a relink".
|
||
|
||
**Ran out:** from the Dart snapshot inward. Everything past `pp.txt` is fine; getting *to* `pp.txt`
|
||
from the binary requires a container resolver the kit does not have. That is one dependency, not a
|
||
subject area.
|
||
|
||
**Not exercised at all:** packers, code virtualization, custom linkers, integrity-check redirection,
|
||
tamper-triggered suicide. This target has none of those, and the unmodified app's own startup failure
|
||
also removes the repack loop those scenarios need.
|
||
|
||
---
|
||
|
||
## F7 — A trap in this specific environment that will produce false confirmations
|
||
|
||
`androguard` 4.x (the version installed here) has **four independent reference-analysis failures on
|
||
this sample**: `get_xref_from()` returns 0 for everything, including `AdLoader.loadAd()V` which has
|
||
20+ callers by other means; `get_ref()` raises; a full `get_operands()` sweep returns 0 strings.
|
||
|
||
Anyone who reaches for androguard to double-check a "zero references" conclusion will get **zero, and
|
||
therefore agreement**. The zero-reference result in `TARGET-FACTS.md` does not come from androguard;
|
||
it comes from `dexutil.py` plus raw-byte re-checks of the bodies that would not decode cleanly. This
|
||
is worth a line in the environment documentation because it is a tool that lies in the direction of
|
||
the hypothesis being tested.
|
||
|
||
---
|
||
|
||
## F8 — Reconciling the two failures: a genuine but unproven loader signal
|
||
|
||
Recorded as a boundary between what was established and what was guessed, because collapsing them
|
||
would be its own error.
|
||
|
||
The launch does show non-standard startup plumbing (observed):
|
||
|
||
- `E LoaderLog: 10026 / 101004` on the device, `10020 / 101003` on the emulator — an SDK-side
|
||
loader logging numeric status codes
|
||
- `E Instrumentation: Uninitialized ActivityThread, likely app-created Instrumentation, disabling
|
||
AppComponentFactory` — the process is not taking the ordinary `ActivityThread` path
|
||
- `E m.tingfeng.too: Unknown bits set in runtime_flags: 0x40000000`
|
||
|
||
And yet the dex-side packer verdict is clean: no hardening library in the native set, an ordinary
|
||
`application android:name` (the app's own class, not a packer stub), no known packer markers. Those two
|
||
observations sit in tension.
|
||
|
||
**Not resolved.** The `LoaderLog` emitter was not attributed to a specific class — the string exists
|
||
in `classes2.dex` and `classes3.dex` but the classes that declare it are third-party, and this pass
|
||
did not trace the call chain. Whether this plumbing is a benign logging SDK, part of the Flutter
|
||
plugin bootstrap, or a light-weight loader contributing to the abort is **unverified**. Stating it
|
||
as established would be exactly the kind of overreach this report is trying to document.
|