143 lines
8.1 KiB
Markdown
143 lines
8.1 KiB
Markdown
# 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.
|