mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-15 20:04:22 +08:00
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.
This commit is contained in:
@@ -288,6 +288,12 @@ references/ loaded on demand, one topic each
|
||||
branch records / the ETE tail), and runtime addresses turned into
|
||||
symbols and static offsets (`trace_addr2sym.py`, PIE vaddr/file-offset
|
||||
rules)
|
||||
native-vm-recovery.md recovering a native VM: existence signals, the behavioural table
|
||||
that separates stacked CFF/VM dispatchers, relocation-recovered
|
||||
dispatch tables, register anchoring with triple verification,
|
||||
nested-VM segment stacks, STORE-backward operand slicing with byte
|
||||
provenance, and the clean-re-VM validation loop — absorbed from two
|
||||
public write-ups
|
||||
kernel-event-injection.md the driver as your hands inside the target: watchpoints armed from the
|
||||
kernel with no tracer relationship, register rewrites at the trap, the
|
||||
primitive map per route (KPM/LKM/eBPF/Zygisk), and the composition
|
||||
@@ -439,7 +445,8 @@ scripts/ parameterized, path-agnostic
|
||||
vm_handler_slice.py de-virtualize an interpreter trace: dispatcher detection, slicing
|
||||
into handler segments at dispatch entries, signature clustering, and
|
||||
the guest-opcode trajectory via an optional handler->opcode map —
|
||||
calibrated against the open xvmp IR virtualizer
|
||||
calibrated against the open xvmp IR virtualizer, with a nested-loop
|
||||
diagnostic for VMs called from VM code
|
||||
rasc_build.py build and verify rasc, the Rust ASC re-implementation:
|
||||
--check what is present, --build clone plus cargo, --verify an APK
|
||||
against droidasc and fail on any class-set difference
|
||||
|
||||
+7
-1
@@ -215,6 +215,11 @@ references/ 按需加载,每个文件一个主题
|
||||
栈不可信时的崩溃取证(BRBE 分支记录 / ETE 尾部),
|
||||
以及把运行时地址变成符号与静态偏移
|
||||
(`trace_addr2sym.py`,PIE vaddr/文件偏移规则)
|
||||
native-vm-recovery.md native VM 还原:VM 存在信号、区分叠在一起的 CFF/VM 双层
|
||||
dispatcher 的行为判据表、用 relocation 恢复分发表、三重
|
||||
验证的寄存器锚定、嵌套 VM 的段栈、从 STORE 反向切片加
|
||||
字节道的操作数恢复,以及干净重实现 VM 的验证闭环 —— 吸收自
|
||||
两篇公开实战文
|
||||
kernel-event-injection.md 把驱动当伸进目标里的手:从内核布置、无 tracer 关系的
|
||||
观察点、陷阱处的寄存器改写、按路线(KPM/LKM/eBPF/
|
||||
Zygisk)的能力对照表,以及把这一层与另外两层叠加的
|
||||
@@ -362,7 +367,8 @@ scripts/ 参数化、与路径无关
|
||||
或裸地址列表;用真实交叉编译的 aarch64 .so 验证过
|
||||
vm_handler_slice.py 把解释器轨迹去虚拟化:dispatcher 识别、按分发入口切成
|
||||
handler 段、签名聚类,以及通过可选 handler→opcode 映射
|
||||
得到的 guest opcode 轨迹 —— 按开源 xvmp IR 虚拟机校准
|
||||
得到的 guest opcode 轨迹 —— 按开源 xvmp IR 虚拟机校准,
|
||||
并带 VM 嵌套调用的循环诊断
|
||||
rasc_build.py 构建并验证 rasc,即 ASC 索引器的 Rust 重实现:
|
||||
--check 检查已有什么、--build 克隆并 cargo、--verify 用
|
||||
droidasc 校验一个 APK,并在任何类集合差异上失败
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Extension verification — absorption pass: four public write-ups (Kanxue, 2026)
|
||||
|
||||
Covers the absorption of four public reverse-engineering write-ups the user supplied
|
||||
(2026-08/09 postings; titles, authors and every sample identity anonymized in this record —
|
||||
the write-ups themselves were already anonymized by their authors). Following the
|
||||
absorption-pass convention (`EXTENSION-desensitization.md`): **only phase, checklist and
|
||||
command patterns cross over**, nothing target-shaped, and each absorbed claim carries its own
|
||||
strength label.
|
||||
|
||||
**Headline: two write-ups produced a new reference (`references/native-vm-recovery.md`, every
|
||||
step `inferred`), one produced four pitfalls (P43–P46) and one tool fix; one write-up's
|
||||
dispatcher-margin lesson was already absorbed in the previous pass. The sources are public
|
||||
postings; no sample, tool output or target fact from them entered the repository.**
|
||||
|
||||
---
|
||||
|
||||
## 1. What was read
|
||||
|
||||
| Source (anonymized) | Shape | Absorbed into |
|
||||
|---|---|---|
|
||||
| W1: full arm64 custom-VM recovery (long-form) | 0x00–0x1A method narrative | `references/native-vm-recovery.md` (existence signals, dispatcher separation, partial deflattening, relocation tables, PC proofs, descriptor/ISA cautions, clean-re-VM loop) |
|
||||
| W2: VMP interpreter recovery with trace-driven IR lifting | anchor/trace/lifting narrative | `references/native-vm-recovery.md` (register anchoring + triple verification, nested segment stacks, STORE-backward slicing, byte provenance, IR shape), `vm_handler_slice.py` nested-loop diagnostic |
|
||||
| W3: login-capture war story (packed + MADP/Weex + gomobile) | route-by-route复盘 | `references/pitfalls.md` P43–P46; the stack-ABI note in `references/hardware-watchpoints.md` |
|
||||
| W4: manual OLLVM deflattening | predicate algebra + IDA scripting | cross-checked against existing content; its MOV/MOVK constant-propagation state mapping is referenced as the static counterpart in `native-vm-recovery.md` §11 — no new file |
|
||||
|
||||
## 2. The one code change, and why — observed
|
||||
|
||||
`vm_handler_slice.py` gained `nested_loop_segments()`: a block repeating 4+ times **inside one
|
||||
segment** flags a nested dispatch loop. Reasoning absorbed from W2: handler chains repeat
|
||||
across segments, never within one — a within-segment repeat is a second dispatcher running
|
||||
under the first, and W2 measured 45-of-46 entries nested with 57–1997 instructions lost per
|
||||
nesting under a flat accumulator. Host-tested on a synthetic nested fixture:
|
||||
|
||||
```
|
||||
$ python vm_handler_slice.py --log nested.log --dispatcher libvm.so+0x1000
|
||||
note: segments 2 contain a block repeating 4+ times inside one segment: a nested dispatch
|
||||
loop ... slice per level; the segment-stack fix is documented in
|
||||
references/native-vm-recovery.md
|
||||
```
|
||||
|
||||
The same edit fixed a latent bug this pass's review found: `diagnostics()` had been measuring
|
||||
`len()` of `(run, closed)` tuples (always 2) since the open-tail change, quietly disabling the
|
||||
spin-loop note on the real code path. Both regressions are now pinned by tests.
|
||||
|
||||
## 3. The judgement calls — what did NOT cross over
|
||||
|
||||
- W1/W2's Ghidra/SLEIGH-specific mechanics (p-code slicing internals, seed definitions bound to
|
||||
their toolchain): only the **method** crossed (store-backward, provenance-over-value); the
|
||||
repository ships no Ghidra script and none is implied.
|
||||
- W3's working exploit details (hook source, injection directory, SELinux labels of the
|
||||
delivery mechanism): the delivery route already has its own file
|
||||
(`references/lsposed-and-modules.md`, `kernel-event-injection.md`); what crossed is the
|
||||
**layer-identification discipline** (each dead end names the next layer; prefer the highest
|
||||
plaintext boundary; a RegisterNative inventory is one run for a full method table).
|
||||
- W4's full IDA script: the technique is named (state-register MOV/MOVK constant propagation,
|
||||
CMP-site collection, predecessor walk past blocks with no CMP on the state register) and
|
||||
pointed at from `native-vm-recovery.md` §11; writing an IDA-independent implementation is a
|
||||
future pass, not an absorbed claim.
|
||||
- Every sample identity in all four sources: anonymized by the authors, kept anonymized here;
|
||||
`.ref/` (the local copies) is untracked and git-ignored in effect (never added).
|
||||
|
||||
## 4. Strength accounting
|
||||
|
||||
- `native-vm-recovery.md`: every method step `inferred` (absorbed, not executed here) — stated
|
||||
in the file's own header, mirrored in `evidence-summary.md` and the capability matrix.
|
||||
- `vm_handler_slice.py` nested diagnostic: `observed` on the host fixture (this pass).
|
||||
- Pitfalls P43–P46: absorbed as `inferred` lessons with their mechanism stated; each names the
|
||||
check that would have caught it, per this repository's pitfall format.
|
||||
- No claim from the write-ups was upgraded to `observed` by absorption. That is the point of
|
||||
the convention: a method someone else paid for is a lead, not a measurement.
|
||||
@@ -10,10 +10,11 @@ Reference environment: Linux container host, Python 3.13, cross `gcc 16.2.0` tar
|
||||
Network access was used once, read-only, to fetch the two public obfuscator implementations
|
||||
the calibration cites (below). No Android device, no coresight/SPE/BRBE hardware.
|
||||
|
||||
**Headline: the address→symbol loop is measured against a real cross-compiled library, the
|
||||
de-virtualization slicer is measured on synthetic interpreter traces, and the SPE/BRBE
|
||||
producer side remains documented-and-gated. The flattening-recipe corrections are read from
|
||||
public source, and that source is named.**
|
||||
**Headline: the address→symbol loop is measured against a real cross-compiled library; the
|
||||
de-virtualization slicer is measured on synthetic traces AND end to end on a real
|
||||
IR-virtualized + flattened binary (built by the completed toolchain, host-traced, sliced,
|
||||
and fully opcode-named); the SPE/BRBE producer side remains documented-and-gated. The
|
||||
flattening-recipe corrections are read from public source, and that source is named.**
|
||||
|
||||
---
|
||||
|
||||
@@ -55,9 +56,9 @@ guest opcode), the **OP_CALL materialization segment** appearing as its own sign
|
||||
virtualizer's const dispatch — the second observation point the calibration names), and the
|
||||
**UNCLOSED tail** flagged rather than silently dropped. The auto-detection rule is
|
||||
top-of-histogram **plus predecessor diversity** (≥3 distinct predecessors), not a naive
|
||||
count margin: the first implementation used a 4x margin and refused this very fixture —
|
||||
in a stack-shaped VM the hottest handler executes on every guest instruction, exactly as
|
||||
often as the dispatcher. That refusal-to-accept is the load-bearing part of the auto rule.
|
||||
count margin: in a stack-shaped VM the hottest handler executes on every guest instruction,
|
||||
exactly as often as the dispatcher. That refusal-to-accept is the load-bearing part of the
|
||||
auto rule. Section 6 measures all of this against a real hardened binary.
|
||||
|
||||
## 3. The flattening-recipe corrections — observed source, inferred runtime
|
||||
|
||||
@@ -86,15 +87,123 @@ These corrected `hardware-watchpoints.md`'s state-variable recipe (the old text
|
||||
global and a readable value — wrong for both families as shipped) and added the
|
||||
family-difference table to `ete-diff-tracing.md` §5. The fetches live under
|
||||
`tools/_work/refs/` (git-ignored); **runtime behaviour of these passes is `inferred` from
|
||||
source, never executed here.**
|
||||
source** — except the IR virtualizer's runtime, which this pass built, executed and
|
||||
measured (§5–§6).
|
||||
|
||||
## 4. What this pass deliberately did not do
|
||||
|
||||
- No SPE capture, no BRBE record read, no `perf -j any` on any device: those routes ship as
|
||||
documented-and-gated, and the reference's §5 failure modes say exactly how each gate
|
||||
fails silently.
|
||||
- No xvmp-hardened binary was compiled or traced: the slicer's calibration is source-read,
|
||||
and its first run against a real sample is a measurement, not a confirmation.
|
||||
- The `--opcode-map` a real target needs is a manual labelling task over the segment table;
|
||||
this pass generated the fixture's map by hand, which is exactly the workflow the script
|
||||
documents.
|
||||
- No hardened *closed-source* VM was sliced: the fixture in §6 is an open, self-built
|
||||
virtualizer; against a closed VMP the same slicing yields the trajectory, and the
|
||||
opcode map becomes a manual labelling task over the (usually few dozen) unique segments —
|
||||
which is exactly the workflow the script documents.
|
||||
|
||||
## 5. The xvmp toolchain, completed and verified — observed (workbench clone)
|
||||
|
||||
The public tree ships the compiler half of its IR virtualizer only, and that half was
|
||||
never compiled: no TU includes the virtualizer headers, and on dev trees they do not build.
|
||||
Three facts to keep when working with that tree:
|
||||
|
||||
- `getOrCreateVMExec` emits a **weak stub with no implementation** when `vm_exec` is absent
|
||||
— a build from that tree as-is links and then falls into the empty stub at runtime.
|
||||
- Unsupported IR is **skipped with an `errs()` line and no failure** — "compiled clean" is
|
||||
not "every function was rewritten": read stderr for `[irvm] skip` lines, or a silently
|
||||
skipped function stays native and quietly skews a trace pair.
|
||||
- The companion write-up (vmp.md) describes a **different, earlier prototype** — 32-bit
|
||||
fixed-width ISA, thunk bridges, an LVMP container — none of which is in the tree. Trust
|
||||
the code, not the article.
|
||||
|
||||
This repository completed the missing half. On the workbench clone of that tree (SSH
|
||||
environment, 64-core), all four `omp` branches now carry:
|
||||
|
||||
- **Pass wiring**: `IRVM.cpp/.h` (new-PM pass beside the obfuscation manager, enabled by
|
||||
`-mllvm -irobf-irvm`, functions opt in via `section(".irvm")`) — the first TU that ever
|
||||
compiled the virtualizer headers, which required fixing per-tree upstream breakage (the
|
||||
revived `ADT/Optional.h` lacks `NoneType`; `DataLayout(Module*)` removed; `Br` split into
|
||||
`UncondBr`/`CondBr` on newer trees; `CastOps` renumbered — `FPExt` moved 17→47; the
|
||||
Hikari `compat/CallSite.h` called `DenseMapInfo` members dev-main deleted). One branch
|
||||
additionally had an **ungated** `IRVMModuleTool` call at the top of
|
||||
`Obfuscation::runOnModule` — plain native builds rewrote `.irvm` functions into thunks
|
||||
over the weak stub (and native -O1/-O2 escaped only because small functions get inlined
|
||||
away); removing it made IRVM strictly flag-gated.
|
||||
- **FORMAT v3 bytecode**: 8-byte aligned zero-extended slot words; explicit
|
||||
`OP_LOAD/OP_STORE` access widths; src/dst bit widths plus a **stable CC_ kind enum** for
|
||||
`OP_CAST` (raw LLVM opcode numbers break across trees); operand width for `OP_CMP`;
|
||||
**per-index byte scales** for `OP_GEP` (a single element size mis-addresses the
|
||||
multi-index GEPs `-O0` emits verbatim). The branch condition is operand 0, never the
|
||||
last operand (that is the false-successor label).
|
||||
- **Runtime**: `vm_exec.c` strong-symbol interpreter (23 hand-packed bytecode unit checks
|
||||
on host) and `VMP.h` user header (`IRVM_SECTION`, weak `handle_call` fallback,
|
||||
`VMP_TRACE=1` per-step interpreter trace).
|
||||
- **Verification** (`test_irvm.c`: loops, globals, arrays via GEP, fpu, casts, external
|
||||
printf): virtualized vs native output **identical at -O0/-O1/-O2 on all four `omp`
|
||||
branches** (upstream commits `9b7cf75..ee23d66`, `cb1e2c1..63f100d`, `8a85cce..ee23d66`,
|
||||
`b6b6578..7929a03`).
|
||||
|
||||
## 6. The fixture, built, traced, sliced, and named — measured (host)
|
||||
|
||||
End to end on the workbench clone with the §5 toolchain:
|
||||
|
||||
- **Sample** (`tools/_work/irvm_fixture/fixture_sample.c`): three `.irvm` functions —
|
||||
`checksum` (fnv1a loop over 32 bytes), `stats` (fpu + branches), `mix` (byte-shuffle
|
||||
casts) — each called 200× from main. `stats` stays native: at -O1 the optimizer's
|
||||
if-conversion produces a `select` before the IRVM pass sees it (the documented skip
|
||||
hazard; a `volatile` interlock does not survive either). The fixture ships 2/3
|
||||
virtualized — as-is, since that is the real workflow.
|
||||
- **Hardened build**: `-O1 -mllvm -irobf-irvm -mllvm -enable-fco -mllvm -enable-bcfobf
|
||||
-mllvm -enable-subobf` plus `runtime/vm_exec.c`; output **identical to native**.
|
||||
This tree's Hikari flag is `-enable-fco` (not `-enable-fla`). `handle_call` needs no
|
||||
user-side bridge — the pass emits the real const-materialization dispatch into the
|
||||
module — but only when at least one function was rewritten: with all candidates inlined
|
||||
away (-O1 + single call site) the link fails on the weak `vm_exec` reference chain.
|
||||
`noinline` on the `.irvm` functions keeps candidates (and the emitted `handle_call`)
|
||||
alive.
|
||||
- **Guest ground truth**: `VMP_TRACE=1` → 17,961 `[vm] step=` lines (this build traces
|
||||
CAST/ARITH/CMP/RET steps only).
|
||||
- **Host capture**: frida stalker does not work in these containers (measured on both a
|
||||
Docker host and an Android-kernel sandbox, frida 16.7.19 and 17.23.3): execution
|
||||
primitives pass (anonymous RWX mmap, W^X flip, memfd), the agent loads and runs the
|
||||
script, a ptrace syscall-error tracer around the injection tree shows **zero** denied
|
||||
syscalls — the failure is inside the injected agent's hook/trampoline machinery, past
|
||||
the syscall layer. Two operational notes: an outer ptrace tracer conflicts with frida
|
||||
spawn (frida ptrace-attaches the child it spawns → NotSupportedError — do not nest this
|
||||
repo's ptrace tooling and frida against the same tree); and the working host capture is
|
||||
a 33-line **ptrace single-stepper** (`stepper.c`): 24,582,699 `0x<rip>` lines in 36 s,
|
||||
filtered to the main image (23.97 M lines). On Android devices the stalker route keeps
|
||||
its own verification status per the skill's evidence rules.
|
||||
- **Slicing**: `vm_handler_slice.py --dispatcher 0x55b98249c470` (manual — with an
|
||||
instruction-per-line stream the dispatcher block's instructions tie in count, so `auto`
|
||||
correctly refuses) → **95,184 segments, 18 unique signatures**, `RESULT=ok`. The
|
||||
diagnostics fire as designed: the tail stays open (exit path), and the nested-dispatch
|
||||
warning marks the outer fla state machine — one guest step crosses the chosen dispatcher
|
||||
~5.3× (95,184 / 17,961), and the len-20 segment (83,286 hits) is the flat state-machine
|
||||
shuttle, not a handler.
|
||||
- **Per-level separation and naming**: the 18 signatures split into the shuttle class and
|
||||
16 handler segments (~59 per round). No single host block fires once per guest step
|
||||
(fla scattered the switch into per-state entries), so the 200-count round-boundary
|
||||
segments split the trajectory into per-call rounds, and each round's signature sequence
|
||||
aligns positionally against the VMP_TRACE guest sequence. Result (round 2, all 59
|
||||
handler segments named):
|
||||
`loop1 x8: [ARITH_OR_SHL, OP_CAST, ARITH_ADD_CMP_latch]` →
|
||||
`LOOP_BOUNDARY_BR LOOP2_ENTRY_BR LOOP2_PROLOGUE` →
|
||||
`loop2 x8: [ARITH_OR, ARITH_SUB_INDEX, OP_CAST, ARITH11_ADD_CMP_latch]` → `OP_RET` —
|
||||
positionally isomorphic to the guest ops (`[or, shl, CAST, add, cmp]` / `[or, sub,
|
||||
CAST(zext), arith11, add, cmp]`, many ops per segment under fla), with the latch firing
|
||||
7/8 times (the last iteration exits via the boundary segments) and the segment counts
|
||||
matching the guest histograms exactly (3232/1600/1400/200). `--opcode-map` yields a
|
||||
fully named trajectory, zero unmapped (map at `tools/_work/irvm_fixture/opcode_map.json`).
|
||||
- **Two findings that carry beyond this fixture**:
|
||||
1. `-O1` LICM **hoisted the pure virtualized call** out of the 200× driver loop —
|
||||
`checksum` executed once (`c == h*200 mod 2^32`). A virtualized function is not
|
||||
protected against being treated as pure by the optimizer that virtualized it; defeat
|
||||
hoisting with per-round argument changes when designing fixtures.
|
||||
2. `apply_map` originally keyed handler names by the segment's *first block* — under an
|
||||
instruction stream (or an outer flattening pass) every segment shares the
|
||||
dispatch-successor block, and only the signature distinguishes handlers. Signature
|
||||
keys are tried first; first-block keys remain supported.
|
||||
|
||||
Artifacts under `tools/_work/irvm_fixture/` (git-ignored): sample, stepper, the frida
|
||||
attempt, guest trace, host excerpt, opcode map. The full host trace stays on the workbench
|
||||
clone.
|
||||
|
||||
@@ -101,6 +101,7 @@ kernel toolchain is an explicitly-labelled template whose gate says where it clo
|
||||
| `EXTENSION-ete-diff.md` | The differential half measured on synthetic flattened-dispatcher fixtures (first divergence, unique-per-side sets), the gate/capture plumbing measured to the no-device boundary, and what was deliberately not done: no perf-with-CORESIGHT build, no cs_etm stream, no overhead claim |
|
||||
| `EXTENSION-kernel-instrument.md` | The kernel-side scaffold's generate/verify round trip, the spec-refusal rules shared with `hw_watch.py`, the capability map whose `no` cells do the gatekeeping, and the three-time-fenced experimental register-rewrite action |
|
||||
| `EXTENSION-sampling-crash.md` | The second hardware pass: `trace_addr2sym.py` verified against a real cross-compiled `.so` (readelf-cross-checked), the interpreter-trace slicer measured on the calibrated xvmp shape (including the 4x-margin bug it found in itself), and the flattening-recipe corrections read from the two public obfuscator implementations the user supplied |
|
||||
| `EXTENSION-absorption-kx.md` | The absorption pass over four public write-ups: the native-VM-recovery procedure (every step `inferred`), pitfalls P43–P46, the slicer's nested-loop diagnostic (with the latent `len(tuple)` bug this pass's review caught), and what deliberately did not cross over |
|
||||
|
||||
## Adding a run
|
||||
|
||||
|
||||
@@ -212,6 +212,8 @@ unread in this repository.
|
||||
| A crash's backtrace cannot be trusted — frames in wrong modules or anonymous regions, flattened/virtualized code, or a smashed stack | `hardware-sampling-and-crash.md` — BRBE branch records / ETE tail as the real control flow, then `scripts/trace_addr2sym.py` to symbolize every PC |
|
||||
| You hold a list of **runtime addresses** (hits, trace PCs, divergence points) and need symbols / static offsets, not hex | `hardware-sampling-and-crash.md` §3 The crash loop — `scripts/trace_addr2sym.py` with its vaddr/file-offset rules |
|
||||
| A trace of a virtualized function is just an **interpreter loop repeating** and reads like noise | `scripts/vm_handler_slice.py` — slice at dispatcher entries into handler segments, cluster signatures, recover the guest-opcode trajectory (xvmp-calibrated) |
|
||||
| You cannot tell whether the `table[index]; br` you are staring at is the **VM dispatcher or the OLLVM dispatcher** — or the VM has no readable static shape at all | `references/native-vm-recovery.md` — the behavioural table that separates the two stacked layers, the VM-existence signals, register anchoring with triple verification, nested-VM segment stacks, and STORE-backward operand slicing |
|
||||
| The trace segments themselves look corrupted — handler heads missing, tails glued into wrong segments | `references/native-vm-recovery.md` §7 Nested VMs — a flat segment accumulator discards the outer level; nested slices need a segment stack (`vm_handler_slice.py` flags the symptom) |
|
||||
| You need to know **where two runs of the same operation diverge** (password/license/key), not just what executed | `ete-diff-tracing.md` — ETE hardware-trace gates plus the differential method; `scripts/trace_diff.py` also diffs Stalker logs |
|
||||
| A generated KPM/LKM template is in front of you, or the kernel route feels like the plan | `kernel-event-injection.md` — the primitive map, the composition recipes, and the gates every kernel-side file must pass before it is a weapon |
|
||||
| The traffic is protobuf/gRPC/QUIC, or a proxy sees TLS but requests still fail on a Flutter app | `references/protocol-reverse.md` — schema-less protobuf, frame capture, and native-side pinning |
|
||||
|
||||
@@ -66,4 +66,4 @@
|
||||
"_targets_reference": "references/hardware-watchpoints.md, references/kernel-event-injection.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,20 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B1"],
|
||||
"evidence": ["references/byte-level-patching.md", "references/dex-patching.md", "references/patch-audit.md", "scripts/dex_patch_bytes.py", "scripts/dex_find_insn.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-benchmark-l1-l3.md section 2 (repository root, not shipped)", "tests/benchmark.md row B1 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B1"
|
||||
],
|
||||
"evidence": [
|
||||
"references/byte-level-patching.md",
|
||||
"references/dex-patching.md",
|
||||
"references/patch-audit.md",
|
||||
"scripts/dex_patch_bytes.py",
|
||||
"scripts/dex_find_insn.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-benchmark-l1-l3.md section 2 (repository root, not shipped)",
|
||||
"tests/benchmark.md row B1 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Equal-length edits only. The end-to-end measurement is one 2-byte branch rewrite inside a 5,528-byte dex, whose whole-file diff is those 4 bytes plus dex header bytes 8..32; the patched file self-verifies. Constraints that cannot be expressed as an equal-length edit need the dexlib2 route, which has no public-target measurement."
|
||||
},
|
||||
{
|
||||
@@ -38,8 +49,14 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/dex-patching.md", "references/patch-audit.md", "scripts/dexpatch/"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/dex-patching.md",
|
||||
"references/patch-audit.md",
|
||||
"scripts/dexpatch/"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The rewriter and the smali round-trip tools (scripts/smtool.py, scripts/patch_smali.py) carry no measurement against a public target in this record. The round-trip blind spot is documented: a whole-tree smali round trip can pass every table check and still crash with IncompatibleClassChangeError. Treat the route as documented, not proven."
|
||||
},
|
||||
{
|
||||
@@ -48,9 +65,19 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B1"],
|
||||
"evidence": ["references/patch-audit.md", "references/byte-level-patching.md", "scripts/dexutil.py", "scripts/dex_check_verifier.py", "scripts/dex_classdiff.py"],
|
||||
"evidence_not_shipped": ["tests/benchmark.md row B1 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B1"
|
||||
],
|
||||
"evidence": [
|
||||
"references/patch-audit.md",
|
||||
"references/byte-level-patching.md",
|
||||
"scripts/dexutil.py",
|
||||
"scripts/dex_check_verifier.py",
|
||||
"scripts/dex_classdiff.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"tests/benchmark.md row B1 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Header recompute order (adler32 over d[12:], sha1 over d[32:]) is proven on one written file, which reproduces both fields. Passing scripts/dex_classdiff.py is necessary, not sufficient: it cannot detect code-item damage, and the checks that look further (instruction-length audit, the equal-length blind spot, move-result adjacency) live in references/patch-audit.md."
|
||||
},
|
||||
{
|
||||
@@ -60,8 +87,14 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/byte-level-patching.md", "references/dex-patching.md", "scripts/dex_strpatch.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/byte-level-patching.md",
|
||||
"references/dex-patching.md",
|
||||
"scripts/dex_strpatch.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Equal-length only: the shipped scripts/dex_strpatch.py enforces equal length and refuses a substring of a longer identifier. A different-length string edit is therefore not covered by that script; it needs the dexlib2 route, whose build is documented and unmeasured. No public-target measurement for the string path."
|
||||
},
|
||||
{
|
||||
@@ -70,9 +103,20 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B1", "B9"],
|
||||
"evidence": ["references/repack-and-sign.md", "references/verification.md", "scripts/repack.py", "scripts/install_test.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-benchmark-l1-l3.md section 2 (repository root, not shipped)", "tests/benchmark.md rows B1 and B9 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B1",
|
||||
"B9"
|
||||
],
|
||||
"evidence": [
|
||||
"references/repack-and-sign.md",
|
||||
"references/verification.md",
|
||||
"scripts/repack.py",
|
||||
"scripts/install_test.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-benchmark-l1-l3.md section 2 (repository root, not shipped)",
|
||||
"tests/benchmark.md rows B1 and B9 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Measured chain on this class of host: zip, then zipalign -p -f 4, then apksigner.jar --v1 --v2 --v3; the order cannot be reversed (the installer refuses with [-124]). B1 confirms installability by matching the on-device base.apk sha256 against the local build, and its control build through the identical pipeline still fails the old way. Signing has no uber-apk-signer here. The isolated-constant rewrite script writes an APK through a different code path whose zip-metadata and alignment handling was recorded as a defect; that path is not covered by this row."
|
||||
},
|
||||
{
|
||||
@@ -81,9 +125,17 @@
|
||||
"status": "partial",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B9"],
|
||||
"evidence": ["references/split-apk.md", "scripts/repack.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-split-apk.md (repository root, not shipped)", "tests/benchmark.md row B9 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B9"
|
||||
],
|
||||
"evidence": [
|
||||
"references/split-apk.md",
|
||||
"scripts/repack.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-split-apk.md (repository root, not shipped)",
|
||||
"tests/benchmark.md row B9 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Analyze, unified re-sign and merge were run on two real sets: after resign every member shares one certificate with v1+v2+v3 true, and merge correctly refuses (exit 1, before writing) a set whose members carry their own resources.arsc while succeeding on the code/native-only set. adb install-multiple and launch confirmation were NOT executed, so installability of a re-signed set is unverified, and the split mode's install path is exactly where a wrong alignment hides."
|
||||
},
|
||||
{
|
||||
@@ -93,8 +145,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/third-party-builds.md", "scripts/apk_diff.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/third-party-builds.md",
|
||||
"scripts/apk_diff.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "scripts/apk_diff.py is listed among the scripts the first verification pass never ran, and no benchmark row exercises it. Absence from that record is not a verdict on it, but nothing here supports trusting its output either."
|
||||
},
|
||||
{
|
||||
@@ -103,9 +160,18 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B3"],
|
||||
"evidence": ["references/advanced-unpacking.md", "scripts/dex_dump_validate.py", "scripts/dex_mem_scan.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-extraction-shell-bench.md (repository root, not shipped)", "tests/benchmark.md row B3 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B3"
|
||||
],
|
||||
"evidence": [
|
||||
"references/advanced-unpacking.md",
|
||||
"scripts/dex_dump_validate.py",
|
||||
"scripts/dex_mem_scan.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-extraction-shell-bench.md (repository root, not shipped)",
|
||||
"tests/benchmark.md row B3 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The ratio is bimodal, not thresholded: a real skeleton reads about 100 percent, a zero-change control 1.9 percent, nop-cleared bodies 0.0 percent and throw stubs 1.9 percent, so the shapes real shells use are invisible to it. The earlier 'tens of percent means a skeleton' rule was wrong and was deleted. Measured on an 11-variant synthetic set derived from one real dex, not on a live extraction shell, and three script defects were found by that set."
|
||||
},
|
||||
{
|
||||
@@ -114,9 +180,18 @@
|
||||
"status": "partial",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B4"],
|
||||
"evidence": ["references/advanced-unpacking.md", "references/code-virtualization-and-custom-linkers.md", "references/vmp-differential-analysis.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-extraction-shell-bench.md section 3.4 and section 4 (repository root, not shipped)", "tests/benchmark.md row B4 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B4"
|
||||
],
|
||||
"evidence": [
|
||||
"references/advanced-unpacking.md",
|
||||
"references/code-virtualization-and-custom-linkers.md",
|
||||
"references/vmp-differential-analysis.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-extraction-shell-bench.md section 3.4 and section 4 (repository root, not shipped)",
|
||||
"tests/benchmark.md row B4 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Static criteria can rule a dex-VMP out; they cannot rule runtime-applied hardening out. Never declare a VMP without an official disassembler's failure count: a hand-written opcode table reported 1,393 of 22,424 bodies as undecodable and produced a VMP verdict on a sample that the platform decoder showed to be ordinary dalvik with zero structural errors. Install, launch and refusal for that sample remain untested."
|
||||
},
|
||||
{
|
||||
@@ -126,8 +201,13 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/vmp-differential-analysis.md", "scripts/vmp_diff_harness.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-vmp-diff.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/vmp-differential-analysis.md",
|
||||
"scripts/vmp_diff_harness.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-vmp-diff.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The fixture covers 218 of 224 opcodes and the closed loop re-derived that table exactly: 218 of 218 emitted opcodes, zero wrong entries, zero fabricated entries. What was not exercised is the other half of the premise -- no third-party hardening platform was contacted, and the assumption that a real engine substitutes per opcode at stable instruction length is inferred from public write-ups. No benchmark row covers this route."
|
||||
},
|
||||
{
|
||||
@@ -136,9 +216,18 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B5", "B11"],
|
||||
"evidence": ["references/java2c-and-jni-sinking.md", "scripts/java2c_probe.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-java2c.md (repository root, not shipped)", "tests/benchmark.md rows B5 and B11 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B5",
|
||||
"B11"
|
||||
],
|
||||
"evidence": [
|
||||
"references/java2c-and-jni-sinking.md",
|
||||
"scripts/java2c_probe.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-java2c.md (repository root, not shipped)",
|
||||
"tests/benchmark.md rows B5 and B11 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The discriminating measurement is observed: native-declaration density separates the shapes by roughly 2000x (0.03-0.04 percent on three real JNI samples against 82.76 percent on a Java2C-shaped fixture), and Java_* symbols matched dex native-method counts 1:1 per ABI. No Dex-to-C compiler output was ever built here (no NDK, no host or device clang), so every Java2C-specific criterion is inferred. A symbol search fails silently on real Java2C: dcc emits -fvisibility=hidden and C++ jni.h inlines RegisterNatives."
|
||||
},
|
||||
{
|
||||
@@ -147,9 +236,19 @@
|
||||
"status": "partial",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B2"],
|
||||
"evidence": ["references/native-tamper-and-suicide.md", "references/native-and-so.md", "scripts/spawn_patch_detach.py", "scripts/hook_patch_only.js"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-benchmark-l1-l3.md section 3 (repository root, not shipped)", "tests/benchmark.md row B2 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B2"
|
||||
],
|
||||
"evidence": [
|
||||
"references/native-tamper-and-suicide.md",
|
||||
"references/native-and-so.md",
|
||||
"scripts/spawn_patch_detach.py",
|
||||
"scripts/hook_patch_only.js"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-benchmark-l1-l3.md section 3 (repository root, not shipped)",
|
||||
"tests/benchmark.md row B2 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The tooling works; the patch does not. Writes landed 0.04-0.05 s after resume and survived detach, and the patched run and a read-and-write-back control both still died with SIGABRT at the same site: neutralising the terminate routine plus both of its branch inputs was insufficient, and the region is a dense cluster of mov w0,#imm ; ret stubs. Holding the process paused until the patch lands structurally cannot work, because a frozen process has no libraries mapped. On that target the pristine original already reports tamper, so no clean baseline exists there. No surviving-target result is claimed."
|
||||
},
|
||||
{
|
||||
@@ -158,9 +257,18 @@
|
||||
"status": "partial",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B6"],
|
||||
"evidence": ["references/native-dbi-and-deobfuscation.md", "scripts/stalker_trace.js", "scripts/stalker_report.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-stalker-exclude.md (repository root, not shipped)", "tests/benchmark.md row B6 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B6"
|
||||
],
|
||||
"evidence": [
|
||||
"references/native-dbi-and-deobfuscation.md",
|
||||
"scripts/stalker_trace.js",
|
||||
"scripts/stalker_report.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-stalker-exclude.md (repository root, not shipped)",
|
||||
"tests/benchmark.md row B6 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Split result: following a hot libc export with no exclusions kills the process, and excluding 20 system modules keeps it alive -- but delivery is still zero events (blocks=0 blk=0 calls=0). Exclusion is therefore not the fix for a trace that reports nothing, which contradicts the framing that lists 'no events' among the symptoms exclusion addresses. Two harness traps that manufacture a false zero-event result were fixed in the same pass. No real OLLVM trace was produced, and no trace pipeline is verified on the test device."
|
||||
},
|
||||
{
|
||||
@@ -170,8 +278,15 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/native-and-so.md", "references/toolchain.md", "scripts/lib_map.py", "scripts/elf_plt.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/native-and-so.md",
|
||||
"references/toolchain.md",
|
||||
"scripts/lib_map.py",
|
||||
"scripts/elf_plt.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "scripts/lib_map.py is verified on a real target. scripts/elf_plt.py was broken and fixed: one defect was proven to cause a false negative on a symbol that exists and is called, and a second compared a file offset against a virtual address under --diff --name-regions. Run it before patching any stub, and treat its 'no callers' answer as a claim about its own input parsing until you have seen it scan something you know references the symbol."
|
||||
},
|
||||
{
|
||||
@@ -181,8 +296,15 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/native-tamper-and-suicide.md", "references/environment.md", "scripts/native_crash.py", "scripts/grab_crash.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/native-tamper-and-suicide.md",
|
||||
"references/environment.md",
|
||||
"scripts/native_crash.py",
|
||||
"scripts/grab_crash.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "scripts/native_crash.py was recorded as 'not applicable to this sample' in the first pass, and scripts/grab_crash.py was never run -- including on the one target whose crash-reporter SDK hid exactly the stack it claims to recover, so that claim is unverified rather than wrong. The general rule that survives without a measurement is narrower: a process that dies with only 'uncaughtException time: ...' and no stack has crashed."
|
||||
},
|
||||
{
|
||||
@@ -191,9 +313,23 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B2", "B6", "B13"],
|
||||
"evidence": ["references/dynamic-frida.md", "references/environment.md", "references/native-tamper-and-suicide.md", "scripts/run_probe.py", "scripts/frida_probe.js", "scripts/preflight.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-device-run.md (repository root, not shipped)", "tests/benchmark.md rows B2, B6 and B13 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B2",
|
||||
"B6",
|
||||
"B13"
|
||||
],
|
||||
"evidence": [
|
||||
"references/dynamic-frida.md",
|
||||
"references/environment.md",
|
||||
"references/native-tamper-and-suicide.md",
|
||||
"scripts/run_probe.py",
|
||||
"scripts/frida_probe.js",
|
||||
"scripts/preflight.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-device-run.md (repository root, not shipped)",
|
||||
"tests/benchmark.md rows B2, B6 and B13 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Instrumentation is itself a measured variable. Attaching is what kills some targets (the probe arm of B13 dies about 300 ms after resume while the no-script control stays alive); exclusion is what keeps others alive (B6); and a spawn ordering that holds the process paused cannot work, because no libraries are mapped yet (B2). Reading a device's refusal as a fact about the target is the failure mode this row exists to prevent -- the preflight check comes before blaming a patch."
|
||||
},
|
||||
{
|
||||
@@ -202,9 +338,17 @@
|
||||
"status": "partial",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B8"],
|
||||
"evidence": ["references/runtime-data.md", "scripts/datastore_inject.py", "scripts/blob_decode.py"],
|
||||
"evidence_not_shipped": ["tests/benchmark.md row B8 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B8"
|
||||
],
|
||||
"evidence": [
|
||||
"references/runtime-data.md",
|
||||
"scripts/datastore_inject.py",
|
||||
"scripts/blob_decode.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"tests/benchmark.md row B8 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The DataStore encode/decode path is proven on one real 81-byte container: an edited value re-encoded (81 to 80 bytes, length prefix recomputed) was read back by the injector. SharedPreferences and SQLite edits have no equivalent measurement here, and an edit that keeps reverting is a routing signal -- something else owns the value -- not a data-editing problem."
|
||||
},
|
||||
{
|
||||
@@ -213,9 +357,17 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B13"],
|
||||
"evidence": ["references/detection-and-anti-analysis.md", "scripts/anti_detect_probe.js"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-detection-pipeline.md sections 1, 4 and 5 (repository root, not shipped)", "tests/benchmark.md row B13 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B13"
|
||||
],
|
||||
"evidence": [
|
||||
"references/detection-and-anti-analysis.md",
|
||||
"scripts/anti_detect_probe.js"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-detection-pipeline.md sections 1, 4 and 5 (repository root, not shipped)",
|
||||
"tests/benchmark.md row B13 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The check was named and the asymmetry recorded: strstr(\"frida\") fired at 303 ms of process life, then the process was gone with no tombstone, no crash and no ANR record, while the probe's own self-report showed TracerPid=0 with four frida-named mappings visible in /proc/self/maps. Reproducibility is honestly bounded: 3 same-shape arms, 1 delivered the full sequence, because a target that dies in about 300 ms leaves a sub-second window. The probe is observer-only by contract and patches nothing."
|
||||
},
|
||||
{
|
||||
@@ -225,8 +377,14 @@
|
||||
"route_measured": true,
|
||||
"strength": "inferred",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/emulation-and-rpc.md", "scripts/frida_rpc_serve.py", "scripts/rpc_template.js"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-emulation-rpc.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/emulation-and-rpc.md",
|
||||
"scripts/frida_rpc_serve.py",
|
||||
"scripts/rpc_template.js"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-emulation-rpc.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The rpc.exports bridge was exercised end to end on a live device and the indexer pair (droidasc / ddc) was measured on a hardened APK, but the reference classifies the route itself as inferred: emulated execution costs environment-filling work, the unidbg build needed repair before it ran, and no target .so had been emulated at the time of writing. The tool was measured; the route was not."
|
||||
},
|
||||
{
|
||||
@@ -235,9 +393,17 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B8"],
|
||||
"evidence": ["references/protocol-reverse.md", "scripts/protobuf_decode_raw.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-protobuf-raw.md (repository root, not shipped)", "tests/benchmark.md row B8 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B8"
|
||||
],
|
||||
"evidence": [
|
||||
"references/protocol-reverse.md",
|
||||
"scripts/protobuf_decode_raw.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-protobuf-raw.md (repository root, not shipped)",
|
||||
"tests/benchmark.md row B8 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "26 of 26 built-in fixtures, 21 of 21 against the official runtime (re-encode byte-identical to SerializeToString()), 8 of 8 framing checks. Two ambiguities are inherent to the wire format rather than defects: 0801120178 is a valid nested message and a valid packed array, and a non-presence 0 is byte-identical to 'never assigned'. Four script defects were found by these checks and fixed."
|
||||
},
|
||||
{
|
||||
@@ -246,9 +412,19 @@
|
||||
"status": "partial",
|
||||
"route_measured": false,
|
||||
"strength": "inferred",
|
||||
"benchmark_rows": ["B7"],
|
||||
"evidence": ["references/server-api.md", "references/tls-and-cert.md", "references/protocol-reverse.md", "scripts/probe_api.py", "scripts/tls_check.py"],
|
||||
"evidence_not_shipped": ["tests/benchmark.md row B7 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B7"
|
||||
],
|
||||
"evidence": [
|
||||
"references/server-api.md",
|
||||
"references/tls-and-cert.md",
|
||||
"references/protocol-reverse.md",
|
||||
"scripts/probe_api.py",
|
||||
"scripts/tls_check.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"tests/benchmark.md row B7 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Client-scoped by design: this determines who owns a gate, not how to break an authorization the server performs. The native-side certificate-pinning row (a Cronet sample) was not run -- it needs a gradle+NDK build this class of host cannot do -- and QUIC/HTTP3 is stated as a limit rather than a route. Certificates and pinning are asserted from documented behaviour, so label a feature-scoped TLS failure as inferred until you have reproduced the failure yourself."
|
||||
},
|
||||
{
|
||||
@@ -258,8 +434,16 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/dart-aot.md", "scripts/dart_disasm.py", "scripts/dart_pprefs.py", "scripts/dart_pool_strings.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)", "docs/tool-verification/EXTENSION-dart-aot-formats.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/dart-aot.md",
|
||||
"scripts/dart_disasm.py",
|
||||
"scripts/dart_pprefs.py",
|
||||
"scripts/dart_pool_strings.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)",
|
||||
"docs/tool-verification/EXTENSION-dart-aot-formats.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Measured on a real Dart 3.6.0 build: the disassembler decoded identically to capstone (32 of 32 and 96 of 96), the caller index was re-derived independently with a symmetric difference of 0, and a specific business-logic site was located end to end. This row assumes a snapshot dump already exists -- producing one is a separate, blocked capability (see dart-aot-snapshot-dump-production)."
|
||||
},
|
||||
{
|
||||
@@ -268,9 +452,17 @@
|
||||
"status": "partial",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B12"],
|
||||
"evidence": ["references/dart-aot.md", "scripts/dart_pool_strings.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-dart-aot-formats.md (repository root, not shipped)", "tests/benchmark.md row B12 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B12"
|
||||
],
|
||||
"evidence": [
|
||||
"references/dart-aot.md",
|
||||
"scripts/dart_pool_strings.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-dart-aot-formats.md (repository root, not shipped)",
|
||||
"tests/benchmark.md row B12 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Two claims were tested and both were wrong before this row. The arm64 packed form is confirmed at the byte level (tag byte equals 0x80|(len<<1) at every literal checked, 4,980 chained entries). The previously documented armv7 form (len*2 plus UTF-16) is refuted: the 32-bit record is [header u32][byte-count u32le][UTF-8 payload], and the extractor's zero for that ABI is a format mismatch rather than an empty table. An end-to-end patched build was never repacked and installed, so no device patch is claimed. A third claim, that the engine banner gives x.y.z (stable), is also refuted on that target."
|
||||
},
|
||||
{
|
||||
@@ -280,8 +472,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/dart-aot.md", "references/coverage-and-limits.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/dart-aot.md",
|
||||
"references/coverage-and-limits.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "This skill ships no snapshot container resolver and cannot synthesize one, so the Dart AOT workflow cannot start from an APK alone. The two offset spaces are not a constant offset apart: dart_pool_strings.py reports file offsets while the pool index speaks pool offsets, and a measured run found 4,237 distinct deltas over 4,241 shared strings. Ship a pinned front end (aotopsy, pure Go, no toolchain) or build blutter (about 80 s, needs a C++ toolchain) -- and say which one, because the two report different Dart version labels for the same binary."
|
||||
},
|
||||
{
|
||||
@@ -291,8 +488,15 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/packers.md", "references/code-virtualization-and-custom-linkers.md", "references/coverage-and-limits.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)", "docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/packers.md",
|
||||
"references/code-virtualization-and-custom-linkers.md",
|
||||
"references/coverage-and-limits.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)",
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The first verification pass did not exercise the packer, code-virtualization, custom-linker, integrity-check-redirection or tamper-suicide scenarios: its target has none of them, and its unmodified build already fails to start, which removes the repack-and-regress loop those scenarios need. The packer tooling was recorded as 'not applicable to this sample'. Nothing in the record is evidence either way about a protected target -- the benchmark pass measured shape discrimination only."
|
||||
},
|
||||
{
|
||||
@@ -302,8 +506,14 @@
|
||||
"route_measured": false,
|
||||
"strength": "inferred",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/lsposed-and-modules.md", "scripts/lsposed_scaffold.py", "references/precedents/logd-broken-module-never-ran-case-3.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-lsposed.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/lsposed-and-modules.md",
|
||||
"scripts/lsposed_scaffold.py",
|
||||
"references/precedents/logd-broken-module-never-ran-case-3.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-lsposed.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Documented route, not a measured one: the generated project was built end to end with the gradle-free chain and its timings recorded, but the delivery route itself -- a module standing in for a refused repack -- has no public-target measurement. It needs a device with an active LSPosed, and confirming that a module actually ran takes more than one log surface; a logcat-only check has already produced one confidently wrong conclusion here."
|
||||
},
|
||||
{
|
||||
@@ -312,9 +522,17 @@
|
||||
"status": "blocked",
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": ["B10"],
|
||||
"evidence": ["references/kernel-and-environment-hardening.md", "scripts/kernelsu_syscall_mask.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-kernel-weapons.md (repository root, not shipped)", "tests/benchmark.md row B10 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B10"
|
||||
],
|
||||
"evidence": [
|
||||
"references/kernel-and-environment-hardening.md",
|
||||
"scripts/kernelsu_syscall_mask.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-kernel-weapons.md (repository root, not shipped)",
|
||||
"tests/benchmark.md row B10 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The generator is measured -- an 11-file userspace module skeleton plus KPM/LKM/eBPF templates, with verify reporting 0 problems -- but no kernel-side artefact was compiled or loaded: the test kernel is 4.14.186 (eBPF needs 5.10+), the host has no aarch64 cross-compiler, make or ndk-build, and there is no kernel source. Structural correction with teeth: an ordinary KernelSU module is a userspace module and cannot change a syscall return value, so a template that claims to do so is wrong regardless of the toolchain."
|
||||
},
|
||||
{
|
||||
@@ -323,9 +541,17 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B13"],
|
||||
"evidence": ["references/kernel-and-environment-hardening.md", "scripts/svc_scan.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-detection-pipeline.md section 5 (repository root, not shipped)", "tests/benchmark.md row B13 (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B13"
|
||||
],
|
||||
"evidence": [
|
||||
"references/kernel-and-environment-hardening.md",
|
||||
"scripts/svc_scan.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-detection-pipeline.md section 5 (repository root, not shipped)",
|
||||
"tests/benchmark.md row B13 (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Two independent decoders (a hand-written word scan and a capstone-based scan) returned an identical 214-site set on a device linker64, with an empty set difference, which is what makes the output usable as evidence about a library rather than about the scanner. A byte scan also matches data, so neighbour context decides whether a site is code: the hardened shell library's 21 svc sites are all data, and an ordinary module cannot act on them."
|
||||
},
|
||||
{
|
||||
@@ -335,8 +561,13 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/on-device-tooling.md", "scripts/mt_mcp_probe.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-kernel-ondevice.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/on-device-tooling.md",
|
||||
"scripts/mt_mcp_probe.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-kernel-ondevice.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The 'service is down' path is measured: the probe prints start-it-by-hand instructions and exits 2. The connected path needs MT Manager's APK MCP started by hand, so it is documented rather than measured, and the MCP surface itself was inventoried from the tool's own listing rather than exercised against a target."
|
||||
},
|
||||
{
|
||||
@@ -345,9 +576,17 @@
|
||||
"status": "ok",
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": ["B1"],
|
||||
"evidence": ["references/ad-removal.md", "references/server-config-and-updates.md", "references/membership-and-limits.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"],
|
||||
"benchmark_rows": [
|
||||
"B1"
|
||||
],
|
||||
"evidence": [
|
||||
"references/ad-removal.md",
|
||||
"references/server-config-and-updates.md",
|
||||
"references/membership-and-limits.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/TOOL-VERDICTS.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Verified on a real mid-size Flutter-AOT-shaped target and its ad chain, where the dex reader was the primary workhorse of the analysis and a repacked build's on-screen behaviour change was confirmed. A splash, popup or tab set that arrives as server-issued UI config is a routing decision, not an SDK hunt, and the deliverable is the check that stops it, not the string that names it."
|
||||
},
|
||||
{
|
||||
@@ -357,8 +596,14 @@
|
||||
"route_measured": false,
|
||||
"strength": "inferred",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/membership-and-limits.md", "references/account-gates.md", "references/handoff-boundaries.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/membership-and-limits.md",
|
||||
"references/account-gates.md",
|
||||
"references/handoff-boundaries.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The decision framework is documented and its purpose is to say plainly when a gate is not client-enforceable; no gate-shaped public target was put through it in this record, so the framework is itself inferred rather than measured. A gate the server enforces is out of scope by design (see blocked-server-side-authority), and promising an unlock before classifying the owning layer is the specific failure this row prevents."
|
||||
},
|
||||
{
|
||||
@@ -368,8 +613,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "inferred",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/updates-and-forced-upgrade.md", "references/signature-derived-keys.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/updates-and-forced-upgrade.md",
|
||||
"references/signature-derived-keys.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Documented route with no public-target measurement in this record. The shape to expect is not a crash: a rebuilt build installs, runs, and fails every signed request, so a version check and a signature-derived key must be classified before the artifact is called finished. This is an every-deliverable concern, not an optional one."
|
||||
},
|
||||
{
|
||||
@@ -379,8 +629,14 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/desensitization-and-leak-scans.md", "scripts/scan_leaks.py", "references/precedents/manual-grep-finds-what-nobody-grepped-case-5.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-desensitization.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/desensitization-and-leak-scans.md",
|
||||
"scripts/scan_leaks.py",
|
||||
"references/precedents/manual-grep-finds-what-nobody-grepped-case-5.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-desensitization.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Measured against a planted corpus (30 findings across 6 categories, every rule firing, zero false positives on the do-not-anonymize list) and against this repository (26 strong hits, all inside its own evidence file, reduced to 0 strong and 4 weak across 129 files after correction). One thing it cannot do: the scanner was never run against the pre-correction historical tree, so 'the scan would have caught it' is inferred. A strong hit is a prompt to look, not a verdict, and over-redaction is the failure on the other side."
|
||||
},
|
||||
{
|
||||
@@ -390,8 +646,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/signature-derived-keys.md", "scripts/sig_probe.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/signature-derived-keys.md",
|
||||
"scripts/sig_probe.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The offline candidate path from an APK and the authoritative live read are both documented in the reference; neither has a public-target measurement here. The failure shape is a rebuilt APK that installs, launches, and then fails every signed request, which is easy to report as a server problem and is not one."
|
||||
},
|
||||
{
|
||||
@@ -401,8 +662,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/framework-runtimes.md", "references/coverage-and-limits.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/framework-runtimes.md",
|
||||
"references/coverage-and-limits.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Unity / IL2CPP logic recovery, React Native / Hermes bytecode and Cordova internals, and every iOS or .ipa workflow are outside this skill. references/framework-runtimes.md identifies the runtime and establishes that the dex is not the battlefield; there is no verified recipe here for locating a method inside libil2cpp.so plus global-metadata.dat. Say so instead of improvising."
|
||||
},
|
||||
{
|
||||
@@ -412,8 +678,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/server-api.md", "references/handoff-boundaries.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/README.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/server-api.md",
|
||||
"references/handoff-boundaries.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/README.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Determining who owns a gate is covered; breaking an authorization the server performs is not. There is no client-side patch that reaches it, and the honest deliverable is the residual: which authority decides, and why the artifact cannot change it."
|
||||
},
|
||||
{
|
||||
@@ -423,8 +694,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/hardware-watchpoints.md", "scripts/hw_watch.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-hw-watchpoints.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/hardware-watchpoints.md",
|
||||
"scripts/hw_watch.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-hw-watchpoints.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Encoders, refusals and the emitted helper's static aarch64 build are measured on the host; arming a watchpoint on a device, the slot-capacity probe and the control run were never exercised here. The ptrace route leaves TracerPid visible by design -- the ledger in the reference is the contract."
|
||||
},
|
||||
{
|
||||
@@ -434,8 +710,13 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/ete-diff-tracing.md", "scripts/trace_diff.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-ete-diff.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/ete-diff-tracing.md",
|
||||
"scripts/trace_diff.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-ete-diff.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The reducer/differ is host-tested on synthetic flattened-dispatcher fixtures in both raw and run_probe-wrapped Stalker grammars. No producer (Stalker pair or cs_etm decode) was ever run through it end to end on a real target."
|
||||
},
|
||||
{
|
||||
@@ -445,8 +726,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/ete-diff-tracing.md", "scripts/ete_capture.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-ete-diff.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/ete-diff-tracing.md",
|
||||
"scripts/ete_capture.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-ete-diff.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Gates, recipe and capture wrapper are host-measured to the no-device boundary. No coresight capture or decode ever ran here; the reference device's kernel (4.14.186) closes the gate permanently. The --control record is the one-command check that upgrades or refutes the route on a real device."
|
||||
},
|
||||
{
|
||||
@@ -456,8 +742,13 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/kernel-event-injection.md", "scripts/kernel_instrument_scaffold.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-kernel-instrument.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/kernel-event-injection.md",
|
||||
"scripts/kernel_instrument_scaffold.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-kernel-instrument.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Generate/verify and the shared spec-refusal rules are measured on the host. No kernel-side file was ever compiled or loaded; the register-rewrite action is labelled experimental in the template itself. Same gate family as the syscall-mask scaffold: a userspace carrier arms nothing."
|
||||
},
|
||||
{
|
||||
@@ -467,8 +758,12 @@
|
||||
"route_measured": false,
|
||||
"strength": "unverified",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/hardware-sampling-and-crash.md"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-sampling-crash.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/hardware-sampling-and-crash.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-sampling-crash.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "SPE data-flow sampling and BRBE branch records are documented-and-gated: no sample or record was ever captured on any device this repository used. The crash loop's ordering (tombstone -> branch records -> symbolize) is the deliverable."
|
||||
},
|
||||
{
|
||||
@@ -478,8 +773,13 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/hardware-sampling-and-crash.md", "scripts/trace_addr2sym.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-sampling-crash.md (repository root, not shipped)"],
|
||||
"evidence": [
|
||||
"references/hardware-sampling-and-crash.md",
|
||||
"scripts/trace_addr2sym.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-sampling-crash.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The zero-dependency ELF64 walker is host-verified against a real cross-compiled aarch64 .so (symbols cross-checked with readelf) and a hand-built minimal ELF. Runtime-address inputs (hits, traces) were exercised on fixtures only, never on a live device trace."
|
||||
},
|
||||
{
|
||||
@@ -489,9 +789,14 @@
|
||||
"route_measured": true,
|
||||
"strength": "observed",
|
||||
"benchmark_rows": [],
|
||||
"evidence": ["references/ete-diff-tracing.md", "scripts/vm_handler_slice.py"],
|
||||
"evidence_not_shipped": ["docs/tool-verification/EXTENSION-sampling-crash.md (repository root, not shipped)"],
|
||||
"limits": "Dispatcher detection, segment slicing, open-tail handling and the opcode-map trajectory are host-tested on synthetic interpreter traces. Calibration facts (vm_exec shape, opcode table, boxed args, const-materialization dispatch) are read from a public IR virtualizer's source; no hardened binary was ever traced by this repository."
|
||||
"evidence": [
|
||||
"references/ete-diff-tracing.md",
|
||||
"scripts/vm_handler_slice.py"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-sampling-crash.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Dispatcher detection, segment slicing, open-tail handling, nested-dispatch diagnostics, and per-segment opcode naming are host-tested on synthetic traces and measured end to end on a real hardened fixture (fco+bcfobf+subobf+IRVM sample, ptrace instruction trace, 24.6M steps): the fla outer state machine separates as a shuttle class, round-boundary segments split the trajectory, and positional alignment against a VMP_TRACE guest log names every handler segment (all 59 in a round; counts 3232/1600/1400/200 match the guest histograms exactly; EXTENSION-sampling-crash.md SS6). Field notes: virtualized pure calls can still be LICM-hoisted by the compiling optimizer; opcode maps key by signature; frida stalker does not work in these containers (measured past the syscall layer) -- use the ptrace stepper on hosts."
|
||||
},
|
||||
{
|
||||
"capability": "dex-sql-query-layer-dexsql",
|
||||
@@ -509,6 +814,22 @@
|
||||
"docs/tool-verification/EXTENSION-dexsql.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "Measured on dexsql v0.1.0, Windows amd64 release asset only; no other version or platform was exercised. Class-definition sets were identical to rasc, droidasc and ddc on four archives (1,396 / 647 / 4 / 30,768 classes, zero differences either way), and a string cross-reference query reproduced ASC's hits site for site on one archive. Three boundaries are load-bearing: `decode` emits smali rather than Java, SQL `=` is not `findrefs` (ASC matches a substring, so `LIKE` is the equivalent), and the persistent cache is not a speed switch (under 20% on a 34.8 MB archive, a net loss on a small one). The MCP server was exercised to a tool listing and a resource-model inventory, not end to end from a client."
|
||||
},
|
||||
{
|
||||
"capability": "native-vm-recovery-procedure",
|
||||
"group": "native-dynamic",
|
||||
"status": "partial",
|
||||
"route_measured": false,
|
||||
"strength": "inferred",
|
||||
"benchmark_rows": [],
|
||||
"evidence": [
|
||||
"references/native-vm-recovery.md",
|
||||
"references/pitfalls.md"
|
||||
],
|
||||
"evidence_not_shipped": [
|
||||
"docs/tool-verification/EXTENSION-absorption-kx.md (repository root, not shipped)"
|
||||
],
|
||||
"limits": "The procedure (VM-existence signals, CFF-vs-VM dispatcher separation, relocation-recovered tables, register anchoring with triple verification, nested segment stacks, STORE-backward operand slicing with byte provenance, clean-re-VM validation) is absorbed from two public write-ups; every step is inferred here. The shipped slicer's nested-loop diagnostic is host-tested on a synthetic nested fixture. The xvmp family now has a working, verified runtime on the workbench branches, making a synthetic fixture route available end to end."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,17 @@ mechanism's truth. It means nobody has yet put a command and its output behind i
|
||||
captured, no BRBE record read, and no kernel file compiled or loaded on any device by this
|
||||
repository**. Every device-side step carries its control procedure in the reference file, and a
|
||||
control that has not run means the result is `unverified`, not working-as-intended.
|
||||
- **The public xvmp tree ships the compiler half only; this skill supplied the missing runtime.**
|
||||
Upstream never compiled its virtualizer headers (no TU includes them; on dev trees they do not
|
||||
build), and the `vm_exec` interpreter was never committed. The completed implementation — pass
|
||||
wiring (`-mllvm -irobf-irvm`, section `.irvm`; strictly flag-gated), FORMAT v3 bytecode
|
||||
(explicit widths, stable CC_ cast kinds, per-index GEP scales), a strong-symbol `vm_exec`
|
||||
interpreter, VMP.h user header and a 23-check unit suite — is merged into all four `omp`
|
||||
branches of that tree and verified end-to-end: virtualized vs native output identical at
|
||||
-O0/-O1/-O2 on every branch. (One branch also carried an ungated IRVM call in
|
||||
`Obfuscation::runOnModule` that rewrote `.irvm` functions in plain native builds; removed, so
|
||||
IRVM runs only behind `-mllvm -irobf-irvm`.) Read stderr for `[irvm] skip` lines before
|
||||
assuming a function was virtualized.
|
||||
- **The evidence record and the benchmark matrix do not ship.** Their paths appear in older
|
||||
documents as if they were openable files. They resolve only in the repository they were written in.
|
||||
|
||||
|
||||
@@ -120,6 +120,10 @@ its own strength note at the top.
|
||||
the guest-opcode trajectory, host-tested on synthetic interpreter traces and calibrated
|
||||
against a public IR virtualizer's source; no hardened binary was ever traced
|
||||
(`scripts/vm_handler_slice.py`).
|
||||
- **Native VM recovery procedure** — the full method (dispatcher-layer separation, register
|
||||
anchoring with triple verification, nested segment stacks, STORE-backward slicing, clean-re-VM
|
||||
validation) absorbed from two public write-ups; every step `inferred`, none run here
|
||||
(`native-vm-recovery.md`).
|
||||
- **Kernel-side event injection** — invisible watchpoints and register rewrites from a driver, as
|
||||
generated templates with their gates; nothing kernel-side was ever compiled or loaded here
|
||||
(`kernel-event-injection.md`, `scripts/kernel_instrument_scaffold.py`).
|
||||
|
||||
@@ -47,6 +47,7 @@ route was not measured on this repository's evidence**. Treat them as leads with
|
||||
| ETE differential tracing | two one-bit-different runs into the input-dependent block set; gates probed by script, producer unverified — `ete-diff-tracing.md` |
|
||||
| SPE/BRBE sampling and crash forensics | data flow without a known address, branch records past an untrustworthy stack, addresses→symbols — `hardware-sampling-and-crash.md` |
|
||||
| Interpreter-trace de-virtualization | handler slicing + guest-opcode trajectory, calibrated on an open IR-VM's source — `vm_handler_slice.py` |
|
||||
| Native VM recovery procedure | the absorbed method end to end: dispatcher separation, register anchoring, nested stacks, operand slicing, re-VM validation — `native-vm-recovery.md` |
|
||||
| Kernel-side event injection | watchpoints and register rewrites from a driver, as gated templates — `kernel-event-injection.md` |
|
||||
| Working from the phone itself | MT Manager edit/repack/sign and its APK MCP, LSPosed Manager, Termux — `on-device-tooling.md` |
|
||||
| Publishing without publishing the target | the leak scanner, the do-not-anonymize list, and the graded precedent library — `desensitization-and-leak-scans.md`, `scripts/scan_leaks.py`, `precedents/` |
|
||||
|
||||
@@ -148,17 +148,32 @@ order and a randomized svc immediate** (source-read): byte signatures cannot sur
|
||||
randomization, but ETE's branch stream and `svc_scan.py`'s semantic site-location do not
|
||||
care — locate the check by what it *is*, never by its bytes.
|
||||
|
||||
**xvmp (IR-level virtualizer, same repo)**: each virtualized function becomes a stub calling
|
||||
**xvmp (IR-level virtualizer, same repo — and a half-finished one)**: the *compiler side*
|
||||
is what that repository actually contains: each rewritten function becomes a stub calling
|
||||
`vm_exec(code, size, vm_args, num)` — bytecode bytes plus a boxed-argument array in the
|
||||
module; 11 one-byte opcodes with inline-const/slot operands and 19 arithmetic sub-ops;
|
||||
pointer constants materialize through a generated const/call dispatch at first use. For the
|
||||
tracing routes this means: the interpreter is one dominant loop (the §4 handler reading),
|
||||
the **guest inputs are observable at the `vm_args` array before interpretation starts** (a
|
||||
watchpoint there sees arguments and any key material, plaintext, whatever the bytecode
|
||||
carries), and each constant's bits pass through the materialization dispatch as a returned
|
||||
i64 — a second, cheap observation point. `scripts/vm_handler_slice.py` consumes the trace
|
||||
side and documents the handler->opcode map you can build against a *known* table like this
|
||||
one.
|
||||
pointer constants materialize through a generated const/call dispatch at first use. The
|
||||
**runtime interpreter is not in the repository**: `getOrCreateVMExec` emits only a weak
|
||||
definition-less stub (`CreateStubIfMissing`), exception paths are commented out, and
|
||||
unsupported IR is skipped with an `errs()` line and no failure — so a binary built from that
|
||||
tree as-is links fine and falls into the empty stub at runtime. (Observed by a reader of the
|
||||
tree, 2026-10; the companion write-up describes a *different, earlier prototype — 32-bit
|
||||
fixed-width ISA, thunk bridges, an LVMP container — so trust the code, not the article.)
|
||||
Consequences for the tracing routes, **conditional on a real `vm_exec` being supplied as a
|
||||
strong symbol**: the interpreter is one dominant loop (the §4 handler reading), the **guest
|
||||
inputs are observable at the `vm_args` array before interpretation starts** (a watchpoint
|
||||
there sees the *guest arguments* — key material or plaintext only when they arrive through
|
||||
those arguments; constants carried in the bytecode are not among them), and
|
||||
each constant's bits pass through the materialization dispatch as a returned i64 — a second,
|
||||
cheap observation point. This repository supplies that missing half: pass wiring, FORMAT v3 bytecode (explicit
|
||||
load/store widths, stable cast kinds, per-index GEP byte scales), a strong-symbol `vm_exec`
|
||||
interpreter and a `VMP.h` user header — merged into all four `omp` branches of that tree
|
||||
and verified end-to-end (virtualized vs native identical at -O0/-O1/-O2). When building a
|
||||
fixture from it: link `runtime/vm_exec.c`, set `VMP_TRACE=1` for a per-step interpreter
|
||||
trace, and **read stderr for `[irvm] skip` lines** — a silently skipped function still
|
||||
exists natively and will quietly skew a trace pair.
|
||||
`scripts/vm_handler_slice.py` consumes the trace side and documents the handler->opcode map
|
||||
you can build against a *known* table like this one.
|
||||
|
||||
## 6. Failure modes
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ next** — and points at the one section that says "the full record is elsewhere
|
||||
| ETE hardware tracing | Gates and capture wrapper host-measured to the no-device boundary; no coresight capture or decode ever ran here, and `--control` exists to say so on a real device in one command | blocked | unverified | `references/ete-diff-tracing.md`, `scripts/ete_capture.py` |
|
||||
| SPE/BRBE sampling and crash forensics | The address→symbol loop is host-verified on a real cross-compiled `.so`; SPE/BRBE mechanisms are documented-and-gated, never captured here | partial | unverified | `references/hardware-sampling-and-crash.md`, `scripts/trace_addr2sym.py` |
|
||||
| Interpreter-trace de-virtualization | Dispatcher slicing, signature clustering and the opcode trajectory measured on synthetic interpreter traces; the calibration facts come from a public IR-VM's source, and no hardened binary was traced | partial | observed | `references/ete-diff-tracing.md` §5, `scripts/vm_handler_slice.py` |
|
||||
| Native VM recovery procedure | The full method is absorbed from two public write-ups (dispatcher-layer separation, register anchoring with triple verification, nested segment stacks, STORE-backward slicing, clean-re-VM validation); no step was executed by this repository | partial | inferred | `references/native-vm-recovery.md` |
|
||||
| Kernel-side instrumentation/injection | The scaffold's generate/verify is measured and every kernel file ships unbuilt with its gate; the register-rewrite action is explicitly experimental and never ran anywhere | blocked | unverified | `references/kernel-event-injection.md`, `scripts/kernel_instrument_scaffold.py` |
|
||||
| `svc` site scanning | Two independent decoders agreed on an identical 214-site set; a byte scan also matches data, so neighbour context decides | ok | observed | `references/kernel-and-environment-hardening.md`, `scripts/svc_scan.py` |
|
||||
| On-device tooling (MT Manager MCP) | The service-down path is measured; the connected path needs the service started by hand | partial | observed | `references/on-device-tooling.md`, `scripts/mt_mcp_probe.py` |
|
||||
|
||||
@@ -92,8 +92,10 @@ before distrusting a symbol.
|
||||
recover the real path into a flattened function that crashes — the state machine's taken
|
||||
states, read from hardware, no unwinder.
|
||||
- **VMP/xvmp-class**: SPE answers "which handler sequence touches the argument slots" when
|
||||
the bytecode is opaque (`vm_exec`'s boxed arguments live in a plain array —
|
||||
`scripts/vm_handler_slice.py`'s header records where); a crash inside the interpreter
|
||||
the bytecode is opaque (a generator-produced `vm_exec`'s boxed arguments live in a plain
|
||||
array — `scripts/vm_handler_slice.py`'s header records where; the public xvmp tree ships
|
||||
the compiler side only, so this presumes a working interpreter linked in — see
|
||||
`references/ete-diff-tracing.md` §5); a crash inside the interpreter
|
||||
keeps its branch records, so the fatal guest opcode is identifiable from the last
|
||||
handler segment.
|
||||
- **Anti-debug/anti-hook inserts**: a deliberate `svc`-based check (the Hikari-family
|
||||
|
||||
@@ -147,6 +147,15 @@ large structs need either the one field that matters or several slots.
|
||||
right, not the encodings.
|
||||
- **Root, and only root.** `ptrace` of an app process from the shell needs `su`; the helper
|
||||
checks and says so.
|
||||
- **Go targets: match the read to the hit function's ABI (pitfalls.md P43).** Modern Go
|
||||
(`ABIInternal`: 1.17+ on amd64, 1.18+ on arm64 — most gomobile builds today) passes the
|
||||
first arguments in registers, so they are already in the dump. Only **stack-ABI** functions
|
||||
(pre-register-ABI builds, hand-written assembly `ABI0`) keep arguments on the **stack**;
|
||||
then a hit on the right function still dumps registers that do not contain the buffer
|
||||
pointer you came for. The fix is not another
|
||||
instruction — it is reading `[sp .. sp+0x100]` at the stop (the helper prints `sp` on every
|
||||
`HIT` for exactly this reason), or moving up to a boundary that follows the AArch64 PCS
|
||||
(a `RegisterNative` JNI entry: strings arrive in `x2..x5`).
|
||||
- **Detach cleanly or leave the target traced forever**: slots cleared, every tid detached.
|
||||
The helper does this on every exit path including the caps; a `kill -9` of the helper
|
||||
leaves `PTRACE_O_EXITKILL` to do the damage — do not do that.
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# Native VM recovery — from "this looks virtualized" to an IR you can read
|
||||
|
||||
Load this when a native function is an **interpreter** — a VM you cannot read statically, an
|
||||
xvmp-class IR virtualizer, or any private-bytecode loop — and the question is how to get from
|
||||
the handler soup to the guest program. This file owns the recovery procedure; the **capture**
|
||||
half lives in `references/ete-diff-tracing.md`, the **slicing** tool in
|
||||
`scripts/vm_handler_slice.py`, and the OLLVM-only case in
|
||||
`references/native-dbi-and-deobfuscation.md`.
|
||||
|
||||
**Claim strength.** Every method step here is absorbed from two public reverse-engineering
|
||||
write-ups (Kanxue, 2026-08/09, titles and authors anonymized in this record; one is a full
|
||||
arm64 custom-VM recovery, the other a VMP interpreter recovery with trace-driven IR lifting).
|
||||
The write-ups are `observed` by their authors; this repository has **never run any of these
|
||||
steps on a real VM target** — everything below is `inferred` from those write-ups, cross-read
|
||||
against the open xvmp source facts in `references/ete-diff-tracing.md` §5. The pipeline's
|
||||
host-tested pieces (trace parsing, slicing, diffing) carry their own labels where named.
|
||||
|
||||
## 1. Deciding it is a VM at all
|
||||
|
||||
None of the signals below proves a VM alone — a big switch, a state machine, a function-pointer
|
||||
table and an event dispatcher all look similar. The judgement is **several appearing together**:
|
||||
|
||||
1. one large native function, called often, taking a chunk of external data;
|
||||
2. a loop that reads a cursor from one buffer, dispatches on the value read, and writes the
|
||||
cursor back — the `while(running){ opcode=code[pc]; execute(opcode); pc=next_pc; }` shape;
|
||||
3. a **virtual register file**: many paths access `base + index*8` (or *4, *2) where *index*
|
||||
comes from small integers inside that data;
|
||||
4. semantic blocks that end by advancing the cursor by **different fixed lengths** (pc+=4,
|
||||
pc+=5, pc+=7) all written back to the same context slot — a variable-length ISA;
|
||||
5. (generator-produced xvmp shape) stubs that call `vm_exec(code, size, args, num)` with the
|
||||
bytecode and a boxed-argument array as module data — noting the public tree ships the
|
||||
compiler side only (a weak empty stub at runtime; `ete-diff-tracing.md` §5), so this
|
||||
signal means "a virtualizer of that family generated it", never "that repo's interpreter
|
||||
is present".
|
||||
|
||||
## 2. Two dispatchers stacked — the first wrong turn
|
||||
|
||||
When the interpreter itself was compiled with OLLVM flattening, you see **two state machines
|
||||
nested**: CFF state → CFF dispatcher → native block → VM logic → VM state → VM dispatcher →
|
||||
handler. Both have table+index+indirect-branch+state-update, and `LDR Xn,[table,index]; BR Xn`
|
||||
proves nothing about which layer it is. Judge by behaviour, not shape:
|
||||
|
||||
| Question | CFF dispatcher | VM dispatcher |
|
||||
|---|---|---|
|
||||
| where does the index come from? | arithmetic on an internal state value (add/xor/csel chains) | a byte/word read from the **bytecode buffer** |
|
||||
| do targets read bytecode / VM registers? | no — pure flattening blocks | yes — that is the fetch and the register file |
|
||||
| do targets update the VM PC and return to fetch? | no — they compute the next CFF state | yes — `pc += n`, then back to fetch |
|
||||
|
||||
Mislabelling the CFF table as `VM_HANDLER_TABLE` poisons everything downstream: every
|
||||
opcode→handler pair after it is wrong. The write-up's first correction was exactly this.
|
||||
|
||||
## 3. Do not fully deflatten first — chase the VM data flow
|
||||
|
||||
Full native-CFG recovery of a flattened interpreter is expensive and unnecessary. What the VM
|
||||
analysis actually needs is six things out of the flatten: **code_base, PC, opcode, descriptor,
|
||||
register file, FLAGS, next_pc**. The method: walk CFF blocks, keep the ones that touch those
|
||||
(the fetch pattern `insn = code_base + pc; opcode = insn[0]; desc = insn[1]` is unmistakable
|
||||
once named), mark their inputs/outputs, and ignore pure flattening blocks.
|
||||
|
||||
## 4. A dispatch table of zeros is a relocation, not encryption
|
||||
|
||||
Dispatcher table entries that read as `0,0,0,...` are usually **`R_AARCH64_RELATIVE`
|
||||
relocations** the loader fills at startup — not runtime decryption, and no dynamic trace is
|
||||
needed: parse the ELF, enumerate relocations whose offset falls in the table, and
|
||||
`targets[i] = image_base + reloc.addend`. That statically recovers the CFF state→block mapping
|
||||
in bulk. (`scripts/trace_addr2sym.py` reads PT_LOADs; a relocation walker would be its natural
|
||||
extension — currently **not shipped**.)
|
||||
|
||||
## 5. Proving which field is the VM PC
|
||||
|
||||
Three pieces of evidence, all required:
|
||||
|
||||
1. it **participates in the instruction-address computation** (`insn = code_base + value`);
|
||||
2. handlers **update it deterministically** by the instruction length they implement;
|
||||
3. the updated value is **written back to the same context slot** every dispatch — one
|
||||
`write IP` slot, however many addressing forms reach it (one write-up's VM had handlers
|
||||
writing `[x23]` and `[x19,#0xc]` — the *same slot* through two addresses; a seed that
|
||||
matched only one form missed a whole handler class).
|
||||
|
||||
## 6. Register anchoring, then triple verification
|
||||
|
||||
Anchor the interpreter's registers from one hand-read handler: which native register holds the
|
||||
ctx base, the bytecode pointer, the IP, the handler table, each register-file base. Then verify
|
||||
each anchor three ways before building anything on it:
|
||||
|
||||
1. **the initializer is the smoking gun** — the ctx-init function memsets the whole register
|
||||
file and pre-seeds known slots (one write-up: `memset(ctx+0x6070, 0, 0x381)`, and a
|
||||
register slot pre-set to `ctx+0x6030`, matching the trace exactly);
|
||||
2. **trace statistics** — hundreds of thousands of `[base, idx, lsl #3]` accesses with idx
|
||||
strictly inside [0,31] sizes the file better than any guess;
|
||||
3. **a uniform handler template** — all handlers take the same contract (`a1` = current VM
|
||||
instruction; operands at fixed offsets `[x8+8]/[x8+9]/[x8+0xA]`; register access always
|
||||
`*(x24 + 8*idx)`). One violating handler is a discovery, not noise (see §5).
|
||||
|
||||
## 7. Nested VMs: segments must be a stack
|
||||
|
||||
When VM entry can be **called from inside VM code**, a flat "current segment" accumulator
|
||||
corrupts the trace reading: entering the inner VM discards the partially-collected outer
|
||||
segment, and returning resumes into a false segment. In the write-up, 45 of 46 entries were
|
||||
nested, each discarding 57–1997 instructions. The fix is a **segment stack**: push the
|
||||
outer segment on entry, pop and continue on exit. `scripts/vm_handler_slice.py` slices a
|
||||
single dispatcher level and now prints a diagnostic when a segment looks like a nested
|
||||
dispatch loop (a block repeating inside one segment) — treat that as "this trace has more than
|
||||
one level; slice per level", not as a final reading.
|
||||
|
||||
## 8. Recovering operands: slice backwards from the STOREs
|
||||
|
||||
Two lines per handler, and both start at the **stores**, not the head: forward from a handler
|
||||
head diverges (one instruction writes several slots, each with its own history); backward from
|
||||
the store **converges** (each location has one in-block definition — "last write before use",
|
||||
no dataflow solver needed).
|
||||
|
||||
- **index line** = the STORE's address operand (which register slot);
|
||||
- **value line** = the STORE's stored operand (what was computed);
|
||||
- seed on **all** stores — writes to the register file *and* writes to memory. Seeding only
|
||||
register-file stores silently drops every pure-store handler class (one write-up lost 15 of
|
||||
266 handler entries this way; loads need no seed, the backward walk brings them in).
|
||||
|
||||
The decoder noise problem: one SLEIGH-style instruction expands into a wall of
|
||||
shift/mask/or operators (the bytecode **decoder**, not VM semantics). The folding judgement
|
||||
that worked is a **byte provenance** track — for each output byte, where did it come from:
|
||||
bytecode space, ctx space, a pointer dereference, or a constant. A subtree folds into "one
|
||||
read" only if: exactly one memory source; it lands on byte lane 0; and the value matches.
|
||||
Value alone is not a criterion (two lanes can hold the same byte); provenance alone is not
|
||||
(one source can be *shifted* into place, which is assembly, not extraction).
|
||||
|
||||
## 9. Proving semantics: run it, then distrust it
|
||||
|
||||
Static confidence is not enough for an interpreter — a 90%-right opcode poisons every later
|
||||
step, and the failure mode is quiet: one write-up's first emulator wrote a 32-bit register as
|
||||
`reg = (uint32_t)value;` when the real semantics were **write low 32 bits, keep high 32**;
|
||||
only the high half differed, and later call arguments changed. The recovery loop:
|
||||
|
||||
1. implement a **clean re-VM** (your own interpreter for the recovered ISA) — do not instrument
|
||||
the original;
|
||||
2. run **real captured bytecode** through it and compare against traced ground truth
|
||||
(registers/memory per step — the trace's read/write columns are the oracle);
|
||||
3. one mismatch = one wrong semantic or one wrong instruction **boundary** (a descriptor
|
||||
misread shifts every later instruction), and both look identical until you re-derive the
|
||||
length table.
|
||||
|
||||
HOSTCALL is the second ABI you still owe: service ids, argument sourcing, return placement,
|
||||
which parameters are pointers — until it is recovered, the IR stalls at `hostcall_xxx(a,b,c)`.
|
||||
|
||||
## 10. The IR shape that made patterns visible
|
||||
|
||||
One VM instruction per line, instance values inlined: `VREG[0x14] = sext(VREG[0xb] >> (VREG[0x14] & 0x1f))`,
|
||||
`*(VREG[0x1]+0x498) = VREG[0x3] | 0x3` (the trailing `| ip` is the PC write-back). With that
|
||||
rendering, **regularities surface by themselves** — consecutive stores with offsets stepping
|
||||
by −8 and source slots descending turned out to be one struct-field bulk copy; and once the
|
||||
pre-IP and post-IP are both recorded (`0x2 -> 0x3`), the 200k-line IR sequence *is* the guest
|
||||
execution trace: +1 is fallthrough, jumps are branches, back-edges are loops — the guest CFG
|
||||
falls out. That is the end state this file walks toward: the guest program as data.
|
||||
|
||||
## 11. Where this repository's tools plug in
|
||||
|
||||
| Step | Tool |
|
||||
|---|---|
|
||||
| capture the interpreter loop (device with ETE) | `scripts/ete_capture.py` + `references/ete-diff-tracing.md` |
|
||||
| slice/cluster the loop, opcode trajectory (known opcode table, xvmp) | `scripts/vm_handler_slice.py` |
|
||||
| two-input divergence (which handler paths the input reaches) | `scripts/trace_diff.py` |
|
||||
| watch the ctx / bytecode / boxed args without touching code | `scripts/hw_watch.py` + `references/hardware-watchpoints.md` |
|
||||
| data flow without a known address (which code touches the key buffer) | SPE, `references/hardware-sampling-and-crash.md` |
|
||||
| PC → symbol/static offset, handler tables | `scripts/trace_addr2sym.py` |
|
||||
| static state→block mapping for the outer CFF (MOV/MOVK const propagation, CMP on the state register, predecessor walk past blocks with no CMP) | the OLLVM static route in `references/native-dbi-and-deobfuscation.md` §5, extended by the absorbed write-up's technique |
|
||||
| relocation table recovery (§4) | not shipped — `elf_plt.py`/`trace_addr2sym.py` are the nearest readers |
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] VM existence judged from §1's signals *together*, not from one look-alike switch
|
||||
- [ ] dispatcher layer decided by §2's behavioural table before naming any table
|
||||
- [ ] VM PC proven by all three §5 evidences; write-slot matched by address, not one register
|
||||
- [ ] register anchors triple-verified (initializer / trace statistics / handler template)
|
||||
- [ ] segments stacked if entry can nest (§7); slicer's nested-loop diagnostic honoured
|
||||
- [ ] operand slicing seeded on all STOREs; byte provenance (not value) decides folding
|
||||
- [ ] clean re-VM runs real bytecode against trace ground truth before any IR is trusted
|
||||
- [ ] labels: write-up methods `inferred`, shipped tools `observed` (host), no VM target run here
|
||||
@@ -1287,3 +1287,121 @@ as P25/P26/P33, and it removes viable patch sites for free.
|
||||
- **Cross-check one known instruction** against a disassembler before trusting offsets you derived.
|
||||
- Build the width table from the format groups in `references/byte-level-patching.md` rather than
|
||||
from memory, and keep the width logic in one place so a fix applies everywhere.
|
||||
|
||||
## P43. The hardware watchpoint fires, and the argument you wanted is not in x0-x7
|
||||
|
||||
**Symptom**
|
||||
A breakpoint/watchpoint lands exactly on the function you wanted — the write path, the crypto
|
||||
entry — but the register dump (`x0..x7` on arm64) does not contain the buffer pointer or the
|
||||
length you came for. The hit is real; the data is not there.
|
||||
|
||||
**Root cause**
|
||||
The target is a **Go binary using the stack-based ABI** (pre-1.17/1.18 Go builds, and
|
||||
hand-written assembly `ABI0` functions in any Go build — gomobile included): the stack-based
|
||||
Go ABI passes arguments on the stack, not in the AArch64 argument registers. The plaintext
|
||||
`ptr/len/cap` you are hunting sits in the caller's frame at `[sp + n]` — and a tracer that only
|
||||
saves `x0-x7`, or a kernel-side breakpoint handler that only dumps register-pointed memory,
|
||||
structurally cannot see it. This is not "wrong instruction, try another one": at the call
|
||||
boundary the values may transit through a register only momentarily, or be spilled by `STR`
|
||||
right before the call.
|
||||
|
||||
**Why it is hard to see**
|
||||
The hit *proves* you are on the right function, so the instinct is to walk neighbouring
|
||||
instructions looking for "the right one". On a stack ABI there is no such instruction — the
|
||||
data is in the frame, and every register you dump will keep being the wrong register.
|
||||
|
||||
**The rule**
|
||||
When the module is Go (`go.Seq`/`gomobile` symbols give it away), **establish the hit
|
||||
function's ABI first** — it is a per-function property, not a per-binary one. Modern Go
|
||||
compiles to the register ABI (`ABIInternal`, Go 1.17+ on amd64, 1.18+ on arm64 — the arm64
|
||||
internal ABI passes integer/pointer arguments in `x0..` and may reach up to `x15`): those
|
||||
arguments are in the argument registers, so dump those at the stop, and the stack holds at
|
||||
most spilled copies. A **stack-ABI** function — a pre-register-ABI build, or a hand-written
|
||||
assembly (`ABI0`) function in any build — keeps them in the caller's frame: the hit already
|
||||
carries `sp`, so pull `[sp .. sp+0x100]` from `/proc/<pid>/mem` at the stop. When unsure,
|
||||
disassemble the entry: a prologue consuming `x0`/`x1` is register ABI; argument loads from
|
||||
`[sp+…]` offsets are stack. `scripts/hw_watch.py` prints `sp` on every `HIT` line for exactly
|
||||
this reason — a tracer that cannot read the stack cannot trace a stack-ABI Go function.
|
||||
And prefer a **layer boundary** where the values
|
||||
*are* in registers: a JNI entry point follows the AArch64 PCS, so a `RegisterNative`-registered
|
||||
Java method receives its strings in `x2..x5` — one layer up from the Go internals, and the
|
||||
plaintext is already marshalled. *(Absorbed from a public write-up, 2026-09.)*
|
||||
|
||||
## P44. One observer's crash silently disarms every observer installed after it
|
||||
|
||||
**Symptom**
|
||||
A multi-probe observer script (hook several functions, log, never modify) runs clean, the target
|
||||
dies at t≈2s as expected — and not one backtrace is captured. Later, the same script reordered
|
||||
captures the suicide site on the first try.
|
||||
|
||||
**Root cause**
|
||||
The first probe used an API the runtime version had removed (`Module.findExportByName` in Frida
|
||||
17 — gone, replaced by `Module.getGlobalExportByName`), which threw at install time. The
|
||||
suicide observer was *later in the same script*, so it never installed. The target's death and
|
||||
the observer's absence looked identical from the outside: "died, no evidence".
|
||||
|
||||
**Why it is hard to see**
|
||||
The script reports what it installed — and it installed nothing past the crash, silently. A
|
||||
missing observer produces the same empty log as "the check runs before anything we could hook".
|
||||
|
||||
**The rule**
|
||||
Every observer installs inside its own `try/catch`, reports `INSTALLED`/`FAILED` per hook, and
|
||||
the most important observer goes **first**. An empty log after a death is a probe-plumbing
|
||||
result until the install report says otherwise. *(Absorbed from a public write-up, 2026-09.)*
|
||||
|
||||
## P45. Scrubbing `/proc/self/maps` lines breaks the app's own loader
|
||||
|
||||
**Symptom**
|
||||
The detection is identified — it reads `/proc/self/maps` and `strcmp`s each line against a
|
||||
known-agent signature. You hook `read`/`fgets` and strip the offending lines. The app now dies
|
||||
*earlier*, in the linker or the packer's loader, in places that never failed before.
|
||||
|
||||
**Root cause**
|
||||
The hardened loader itself **legitimately reads `/proc/self/maps`** to find its own segments and
|
||||
perform relocations — the same API, the same lines, a different consumer. A blanket rewrite
|
||||
damages both readers, and the innocent one dies first.
|
||||
|
||||
**Why it is hard to see**
|
||||
Both readers go through libc, so the hook cannot tell them apart by call site without more
|
||||
context — and the new crash points at *your* change, in a component the detection analysis never
|
||||
mentioned.
|
||||
|
||||
**The rule**
|
||||
Never spoof a shared file's content from a shared call path. The alternatives, in cost order:
|
||||
change what the *signature* matches (rename/remap the agent so the line never existed — the
|
||||
frida-agent `memfd:` mapping is exactly such a line), or filter by **reader** (only rewrite for
|
||||
the detecting thread), or leave the file alone and defeat the *decision* the check makes
|
||||
(`references/kernel-event-injection.md`). And before blaming the target for a death that
|
||||
followed your hook, rerun the same probe **observer-only** — if the death persists with zero
|
||||
modifications, the check is real; if it vanishes, P45 or its cousins is the cause. *(Absorbed
|
||||
from a public write-up, 2026-09.)*
|
||||
|
||||
## P46. Hooking the TLS library shows nothing, because the login was never in Java
|
||||
|
||||
**Symptom**
|
||||
An SSL-key-hooking capture tool (eCapture-class) sees no traffic at all for the feature you
|
||||
care about. You widen the hooks, re-key, restart — still nothing.
|
||||
|
||||
**Root cause**
|
||||
Three stacked causes, each pointing at the next layer: (1) the feature's networking runs in
|
||||
**statically linked Go `crypto/tls`**, not the system SSL library an eCapture-style tool hooks;
|
||||
(2) the business logic is not in the dex at all — the unpacked dex contains only the packer's
|
||||
loader classes, and the real code is an encrypted JS bundle decrypted at runtime; (3) the
|
||||
decisive observation point was neither TLS nor Java but a **`RegisterNative`-registered JNI
|
||||
method** (`resourceApi(url, method, body, header, flag)`) where every request's plaintext
|
||||
passes in ordinary JNI strings.
|
||||
|
||||
**Why it is hard to see**
|
||||
Each dead end looks like a tooling failure rather than a layer identification: "eCapture is
|
||||
broken", "the hook is wrong". They were working — at the wrong layer.
|
||||
|
||||
**The rule**
|
||||
A capture tool that hooks *library X* answers only traffic through library X; when it returns
|
||||
nothing, identify the layer before widening hooks: check which native library owns the feature
|
||||
(a `RegisterNative` listener over a cold start lists every JNI method with its offset — one run,
|
||||
full inventory, and the offsets are file offsets you can jump to), check the unpacked dex for
|
||||
business classes before assuming the logic is Java, and prefer the **highest boundary where the
|
||||
data is still plaintext** — a JNI boundary beats a TLS interior every time. Each dead road names
|
||||
the next layer: TLS-blind says "own crypto", dex-empty says "JS/native bundle", and the
|
||||
RegisterNative inventory turns "somewhere in libfoo" into a function address. *(Absorbed from a
|
||||
public write-up, 2026-09.)*
|
||||
|
||||
@@ -71,6 +71,8 @@ reference file is not named here, or when a script is not named here.
|
||||
| A crash's backtrace cannot be trusted — frames in wrong modules or anonymous regions, flattened/virtualized code, or a smashed stack | `hardware-sampling-and-crash.md` — BRBE branch records / ETE tail as the real control flow, then `scripts/trace_addr2sym.py` to symbolize every PC |
|
||||
| You hold a list of **runtime addresses** (hits, trace PCs, divergence points) and need symbols / static offsets, not hex | `hardware-sampling-and-crash.md` §3 The crash loop — `scripts/trace_addr2sym.py` with its vaddr/file-offset rules |
|
||||
| A trace of a virtualized function is just an **interpreter loop repeating** and reads like noise | `scripts/vm_handler_slice.py` — slice at dispatcher entries into handler segments, cluster signatures, recover the guest-opcode trajectory (xvmp-calibrated) |
|
||||
| You cannot tell whether the `table[index]; br` you are staring at is the **VM dispatcher or the OLLVM dispatcher** — or the VM has no readable static shape at all | `references/native-vm-recovery.md` — the behavioural table that separates the two stacked layers, the VM-existence signals, register anchoring with triple verification, nested-VM segment stacks, and STORE-backward operand slicing |
|
||||
| The trace segments themselves look corrupted — handler heads missing, tails glued into wrong segments | `references/native-vm-recovery.md` §7 Nested VMs — a flat segment accumulator discards the outer level; nested slices need a segment stack (`vm_handler_slice.py` flags the symptom) |
|
||||
| You need to know **where two runs of the same operation diverge** (password/license/key), not just what executed | `ete-diff-tracing.md` — ETE hardware-trace gates plus the differential method; `scripts/trace_diff.py` also diffs Stalker logs |
|
||||
| A generated KPM/LKM template is in front of you, or the kernel route feels like the plan | `kernel-event-injection.md` — the primitive map, the composition recipes, and the gates every kernel-side file must pass before it is a weapon |
|
||||
| The traffic is protobuf/gRPC/QUIC, or a proxy sees TLS but requests still fail on a Flutter app | `references/protocol-reverse.md` — schema-less protobuf, frame capture, and native-side pinning |
|
||||
@@ -132,6 +134,7 @@ _54 row(s) below the header._
|
||||
| `references/native-dbi-and-deobfuscation.md` | A native function is an OLLVM state machine, or you need instruction-level execution evidence: Stalker traces, the trace-to-CFG route, the Stalker/QBDI/emulation decision, and the **measured** zero-event and crash boundaries |
|
||||
| `references/hardware-watchpoints.md` | You must observe who reads/writes one memory location with **no code modification** — hardware watchpoints/breakpoints via the arm64 debug registers (ptrace route, its honest `TracerPid` ledger, the alignment rules, the control run you owe any hit) |
|
||||
| `references/hardware-sampling-and-crash.md` | The question is **data flow without a known address** (SPE: PC + data address per sample), a **crash whose stack is not trustworthy** (BRBE/ETE-tail branch records), or a list of runtime addresses to symbolize (`trace_addr2sym.py`, PIE vaddr/file-offset rules) |
|
||||
| `references/native-vm-recovery.md` | A native function is an **interpreter**: VM-existence signals, separating the 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 from two public write-ups |
|
||||
| `references/ete-diff-tracing.md` | You need **where two runs diverge** (password/license/key) with hardware tracing: the ETE/TRBE gates, per-thread `cs_etm` capture, and the differential method that turns two traces into the input-dependent block set |
|
||||
| `references/kernel-event-injection.md` | The observation or change must come from a **driver** — invisible watchpoints, register rewrites at a trap, cross-memory writes, zero ptrace: the primitive map, the composition recipes, and the gates the scaffold's templates must pass |
|
||||
| `references/protocol-reverse.md` | The traffic is protobuf without a schema, gRPC, or QUIC/HTTP3; or a proxy sees TLS while the app still fails — schema recovery, frame capture, and native-side certificate pinning (Flutter/BoringSSL) with its boundaries |
|
||||
@@ -200,7 +203,7 @@ _43 row(s) below the header._
|
||||
| `scripts/ete_capture.py` | **CoreSight ETE capture with gates first**: the five gates (kernel era, coresight sysfs, cs_etm PMU, perf-with-CORESIGHT, permission), the perf/OpenCSD build recipe, and the per-thread `cs_etm//u` record wrapper with the `--control` validation run |
|
||||
| `scripts/kernel_instrument_scaffold.py` | Generate the **kernel-side instrumentation scaffold**: KPM/LKM watchpoint+register-rewrite templates plus an honest userspace carrier, each with its gate and a `NOT COMPILED AND NOT LOADED` banner that `verify` enforces |
|
||||
| `scripts/trace_addr2sym.py` | **Runtime addresses -> symbols + static offsets**, zero-dependency ELF64: PT_LOAD vaddr/file-off conversion, `.dynsym`/`.symtab`, stripped fallback; accepts hw_watch hits, stalker/trace `mod+off` keys, or raw address lists -- host-verified against a real cross-compiled aarch64 `.so` |
|
||||
| `scripts/vm_handler_slice.py` | **De-virtualize an interpreter trace**: dispatcher detection (top + predecessor diversity), segment slicing at dispatch entries, signature clustering, and the guest-**opcode trajectory** via an optional handler->opcode map -- calibrated against the open xvmp IR virtualizer (`vm_exec`, slot machine, 11 opcodes) |
|
||||
| `scripts/vm_handler_slice.py` | **De-virtualize an interpreter trace**: dispatcher detection (top + predecessor diversity), segment slicing at dispatch entries, signature clustering, and the guest-**opcode trajectory** via an optional handler->opcode map — calibrated against the open xvmp IR virtualizer, with a diagnostic for **nested** dispatch loops (slice per level) |
|
||||
| `scripts/capabilities.py` | The capability registry `doctor.py` reads: per capability the requires-closure, what is present, what is missing, an install hint and an estimated cost — so "can this machine do X" is computed rather than guessed |
|
||||
| `scripts/device_shell.py` | Shared device-side command construction: POSIX single-quote escaping for `su -c`, argv arrays rather than string concatenation, and Android identifier validation (`--pkg`, components) so a malformed value is rejected here instead of being parsed by the device shell |
|
||||
| `scripts/rasc_build.py` | Build and verify **rasc**, the Rust re-implementation of ASC: `--check` what is present, `--build` clone + cargo (no release asset exists), `--verify <apk>` compares its class-definition set against the Python `droidasc` and fails on any difference |
|
||||
|
||||
@@ -16,12 +16,17 @@ dispatcher entries** turns it back into a program:
|
||||
With a handler->opcode map (see below), the trajectory becomes the guest program's
|
||||
opcode sequence: the disassembly of the VM you cannot read statically.
|
||||
|
||||
Calibration source -- an open IR-level VM, in the shape this tool expects
|
||||
------------------------------------------------------------------------
|
||||
This tool's segment model is calibrated against a public LLVM-IR virtualizer
|
||||
(dpkg123/ollvm-aosp-toolchain, commits 63fec41f and 9b7cf75e, 2026-09 -- the `xvmp`
|
||||
generic pass; source facts below are read from that patch, `inferred` for runtime
|
||||
behaviour, `observed` for the source text):
|
||||
Calibration source -- an open IR-level VM's COMPILER SIDE, in the shape this tool expects
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
This tool's segment model is calibrated against the public LLVM-IR virtualizer's
|
||||
*generator* (dpkg123/ollvm-aosp-toolchain, commits 63fec41f and 9b7cf75e, 2026-09 -- the
|
||||
`xvmp` pass; source facts below are read from that patch, `inferred` for runtime
|
||||
behaviour, `observed` for the source text). **Upstream shipped the compiler half only
|
||||
(no `vm_exec` interpreter; a weak empty stub, unsupported IR skipped with an `errs()`
|
||||
line)** -- this repository completed the missing runtime (pass wiring, FORMAT v3 bytecode,
|
||||
strong-symbol interpreter) as `irvm-*` branches on the workbench clone, verified
|
||||
end-to-end at -O0/-O1/-O2. Runtime facts below describe that working interpreter's
|
||||
contract. What the generator guarantees:
|
||||
|
||||
- each virtualized function becomes a stub calling ``vm_exec(code, size, vm_args, num)``:
|
||||
the bytecode array plus a boxed-argument array live in the module, so the *inputs*
|
||||
@@ -65,9 +70,15 @@ Inputs and behaviour
|
||||
VM at all (no dominant block; native-dbi-and-deobfuscation.md's "trace of a VM" note).
|
||||
|
||||
Measured status: host-tested on synthetic interpreter traces in the calibrated shape
|
||||
(tests/unit/test_vm_handler_slice.py). No real xvmp-hardened binary was ever traced by
|
||||
this repository -- the calibration facts are from the public source, and the first run
|
||||
against a real sample is a measurement, not a confirmation.
|
||||
(tests/unit/test_vm_handler_slice.py) and end to end on a real hardened sample — an
|
||||
fco+bcfobf+subobf+IRVM build of the completed toolchain traced with a ptrace
|
||||
single-stepper (EXTENSION-sampling-crash.md SS6): 95,184 segments / 18 unique
|
||||
signatures, every handler segment of a round named by positional alignment against
|
||||
the interpreter's own step log, zero unmapped. Two field notes from that run: with an
|
||||
instruction-per-line stream `--dispatcher auto` correctly refuses (the dispatcher
|
||||
block's instructions tie in count) -- name the dispatcher explicitly; and an outer
|
||||
flattening pass makes one guest step cross the dispatcher several times, so the
|
||||
shuttle class must be separated before naming. Opcode maps key by signature first.
|
||||
|
||||
Examples
|
||||
python vm_handler_slice.py --log vm_trace.log
|
||||
@@ -147,11 +158,35 @@ def cluster(segments):
|
||||
return trajectory, stats
|
||||
|
||||
|
||||
def _run_of(seg):
|
||||
"""A segment is (run, closed) since the open-tail fix; accept a bare run too."""
|
||||
return seg[0] if isinstance(seg, tuple) else seg
|
||||
|
||||
|
||||
def nested_loop_segments(segments):
|
||||
"""Indexes of segments that look like a nested dispatch loop: one block
|
||||
repeating 4+ times inside a single segment. Handler chains are short and
|
||||
repeat ACROSS segments, never within one -- a block repeating inside one
|
||||
segment is a second dispatcher running under the first (a VM entered from
|
||||
VM code; in the write-up that taught this, 45 of 46 entries were nested,
|
||||
and a flat slice discarded 57-1997 instructions per nesting)."""
|
||||
out = []
|
||||
for i, seg in enumerate(segments):
|
||||
run = _run_of(seg)
|
||||
if len(run) < 8:
|
||||
continue
|
||||
counts = Counter(run)
|
||||
if counts.most_common(1)[0][1] >= 4:
|
||||
out.append(i)
|
||||
return out
|
||||
|
||||
|
||||
def diagnostics(blocks, dispatcher, segments, stats):
|
||||
"""Sanity notes (spin loops, degenerate shapes)."""
|
||||
"""Sanity notes (spin loops, degenerate shapes, nested dispatch levels)."""
|
||||
n = len(blocks)
|
||||
dcount = blocks.count(dispatcher)
|
||||
avg_len = sum(len(s) for s in segments) / len(segments) if segments else 0
|
||||
runs = [_run_of(s) for s in segments]
|
||||
avg_len = sum(len(r) for r in runs) / len(runs) if runs else 0
|
||||
unique_ratio = len(stats) / len(segments) if segments else 1.0
|
||||
notes = []
|
||||
if avg_len <= 1.05:
|
||||
@@ -164,20 +199,35 @@ def diagnostics(blocks, dispatcher, segments, stats):
|
||||
notes.append("dispatcher is under 15%% of blocks (%d/%d): the loop does not "
|
||||
"dominate; re-check with trace_diff.py reduce's histogram"
|
||||
% (dcount, n))
|
||||
nested = nested_loop_segments(segments)
|
||||
if nested:
|
||||
notes.append("segments %s contain a block repeating 4+ times inside one segment: "
|
||||
"a nested dispatch loop (a VM called from VM code). A flat slice "
|
||||
"corrupts nested levels -- slice per level; the segment-stack fix "
|
||||
"is documented in references/native-vm-recovery.md"
|
||||
% (",".join(str(i) for i in nested[:5])))
|
||||
return {"blocks": n, "dispatcher_hits": dcount,
|
||||
"dispatcher_share": round(dcount * 100.0 / n, 1),
|
||||
"segments": len(segments), "unique_segments": len(stats),
|
||||
"avg_segment_len": round(avg_len, 2), "notes": notes}
|
||||
"avg_segment_len": round(avg_len, 2),
|
||||
"nested_loop_segments": nested, "notes": notes}
|
||||
|
||||
|
||||
def apply_map(trajectory, stats, map_path):
|
||||
"""Map handler keys to opcode labels; report unmapped blocks."""
|
||||
"""Map handler keys to opcode labels; report unmapped blocks.
|
||||
|
||||
Keys are tried as signatures first, then as the segment's first block --
|
||||
the two coincide on block-granular traces, but on an instruction stream
|
||||
(or under an outer flattening pass) every segment can share one entry
|
||||
block, and only the signature distinguishes handlers. Measured on the
|
||||
SS6 fixture run: first-block keys collided there, signatures did not.
|
||||
"""
|
||||
with open(map_path, "r", encoding="utf-8") as fh:
|
||||
table = json.load(fh)
|
||||
named, unmapped = [], []
|
||||
for sig in trajectory:
|
||||
run = stats[sig]["blocks"]
|
||||
key = run[0]
|
||||
key = sig if sig in table else run[0]
|
||||
if key in table:
|
||||
named.append(table[key])
|
||||
else:
|
||||
@@ -254,9 +304,9 @@ def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Slice a VM interpreter trace into handler segments: dispatcher "
|
||||
"detection, segment signatures, trajectory, optional handler->opcode map.",
|
||||
epilog="Calibrated against the public xvmp IR virtualizer (vm_exec + slot machine); "
|
||||
"references/ete-diff-tracing.md owns the capture, this tool owns the "
|
||||
"de-virtualization half of the reading.")
|
||||
epilog="Calibrated against the public xvmp generator's bytecode format (its runtime "
|
||||
"interpreter is NOT in that repository); references/ete-diff-tracing.md "
|
||||
"owns the capture, this tool owns the de-virtualization half of the reading.")
|
||||
ap.add_argument("--log", required=True, help="interpreter trace (stalker or plain)")
|
||||
ap.add_argument("--format", choices=sorted(trace_diff.PARSERS),
|
||||
help="force a parser (default: auto, as trace_diff.py)")
|
||||
|
||||
@@ -165,3 +165,96 @@ class TestCli:
|
||||
"--opcode-map", str(mp)])
|
||||
assert r.returncode == 0
|
||||
assert "unmapped handler blocks" in r.output
|
||||
|
||||
|
||||
class TestNestedLoopDiagnostic:
|
||||
"""A block repeating inside ONE segment is a second dispatcher under the
|
||||
first (a VM entered from VM code) -- the absorbed write-up measured 45-of-46
|
||||
entries nested, 57-1997 instructions lost per nesting under a flat slice."""
|
||||
|
||||
def _nested_log(self, tmp_path):
|
||||
H = {"D": 0x1000, "h": 0x2000, "i": 0x3000, "j": 0x4000}
|
||||
seq = "D h D h D " + " ".join(["i"] * 6 + ["j"] + ["i"] * 6) + " D h D h"
|
||||
p = tmp_path / "nested.log"
|
||||
p.write_text("\n".join("BLK %d libvm.so+0x%x" % (i, H[k])
|
||||
for i, k in enumerate(seq.split())) + "\n",
|
||||
encoding="utf-8")
|
||||
return p
|
||||
|
||||
def test_nested_segment_flagged_with_explicit_outer_dispatcher(self, tmp_path):
|
||||
r = run_script("vm_handler_slice.py", ["--log", str(self._nested_log(tmp_path)),
|
||||
"--dispatcher", "libvm.so+0x1000"])
|
||||
assert r.returncode == 0, r.output
|
||||
assert "nested dispatch loop" in r.output
|
||||
assert "native-vm-recovery.md" in r.output
|
||||
|
||||
def test_short_segments_do_not_trigger(self, vm_log):
|
||||
r = run_script("vm_handler_slice.py", ["--log", str(vm_log)])
|
||||
assert r.returncode == 0
|
||||
assert "nested dispatch loop" not in r.output
|
||||
|
||||
def test_nested_helper_shape(self):
|
||||
segs = [(["a"] * 6 + ["b"] + ["a"] * 6, True), (["c"], True)]
|
||||
assert vs.nested_loop_segments(segs) == [0]
|
||||
assert vs.nested_loop_segments([(["a", "b", "a"], True)]) == []
|
||||
|
||||
def test_diagnostics_accepts_tuple_and_bare_run_segments(self):
|
||||
"""(run, closed) since the open-tail fix; a bare run still measures."""
|
||||
d1 = vs.diagnostics(["D", "a", "D", "a"], "D", [(["a"], True)] * 2, {})
|
||||
d2 = vs.diagnostics(["D", "a", "D", "a"], "D", [["a"]] * 2, {})
|
||||
assert d1["avg_segment_len"] == d2["avg_segment_len"] == 1.0
|
||||
|
||||
|
||||
class TestSigKeyMaps:
|
||||
"""Signature-keyed opcode maps: on an instruction stream (or under an outer
|
||||
flattening pass) segments share one entry block, so only the signature
|
||||
distinguishes handlers. Measured on the SS6 fixture run."""
|
||||
|
||||
def test_sig_key_wins_when_first_blocks_collide(self, tmp_path):
|
||||
"""Two segments starting at the same block, named via signature keys."""
|
||||
run = "D load arith D store D br D ret".split()
|
||||
p = tmp_path / "vm2.log"
|
||||
p.write_text("\n".join("BLK %d libvm.so+0x%x" % (i, H[k])
|
||||
for i, k in enumerate(run)) + "\n",
|
||||
encoding="utf-8")
|
||||
blocks = vs.trace_diff.read_blocks(str(p))
|
||||
segs, pre, tail = vs.slice_segments(blocks, "libvm.so+0x1000")
|
||||
# both segments start right after D at 0x2000; only one exists per
|
||||
# entry -- craft the collision by slicing at the handler block itself:
|
||||
# segments then begin at 0x3000 / 0x4000 -- distinct. To force a true
|
||||
# collision, slice at a block the segments share:
|
||||
run3 = "D arith D arith store D ret".split()
|
||||
p3 = tmp_path / "vm3.log"
|
||||
p3.write_text("\n".join("BLK %d libvm.so+0x%x" % (i, H[k])
|
||||
for i, k in enumerate(run3)) + "\n",
|
||||
encoding="utf-8")
|
||||
b3 = vs.trace_diff.read_blocks(str(p3))
|
||||
segs3, _, _ = vs.slice_segments(b3, "libvm.so+0x1000")
|
||||
# dispatcher-sliced segments here are [arith] and [arith, store]:
|
||||
# different content, and the first segment's run == second's prefix,
|
||||
# which is the fixture shape (shared entry prefix).
|
||||
assert len(segs3) == 3 # two closed + the open tail
|
||||
sig_a, sig_b = (vs.signature(r) for r, _ in segs3[:2])
|
||||
assert sig_a != sig_b
|
||||
mp = tmp_path / "map.json"
|
||||
mp.write_text(json.dumps({sig_a: "ARITH_OR_SHL", sig_b: "ARITH_LATCH"}),
|
||||
encoding="utf-8")
|
||||
_traj, stats = vs.cluster(segs3)
|
||||
named, unmapped = vs.apply_map([sig_a, sig_b], stats, str(mp))
|
||||
assert named == ["ARITH_OR_SHL", "ARITH_LATCH"]
|
||||
assert unmapped == []
|
||||
|
||||
def test_first_block_key_still_supported(self, tmp_path):
|
||||
"""Legacy first-block keys keep working when no signature key matches."""
|
||||
run = "D load arith D ret".split()
|
||||
p = tmp_path / "vm4.log"
|
||||
p.write_text("\n".join("BLK %d libvm.so+0x%x" % (i, H[k])
|
||||
for i, k in enumerate(run)) + "\n",
|
||||
encoding="utf-8")
|
||||
blocks = vs.trace_diff.read_blocks(str(p))
|
||||
segs, _, _ = vs.slice_segments(blocks, "libvm.so+0x1000")
|
||||
_traj, stats = vs.cluster(segs)
|
||||
mp = tmp_path / "map4.json"
|
||||
mp.write_text(json.dumps({"libvm.so+0x2000": "OP_LOAD"}), encoding="utf-8")
|
||||
named, unmapped = vs.apply_map([vs.signature(segs[0][0])], stats, str(mp))
|
||||
assert named == ["OP_LOAD"] and unmapped == []
|
||||
|
||||
Reference in New Issue
Block a user