跳转至

Flutter 构建识别

REA 通过纯解析 APK 来识别 Flutter 应用的构建事实:包括 Dart VM 快照哈希、各 ABI 库摘要、引擎的 GNU build-id,以及任何带标签的版本字符串。无需引擎、无需安装工具、无需执行,也无需外部查询——这一系列操作能在更深入的原生分析之前回答“这是哪个 Dart SDK 构建产物,载荷中究竟包含什么?”。

这是 Flutter 逆向中的初步分流步骤:快照哈希是一种工件,可通过外部数据集(例如 reFlutter 维护的数据集)映射到 Dart SDK 版本;而知道了 SDK 版本,才能让特定于 SDK 布局的快照解析器得以适用。

回答分析人员的问题

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

对应的 MCP 方法是 identify_flutter_build(带 path)和 inspect_dart_aot(带 path 以及可选的 abi;默认优先选择 arm64-v8a,然后是 x86_64,再是 x86,接着是 armeabi-v7a,最后按字母顺序处理其余 ABI)。显式指定的 abi 必须与 identify_flutter_build 报出的目录标签完全一致,因此位于非标准标签下的载荷仍可选择,并会在结果中保留该标签。

可选的保留式 AOT 交付

MCP 的 inspect_dart_aot 在省略 detail 或指定 detail: "complete" 时会保留完整输出。显式的 detail: "summary" 则返回所选目标/ABI/库身份、所有快照头部、结构覆盖率、字符串池覆盖率以及原始下界计数,但不包含字符串列表。它会保留完整的源 Evidence,并返回一个独立的、关联的 inspect_analysis_view 摘要。分析与调用方的 ABI 选择保持不变;一次性 CLI 输出仍然完整。

使用 normalized_result.parent_evidence_id 作为 inspect_analysis_view 的保留源,配合 view: {kind: "dart-aot", collection: "package_uris", offset: 0, limit: 64}。集合包括 package_uris、dart_uris 和 class_like_tokens。分页会保留原始字符串及其顺序、所选 ABI、库身份和源覆盖率。returned_counts 描述生产者返回的行数;以 _at_least 结尾的计数仍为下界。遍历完一页页序列并不会使不完整的池变为完整,也不会把字节模式 token 变成类型化的类/函数。视图不会重新打开 APK。原始 Evidence 仍可在其现有会话生命周期内导出。摘要需要一个保留型服务器;较旧的服务器在 schema 发现后会保留完整请求。

AOT 检查返回的内容

inspect_dart_aot 会完整读取一个 ABI 的 libapp.so,并报告:

  • 快照节区 —— 通过 ELF 动态符号表(ELF32 和 ELF64,小端序,每次读取都有边界检查)定位到的四个 _kDart*Snapshot* 符号;每个符号都带有文件偏移、大小,对于数据节区(带有 f5f5dcdc 魔数)还会解析出头部种类和长度(存储的长度不包含四个魔数字节)。如果某个数据头部声明的快照长度与其符号不匹配,则会保留观测到的字段并报告部分覆盖率。完整的结构覆盖率目前要求 kind 3,即受支持的 Dart 2.14 布局 中的 FullAOT 值。kind 编号在不同 Dart 版本之间会变化;其他值会以部分覆盖率保留下来,而不会猜测其含义。 统一的 _kDartSnapshotData / _kDartSnapshotText 导出属于另一种不受支持的配置。上游 VM-isolate 移除 同时改变了源码布局和 FullAOT 的 kind 值。当只有统一导出可用时,REA 会报告观测到的导出名称和库身份,并给出类型化的“不受支持配置”拒绝;它不会把一个头部当作两个独立来源的别名。 地址解析会检查每一个重叠的 PT_LOAD 解释,包括部分重叠。等价映射会被合并;冲突映射会使受影响的符号无法解析,并将覆盖率标为部分。动态哈希表、符号表和字符串表使用相同的映射检查。 没有导出快照符号的载荷会被拒绝,而不是被猜测。
  • 快照哈希 —— 来自每个导出数据符号起始处的头部。完整的哈希覆盖率要求 VM 和 isolate 都有有效头部;一个符号内的重复记录不能代替另一个符号的头部。别名化或重叠的数据符号会报告部分覆盖率。
  • 来自 isolate 数据节的字符串池投影:不同的 package: 源 URI(应用自身的及其依赖项的 Dart 文件——即依赖清单)、dart: SDK URI(应用使用了哪些 SDK 库),以及标识符形状的 token,每项都带有计数,截断时标记为 coverage: "partial"。在参考目标上,arm64-v8a 和 x86_64 会统计到 756 个 package URI 和 201 个 dart URI,而 armeabi-v7a 上为 212 / 48,响应中每种最多保留 500 个。 libapp.coverage 描述结构快照检查;string_pool.coverage 描述有界投影。只要任一方面的覆盖率是部分的,总体覆盖率就是部分的。

