re-flutter について
re-flutterは、FlutterおよびDartアプリのリバースエンジニアリング向けスキルです。AOTスナップショット解析、シンボル復元、Dart VMの動的解析を組み合わせ、アプリの処理とFlutterエンジンを区別します。
主な機能
- 01Flutterのdebug・release成果物の識別
- 02Dart AOTスナップショットと文字列の解析
- 03シンボルマップによる名前の復元
- 04Ghidraによるアプリ関数の特定
- 05FridaとVM Serviceによる動的解析
活用シーン
- 01AndroidまたはiOSのFlutterアプリ解析
- 02スナップショットからの通信先やプロトコルの調査
- 03署名、暗号処理、MethodChannelの追跡
- 04管理下のデバイスでのDart実行動作の観察
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dslsdzc/ rev-skills --skill re-flutterインストール後、Claude Code で/re-flutterを入力して読み込まれたか確認します。
詳細
Flutter / Dart AOT 逆向(快照解析 / 符号还原 / VM 动态分析)
何时使用 / 何时不用
- 用:Android 包内发现
lib/<abi>/libapp.so(Dart AOT 快照)与libflutter.so(引擎)——业务逻辑在 libapp.so - 用:iOS 的
App.framework/App(AOT 快照嵌入 Mach-O,Flutter.framework为引擎) - 用:debug 构建的
assets/flutter_assets/kernel_blob.bin(Dart kernel 二进制,含类/函数名与 AST) - 用:需要还原 Flutter App 的 Dart 业务逻辑(网络接口、签名/加密算法、协议、MethodChannel 通道)
- 不用:纯原生 Android App(无 libapp.so / kernel_blob,走 [[re-apk]])
- 不用:纯原生 iOS App(无 App.framework,走 [[re-ios]])
- 不用:Flutter Web(产物是 JS/WASM 而非快照,走 [[re-wasm]])
- 不用:引擎层本身(libflutter.so / Flutter.framework 内部逻辑,见步骤 4)
- 注意:动态步骤在受控设备 / 模拟器快照内执行([[re-analyze/platform-tips]] 最高原则);静态优先(大型样本原则)
工具准备
参考 [[re-analyze/platform-tips]]——Flutter 产物大(libapp.so 数十 MB 起),遵循「静态优先(大型样本)」:先静态定位、动态按需补充;动态(运行 / 重打包 App)默认在受控设备 / 模拟器快照内。
python3(快照头部 / 分区解析主力)
- Linux:
apt install python3/dnf install python3/pacman -S python - macOS: 自带;Windows: 官方安装器或
choco install python - 验证:
python3 --version
binutils(readelf / strings / objcopy,快照定位与段提取)
- Debian/Ubuntu:
apt install binutils;Fedora:dnf install binutils;Arch:pacman -S binutils - macOS: Xcode 命令行工具自带(
xcode-select --install) - Windows: 无自带 binutils——用 WSL,或 Ghidra 内置解析替代
- 验证:
readelf --version && strings --version
Ghidra(指令段反汇编 / 反编译与交叉引用)
- 装法与验证见 [[re-ghidra]];无 Ghidra 时 IDA / radare2 等价([[re-ida]] / [[re-radare2]])
- 验证: 导入 libapp.so 后能定位
_kDart*符号
frida(动态侧,Dart VM 入口 hook)
- 安装、frida-server 部署与版本匹配见 [[re-frida]] 工具准备(主机与设备版本必须一致)
- 验证:
frida --version;frida-ps -U列出设备进程
blutter(AOT 快照自动还原,可选)
- release libapp.so 的类/函数/对象池自动还原,安装与用法见 [[re-hybrid-app]] 工具准备;本技能步骤 2 以手动解析为核心方法,blutter 作自动化替代与核对
- 验证: 输出目录出现
pp.txt且含目标 App 类名
操作步骤
按顺序执行;每步产物(段提取 / 解析脚本 / hook 脚本 + sha256)存档。
-
识别构建模式(debug / release):
unzip -l app.apk | grep -E 'kernel_blob|libapp|libflutter' # Android 容器内定位 file lib/arm64-v8a/libapp.so readelf -sW lib/arm64-v8a/libapp.so | grep _kDart # release: AOT 快照符号 xxd -l 32 assets/flutter_assets/kernel_blob.bin # debug: 前 4 字节应为 ef cd ab 90(kernel magic 0x90abcdef 的小端) readelf -sW libapp.so | grep -c _kDartIsolateSnapshot # AOT 段符号(data/instructions),比按 ASCII 串搜可靠- debug:
assets/flutter_assets/kernel_blob.bin存在(header 共 8 字节 = magic0x90abcdef+ formatVersion,Dart kernel 二进制,类/函数名与 AST 保留,还原成本低)→ 直接步骤 3 - release: 无 kernel_blob,业务代码在 libapp.so 的
_kDartIsolateSnapshotData(数据)/_kDartIsolateSnapshotInstructions(指令)→ 步骤 2 - iOS:
Payload/<App>.app/Frameworks/App.framework/App(业务)与Flutter.framework/Flutter(引擎),Mach-O 内同段名,otool -l/ strings 定位
- debug:
-
Dart AOT 快照分区解析(release):
objcopy --dump-section .rodata=snap.rodata libapp.so # 数据段(含 VM + isolate 两份快照) objcopy --dump-section .text=snap.text libapp.so # 指令段 python3 - <<'PY' import struct data = open('snap.rodata', 'rb').read() MAGIC = 0xDCDCF5F5 # Dart VM snapshot magic(int32;文件里是小端 f5 f5 dc dc) KINDS = {0: 'full', 1: 'full-jit', 2: 'full-aot'} off = 0 while True: off = data.find(struct.pack('<I', MAGIC), off) if off < 0: break length, kind = struct.unpack('<qq', data[off+4:off+20]) # 基础头共 20 字节 print('snapshot @', hex(off), 'kind:', KINDS.get(kind, kind), 'length:', length) off += 4 PY- 头部格式:
magic(4, 0xdcdcf5f5)+length(8)+kind(8)= 20 字节基础头(其后是哈希等字段)。没有 ASCII"SNAPSHOT",也没有固定 12 字节版本串——按字符串搜SNAPSHOT在真实 AOT 样本上找不到任何东西 - 头之后是版本相关的内部序列化布局(子 blob 的顺序与结构随 Dart 版本变化),不要当稳定格式承诺:要精确解析就对着目标 SDK 版本的
runtime/vm/snapshot.h、序列化器与 image snapshot 实现来读,而不是套一份写死的分区表 - 机器码本体在独立的指令段(
_kDartIsolateSnapshotInstructions),与数据段分开;kind为full-aot时即 AOT 快照 _kDartVmSnapshot*是 VM 快照(dart:core/io 等标准库,所有 Flutter App 共用),业务只在_kDartIsolateSnapshot*- strings blob 提取业务字符串:
strings -n 6 -t x snap.rodata | grep -E 'https?://|token|secret|api/|MethodChannel' - 指令段导入 Ghidra:自动分析常认不出 AOT 代码对象,需按目标版本的代码对象元数据(子 blob 布局随版本变化,见上条)划分函数边界;反编译后按字符串交叉引用定位业务函数
- 自动替代:blutter(工具准备)还原 asm/对象池/类名;手动解析用于核对与版本兜底(坑 5)
- 头部格式:
-
符号还原:
- 构建侧留了映射(
--obfuscate --split-debug-info):# 构建命令: flutter build apk --obfuscate --split-debug-info=symbols/ ls symbols/ # app.android-arm64.symbols 等(每 ABI 一个) python3 -c "import json; d=json.load(open('symbols/app.android-arm64.symbols')); print(len(d['data']))" # JSON 结构: {"data": {"混淆名": "原名"}} —— 反向映射表,查表即得原名 - 无映射时字符串交叉引用反推:strings blob 里的 UI 文案 / 错误提示 / URL → Ghidra 交叉引用找引用指令 → 沿调用链上下游展开(方法对应 [[re-binary-core]] 交叉引用)
- debug kernel_blob.bin:kernel 格式含类/函数名与 AST,strings + 结构解析直接还原调用关系,无需符号表
- 构建侧留了映射(
-
引擎与业务区分:
libflutter.so/Flutter.framework:Flutter 引擎(C++:Dart VM、Skia、通道实现),各 App 基本一致——不分析,仅用于取 VM 导出符号(步骤 5)libapp.so/App.framework/App:Dart 业务代码 AOT 快照——分析目标_kDartVmSnapshot*(VM 快照 / 标准库)≠_kDartIsolateSnapshot*(业务):业务字符串与类名只在 isolate 快照- 原生插件与 MethodChannel 原生端在业务 so 之外,走 [[re-binary-core]]
-
动态侧(受控设备 / 模拟器快照内,[[re-analyze/platform-tips]] 默认沙箱):
- hook Dart VM 类库注册点——libflutter.so 导出的
Dart_CreateRootLibrary(root library 注册入口,onEnter 读库 URI):var f = Process.getModuleByName("libflutter.so").findExportByName("Dart_CreateRootLibrary"); if (f) Interceptor.attach(f, { onEnter: function (args) { var uri = args[1].readCString(); // 库 URI(C 字符串) console.log("root library: " + uri); } });- release AOT 下该 C API 可能不触发(库直接由快照反序列化)——改 hook VM 内部
dart::Library::New(libflutter.so 内符号,随版本裁剪)或直接走下面的 VM Service 枚举
- release AOT 下该 C API 可能不触发(库直接由快照反序列化)——改 hook VM 内部
- Dart VM Service 枚举 isolate / 库(release 需先开启 VM service:hook
Dart_SetVMFlags/Dart_InitIsolate注入--enable-vm-service,或改用 debug 构建验证动态行为):adb forward tcp:8181 tcp:8181 curl http://127.0.0.1:8181/getVM curl "http://127.0.0.1:8181/getIsolate?isolateId=isolates/1" curl "http://127.0.0.1:8181/getInstalledLibraries?isolateId=isolates/1"getInstalledLibraries列出全部业务库(库名 = 包名 / 路径);getObject按 objectId 枚举类/字段;VM service 元数据里类名与字符串仍可读
- 定位到目标函数后按地址 hook(
_kDartIsolateSnapshotInstructions基址 + 代码对象偏移),插桩底座见 [[re-frida]]
- hook Dart VM 类库注册点——libflutter.so 导出的
跨域联合
- [[re-mobile]]:移动 App 分析网关——Flutter 分支由 [[re-hybrid-app]] 与本技能承接(容器侧静态与动态跑通走网关)
- [[re-hybrid-app]]:Flutter 快照自动还原(blutter / reFlutter)与 MethodChannel 拦截——与本技能(手动快照解析 + VM 层动态)互补
- [[re-apk]] / [[re-ios]]:Android / iOS 容器侧静态(解包、manifest、签名、DEX/ObjC 原生桥接)
- [[re-frida]]:动态插桩底座(版本匹配、spawn/attach、反检测、脚本模板)
- [[re-binary-core]]:libapp.so / libflutter.so 的原生层([[re-format-elf]] + [[re-ghidra]] 反编译引擎与 JNI 桥)
常见坑与陷阱
- Release に kernel_blob.bin がない:現象——デバッグチュートリアルに従って
assets/flutter_assets/kernel_blob.binを探しても見つかりません。原因——ビルドモードによって生成物が決まります。Debug → JIT(kernel バイナリ。アプリのコードは kernel_blob 内)、Profile と Release はどちらも AOT(アプリのコードは libapp.so のスナップショットセグメント内)です。——Profile は kernel_blob によってアプリのコードを実行するわけではありません。Release との違いは、実行モデルではなく profiling/service/最適化の設定です。対策——まず手順 1 に従ってモードを確認します。kernel_blob がない場合は、手順 2 のスナップショットパーティション解析に進み、assets 内を探し続けないでください - obfuscate/tree-shaking 後に名前が短くなる:現象——スナップショット内の関数名/クラス名がすべて
a/bなどの短い名前になっているか、完全に消えています。原因——--obfuscateによる名前の難読化と、tree-shaking による未参照コードの削除です。対策——ビルド側に--split-debug-infoのマッピングファイルがあれば、直接照合します(手順 3 の JSON)。マッピングがなければ、strings blob の文言/URL を手がかりに相互参照して呼び出しチェーンを推定します。動的解析では、主要な関数を hook して動作を観察し、補完します - エンジンとアプリのコードの混同:現象——libflutter.so の
dart::内部関数や_kDartVmSnapshot*セグメント(dart:core/io 標準ライブラリ)の分析に深入りしてしまい、時間を浪費します。原因——エンジン層と VM スナップショットはすべての Flutter アプリに共通するコードであり、アプリのコードではありません。対策——アプリのコードは libapp.so の_kDartIsolateSnapshot*のみを分析します。libflutter.so から取得するのは VM のエクスポート情報のみです(手順 4/5) - frida のバージョン不一致:現象——
frida-ps -Uでプロトコルエラー/unable to communicate が発生します。原因——ホスト側の frida-tools とデバイス側の frida-server のバージョンが一致していないか、アーキテクチャが異なります。対策——[[re-frida]] のツール準備に厳密に従います。frida --versionを確認して同じバージョンの frida-server をダウンロードし、正しいアーキテクチャ(arm64/arm/x86_64)を選びます - スナップショットのバージョンと解析スクリプトの不一致:現象——パーティションのオフセットがずれたり、strings blob の内容が文字化けしたり、解析が中断したりします。原因——Dart のバージョンによってスナップショットのレイアウトが異なります(バージョン文字列はヘッダーで確認できます)。手作業やサードパーティ製スクリプトは特定のバージョンのみを対象としている場合があります。対策——まずヘッダーのバージョン文字列を読み取ります。バージョンごとに解析を分岐します。新しすぎるバージョンでは、汎用的な方法(magic のスキャン+パーティションの意味を推定)で補います
- hook で読み取った Dart 文字列が文字化けする:現象——Dart オブジェクトのポインターを hook できても、出力が文字化けします。原因——Dart 文字列は C 文字列ではありません(オブジェクトポインターの +7 オフセットに 4 バイトの Smi エンコードされた長さがあり、右に 1 ビットシフトすると実際の長さになります。+15 から UTF-8 データが始まります。このオフセットはコミュニティでの実測値です)。対策——このレイアウトに従って読み取り、長さの上限を設ける readDartString ユーティリティ関数を書きます(レイアウトの詳細は [[re-hybrid-app]] の落とし穴の項目を参照してください)。オフセットは Dart のバージョンによって変動します——
gen_snapshot --print-object-layout-toで出力した信頼できるオフセット、または実測値を基準にし、異なるバージョンで固定オフセットをそのまま使わないでください - オブジェクトプールのデシリアライズで VLE を固定幅として読み取る:現象——blutter が custom engine/構造的に並べ替えられたスナップショットで失敗(
No cluster defined for cid Nまたは空の出力)した後、自作のパーサーで読み取ると ref がずれ、文字列がすべて乱れます。原因——Dart AOT のオブジェクトプールはタグ付きエントリーの配列です(X27 がプールのベースアドレスを保持し、LDR Xn,[X27,#off]で off/8 番目のスロットを読み込みます)。埋め込み領域では、各エントリーが 1 バイトの type を持ちます(旧バージョンではTypeBits = eb & 0x7f、PatchableBit = 0x80)。type 0(kTaggedObject)は VLE ref(可変長)を持ち、**type 1(kImmediate)のペイロードは固定幅の整数(intptr_t であり、VLE ではありません)**です(よくある落とし穴)。符号なし VLE は各バイトのデータが 7 ビットで、最終バイトの bit7 が 1 になります(5→0x85、27673→19 58 81)。なお、上記の type 番号/ビット幅は Dart 2.9 時代のレイアウトです。現行バージョンでは TypeBits のビット数と列挙順が変わっているため、解析前にスナップショットのバージョンに応じたレイアウト表を確認し、異なるバージョン間で使い回さないでください。対策——先頭の length を VLE で読み取った後、各 type に従ってデコードします。slot_offset = idx × 8 です。全体を信頼する前に、既知の ref→offset を 2~4 個使って検証します。アンカーが一致しない場合は、スキャン範囲を絞って再試行します。オブジェクトプールは、符号のないバイナリのシンボルテーブルです——pool_offset → {それを読み込むすべての PC} → {所属する関数}と辿れば、「どの関数が文字列 ref N に触れるか」を静的に特定できます(出典:reverse-skills(inliver233)、MIT) - strings で中国語の文言が見つからない:現象——libapp.so に
strings/grep を実行しても、中国語/CJK/絵文字のアプリ文言が見つからず、手がかりが途切れます。原因——Dart スナップショットには 2 種類の文字列ストレージがあります。OneByteString(Latin-1/UTF-8。通常の grep で検索可能)と TwoByteString(UTF-16LE)です。中国語/CJK/絵文字のほとんどは TwoByteString に格納されているため、UTF-8 のバイト列で検索しても、UTF-16LE で格納された文字列には当然一致しません。CID 番号は Dart のバージョンによって変動します(旧バージョンでは 85/86、現行 3.x では 93/94/95 など)。CID を固定せず、スナップショットのクラス表から動的に読み取ります。対策——対象の文字列を UTF-8 と UTF-16LE の両方のエンコードで検索し、オフセット+エンコード+出現回数を記録します。--dump-utf16で CJK 文字列をすべて出力してから grep します。オブジェクトプールでは関数 Code とそれが読み込む文字列が隣接しているため、シンボルがなくてもプール内の近接性から文字列と関数の対応を推定できます(出典:reverse-skills(inliver233)、MIT) - プールスロットの文字列のクロスリファレンスが見つからない:現象——プール内の文字列のオフセットがわかっているのに、逆アセンブラのクロスリファレンス一覧が空で、参照関数を特定できません。原因——プールの読み出しには、標準的な idiom が 2 種類しかありません。
ADD xR,x27,#page,LSL#12の後にLDR xD,[xR,#off]が続く形式(offset=(page<<12)+off)と、page==0 の場合の直接LDR xD,[x27,#off]です。汎用 xref 解析では、こうした間接アドレス指定を見落としがちです。対策——逆アセンブラを必要としない、元のバイト列に対して 2 種類の idiom を照合するパターンスキャナーを書きます。関数の特定には prologue の特徴(fd 79 bf a9 fd 03 0f aa)を使い、逆方向にスキャンして関数の開始位置を特定します。全体を信頼する前に、既知の読み込み位置を 1 つ使って調整します。「スキャナーで見つからない」≠「読み込み元がない」——標準的でないエンコードの読み込みは見落とされるため、陰性の結果は別の方法でも検証が必要です。ちょうど 1 つの関数だけが読み込む文字列は feature-dedicated であり、機能 UI builder の強い手がかりになります(出典:reverse-skills(inliver233)、MIT) - build/仮想メソッドの callers=0 をデッドコードと誤判定する:現象——build または仮想メソッドに対して BL xref スキャンを実行すると、呼び出し元が 0 件となり、デッドコードと誤判定して分析を打ち切ってしまいます。原因——Dart の build/仮想メソッドは BL 呼び出しではなく、ディスパッチ stub を介して呼び出されます。オブジェクトの cid を読み取り、ディスパッチテーブルを検索して、
blr x30で仮想ディスパッチするため、静的な BL スキャンでは当然見つかりません。対策——callers=0 は通常の状態であり、デッドコードの証拠にはなりません。呼び出し元を見つけるには動的解析が必要です。ディスパッチ stub または対象関数の onEnter でlrとThread.backtrace(Backtracer.ACCURATE)を取得し、ディスパッチポイントを特定します(「while rebuilding dirty elements」フレームはフレームワークのレンダリング経路であり、アプリの builder はすでに戻っています)。stub は cid を x0(下位 16 ビット)に置き、オブジェクトポインターは x1 に置きます。代替手段として、オブジェクトのフィールド @1 にも cid が格納されています。cid を取得した後、静的にmovk xR,#<cid>,lsl#16を検索してコンストラクターを見つけ、その BL 呼び出し元が親 build です。テーブル全体を対象にする場合は、X21 がディスパッチテーブルのベースアドレスであるため、cid→コンストラクターの静的マッピングと組み合わせます(出典:reverse-skills(inliver233)、MIT) - Dart の bool を 0/1 として扱う:現象——AOT コードを逆コンパイルする際に bool を通常の 0/1 整数として読み取り、ロジックが何度分析しても一致しません。原因——Dart AOT には固定レジスター規約があります。X15=shadow-stack、X26=Thread、X27=Pool、X22=null のセンチネル、X28=ヒープベース(圧縮ポインター)、X21=ディスパッチテーブルベースです。Dart の bool は canonical オブジェクトの tagged pointer であり、0/1 ではありません——True は null からの小さいオフセットにあり、False はさらに大きいオフセットにあります(圧縮されていないポインターでは True = null+0x20(下位 16 ビットは通常 …8061 のような値)、False = null+0x30(…8071)。圧縮ポインター(Flutter の release でデフォルト)では True = null+0x10(…8051)、False = null+0x18(…8059)。消費側の
tbz w0,#4(圧縮ポインターでは#3)は、bit=0 の場合に true 分岐に入ります——判定ビットが 0 なのが True です。対策——固定レジスターの意味に従って逆アセンブリを読み取ります(mov x0,x22は Dart の null を返します。標準的な prologue はstp x29,x30,[x15,#-16]!; mov x29,x15、バイト列はfd 79 bf a9 fd 03 0f aaです)。bool のエンコードは命令の出現頻度から特定できます。add xR,x22,#0x20(圧縮ポインターでは#0x10)は数千回出現し(True)、add xR,x22,#0x30(圧縮時は#0x18)も同様です(False)。オフセット/判定ビットはポインター圧縮とバージョンによって変動するため、実測値または--print-object-layout-toを基準にします。Thread のグローバルG = Thread[x26+0x80]は、フィールドをオフセットに従って読み取ります。割り当て thunk のパターンはmov x2,#imm; movk x2,#cid,lsl16; ldr x4,[x26,#0x228]; br x4です(出典:reverse-skills(inliver233)、MIT) - patch で false を物化する際に movz w0,#0 を使うとクラッシュする(SIGILL):現象——静的 patch または Frida の実行時メモリー書き込みで
movz w0,#0(バイト列 52800000)を使って「偽」を物化すると、実行時にシグナル 4(SIGILL)でクラッシュします。原因——Dart bool は tagged pointer であり、0 は不正な immediate tag です。対策——True の物化にはadd x0,x22,#0x20(バイト列 c0820091)、False には+0x30を使います(圧縮ポインターでは True が+0x10、False が+0x18)。このルールは静的 patch と Frida の実行時メモリー書き込みの両方に適用されます。Dart bool を書き込む場合は、必ず canonical アドレスを書き込みます。エンコードはポインター圧縮モードとバージョンによって変動するため、実測値を基準にします(出典:reverse-skills(inliver233)、MIT) - ダイアログの patch が効かない(三つの発生源を切り分ける):現象——showDialog の呼び出しをすべて NOP してもダイアログが表示されたり、約 60 秒後に定期的に表示されたりして、何度もやり直すうちに時間を浪費します。原因——Flutter のダイアログの発生源は 3 つだけです。誤った切り分けが最大の浪費につながります。(a) 明示的な showDialog BL、(b) 未捕捉例外——MissingPluginException → FlutterError.onError/zone onError → showDialog BL を経由しない非同期ダイアログ、(c) 整合性チェックの集約ゲート——約 7 個のチェックを 1 つの集約関数で OR し、いずれかが true ならダイアログを表示します。対策——logcat と Dart スタックフレームに MissingPluginException/Unhandled Exception があり、定期的に表示される場合は、発生源 (b) です。正しい対処は fault-body コンストラクターを切断することです(body を構築する条件分岐を、無条件でスキップするように変更します)。グローバル handler に表示内容がなくなるため、ダイアログの NOP はやめてください。ダイアログの文言 → プール ref → body builder → 呼び出し元の順に追跡し、callers=0(cid ディスパッチ)で showDialog BL がない場合も、発生源は (b) です。発生源 (c) では、集約関数を探します(多数の check*/get* チャネルメソッド文字列と条件 → ダイアログ構築を参照する関数)。その集約関数の失敗分岐(
CMP W0,W22; B.EQ fail)または entry-null ダイアログ構築 helper を NOP します。あるデバイスでは patch が有効でも別のデバイスでは効かない場合は、文言の内容から新たに起動したチェックの種類を特定します(たとえば、検証メッセージの文言は環境/エミュレーターのチェックに対応します)(出典:reverse-skills(inliver233)、MIT) - kernel 形式の magic を ASCII の
KERNELと誤認する:現象——「ファイルヘッダーは KERNEL」と考えて grep/xxd でkernel_blob.binを判定したり、コンテナー内をこの文字列で検索して kernel ファイルを探したりしても、見つからないか、別のものがヒットします。原因——Dart kernel バイナリの magic は数値0x90abcdef(ソースコードruntime/vm/kernel_binary.hのkMagicProgramFile)です。ファイル内ではリトルエンディアンのバイト列ef cd ab 90として表示され、ASCII 文字列ではありません。ヘッダーは 8 バイト(magic + formatVersion)で、その後に形式バージョン番号が続き、バージョン判定に使えます。対策——4 バイトのマジックナンバーで判定し、文字列検索は使わないでください - ASCII の
SNAPSHOTで AOT スナップショットを探す:現象——実際の libapp.so でgrep SNAPSHOT/data.find(b'SNAPSHOT')を実行しても一件も見つからず、「スナップショットがない」と判断したり、文字列からオフセットを推測したりします。原因——Dart VM スナップショットのヘッダーは、数値 magic0xdcdcf5f5(4B)+ length(8B)+ kind(8B)からなる 20 バイトの基本ヘッダーです。ASCII のSNAPSHOTは存在せず、固定の 12 バイトのバージョン文字列もありません。ヘッダーの後に続くサブ blob のレイアウトは、バージョン依存の内部シリアライズ実装であり、安定した形式として扱うことはできません。対策——0xdcdcf5f5(リトルエンディアンではf5 f5 dc dc)で位置を特定し、length/kind を読み取ります(kindがfull-aotなら AOT スナップショットです)。正確に解析する場合は、対象 SDK バージョンのruntime/vm/snapshot.hとシリアライザーの実装を参照します。機械語の本体は独立した命令セグメントにあります - シンボルのない数万個の関数をどこから調べるかわからない:現象——libapp.so にシンボルがなく、関数が約 2 万個あるため、逆アセンブリを直接調べても手がかりが見つかりません。原因——シンボルがない場合、関数境界、文字列の帰属、呼び出し関係をすべて静的に再構築する必要があり、優先順位を決める手がかりがありません。対策——prologue の特徴バイト(
fd 79 bf a9 fd 03 0f aa)を使い、.text 内を逆方向にスキャンします(ウィンドウは約 0x6000–0x8000 バイト)。まず関数境界を復元します。関数ごとに特徴ベクトルを作成します。アクセスする文字列の集合(最も重要な手がかり)、BL のターゲット/呼び出し元の fan-in((word>>26)==0b100101で imm26 をデコード)、gate 後の分岐点(TBZ/TBNZ/B.EQ)、null 比較(CMP W0,W22/MOV X0,X22)、bool 戻り値のフラグです。4 種類のランキングで 2 万件を短い候補リストに絞ります。キーワード文字列の候補リスト → アクセス数(キーワードに該当する文字列を 2 個以上参照 → orchestrator)→ 2 つのアンカー集合の積集合(タイトル文字列と本文文字列の両方を参照する関数集合の積集合 → 正確な builder、0 個または 1 個)→ BL 呼び出し fan-in を上向きに反復します。caller の数はリスク指標です——数が多い(状態を共有する関数)場合は patch の対象にせず、数が 1 の専用 handler は安全な対象です。cmp と条件分岐の数が多い関数を集計してスコア化すると、整合性ゲート/集約述語を特定できます(出典:reverse-skills(inliver233)、MIT)
コメント
Markdown 対応