Skip to content

Latest commit

 

History

History
156 lines (142 loc) · 9.13 KB

flutter-analysis.md

File metadata and controls

156 lines (142 loc) · 9.13 KB

Flutter build identification

REA identifies a Flutter app's build facts by pure parsing of its APK: the Dart VM snapshot hash, per-ABI library digests, the engine's GNU build-id, and any labeled version strings. No engine, no tool installation, no execution, and no external lookups — this family answers "which Dart SDK build is this, and what exactly is in the payload?" before any deeper native analysis.

This is the triage step for Flutter reversing: the snapshot hash is the artifact that maps to a Dart SDK release through an external dataset (such as the one reFlutter maintains), and knowing the SDK version is what makes SDK-layout-specific snapshot parsers applicable.

Answer an analyst question

rea identify-flutter-build /targets/Example.apk
rea inspect-dart-aot /targets/Example.apk
rea inspect-dart-aot /targets/Example.apk --abi armeabi-v7a

The equivalent MCP methods are identify_flutter_build (with path) and inspect_dart_aot (with path and an optional abi; the default prefers arm64-v8a, then x86_64, then x86, then armeabi-v7a, then the remaining ABIs alphabetically).

What the AOT inspection returns

inspect_dart_aot reads one ABI's libapp.so in full and reports:

  • Snapshot sections — the four _kDart*Snapshot* symbols located through the ELF dynamic symbol table (ELF32 and ELF64, little-endian, every read bounds-checked), each with file offset, size, and — for data sections, which carry the f5f5dcdc magic — the parsed header kind and length (the stored length excludes the four magic bytes). Data headers whose declared snapshot length does not fit their symbol retain their observed fields and report partial coverage. Complete structural coverage currently requires kind 3, the FullAOT value in the supported Dart 2.14 layout. Kind numbers vary across Dart releases; other values are preserved with partial coverage, without guessing their interpretation. A payload without exported snapshot symbols is refused rather than guessed at.
  • The snapshot hash from the header at the start of each exported data symbol. Complete hash coverage requires a valid header from both the VM and isolate; duplicate records inside one symbol cannot stand in for the other's header. Aliased or overlapping data symbols report partial coverage.
  • A string-pool projection from the isolate data section: distinct package: source URIs (the app's own and its dependencies' Dart files — a dependency inventory), dart: SDK URIs (which SDK libraries the app uses), and identifier-shaped tokens, each bounded with counts and coverage: "partial" when truncated. On the reference target this counts 756 package URIs and 201 dart URIs on arm64-v8a and x86_64, and 212 / 48 on armeabi-v7a, retaining up to 500 of each in the response. libapp.coverage describes structural snapshot checks; string_pool.coverage describes the bounded projection. Overall coverage is partial when either facet is partial.

Interpret the result

  • flutter_detected is true when any lib/<abi>/libapp.so exists. A negative answer reports complete coverage: absence is an answer.
  • snapshot_hash is the Dart build hash observed 20 bytes after the snapshot magic (f5 f5 dc dc) inside libapp.so. Identification scans byte patterns; AOT inspection reads the exported VM and isolate headers. snapshot_hash_sources counts the valid records observed by the chosen operation, and when they disagree the distinct values are kept in snapshot_hash_candidates with snapshot_hash: null and coverage: "partial" rather than silently picking one.
  • elf_header_valid checks the supported little-endian ELF32/ELF64 header, table entry widths, and full table extents against the library's actual byte length. Truncated, malformed, or unsupported headers report false and partial coverage while retaining byte-scanned hashes. This header check does not establish valid table contents or exported symbols.
  • libflutter facts are per-ABI observations: byte size, SHA-256 digest, GNU build IDs from ELF-declared PT_NOTE / SHT_NOTE ranges, and any labeled strings the build carries. Release engine builds often strip the Dart VM version string — absence is reported as null, not guessed. Android clang/LLVM toolchain lines are reported verbatim as build provenance. build_id_candidates preserves distinct IDs. build_id is populated only when all declared note ranges were examined and one ID was found; conflicting IDs or malformed/bounded note metadata make coverage partial. Matching bytes outside declared note ranges cannot supply a build ID.
  • Labeled strings must fit their complete grammar. dart_version_capped and toolchain_capped independently report labels omitted by grammar or projection bounds; either makes identification coverage partial. Values split across read chunks are retained without publishing truncated prefixes.
  • coverage: "partial" also appears when an ABI carries libapp.so without libflutter.so (or vice versa).
  • The APK itself is digested before parsing, so target.sha256 binds the observation to the exact input.