解读结果

  • flutter_detected 在存在任意 lib/<abi>/libapp.so 时为 true。否定答案会报告完整覆盖率:不存在本身就是一个答案。
  • snapshot_hash 是在 libapp.so 内快照魔数(f5 f5 dc dc)之后 20 字节处观测到的 Dart 构建哈希。识别操作扫描字节模式;AOT 检查则读取导出的 VM 和 isolate 头部。snapshot_hash_sources 统计所选操作观测到的有效记录数量;当它们不一致时,不同值会保留在 snapshot_hash_candidates 中,同时 snapshot_hash: null 且 coverage: "partial",而不是静默地选取其中一个。 识别操作可以保留来自统一载荷的单个字节扫描哈希;其部分状态并不代表截断成立,也不代表支持该配置。
  • elf_header_valid 检查受支持的小端 ELF32/ELF64 头部、表项宽度以及完整表范围是否与该库的实际字节长度匹配。截断、畸形或不支持的头部会报告 false 和部分覆盖率,同时保留字节扫描得到的哈希。此头部检查并不验证表内容或导出符号的有效性。
  • libflutter 事实 是按 ABI 的观测结果:字节大小、SHA-256 摘要、来自 ELF 声明的 PT_NOTE / SHT_NOTE 范围的 GNU build-id,以及构建产物携带的任何 带标签 字符串。发布版引擎构建通常去掉 Dart VM version 字符串——缺失会报告为 null,而不是猜测。Android clang/LLVM 工具链行会按原样报告,作为构建来源信息。 build_id_candidates 保留不同的 ID。只有检查了所有声明的 note 范围且只找到一个 ID 时,build_id 才会被填充;冲突的 ID 或畸形/有界的 note 元数据会使覆盖率变为部分。最长 64 字节的非空 GNU build-id 描述符会以小写十六进制保留,包括短于四个字节的 ID。 声明 note 范围之外的匹配字节不能提供 build-id。压缩的 SHT_NOTE 节区会被跳过,并带有不受支持压缩的限制和部分覆盖率;其他 note 范围内的 ID 仍会是候选。编码后的 note 存储绝不会被解释为 note 头部或被解压。
  • 带标签字符串必须符合其完整语法。dart_version_capped 和 toolchain_capped 会分别报告因语法或投影边界而省略的标签;任一情况都会使识别覆盖率变为部分。跨读取分块拆开的值会完整保留,而不会发布截断后的前缀。
  • coverage: "partial" 也会在某个 ABI 带有 libapp.so 但没有 libflutter.so(或反过来)时出现。
  • APK 本身会在解析前先计算摘要,因此 target.sha256 将观测结果绑定到确切的输入。

