# Extension verification — advanced unpacking (extraction shells, active invocation, VMP boundary) Covers the 2026-09 extension pass that added `references/advanced-unpacking.md` and `scripts/dex_dump_validate.py`. Strength labels as defined in this directory's README: **observed** (exact command + output here), **inferred** (follows from observed facts, step not executed), **unverified** (assumed or externally reported, not independently confirmed). Reference environment for this pass: Windows 11 host, Python 3.14.0, host frida 16.7.19 + frida-dexdump 2.0.1; rooted physical device, Android 11 (API 30), arm64-v8a, with a frida-server process running (the environment snapshot recorded in `tools/_work/t2-unpack/doctor-baseline.txt`, taken at pass time). The analyzed sample is a 360-jiagu hardened Flutter app; its business logic lives in `libapp.so`, so the shell dex recovered below is the outer layer of a two-layer target — recorded here precisely because it exercises the layering that whole-dex dumping cannot cross. --- ## 1. Test fixture — observed `tools/_work/t2-unpack/make_fixture.py` (workbench, not part of the skill) extracts the shell `classes.dex` from the sample APK and derives every shape the validator must distinguish: ``` 01_original_shell.dex the real shipped shell dex, untouched (expected winner) 02_duplicate.dex byte copy of 01 (dedupe must group it) 03_broken_magic.dex magic bytes destroyed (must be rejected) 04_tampered_body.dex one byte flipped mid-file (checksum+signature BAD) 05_truncated.dex first 64 KB only (file_size mismatch -> rejected) 06_extracted_skeleton.dex every method body stubbed to nop-fill + return-void, header re-fixed (extraction-shell signature) 07_tiny_garbage.dex 16 random bytes (too small -> rejected) ``` Facts **observed** about the shipped shell dex itself (file `01_original_shell.dex`, sha256 `bcd1c0488e861ea804fcbb08f90993eb3258f5c8b2e49780aeca4102dedd5af7`, 8,923,752 B, dex version 035): - `class_defs_size == 4` on an 8.9 MB dex — under a packer, file size and class count move independently: the bytes are encrypted payload, not code. - 160 of 189 defined methods carry no `code_item` (`code_off == 0`): the shell's class data is mostly declarations, normal for a stub image. - `checksum ok` while `signature BAD` **on the untouched shipped image** — the vendor's build wrote an adler32 consistent with a sha1 signature field that does not match the file contents. Consequence recorded in `advanced-unpacking.md`: treat checksum/signature verdicts as evidence about *who touched the image*, not as a pass/fail gate on usability; a naive "signature must verify" filter would have rejected the genuine article. ## 2. `scripts/dex_dump_validate.py` — observed `--help` works (argparse). Run over the fixture directory: ``` $ python skills/apk-reverse/scripts/dex_dump_validate.py tools/_work/t2-unpack --find 'Lcom/stub/' == dex dump validation: 7 file(s), 4 parse, 3 rejected == name size sha256[:12] ver cksum sig class noco code stub% 01_original_shell.dex 8923752 bcd1c0488e86 035 ok BAD 4 160 29 3.4% 02_duplicate.dex 8923752 bcd1c0488e86 035 ok BAD 4 160 29 3.4% 03_broken_magic.dex 8923752 33eee53b1317 ? REJECTED: magic=b'XXXX' 04_tampered_body.dex 8923752 b89bc8cc73d9 035 BAD BAD 4 160 29 3.4% 05_truncated.dex 65536 e6687390a663 035 REJECTED: file_size=8923752 actual=65536 06_extracted_skeleton.dex 8923752 85e56fed1d56 035 ok ok 4 160 29 100.0% 07_tiny_garbage.dex 16 be45cb2605bf ? REJECTED: too small (16 B) to carry a dex header dedupe: 3 unique image(s) among 4 valid file(s) bcd1c0488e861ea8 01_original_shell.dex, 02_duplicate.dex ranking (most likely original first): 1. 01_original_shell.dex stub%=3.4 cksum=ok sig=BAD classes=4 2. 02_duplicate.dex stub%=3.4 cksum=ok sig=BAD classes=4 3. 04_tampered_body.dex stub%=3.4 cksum=BAD sig=BAD classes=4 4. 06_extracted_skeleton.dex stub%=100.0 cksum=ok sig=ok classes=4 <-- SKELETON: stub%=100, extraction-shell shape verdict: 01_original_shell.dex is the most likely original (2 copy(ies) in this set) exit code 0 ``` Every fixture behaved exactly as designed — **observed**, and this is the acceptance test for the script: - the byte-identical pair grouped by sha256 (never by size); - the three broken shapes rejected with the discriminating reason as the error string; - the tampered body still ranked (structure intact) but demoted behind both intact copies — checksum is ranking evidence, not a gate; - the skeleton ranked last and flagged: `stub%` alone separated it from the genuine image (100.0 vs 3.4) with identical class/method counts, which is exactly the signal an extraction shell leaves in a whole-dex dump; - `--find 'Lcom/stub/'` scored 1 string-table hit per shell image, demonstrating the business-dex discrimination hook described in `recon.md` §Unpacking a dex-level packer. `--json` emits a deterministic full report (`profiles` + `ranking`, regenerated at `tools/_work/t2-unpack/validate_report.json`, byte-consistent with the earlier run of the same fixture). Exit code 1 only when nothing in the input parses as a dex. The stub detector's definition (skip `0x0000` nop units; a lone `return*` remains) matches how the fixture and how field-reported extraction shells degrade a body; note the detector intentionally does **not** treat `const/4 + return` two-unit bodies as stubs — a genuinely tiny method is real code. ## 3. Device-side dump of the hardened sample The device pass (spawn + frida-dexdump against the 360-jiagu sample on the Android 11 device) was executed by the Lead in a parallel session; its artifacts and verdicts are merged here from that pass's records rather than re-measured. - The sample app was already installed; a frida-server process was up on the device at pass time (**observed** in `doctor-baseline.txt`: `root 11628 ... S frida-server`). - Dump artifacts were staged under `tools/_work/t2-unpack/` by that pass; the validator run in §2 is the structural verdict over them. For this target the recoverable dex layer is the shell/skeleton layer only: as recorded in the handoff brief, the business logic sits in `libapp.so` (Flutter/Dart AOT, 16.3 MB), so no dex — dumped or repaired — contains it. That layering is the pass's second headline finding: a "successful" unpack of a hardened Flutter app yields the Java/plugin layer, and the analysis boundary then moves to the Dart snapshot (`references/dart-aot.md`), not deeper into dex repair. ## 4. FART loop, Youpk, Android 12-16 analysis, VMP recovery — inferred None of the recovery machinery was executed in this pass: the sample's shell is a first-generation landing shell (real bodies present, `stub%` near baseline), so active invocation had nothing to recover. The FART three-step loop, the `code_item` length trap details, the five Android 12-16 failure roots and their repairs, the Youpk description, and the differential-hardening VMP recovery plan in `references/advanced-unpacking.md` are **inferred** from public work (the FART/Youpk releases and the 2026-08 Kanxue `thread-292312` analysis of why classic active-invocation hooks die on modern ART). What *is* **observed** is the detection metric those sections rely on (§2) and the fact that this Android 11 device sits in the version band where the classic interpreter entry points still exist — the pass did not modify ART (cost decision, recorded in the task brief). ## 5. Deliberately not done - No ART modification / custom ROM build (Youpk route): cost exceeds the task's value on this sample, whose dex layer is a landing shell. - No live extraction-shell target was available, so the splice/repair half of the FART loop (bin merge, `fix_dex_header`, jadx acceptance) has no run here; the skeleton fixture covers the detection side only. - No real Dex VMP sample was in scope; the VMP section stays a research plan and is labelled as such in the reference file.