Limits

  • Owned snapshots. Both operations copy the admitted APK into a provider-owned, read-only snapshot before digesting and parsing, so a concurrently modified original cannot mix into one observation; failed reader closure and snapshot cleanup are retained and retried on close(). The snapshot is removed only after all owned readers and file descriptors close successfully. Copying pins one admitted descriptor and compares its identity, size, mode and timestamps after the copy. Source permission failures remain access_denied, and source identity changes remain artifact_changed. Provider-owned storage failures report a resource_constraint with their native I/O cause. Cancellation is checked during copying, metadata/symbol walks, string scanning, and after cleanup.
  • Bounded reads. APK snapshots are bounded at 2 GiB, including while copying. Identification permits 768 MiB of cumulative library extraction; declared sizes are checked before extraction. Library reads are bounded at 256 MiB each and ZIP metadata at 8 MiB; the AOT image buffer is allocated at exactly the declared size before reading. Exceeded budgets report resource_constraint.
  • Engine notes. Engine libraries are spooled into the owned root. ELF headers, note tables and note payloads share an 8 MiB metadata budget; up to 64 distinct build IDs are retained. Exceeding either bound preserves the candidates already examined and reports partial coverage.
  • Metadata and candidate capacity. Both operations refuse duplicate relevant library paths. Inventory is limited to 32 ABIs and 1024 bytes per relevant library path, independently of compressed size. Identification refuses more than 64 distinct snapshot hashes with resource_constraint rather than silently dropping conflicting evidence.
  • Selected ABI. AOT inspection applies the library budget to the selected ABI after selection; an oversized unselected library does not block it. Undefined dynamic-symbol imports are not treated as snapshot exports. Section-stripped images require a bounded symbol count from DT_HASH or DT_GNU_HASH; layouts without either are refused. Symbol names and their terminators must fit their linked string table.
  • REA performs no hash-to-SDK lookup: mapping the snapshot hash to a Dart SDK release requires an external dataset and is deliberately out of scope for this family.
  • The AOT string pool is a byte-pattern projection, not deserialized cluster semantics: typed class and function recovery requires an SDK-specific parser (the snapshot layout changes per release — exactly why the hash is the key), and class_like_tokens include incidental matches. JEB's GUI Dart unit and native analysis of libapp.so cover some ground today.

Real verification

npm run verify:flutter -- --apk PATH [--plain-apk PATH] runs both tools through compiled CLI and stdio MCP, compares Evidence, and checks every ABI. Use --expectations scripts/fixtures/flutter-gallery-expectations.json with the Gallery 2.9.2 release APK to check exact per-ABI engine labels, URI counts and the snapshot hash against its pinned SHA-256. The verifier requires complete structural snapshot coverage on every ABI and explicitly reports the bounded pool projection as partial/unverified when truncated. Other APKs need their own expectations file to verify exact engine labels and URI counts; otherwise those facets are reported unverified. The optional plain APK checks negative detection. Record: Flutter Gallery 2.9.2 (112 MB, three ABIs) — every ABI reports the same agreeing hash 65817c30a78bb44c3dc3771876b6010a from two snapshot sections, per-ABI engine build-ids, no labeled Dart version (as expected for a release engine), and the Android clang toolchain line; a plain aapt2-built APK reports not-detected with complete coverage. The suite uses self-contained synthetic ELF images and scanner payloads; it does not read private local files.