限制

  • 自有快照。 两种操作都会在摘要和解析之前,将已接受的 APK 复制到提供方自有的只读快照中,因此并发修改的原始文件不会混入单次观测;失败的读取器关闭和快照清理会被保留并在 close() 时重试。快照仅在所有自有读取器和文件描述符成功关闭后才会被移除。复制过程会固定一个已接受的描述符,并在复制后比较其标识、大小、模式和时间戳。源权限失败仍为 access_denied,源标识变更仍为 artifact_changed。提供方自有存储失败会报告带有其原生 I/O 原因的 resource_constraint。在复制、元数据/符号遍历、字符串扫描以及清理之后都会检查取消。
  • 有界读取。 APK 快照限制为 2 GiB,包括复制期间。识别允许累计提取 768 MiB 的库;提取前会检查声明的大小。每次库读取限制为 256 MiB,ZIP 元数据限制为 8 MiB。读取前,AOT 映像缓冲区会预留已接受的声明大小加一字节;报告的字节数是实际读取的字节数。超出预算则报告 resource_constraint。
  • 引擎说明。 引擎库被暂存到自有根目录中。ELF 头、note 表和 note 载荷共享 8 MiB 的元数据预算;最多保留 64 个不同的构建 ID 和 1024 个不同的非空 note 范围。范围上限限制了簿记和独立的位置读取,即使对于微小的重叠声明也是如此。完全重复项共享一次读取,空范围不占用清单槽位。超出边界时,会保留从已保留范围中检查到的候选对象,并以 build_id: null 报告部分覆盖。
  • 元数据与候选容量。 两种操作都拒绝重复的相关库路径。清单限制为 32 个 ABI,每个相关库路径限制为 1024 字节,与压缩大小无关。识别过程在遇到超过 64 个不同快照哈希时以 resource_constraint 拒绝,而不是静默丢弃冲突证据。
  • 选定 ABI。 AOT 检查在选定之后将库预算应用于选定的 ABI;超大的未选定库不会阻塞它。未定义的动态符号导入不被视为快照导出。保留的节目录若没有 SHT_DYNSYM,则使用与剥离节的映像相同的受限 PT_DYNAMIC 符号描述。存在但格式错误的符号表节仍视为失败。剥离节的映像需要来自 DT_HASH 或 DT_GNU_HASH 的受限符号计数;没有两者的布局会被拒绝。符号名称及其终止符必须适合其链接的字符串表。只有用于符号发现的动态条目才被解释为数值边界。ABI 忽略的载荷(包括 DT_BIND_NOW)不会改变发现过程;其存储的字节仍计入所选工件的摘要。
  • REA 不执行哈希到 SDK 的查找:将快照哈希映射到 Dart SDK 版本需要外部数据集,并且刻意不属于此系列的范围。
  • AOT 字符串池是字节模式投影,而非反序列化的集群语义:类型化类和函数恢复需要特定于 SDK 的解析器(快照布局随每个版本变化——这正是哈希成为关键的原因),而 class_like_tokens 包含附带匹配。JEB 的 GUI Dart 单元和 libapp.so 原生分析目前覆盖了一部分。

实际验证

npm run verify:flutter -- --apk PATH [--plain-apk PATH] 通过编译后的 CLI 和 stdio MCP 运行两个工具,比较 Evidence,并检查每个 ABI。使用 --expectations scripts/fixtures/flutter-gallery-expectations.json 配合 Gallery 2.9.2 发布版 APK,可以检查每个 ABI 的引擎标签、URI 计数以及快照哈希是否与其固定的 SHA-256 一致。验证器要求每个 ABI 上都有完整的结构快照覆盖;当受限池投影被截断时,会明确报告为部分/未验证。其他 APK 需要自己的期望文件来验证准确的引擎标签和 URI 计数;否则这些方面会被报告为未验证。可选的普通 APK 用于检查否定检测。记录:Flutter Gallery 2.9.2(112 MB,三个 ABI)——每个 ABI 都从两个快照节报告相同的哈希 65817c30a78bb44c3dc3771876b6010a,具有每个 ABI 的引擎构建 ID,没有标注 Dart 版本(发布引擎的预期行为),并且有 Android clang 工具链行;普通 aapt2 构建的 APK 报告为未检测到,且覆盖完整。该套件使用自包含的合成 ELF 映像和扫描器载荷;它不读取私有本地文件。

Gallery 工作流以及全部 147 个聚焦的 Flutter 测试也已在 macOS Sonoma 14.8.9 x86-64 和 Windows 11 x64 build 26300.9457 客户机(Node 24.18.0)上通过,包括 conflicting-load 和 note-range-limit 的 CLI/MCP 回归测试。Windows 验证在非管理员账户下运行。验证器使用共享的临时父目录,以便每个 CLI/MCP 进程都可以拥有并释放自己的快照。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 rea2/rea