
1. 从Flutter APK到Dart源码一次完整的逆向实战最近在分析一个Flutter应用时遇到了一个典型场景拿到手的只有一个APK文件但我们需要理解其核心业务逻辑甚至修复其中的某些问题。传统的Android逆向工具如Jadx、JEB在面对Flutter应用时往往只能看到引擎层和平台通道的代码真正的Dart业务逻辑被编译成了神秘的二进制文件藏在libflutter.so旁边或者isolate_snapshot_data里让人无从下手。这就像拿到一个黑匣子你知道里面有宝藏却找不到开锁的钥匙。这正是blutter工具大显身手的地方。它不是一个万能钥匙而是一套专门针对Flutter引擎AOTAhead-Of-Time编译模式的逆向工具链核心目标是将编译后的Dart二进制代码如libapp.so反编译成可读性相对较高的伪C代码进而帮助我们理解程序流程。结合强大的静态分析工具IDA Pro我们就能深入Flutter应用的腹地。本文将以一次模拟的ACTFApplication Capture The Flag一种常见的安全竞赛解题过程为线索手把手带你走通从Flutter APK中提取Dart逻辑的完整路径。你会发现这个过程虽然有些繁琐但每一步都有清晰的逻辑和工具支撑。2. 逆向环境搭建与核心工具解析工欲善其事必先利其器。Flutter逆向对工具链有特定要求盲目安装最新版往往会导致兼容性问题。以下是我经过多次踩坑后总结出的稳定环境配置。2.1 工具选型与版本控制核心工具是blutter和IDA Pro。blutter本身是一个Python脚本集合它严重依赖特定版本的IDA Pro插件和Python库。IDA Pro: 推荐使用IDA Pro 7.7。这是目前与blutter兼容性最广、社区资料最丰富的版本。更高版本如8.0以上的API变动可能导致插件运行异常。IDA 7.7的稳定性和丰富的插件生态使其成为逆向工程的“瑞士军刀”。Python: IDA Pro 7.7 内置的是Python 2.7。这是关键你必须确保你的blutter脚本运行在Python 2.7环境下。在Windows上你可能需要单独安装Python 2.7并正确配置环境变量或者直接使用IDA自带的Python环境。blutter: 从GitHub获取最新代码。它主要包含两部分用于提取Dart信息的Python脚本blutter.py和用于解析Dart AOT文件的IDA插件dart_ida文件夹内的内容。为什么是这样一个“老旧”的组合因为Flutter的AOT编译模式及其内部对象表示在相当长一段时间内是稳定的而blutter正是针对这套稳定结构开发的。新版本IDA的API变化属于“外部因素”反而会引入不必要的复杂性。在逆向领域稳定可复现的环境比追求最新版更重要。2.2 实战环境配置步骤假设我们的工作目录是D:\FlutterReverse。获取blutter:cd D:\FlutterReverse git clone https://github.com/worawit/blutter.git克隆后目录结构应包含blutter.py、dart_ida/等。安装IDA Pro 7.7按常规流程安装记住安装路径例如C:\IDA_7.7。部署IDA插件将blutter/dart_ida文件夹下的所有文件如dart_ida.py,dart_ida.plw等复制到IDA Pro的插件目录。对于IDA 7.7插件目录通常是C:\IDA_7.7\plugins。这个插件的作用是让IDA能够识别Dart二进制文件的结构解析函数、类、字符串等元数据。准备Python环境如果你系统没有Python 2.7请安装它。安装必要的Python库。在命令行中切换到Python 2.7的环境然后进入blutter目录安装依赖# 确保你使用的是python2.7 python -m pip install protobuf3.20.3protobuf库用于解析Flutter引擎内部的协议缓冲区数据这是提取元信息所必需的。注意一个常见的坑是Python环境混乱。如果你安装了多个Python版本如Python 3.11和Python 2.7在执行blutter.py时务必使用绝对路径指定Python 2.7例如C:\Python27\python.exe blutter.py或者在命令行中先激活Python 2.7环境。3. 目标APK拆解与快照提取现在我们有一个待分析的Flutter APK文件target_app.apk。第一步是将其解包找到核心的Dart编译产物。3.1 解包APK与寻找核心文件APK本质上是一个ZIP压缩包。你可以使用任何解压工具如7-Zip将其解压或者使用命令行unzip target_app.apk -d target_app解压后进入target_app目录我们的目标在lib文件夹下。Flutter支持多种CPU架构ABI通常会在lib/armeabi-v7a、lib/arm64-v8a、lib/x86_64等子目录下存放对应架构的本地库。我们需要找到以下关键文件libflutter.so: Flutter引擎库。libapp.so:这是最重要的文件它包含了你的Dart业务代码经过AOT编译后的本地代码。在较新的Flutter版本中这个文件可能被合并或命名不同有时Dart代码直接内嵌在libflutter.so中。这是blutter的主要处理对象。isolate_snapshot_data、vm_snapshot_data这些是Dart VM的快照数据包含了Dart代码的元信息、字符串常量、类结构等。blutter需要它们来还原符号信息。以lib/arm64-v8a为例我们通常将libapp.so和两个快照文件如果存在复制到我们的工作目录。cp target_app/lib/arm64-v8a/libapp.so D:\FlutterReverse\ cp target_app/lib/arm64-v8a/isolate_snapshot_data D:\FlutterReverse\ cp target_app/lib/arm64-v8a/vm_snapshot_data D:\FlutterReverse\3.2 使用blutter.py提取元信息有了核心文件就可以运行blutter.py脚本了。这个脚本不直接反编译代码它的作用是充当一个“信息提取器”和“IDA脚本生成器”。在工作目录下执行python blutter.py libapp.so或者如果快照文件不在当前目录或命名不同需要指定路径python blutter.py libapp.so -i isolate_snapshot_data -v vm_snapshot_data这个命令会做以下几件事解析快照数据读取isolate_snapshot_data和vm_snapshot_data从中提取出Dart程序的类Class、函数Function、字符串String等所有元数据的偏移量和名称。生成IDA脚本在libapp.so的同目录下生成一个名为libapp.so.py的Python脚本。这个脚本是为IDA Pro量身定做的它包含了所有提取到的符号信息。脚本里会有一系列idc.set_name(offset, “ClassName.methodName”)这样的语句目的是在IDA中给对应的函数偏移地址赋予有意义的名称。输出文本信息在控制台它会输出解析出的类列表、方法列表等摘要信息。这对于快速了解程序结构非常有帮助。实操心得如果执行blutter.py时提示找不到isolate_snapshot_data但APK里明明有请检查文件名是否完全一致。有时文件可能位于assets/flutter_assets/目录下。另外确保你对这些文件有读取权限。如果blutter.py报错关于protobuf请确认你是在Python 2.7环境下安装的protobuf 3.20.x版本版本不匹配是常见错误源。4. 在IDA Pro中重建Dart程序世界至此我们准备好了原材料libapp.so和设计图libapp.so.py。接下来就是在IDA Pro这个“工地”上按照设计图把大楼可读的程序结构盖起来。4.1 初始加载与基础分析用IDA打开libapp.so启动IDA Pro 7.7将libapp.so文件拖入。在加载对话框中保持默认选项即可ELF for ARM 64-bit。IDA会开始初始的自动分析这个过程可能会花点时间。等待分析完成在IDA底部的输出窗口等待“Autoanalysis has been finished”出现。此时IDA完成了基础的代码识别和控制流分析但所有的Dart函数都还是无名的地址比如sub_123456。4.2 运行blutter生成的脚本这是将混乱的二进制代码转化为可理解程序的关键一步。在IDA中点击菜单栏的File-Script file...(或直接按AltF7)。浏览并选择之前由blutter.py生成的libapp.so.py脚本。点击“打开”。IDA会执行这个脚本。执行过程中你会在IDA的输出窗口看到大量的重命名信息滚动例如Renaming 0x123456 to MyHomePage.build Renaming 0x234567 to _MyHomePageState._incrementCounter ...这个脚本执行完毕后IDA左侧的“Functions”窗口将会焕然一新。原本的sub_XXXXXX会被有意义的Dart类和方法名取代比如MyApp.build、LoginPage._validatePassword等。字符串常量也会被正确识别和标注。4.3 深入静态分析定位关键逻辑现在我们面对的不再是冰冷的机器码而是一个有了“姓名”的程序。假设我们在解一道ACTF题目Flag隐藏在某个校验函数里。搜索字符串在IDA中按ShiftF12打开“Strings”窗口。由于blutter已经恢复了字符串你可以直接在这里搜索关键词如“flag”、“correct”、“error”、“key”、“secret”等。双击找到的字符串IDA会跳转到该字符串在数据段.rodata的引用位置。交叉引用分析在字符串所在的数据行按X键或右键选择Jump to xref会列出所有引用了这个字符串的代码位置。这通常能直接把你带到核心的判断逻辑函数。分析关键函数进入引用了关键字符串的函数比如validateFlag。IDA的图形视图按空格键切换在这里非常有用。你可以看到清晰的控制流图条件判断if-else、循环loops都一目了然。识别参数和变量Dart函数调用遵循一定的ABI。在ARM64架构上前几个参数通常存放在X0, X1, X2等寄存器中。你需要结合上下文推断哪个寄存器或栈位置对应的是输入的字符串指针、长度等。理解算法查看汇编指令。常见的加密或校验操作如XOR、加减、比较都有对应的汇编指令。你需要像解谜一样跟踪数据的流动和变换。例如看到一个循环每次从输入字符串中取一个字符与某个常量或另一个数组进行XOR操作然后将结果与预设的数组比较——这很可能就是一个简单的XOR加密。使用Dart IDA插件增强之前安装的dart_ida插件可能会提供一些额外的辅助功能比如更友好地显示Dart对象结构。你可以在IDA的Edit-Plugins菜单下找到它或者某些功能会自动集成到上下文菜单中。踩坑实录有时候blutter生成的脚本并不能恢复所有符号或者恢复的名称是混淆过的如_a、_b。这可能是由于Flutter版本较新、使用了混淆工具如--obfuscate编译选项或快照数据不完整。此时你需要更依赖对控制流和数据流的直接分析。关注函数调用关系、字符串的硬编码值、以及明显的算法模式循环、查表等。不要指望完全还原出原始的Dart源码能理解其核心逻辑即可。5. 模拟ACTF解题逆向一个Flutter校验函数让我们虚构一个简单的ACTF场景。题目描述一个Flutter应用输入一个字符串点击按钮后提示是否正确。我们需要找到正确的Flag。通过上述步骤我们在IDA中找到了一个名为_Validator.validate的函数并定位到它与字符串“Congratulations!”相关。以下是分析过程函数概览在图形视图下该函数有一个明显的分支结构。一个条件判断后分别走向了输出“Congratulations!”和“Try again.”的代码块。分析判断逻辑向上追溯条件判断的来源。发现它依赖于一个布尔值这个布尔值是一个名为_checkFlag的函数返回值。深入_checkFlag进入_checkFlag函数。其控制流显示它首先获取输入字符串的长度并与一个固定值比如0x20即32比较。如果不相等直接返回false。解密循环如果长度相等进入一个循环。循环次数为32。在循环体内观察到以下模式从输入字符串的特定偏移取一个字节比如input[i]。从一个硬编码在.rodata段的字节数组我们命名为secret_array中取一个字节secret_array[i]。将这两个字节进行XOR操作。将XOR的结果与另一个硬编码的字节数组我们命名为target_array中对应位置的字节比较。如果所有32次比较都相等则函数返回true。推导算法这显然是一个逐字节的XOR加密。Flag的加密过程是input[i] ^ secret_array[i] target_array[i]。那么解密获取原始Flag就是input[i] target_array[i] ^ secret_array[i]。提取数据在IDA的数据段中找到secret_array和target_array的起始地址将它们导出来。你可以选中数据区域然后使用IDA的Edit-Export data功能或者手动记录字节值。编写解密脚本使用Python快速计算Flagsecret [0x12, 0x34, 0x56, ...] # 从IDA中复制的secret_array target [0xAB, 0xCD, 0xEF, ...] # 从IDA中复制的target_array flag .join([chr(target[i] ^ secret[i]) for i in range(32)]) print(flag)运行脚本即可得到Flag。这个例子虽然简单但涵盖了Flutter逆向中从定位、分析到解密的完整链条。在实际更复杂的题目中算法可能是AES、RC4、自定义置换等但分析方法是一致的静态分析控制流跟踪数据流识别加密常数和操作模式。6. 进阶技巧与疑难排坑掌握了基本流程后你会遇到更多挑战。这里分享一些进阶经验和常见问题的解决方法。6.1 处理混淆与符号缺失Flutter应用可以通过flutter build apk --obfuscate --split-debug-infodirectory进行混淆。混淆会重命名类、方法和字段使其变成短而无意义的名称但不会改变控制流和算法逻辑。对策即使符号被混淆成a、b、c你仍然可以通过字符串引用、函数调用图以及算法的“形状”来识别关键函数。例如一个函数内部有复杂的循环和位运算并且其输出被用于一个关键判断那它很可能就是校验或加密函数。重点关注那些被多个地方调用的函数以及那些引用了硬编码密钥、IV、常量数组的函数。6.2 应对不同的Flutter版本与打包方式Flutter的编译输出格式可能随版本更新而变化。Dart代码内嵌在某些构建模式下Dart代码可能不再独立为libapp.so而是直接内嵌在libflutter.so中。此时你需要将libflutter.so作为blutter.py的输入文件。分离调试信息使用--split-debug-info参数后会生成单独的app.so.symbols文件。这个文件包含了完整的符号信息。blutter可能无法直接处理这种格式你需要研究如何将这些调试信息与二进制文件关联起来或者尝试其他工具如reFlutter来辅助。版本匹配blutter对Flutter引擎版本敏感。如果遇到解析失败可以尝试在blutter.py的命令行参数中指定引擎版本如果有相关选项或者查看其GitHub issue页面寻找对应版本的解决方案。6.3 动态调试辅助静态分析有时会遇到瓶颈特别是遇到复杂的、动态生成的密钥或流程时。可以考虑动态调试。附加调试器将调试器如IDA Pro本身、GDB附加到运行在模拟器或真机上的Flutter应用进程。你可以在关键函数如输入验证函数的入口处下断点然后单步执行观察寄存器和内存的变化。Frida Hook对于Android平台Frida是强大的动态插桩工具。你可以编写Frida脚本Hook关键的Dart函数直接打印其输入参数和返回值甚至修改其逻辑。这需要你对Flutter的Dart C API有一定了解以定位函数在native层的地址。6.4 心态与思维模式Flutter逆向尤其是较新的版本可能不会一帆风顺。工具链断裂、版本不兼容、混淆加强都是常态。回归本质当工具失效时回归逆向工程的本源阅读汇编代码。ARM/ARM64的汇编指令集是稳定的理解常见的指令模式加载、存储、运算、比较、跳转是根本。大胆假设小心验证对一段代码的功能做出假设比如“这看起来像是一个哈希初始化”然后通过交叉引用、常量值、输入输出关系去寻找证据来验证或推翻它。利用社区blutter的GitHub仓库、逆向工程论坛、CTF write-up是宝贵的学习资源。很多你遇到的问题很可能已经有人遇到并给出了解决方案。逆向Flutter应用就像一场侦探游戏blutter和IDA是你的放大镜和指纹鉴定工具。它们不能直接告诉你答案但能为你揭示出足够多的线索。从解包APK开始到用blutter提取元数据再到IDA中恢复符号并静态分析最后推导出算法逻辑——这条路径为你提供了一套系统性的方法论。记住核心永远是理解程序的行为而不是完美还原源代码。随着Flutter生态的演进工具链也会更新但这份通过静态分析洞察二进制文件逻辑的能力是应对任何编译型语言应用的通用利器。下次当你面对一个Flutter黑盒时不妨就从blutter.py和IDA开始你的探索。