Flutter 逆向基础
Flutter 逆向基础
本文适合
CTF 逆向工程 入门学习者。学完你能:判断一个 APK/移动应用是否为 Flutter,提取 libapp.so 和 libflutter.so,并用字符串、Blutter 或动态 hook 定位 Dart 业务逻辑
Flutter 是 Google 开发的跨平台 UI 框架,使用 Dart 语言编写。Flutter 应用编译后生成 Dart AOT(Ahead-of-Time)快照,传统的反汇编工具无法直接分析。CTF 中 Flutter 逆向题通常要求选手从 AOT 快照中提取和分析业务逻辑。
一句话判断
如果 APK 里同时出现 libflutter.so、libapp.so 和 assets/flutter_assets/,而 Java 层几乎没有业务逻辑,就要按 Flutter/Dart AOT 逆向处理。
Flutter 逆向的关键不是在 jadx 里硬找 Java 代码,而是把 Dart 业务逻辑从 libapp.so、Dart snapshot、字符串和运行时行为中找出来。
题目中常见信号
- APK 中有
lib/*/libflutter.so和lib/*/libapp.so。 assets/flutter_assets/下有AssetManifest.json、FontManifest.json、kernel_blob.bin或资源文件。- jadx 只能看到 FlutterActivity、GeneratedPluginRegistrant,业务代码很少。
strings libapp.so能看到 Dart 类名、函数名、路由、提示语、URL、错误信息。- 常规 native 反编译显示大量 Dart runtime/对象池相关代码。
- 题目提示“Flutter”“Dart”“跨平台 App”或 UI 看起来像 Flutter。
核心概念
Flutter Release 应用通常把 Dart 代码 AOT 编译进 libapp.so。这个文件虽然是 ELF shared object,但内部包含 Dart 对象、函数元数据和编译后的机器码。传统 native 反编译能看到指令,却不一定能直接恢复 Dart 语义。
分析时要分三层:
- 应用识别层:确认是否 Flutter,选对 ABI 的
libapp.so和libflutter.so。 - 静态提取层:用字符串、Blutter/Doldrums、IDA 脚本恢复类、函数、对象池信息。
- 动态验证层:用 Frida/reFlutter/logcat 观察运行时函数、网络、字符串和输入输出。
不同 Flutter/Dart 版本的 snapshot 格式会变,工具失败时要回到字符串、资源和动态行为。
最小分析流程
- 解包 APK,确认
libflutter.so、libapp.so和flutter_assets。 - 选择目标设备架构对应的 ABI,例如
arm64-v8a。 - 对
libapp.so同时搜 ASCII 和 UTF-16 字符串,找flag、check、wrong、success、URL、路由名。 - 用 Blutter/Doldrums 解析
libapp.so,生成函数、类、IDA 脚本或对象列表。 - 在 IDA/Ghidra 中加载辅助脚本,定位含关键字符串或函数名的 Dart 函数。
- 若静态困难,用 reFlutter 或 Frida 观察运行时网络、输入校验函数和解密后字符串。
最小验证示例
apktool d target.apk -o decoded
find decoded/lib -name "libflutter.so" -o -name "libapp.so"
ls decoded/assets/flutter_assets
strings decoded/lib/arm64-v8a/libapp.so | grep -Ei "flag|check|wrong|correct|password|verify" | head再搜索 UTF-16:
from pathlib import Path
data = Path("decoded/lib/arm64-v8a/libapp.so").read_bytes()
for kw in ["flag", "password", "verify", "correct"]:
if kw.encode("utf-16-le") in data:
print("utf16 hit:", kw)如果能定位关键字符串,就把对应 libapp.so 和 libflutter.so 交给 Blutter 生成函数信息,再回到 IDA 查引用。
常见利用 / 解题路线
路线总览:
- 字符串定位路线:搜 ASCII/UTF-16 关键字,从对象池或引用定位 Dart 校验函数。
- Blutter 路线:用
libapp.so + libflutter.so恢复类、函数和 IDA 标注,再分析业务逻辑。 - 资源联动路线:检查
flutter_assets中的 JSON、图片、数据库、密文和配置。 - 网络/证书路线:用 reFlutter 绕过证书固定,观察 API 请求和返回数据。
- 动态 hook 路线:hook
libapp.so函数地址、字符串比较、网络库或 Dart 层关键函数。 - 版本回退路线:工具不支持当前 Flutter 版本时,改用字符串、资源、logcat 和运行时行为做最小闭环。
Flutter 架构概述
Flutter 应用的核心架构:
┌─────────────────────────┐
│ Dart 业务代码 │ <- 逆向分析的目标
├─────────────────────────┤
│ Flutter Framework │ <- Dart 编写的 UI 框架
├─────────────────────────┤
│ Flutter Engine (C++) │ <- Skia 渲染引擎、Dart VM
├─────────────────────────┤
│ 平台嵌入层 │ <- Android/iOS 原生代码
└─────────────────────────┘关键点:业务逻辑在 Dart 层,而不是传统的 Java/Kotlin/Swift/ObjC 层。
Dart AOT 编译
Flutter Release 模式使用 AOT 编译,将 Dart 代码编译为原生机器码。
产物位置(Android):
lib/
├── arm64-v8a/
│ ├── libflutter.so # Flutter Engine
│ └── libapp.so # Dart AOT 编译产物(目标文件)
├── armeabi-v7a/
│ ├── libflutter.so
│ └── libapp.so
└── x86_64/
├── libflutter.so
└── libapp.solibapp.so 包含所有 Dart 业务代码编译后的机器码和 Dart 对象快照。
APK 结构分析
# 解包 APK
apktool d target.apk -o target_apk
# 查看 lib 目录
ls target_apk/lib/arm64-v8a/
# 检查是否是 Flutter 应用
# 方法1: 检查 libflutter.so
ls target_apk/lib/*/libflutter.so
# 方法2: 检查 assets 目录
ls target_apk/assets/flutter_assets/
# 方法3: 检查 Dart snapshot
file target_apk/lib/arm64-v8a/libapp.so
# 输出应包含 "ELF" 或显示为共享库
# 查看 Dart snapshot 头信息
xxd target_apk/lib/arm64-v8a/libapp.so | head -20Flutter Snapshot 格式
Dart Snapshot 有两种类型:
App Snapshot:包含序列化的 Dart 对象堆。主要用于 Debug 模式。
AOT Snapshot:包含编译后的机器码和元数据。Release 模式使用。
Snapshot 结构:
┌──────────────────┐
│ Snapshot Header │ <- 魔数、版本、标志位
├──────────────────┤
│ Symbol Table │ <- 类名、函数名、字符串
├──────────────────┤
│ Object Pool │ <- 常量、类型信息
├──────────────────┤
│ Stubs │ <- 调用桩代码
├──────────────────┤
│ Code Sections │ <- 编译后的机器码
└──────────────────┘工具介绍
reFlutter
reFlutter 可以对 Flutter 应用进行重打包,修改 Flutter Engine 以 dump 运行时信息。
# 安装
pip install reflutter
# 使用:重打包 APK,注入 dump 功能
reflutter target.apk
# 生成的 APK 运行后会 dump:
# - Dart Snapshot(可用于进一步分析)
# - SSL 流量(绕过证书固定)
# 运行重打包后的 APK,连接 ADB 查看输出
adb logcat -s "REFLUTTER"Blutter
Blutter 是专门用于分析 Dart AOT Snapshot 的工具。
# 克隆 Blutter
git clone https://github.com/aspect-security/blutter.git
cd blutter
# 安装依赖
pip install -r requirements.txt
# 准备文件:需要 libapp.so 和 libflutter.so
mkdir input
cp /path/to/libapp.so input/
cp /path/to/libflutter.so input/
# 运行分析
python blutter.py input output
# 输出目录包含:
# - asm/ 反汇编的 Dart 函数
# - pp.txt 类和函数的详细信息
# - ida_script/ IDA Pro 辅助脚本
# - objs.txt 对象列表Doldrums
Doldrumps 是另一个 Dart AOT 分析工具。
git clone https://github.com/aspect-security/doldrums.git
cd doldrums
# 使用
python3 doldrums.py /path/to/libapp.so
# 输出包含:
# - 类层次结构
# - 函数列表
# - 字符串常量
# - 反编译的 Dart 代码(近似)DartRebel
# 使用 DartRebel 进行更深入的分析
# 需要配合 IDA Pro 使用IDA Pro 分析技巧
使用 Blutter 生成的 IDA 脚本辅助分析:
# 在 IDA 中加载 libapp.so
# 然后运行 Blutter 生成的脚本
# File -> Script File -> select ida_script/blutter_ida.pyDart 函数的特征:
# Dart 函数通常以下列模式开头:
# - 保存寄存器
# - 检查栈溢出
# - 加载对象池指针
# x86_64 示例
push rbp
mov rbp, rsp
sub rsp, 0x20
; 检查栈溢出
cmp rsp, [r15 + STACK_LIMIT_OFFSET]
jl stack_overflow_handler
; 加载对象池
mov r14, [r15 + OBJECT_POOL_OFFSET]Dart 字符串特征:
# Dart 字符串在内存中的布局:
# [对象头][长度][哈希][UTF-16 数据]
# 对象头通常包含 Dart 类型标记
# 搜索字符串模式
# 在 IDA 中搜索 UTF-16 编码的字符串代码示例:自动化 Flutter 分析
import struct
import re
import sys
def analyze_dart_snapshot(libapp_path):
"""分析 Dart AOT Snapshot 的基本信息"""
with open(libapp_path, "rb") as f:
data = f.read()
print(f"文件大小: {len(data)} bytes")
# 检查 Snapshot 魔数
# Dart Snapshot 有不同的魔数格式
magic_patterns = {
b'\x89\x44\x41\x52': "Dart AOT Snapshot",
b'\x53\x6e\x61\x70': "Dart App Snapshot",
}
for magic, name in magic_patterns.items():
if data[:len(magic)] == magic:
print(f"Snapshot 类型: {name}")
break
# 提取可读字符串(UTF-8 和 ASCII)
strings = []
current = bytearray()
for byte in data:
if 0x20 <= byte < 0x7f:
current.append(byte)
else:
if len(current) >= 6: # 最小字符串长度
try:
s = current.decode("utf-8", errors="ignore")
if s.strip():
strings.append(s)
except:
pass
current = bytearray()
# 过滤和分类字符串
class_names = []
function_names = []
dart_strings = []
for s in strings:
# Dart 类名通常以字母开头,包含点号
if re.match(r'^[A-Z][a-zA-Z0-9.]*$', s) and '.' in s:
class_names.append(s)
# 函数名
elif re.match(r'^[a-z_][a-zA-Z0-9_]*$', s) and len(s) > 3:
function_names.append(s)
# 普通字符串(可能是用户代码中的字符串常量)
elif len(s) > 10 and not s.startswith(('_', '.', '/', 'lib')):
dart_strings.append(s)
print(f"\n找到的类名 ({len(class_names)}):")
for name in sorted(set(class_names))[:30]:
print(f" {name}")
print(f"\n找到的函数名 ({len(function_names)}):")
for name in sorted(set(function_names))[:30]:
print(f" {name}")
print(f"\n找到的字符串常量 ({len(dart_strings)}):")
for s in sorted(set(dart_strings))[:30]:
print(f" {s}")
return {
"classes": list(set(class_names)),
"functions": list(set(function_names)),
"strings": list(set(dart_strings))
}
def search_flag_patterns(data):
"""搜索可能的 flag 格式"""
patterns = [
rb'flag\{[^}]+\}',
rb'FLAG\{[^}]+\}',
rb'ctf\{[^}]+\}',
rb'CTF\{[^}]+\}',
]
findings = []
for pattern in patterns:
matches = re.findall(pattern, data)
for m in matches:
try:
findings.append(m.decode("utf-8"))
except:
findings.append(m.hex())
return findings
# 使用示例
# if len(sys.argv) > 1:
# result = analyze_dart_snapshot(sys.argv[1])
#
# with open(sys.argv[1], "rb") as f:
# data = f.read()
# flags = search_flag_patterns(data)
# if flags:
# print(f"\n[!] 可能的 flag: {flags}")代码示例:批量搜索关键字符串
import os
import re
def search_flutter_strings(libapp_path, keywords):
"""在 libapp.so 中搜索特定关键字"""
with open(libapp_path, "rb") as f:
data = f.read()
results = {}
for keyword in keywords:
# 搜索 ASCII 版本
ascii_matches = [m.start() for m in re.finditer(keyword.encode(), data)]
# 搜索 UTF-16 版本(Dart 内部字符串)
utf16_keyword = keyword.encode("utf-16-le")
utf16_matches = [m.start() for m in re.finditer(utf16_keyword, data)]
results[keyword] = {
"ascii_offsets": ascii_matches,
"utf16_offsets": utf16_matches,
"total": len(ascii_matches) + len(utf16_matches)
}
return results
# 使用示例
keywords = [
"password", "flag", "secret", "admin", "login",
"api_key", "token", "encrypt", "decrypt", "verify",
"http://", "https://", ".com", ".org",
"username", "auth", "check", "validate"
]
# results = search_flutter_strings("path/to/libapp.so", keywords)
# for kw, info in results.items():
# if info["total"] > 0:
# print(f"'{kw}': {info['total']} 个匹配")
# if info["ascii_offsets"]:
# print(f" ASCII 偏移: {info['ascii_offsets'][:5]}")
# if info["utf16_offsets"]:
# print(f" UTF-16 偏移: {info['utf16_offsets'][:5]}")动态分析
# 使用 Frida hook Dart 函数
# 需要 rooted 设备
# 通用 Flutter hook 脚本
frida -U -f com.target.app -l flutter_hook.js
# flutter_hook.js 内容示例:
# // hook Dart 函数调用
# Interceptor.attach(Module.findExportByName("libapp.so", "_kDartVmSnapshotInstructions"), {
# onEnter: function(args) {
# console.log("Dart function called");
# }
# });常见失败原因
- 用 jadx 找不到业务就放弃:Flutter 业务通常不在 Java 层,而在 Dart AOT 的
libapp.so。 - 只用 Ghidra 直接反编译
libapp.so:它是 Dart AOT 产物,最好配合 Flutter 专用工具和对象池信息。 - 忽略
libflutter.so:Blutter 等工具常需要同版本 engine 辅助解析。 - 只搜 ASCII:Dart 字符串可能以 UTF-16 或对象形式存在,要同时搜多种编码。
- ABI 选错:设备跑 arm64,你分析 x86 或 armeabi-v7a 可能和运行时不一致。
- 工具失败就结束:不同 Flutter 版本格式变化快,换版本工具、动态 hook 或先从资源/字符串入手。
- 把 Flutter Engine 当业务逻辑:
libflutter.so主要是引擎,业务重点通常在libapp.so。
迷你案例
题目 APK 的 Java 层只有 MainActivity extends FlutterActivity。解包后发现:
lib/arm64-v8a/libflutter.so
lib/arm64-v8a/libapp.so
assets/flutter_assets/AssetManifest.json对 libapp.so 搜字符串:
strings libapp.so | grep -Ei "wrong|correct|verify"得到 verifyInput 和 Wrong password。用 Blutter 生成 IDA 脚本后,verifyInput 附近引用了一个常量数组和异或逻辑。提取数组逆运算得到正确输入。这个案例的闭环是:识别 Flutter -> 提取 libapp/libflutter -> 字符串定位 Dart 函数 -> 工具标注 -> 算法还原。