mirror of
https://codeberg.org/reverse-skill-lab/apk-reverse
synced 2026-11-16 06:44:24 +08:00
Four projects were cloned for comparison (reverse-skill, awesome-game-security,
android-reverse-engineering-claude-skill, MobileRE-Skill) and read for what they do that this
repository does not. None is vendored and none becomes a runtime dependency: what crosses over is a
phase order, a checklist or a command pattern, with the source URL and the retrieval date recorded in
the file that absorbed it.
The rule applied to every candidate: does this make a judgement easier or a verification harder? The
repository is a decision-and-verification layer, and the four things kept are all of that kind.
new scripts/scan_leaks.py target identity scanned before publishing, with an explicit
do-not-anonymize list and four RESULT=/exit states
new references/precedents/ the positive half of the record: route including dead ends, a
grade per assertion, measured pits, and a write-back checklist
new scripts/svc_scan.py does a libc-level hook structurally see this call at all
new scripts/anti_detect_probe.js observer-only: name the check, and report the environment it reads
new references/desensitization-and-leak-scans.md
new references/dart-aot.md sections the 32-bit snapshot's real string-record format
check_repo.py now delegates the identity scan to scripts/scan_leaks.py and runs it over
git ls-files, so the gate reads content instead of layout for the first time. An explicit file list
cannot be narrowed by the default work-area exemptions -- that defect was caught by the gate itself.
Two claims that this repository had already written down turned out to be wrong, and both are
recorded as refuted rather than quietly rewritten:
dart-aot.md the armv7 string table was documented as [len*2 u32le][UTF-16LE]. It is not. The same
extractor keeps 4,980 chained entries on arm64 and zero on armv7, and the byte layer
shows the 32-bit record is [header u32][byte-count u32le][UTF-8 payload]: two adjacent
literals carry 0x46 (70) and 0x48 (72), exactly their byte lengths. The zero was a
format mismatch, not an empty table. The engine-version banner on the same target is
66 bytes of "0.0.1" with a blank build id, so the documented regex returns a
version-shaped lie.
leak scan the pass that added the scanner found 26 strong hits inside its own evidence file on
the day it was written -- the fixture's literal match values, quoted to prove the
rules fire. The corpus may contain values; the published surface may not.
Measured, not asserted. The check that fires was named on a public MASTG target: three arms with one
variable, the control alive at the end and the probe arm dead in about 300 ms, with strstr("frida") at
t=303 ms and no tombstone. Reproducibility is bounded honestly -- one full sequence in three arms, on
a target whose whole window is sub-second. svc_scan.py's two independent decoders agree on an
identical 214-site set, and ptrace-free dumping gained a negative boundary: a target whose dex is
deflated inside its APK has no whole-image dex in memory to export, for reasons that say nothing about
protection.
Also in this pass: the detector-locating pipeline (detection-and-anti-analysis.md Step 3) with its
branch conditions; the layered Java-to-svc descent and the dump's honesty boundary in
advanced-unpacking.md; a sourced capability-gap list in toolchain.md where every row is graded
URL-verified or measured; the RESULT=-token convention; and SKILL.md opening with an immediate-action
block, because the symptom index and the gates were sitting in the part of a long file nobody reads.
Gates: check_repo.py 0 problem, check_refs.py 0 dangling / 0 warning, check_budget.py 0 failure /
0 warning / 1 pre-existing note. The scan reports RESULT=leaks_found_strong_only, exit 0: four
endpoint/weak hits, all of them addresses from the RFC 5737 documentation range in usage examples.
184 lines
9.4 KiB
Markdown
184 lines
9.4 KiB
Markdown
# EXTENSION — Dart AOT string-table formats, measured on a real dual-ABI app
|
|
|
|
Evidence file for the `dart-aot.md` changes in this pass. Every claim below carries a grade:
|
|
|
|
- **observed** — a command was run here and its output is quoted in this file.
|
|
- **inferred** — follows from an observed fact or from documented mechanism; the step was not executed.
|
|
- **unverified** — assumed or reported elsewhere, not reproduced here.
|
|
|
|
Target identity is replaced with `<PKG>`; the artifact is referred to as "the sample". The technique
|
|
(tool, library, function, hardening product) is kept, per
|
|
`skills/apk-reverse/references/desensitization-and-leak-scans.md`.
|
|
|
|
## 1. What was measured
|
|
|
|
| Claim under test | Grade | Result |
|
|
|---|---|---|
|
|
| The arm64 Dart AOT string table is `[0x80\|(len<<1)\|two_byte][payload]` and the entries chain | **observed** | confirmed on 4,980 entries |
|
|
| The armv7 (32-bit) snapshot uses the *same* format | **refuted** | the same extractor keeps 0 entries; the tag byte is not present before the literal |
|
|
| An armv7 string record is `[length u32le][UTF-16LE chars]`, so the length field reads `len*2` | **refuted** | the u32 before the payload reads the **byte** count, and the payload is UTF-8 |
|
|
| The engine version banner always yields `x.y.z (stable) (…)` | **refuted** | a real `libflutter.so` carries `0.0.1` with a blank build id and no `(stable)` |
|
|
| An equal-length replacement of a data key is legal on both ABIs without recomputing the tag | **observed** (record layout), **unverified** (end-to-end device run) | the record is self-contained on both ABIs |
|
|
|
|
Two of the five were assumptions that would have been written into the reference as fact. Both were
|
|
killed by a single command each, which is the whole argument for measuring before documenting.
|
|
|
|
## 2. Sample and preparation
|
|
|
|
The sample is the locally supplied "complex encryption and hardening" APK (360 shell + Flutter).
|
|
The two AOT snapshots were extracted from the zip without repacking:
|
|
|
|
```
|
|
$ python -c "import zipfile; z=zipfile.ZipFile(r'存在问题和例子\<sample>.apk'); \
|
|
[print(n, len(z.read(n))) for n in ('lib/arm64-v8a/libapp.so','lib/armeabi-v7a/libapp.so')]"
|
|
lib/arm64-v8a/libapp.so 16352152
|
|
lib/armeabi-v7a/libapp.so 17629776
|
|
```
|
|
|
|
Work copies live under the ignored workspace `tools/_work/dart/`; nothing from this evidence pass is
|
|
committed except the reference text.
|
|
|
|
## 3. The arm64 packed table — confirmed
|
|
|
|
```
|
|
$ python skills/apk-reverse/scripts/dart_pool_strings.py tools/_work/dart/app_arm64.so s64.tsv --min 6
|
|
candidates : 17352
|
|
kept (run >= 3) : 4980 (one-byte 4980 / utf16le 0)
|
|
below threshold : 12372 [--keep-isolated to keep them]
|
|
written s64.tsv
|
|
```
|
|
|
|
The tag arithmetic was checked directly against real literals in the image rather than only through
|
|
the script. For each literal, the byte immediately before it must equal `0x80|(len<<1)`:
|
|
|
|
```
|
|
$ python -c "... for m in (b'AdmobBanner', b'showSplashAd', b'AdPgl_Splash'): ..."
|
|
### app_arm64.so
|
|
AdmobBanner @0x4932e len=11 expected_tag=0x96
|
|
tag_byte_at_-1=0x96 matches=True
|
|
showSplashAd @0x39deb len=12 expected_tag=0x98
|
|
tag_byte_at_-1=0x98 matches=True
|
|
AdPgl_Splash @0x493ee len=12 expected_tag=0x98
|
|
tag_byte_at_-1=0x98 matches=True
|
|
```
|
|
|
|
**observed**: three independent literals of two different lengths, three matching tag bytes. This is
|
|
also the assertion a patch script should perform (`dart-aot.md` §10), and it is what rejects a hit
|
|
that landed mid-string.
|
|
|
|
## 4. The armv7 assumption — refuted, and what is actually there
|
|
|
|
The starting hypothesis (carried over from an external case) was that the 32-bit snapshot stores a
|
|
string as `[tag 0x81][len*2 u32le][UTF-16LE chars]`. Two scanners written against that hypothesis
|
|
found nothing, which is itself the evidence:
|
|
|
|
```
|
|
$ python tools/_work/dart/probe3.py app_armv7.so raw # [8-byte hdr ending 0x81][u32le len][chars]
|
|
hits=0 chained_runs=0 entries_in_runs=0
|
|
|
|
$ python tools/_work/dart/probe3.py app_armv7.so packed # the arm64 tag scheme
|
|
hits=0 chained_runs=0 entries_in_runs=0
|
|
```
|
|
|
|
The same extractor that keeps 4,980 entries on arm64 keeps **zero** on armv7:
|
|
|
|
```
|
|
$ python skills/apk-reverse/scripts/dart_pool_strings.py tools/_work/dart/app_armv7.so s7.tsv --min 6
|
|
candidates : 1355
|
|
kept (run >= 3) : 0 (one-byte 0 / utf16le 0)
|
|
```
|
|
|
|
A zero here is a **format mismatch, not an empty string table**, and that distinction is now stated in
|
|
`dart-aot.md` §7: reading it as "the strings were stripped" is exactly the wrong conclusion the same
|
|
section warns about for UTF-16.
|
|
|
|
What the record actually looks like, from bytes around a known literal:
|
|
|
|
```
|
|
$ python -c "... b.find(b'SystemChrome') ..."
|
|
0x1ffb3c b'\x00\x00\x00\x008\x06U\x00F\x00\x00\x00\xce-\x8a@SystemChrome.setEnab...'
|
|
0x204ce4 b'\x00\x00\x00\x008\x06U\x00H\x00\x00\x00\xa45jVSystemChrome.setSyst...'
|
|
0x2185c4 b' ere8\x07U\x00J\x00\x00\x00\x8c]\xb4GSystemChrome.setPref...'
|
|
```
|
|
|
|
Read as `[header u32][length u32le][payload]`: `0x00550638` then `0x00000046` = **70**, which is the
|
|
byte length of `SystemChrome.setEnabledSystemUIMode` (36 chars) immediately followed by
|
|
`SystemChrome.setSystemUIOverlayStyle` (34 chars) — **70 bytes, UTF-8, not UTF-16**. The second
|
|
literal starts at the exact byte where the first ends, and its own length field reads `0x00000048` =
|
|
72, matching its own continuation.
|
|
|
|
**observed**: `length == payload byte count`, payload is UTF-8, and in-place equal-length replacement
|
|
therefore keeps the record valid without recomputing anything. **refuted**: the `len*2` / UTF-16 form.
|
|
|
|
The header word is a tags value with full 4-byte alignment, not a constant to hard-code:
|
|
|
|
```
|
|
$ python -c "... alignment histogram over the image ..."
|
|
alignment mod 4: {0: 13397}
|
|
```
|
|
|
|
```
|
|
$ python tools/_work/dart/perf.py app_armv7.so
|
|
scanned 17629776 B in 0.94s using 4-byte stride + int.from_bytes
|
|
aligned-header string records: 582
|
|
stride-1 micro scan over 17629776 B took 2.80s (found 2762)
|
|
```
|
|
|
|
The literal search plus a backwards length assertion is the tool-independent method, and it costs
|
|
under a second over a 17.6 MB image — so validating every candidate is affordable and the armv7
|
|
limitation does not justify a hand-built table parser. **unverified**: whether a *different* armv7
|
|
snapshot uses a different header word; the reference tells the reader to read it from the file.
|
|
|
|
## 5. The engine version banner — a version-shaped lie
|
|
|
|
```
|
|
$ python -c "... rfind NUL before the ' on \"android_arm64\"' suffix ..."
|
|
end-of-version offset 0x110821
|
|
version string start 0x1107f1 => total 66
|
|
b'0.0.1 on "android_arm64"'
|
|
```
|
|
|
|
The banner exists, is 66 bytes long, and its numeric field is `0.0.1` with a blank build-id field —
|
|
no `(stable)`, no `(Mon …)`. A regex that takes `\d+\.\d+\.\d+` out of this returns a value that would
|
|
send a reader to build a Dart VM for `0.0.1`.
|
|
|
|
**observed** (the byte string above), **inferred** (that this is a build-time population failure rather
|
|
than a deliberately rewritten banner). The actionable consequence is in `dart-aot.md` §1: assert the
|
|
`(stable) (…)` tail before using the number, and fall back to the snapshot hash otherwise. **The
|
|
sample's Dart version was not pinned in this pass** — that is an unverified item, not a finding.
|
|
|
|
## 6. Cross-validation
|
|
|
|
Every measurement above is a **raw byte search with an independent arithmetic assertion** (find the
|
|
literal in the file; check the byte or word immediately before it against the length of what follows).
|
|
It does not depend on `dart_pool_strings.py`, and the script's numbers are reported alongside the raw
|
|
probe rather than as the source of truth. The two agree on arm64 (4,980 kept entries, tag byte equal to
|
|
`0x80|(len<<1)` at every literal tested) and on armv7 (the packed scheme finds nothing, the byte layer
|
|
shows why).
|
|
|
|
**Not obtained**: a second, independently implemented Dart snapshot parser. `blutter` was not built in
|
|
this pass, so the reference's `pp.txt`-side claims were not re-derived here, and the armv7 conclusion
|
|
rests on the byte layout rather than on a tool's agreement. That is the boundary of this evidence.
|
|
|
|
## 7. What this pass changed in the repository
|
|
|
|
- `skills/apk-reverse/references/dart-aot.md` — §7 gained the armv7 record format and the
|
|
"the armv7 zero is a format mismatch" warning; §1 gained the banner-shape assertion; §10 gained the
|
|
data-key-versus-API-path rule with its measured failure mode, and the longest-first / assert-the-prefix
|
|
rule. No new file, no new index row: the finding belongs in the file that already owns this layer.
|
|
- The equal-length-replacement failure (a 404 whose body is not JSON, `jsonDecode` throwing, the app
|
|
frozen on the launch logo) is documented as the concrete instance of the top-level constraint
|
|
"do not make an API fail to suppress a UI element".
|
|
|
|
## 8. Not done, and known to be outstanding
|
|
|
|
1. **No end-to-end device run.** The string-table patch was not repacked, signed and installed in this
|
|
pass, so "an equal-length key replacement suppresses the feature while the app still starts" is
|
|
`unverified` end-to-end even though the record layout that makes it legal is `observed`.
|
|
2. **No `blutter` build**, so no `pp.txt` comparison and no confirmation of the arm64 pool space from a
|
|
second implementation (§6).
|
|
3. **The sample's Dart version is unpinned** (§5); a structural probe or a snapshot hash is the next
|
|
experiment, not a guessed number.
|
|
4. **The armv7 header word is not characterised.** It is read from the file and used as a validator;
|
|
which component assigns it was not determined.
|