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.
|