ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter逆向实战:使用Blutter工具拆解ACTF题目与安全分析

Flutter逆向实战:使用Blutter工具拆解ACTF题目与安全分析 1. 项目概述Flutter逆向与Blutter工具实战在移动应用开发领域Flutter凭借其出色的跨平台能力和高效的渲染引擎已经成为构建高质量应用的主流选择之一。然而随着Flutter应用的普及其安全性问题也逐渐浮出水面。对于安全研究人员、逆向工程师或是对应用内部机制充满好奇的开发者而言如何拆解一个Flutter应用分析其业务逻辑、网络协议甚至发现潜在漏洞成为了一项颇具挑战性的任务。传统的Android逆向工具在面对Flutter打包的Dart AOTAhead-Of-Time或JITJust-In-Time代码时往往力不从心看到的只是一堆难以理解的机器码或中间字节码。这正是“Flutter对抗”这一主题的核心我们需要一套专门的方法论和工具链来穿透Flutter构建的这层“外壳”。本次分享的核心就是围绕一个名为Blutter的强大工具并结合ACTF一个常见的CTF竞赛平台中的Flutter逆向题目进行一次从工具使用到实战解题的深度剖析。Blutter并非官方工具而是由安全社区逆向爱好者开发的一款专门用于反编译、分析Flutter应用特别是Android平台的利器。它能将Flutter引擎编译后的Dart代码存储在libapp.so或libflutter.so等库中的snapshot尽可能地还原成可读的Dart源码这为我们理解应用逻辑打开了大门。而ACTF的习题则为我们提供了绝佳的“靶场”让我们能在真实的、经过混淆和保护的Flutter应用上验证和磨练我们的逆向技巧。无论你是刚接触移动安全的新手还是想拓展Flutter方向逆向能力的安全从业者这篇文章都将带你走完一个完整的流程从环境准备、工具配置到使用Blutter进行反编译再到分析反编译后的代码逻辑最终解决ACTF中的具体挑战。我会分享整个过程中的关键步骤、遇到的坑以及我的解决思路希望能为你提供一份可直接参考的实战指南。2. 逆向环境搭建与工具链准备工欲善其事必先利其器。进行Flutter逆向首先需要搭建一个合适的环境。这个环境不仅包括逆向分析工具还包括用于辅助理解、调试的配套环境。2.1 核心工具Blutter的获取与配置Blutter是本次实战的绝对主角。它是一个用Python编写的工具主要功能是解析Flutter引擎的snapshot文件并尝试重建Dart类、方法、字符串等信息。其原理是深入分析Flutter引擎特别是libflutter.so的内存布局和Dart VM的运行时数据结构从而从编译后的二进制中提取出符号和代码逻辑。获取方式通常你可以在GitHub上搜索“blutter”找到相关的开源仓库。由于项目可能更新建议使用Git克隆最新版本。例如在一个准备好的Python环境中执行git clone blutter仓库地址。需要注意的是Blutter的正常运行依赖于特定的Flutter引擎版本信息因为它需要对应的“偏移量”来正确解析snapshot中的数据结构。这些偏移量定义了在二进制文件中各种关键数据结构如类表、函数表、字符串池的存储位置。环境依赖Blutter主要依赖Python 3和一些基础库如protobuf,capstone用于反汇编。使用pip install -r requirements.txt即可安装。一个常见的坑是Python版本不兼容务必使用Python 3.7及以上版本。关键配置Blutter的核心配置在于其“偏移量”文件。不同版本的Flutter引擎其内部数据结构在内存中的偏移是不同的。Blutter通常附带一个offsets目录里面存放了针对不同Flutter引擎版本的JSON配置文件。例如offsets/3.16.9.json对应Flutter 3.16.9引擎。如果你要分析的应用使用的引擎版本不在列表中你就需要自己提取偏移量这是一个更高级的操作通常需要对比不同版本的libflutter.so二进制文件。对于ACTF题目出题人通常会使用一个相对固定或常见的Flutter版本我们可以先尝试使用Blutter自带的偏移量文件。注意Blutter是一个社区工具并非万能。对于高度混淆、定制了Flutter引擎或者使用了非标准编译选项如极致优化、剥离符号的应用其反编译效果可能会大打折扣甚至失败。我们的心态应该是“尽力而为”将其作为辅助理解的重要手段而非完全依赖。2.2 辅助工具集分析、调试与查看除了Blutter一个完整的逆向工具链还包括以下工具APK解包工具如apktool。用于解包Flutter Android应用APK文件获取其中的lib目录包含原生库libapp.so,libflutter.so、assets目录包含flutter_assets资源以及AndroidManifest.xml等文件。命令很简单apktool d target_app.apk -o output_dir。反汇编与静态分析工具IDA Pro或Ghidra。当Blutter反编译出的Dart代码逻辑不全或遇到核心原生逻辑通过MethodChannel或FFI调用时我们需要直接分析libapp.so或libflutter.so。IDA/Ghidra可以帮助我们分析原生ARM/ARM64汇编代码定位关键函数。特别是寻找Dart_Initialize、Dart_CreateSnapshot等VM初始化函数有时能发现有用的字符串或线索。动态调试工具frida。这是移动安全分析的“瑞士军刀”。我们可以编写Frida脚本在应用运行时Hook Dart VM的函数、拦截MethodChannel的通信、甚至修改内存中的数据。对于验证猜测、绕过检查逻辑至关重要。例如可以Hookdart::bin::Builtin_Print来捕获所有Dart层的print输出。网络抓包工具Burp Suite或Charles。用于分析应用与服务器的网络通信了解API接口、数据格式和加密方式。Flutter应用可能使用http、dio等库进行网络请求。文件查看与搜索工具strings、grep、jadx。strings可以快速从二进制文件中提取可读字符串。jadx虽然对Dart代码无效但可以完美反编译APK中的Java/Kotlin代码用于分析平台通道Platform Channel的Android端实现。将上述工具准备好你的逆向工作站就初具雏形了。接下来我们以一个模拟的ACTF Flutter逆向题为例开始实战操作。3. 实战演练拆解一个ACTF Flutter逆向题假设我们拿到一个名为actf_flutter_challenge.apk的题目文件。我们的目标是找到隐藏在应用中的Flag。3.1 初步侦察与文件提取首先使用apktool解包APKapktool d actf_flutter_challenge.apk -o actf_unpacked进入解包目录actf_unpacked我们重点关注以下内容lib/armeabi-v7a/或lib/arm64-v8a/存放原生库。Flutter应用的核心Dart代码通常编译后存在于libapp.so中在较新版本中也可能是libflutter.so中包含或分离。我们同时找到libflutter.so。assets/flutter_assets/存放应用的资源文件如图片、字体、配置文件以及最重要的kernel_blob.bin如果应用是JIT模式编译或vm_snapshot_data/isolate_snapshot_data如果应用是AOT模式编译。在Release版的APK中通常是AOT模式Dart代码已被编译为原生指令资源目录下可能没有完整的Dart源码快照核心逻辑都在libapp.so里。使用strings命令快速扫描libapp.so寻找可能的关键词如“flag”、“ACTF”、“check”、“validate”等strings lib/arm64-v8a/libapp.so | grep -i -E “flag|actf|key|secret”这一步有时能直接发现硬编码的字符串线索。3.2 使用Blutter进行反编译假设通过strings在libflutter.so中发现了版本信息“3.16.9”我们就在Blutter的offsets目录下寻找对应的3.16.9.json文件。如果找到就可以开始反编译。Blutter的基本命令格式是python3 blutter.py [snapshot_file] [output_directory] --offsets [offsets_file]这里的snapshot_file就是包含Dart代码的二进制文件。对于AOT编译的应用我们需要分析libapp.so。但Blutter通常需要指定从libflutter.so中提取的“数据段”或直接分析整合后的snapshot。更常见的用法是Blutter提供了一个脚本或模式能自动从APK或解包目录中识别并提取所需部分。我们进入Blutter工具目录运行python3 blutter.py ../actf_unpacked/lib/arm64-v8a/libapp.so ./output_decompiled --offsets offsets/3.16.9.json或者如果工具提供了针对APK的自动化脚本python3 blutter_apk.py ../actf_flutter_challenge.apk ./output_decompiled执行后工具会开始解析。你会在终端看到大量的日志输出解析类、方法、字符串等。这个过程可能会持续几分钟取决于应用的大小。完成后在./output_decompiled目录下你会看到生成的文件结构通常包括classes/以类为单位生成的Dart文件虽然变量名可能被混淆变成a,b,c但类和方法的结构得以保留。main.dart一个入口文件尝试重建了主要的Dart执行流程。strings.json提取到的所有字符串常量。snapshot_info.jsonsnapshot的元信息。第一个实操心得Blutter的输出代码可读性差异很大。如果应用未做高强度混淆你可能会看到比较清晰的逻辑。但如果做了混淆类名、方法名、局部变量名都会失去意义。此时strings.json和main.dart中的函数调用关系图就显得尤为重要。我们的策略要从“阅读代码”转变为“分析控制流和数据流”。3.3 分析反编译代码与定位关键逻辑打开output_decompiled/main.dart和classes/目录下的文件开始搜索。我们的目标是找到与“验证”、“检查”、“提交”或题目描述相关的逻辑。搜索字符串在IDE或编辑器中全局搜索“flag”、“actf”、“success”、“error”、“wrong”等关键词。重点关注strings.json里面可能包含UI文本、API URL、加密密钥等所有字符串常量。例如你可能发现一个字符串“Congrats! The flag is: %s”这立刻指明了输出Flag的位置。定位按钮事件Flutter应用交互的核心是Widget和事件。寻找ElevatedButton、TextButton的onPressed回调方法。这些回调方法里通常包含着核心的业务逻辑。例如你可能会找到一个名为_checkFlag或onSubmit的方法。分析验证函数找到疑似验证输入的函数后仔细分析其逻辑。它可能将用户输入与一个硬编码的字符串进行比较也可能经过一系列复杂的变换如加密、哈希、自定义算法后再进行比较。例如// 反编译后的代码变量名已混淆 bool a(String b) { String c “a_secure_key_123”; String d encrypt(b, c); // 假设的加密函数 return d “9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08”; }这里就能看出输入经过encrypt可能是AES、自定义XOR等后需要等于一个固定的哈希值看起来像SHA-256。那么我们的任务就变成了逆向这个encrypt函数或者暴力破解。追踪加密与算法如果涉及加密需要找到encrypt、decrypt、hmac、md5、sha256等关键词对应的函数实现。Dart常用的加密库是crypto和encrypt。反编译的代码中可能会看到import ‘package:crypto/crypto.dart’;以及sha256.convert(...)的调用。我们需要分析其输入、输出和密钥。第二个实操心得对于复杂的算法不要试图完全从混淆的Dart代码中理解。可以结合动态分析。使用Frida Hook住这个验证函数打印其输入参数、中间变量和返回值。例如写一个Frida脚本在用户点击按钮时打印传入的字符串和最终的比较结果。这能让你快速确认核心逻辑是否正确并获取关键的中间值。3.4 动态调试与Frida辅助验证当我们静态分析遇到瓶颈或者想验证猜想时动态调试就派上用场了。启动应用在已Root的Android设备或模拟器上安装APK并启动应用。编写Frida脚本假设我们通过静态分析找到了验证函数libapp.so!CheckFlag一个原生函数或是Dart层的一个方法。对于Dart层方法Hook难度较大更可行的是Hook平台通道如果验证逻辑在Native侧或Hook加密库的通用函数。// hook_dart.js - 示例尝试Hook Dart的print函数来捕获日志 Java.perform(function() { // 注意直接Hook Dart VM内部函数非常复杂需要符号。 // 一个更简单的方法是Hook MethodChannel的调用。 // 这里假设验证通过Platform Channel调用了一个名为“validate”的方法。 var flutterEngine Java.use(‘io.flutter.embedding.engine.FlutterEngine’); var methodChannel Java.use(‘io.flutter.plugin.common.MethodChannel’); methodChannel.invokeMethod.implementation function(method, arguments) { console.log(‘[MethodChannel] Method called: ‘ method); console.log(‘[MethodChannel] Arguments: ‘ JSON.stringify(arguments)); var result this.invokeMethod(method, arguments); console.log(‘[MethodChannel] Result: ‘ result); return result; }; });如果验证逻辑在Native层libapp.so我们可以尝试用Frida的Interceptor.attach去Hook这个原生函数前提是我们知道它的函数签名或地址可以通过静态分析IDA获得近似地址然后通过Frida的Module.findExportByName或模式搜索来定位。运行脚本frida -U -f com.example.actfapp -l hook_dart.js --no-pause交互与观察在应用界面输入测试内容如“test”点击验证按钮。观察Frida控制台的输出。你可能会看到方法名、传入的参数以及返回结果。这能极大地加速你的分析过程。第三个实操心得常见问题很多时候Blutter反编译出的代码不完整特别是控制流复杂的部分。这时不要纠结于还原每一行代码。我们的目标是找到“判断点”即决定成功失败的那个if语句和用于比较的“目标值”。只要找到了这两个即使中间过程是黑盒我们也可以尝试通过动态调试、输入输出测试来推断其功能或者直接考虑暴力破解、约束求解使用z3等工具等方式来获取正确输入。4. 进阶技巧与疑难问题排查在实际对抗中你会遇到各种问题。下面记录一些典型场景和我的解决思路。4.1 场景一Blutter反编译失败或输出空/混乱可能原因1偏移量文件不匹配。这是最常见的问题。APK使用的Flutter引擎版本与Blutter提供的偏移量文件版本不一致。排查用strings或IDA查看libflutter.so搜索“Flutter引擎版本”或类似字符串。解决尝试在Blutter的offsets目录下寻找最接近的版本。如果都没有可能需要自己计算偏移量这需要对Flutter引擎源码和数据结构有深入了解或者寻找社区是否有其他人分享了该版本的偏移量。可能原因2应用使用了定制化或深度混淆的Flutter引擎。有些应用为了安全会修改Flutter引擎源码改变内部数据结构布局。排查比较libflutter.so的大小和哈希值与官方同版本引擎是否差异巨大。解决这种情况下通用工具很可能失效。需要转向更底层的静态分析和动态调试直接阅读ARM汇编或者尝试使用reFlutter等其它工具。可能原因3Dart代码被剥离或极度优化。AOT编译时如果开启了--strip和--obfuscate并且优化等级很高会导致可恢复的信息非常少。排查Blutter运行日志中会显示找到了多少类、多少函数。如果数量极少比如只有几十个可能就是这种情况。解决重点分析strings.json和残留的函数调用关系。结合动态调试理解程序的大致流程。4.2 场景二验证逻辑依赖Native代码FFI/Platform Channel越来越多的Flutter应用将核心校验逻辑放在Native侧C/C通过dart:ffi或MethodChannel调用。识别在反编译的Dart代码中你会看到DynamicLibrary.open(‘libsecret.so’)或MethodChannel(‘com.example/validate’)的调用。应对定位Native库在APK的lib目录下找到对应的libsecret.so。静态分析用IDA Pro或Ghidra加载这个so文件寻找导出函数如Java_com_example_app_ValidateModule_checkFlag或根据Dart代码中调用的函数名FFI来定位。动态调试使用Frida Hook这些Native函数监视输入输出。对于简单的比较逻辑可能直接就在汇编层面看到明文的Flag或密钥。算法还原如果Native函数实现了复杂算法就需要耐心地分析汇编代码用C或Python重写其逻辑。4.3 场景三字符串和代码被加密或动态加载高级的混淆会在运行时解密关键的字符串和代码段。迹象静态的strings命令搜不到任何有意义的关键词Blutter提取的strings.json里也都是乱码或无意义字符。应对寻找解密函数在应用初始化阶段如main()或某个initState()一定会存在解密逻辑。通过Blutter反编译的代码寻找在build之前调用的、涉及大量字节数组操作的方法。动态获取在解密函数执行之后、使用这些字符串之前使用Frida Hook内存读操作或者直接Hook解密函数将解密后的内容打印出来。也可以使用内存dump工具在应用完全启动后dump整个进程内存然后在内存中搜索可读字符串。模拟执行如果解密算法不复杂可以尝试用Python模拟Dart的解密过程直接从APK资源中读取加密数据并解密。4.4 场景四网络交互与协议分析Flag可能需要通过正确的网络请求从服务器获取。分析使用Burp Suite设置代理抓取应用的所有HTTP/HTTPS流量。查看登录、验证、获取数据等请求。难点请求参数可能被加密或签名。定位加密位置在Dart代码中搜索发送网络请求的库如http.post,dio找到封装请求参数的函数。动态HookHook网络库的底层发送函数或者Hook自定义的加密函数在参数加密前打印出明文。重放与修改在Burp Suite中尝试重放请求并系统地修改参数观察响应变化有时能发现逻辑漏洞。5. ACTF习题实战案例剖析让我们虚构一个ACTF题目“FlutterGuard”来串联上述技巧。题目描述一个简单的Flutter应用只有一个输入框和一个提交按钮。输入正确的Flag即可通过。解题步骤实录解包与侦察apktool d解包后在lib/arm64-v8a/libflutter.so中发现版本“3.13.9”。strings libapp.so | grep -i flag无果。Blutter反编译使用Blutter带3.13.9偏移量反编译libapp.so。输出代码中在classes/main_screen.dart里发现一个_SubmitButton的onPressed调用了_Validator.validate(input)。分析验证逻辑找到_Validator类其validate方法核心代码如下已人工整理static bool validate(String input) { if (input.length ! 32) return false; var bytes utf8.encode(input); for (int i 0; i 16; i) { bytes[i] ^ 0xAA; bytes[i16] ^ 0x55; } var target [0xDE, 0xAD, 0xBE, 0xEF, ...]; // 一个48字节的数组 return ListEquality().equals(bytes, target); }逻辑清晰输入长度32前16字节每位异或0xAA后16字节每位异或0x55结果需要等于一个固定的48字节数组等等bytes长度是32target长度是48这不匹配。这里反编译可能出错了。动态验证编写Frida脚本直接Hook这个validate函数需要先找到其在内存中的地址可以通过Blutter输出的符号信息结合Frida的Module.enumerateExports尝试。Hook后发现target数组长度确实是32反编译的数组显示有误。我们成功打印出了target数组的准确值[222, 173, 190, 239, ...]。逆向算法算法很简单是异或加密。由于异或运算是自反的A ^ B ^ B A要得到原始输入input只需用target数组再异或一次同样的密钥即可。前16字节target[i] ^ 0xAA后16字节target[i16] ^ 0x55计算Flag写一个简单的Python脚本target [0xDE, 0xAD, 0xBE, 0xEF, ...] # 从Frida输出中复制完整的32个字节值 key1 0xAA key2 0x55 flag_bytes [] for i in range(32): if i 16: flag_bytes.append(target[i] ^ key1) else: flag_bytes.append(target[i] ^ key2) flag bytes(flag_bytes).decode(‘utf-8’) print(flag)运行脚本得到Flag字符串提交成功。复盘这道题相对简单核心验证逻辑在Dart层且算法简单。解题的关键在于使用Blutter快速定位到核心验证函数并结合Frida动态调试纠正了静态反编译的错误最终轻松逆向算法。对于更复杂的题目可能需要将静态分析、动态调试、算法还原、网络协议分析等多种手段组合使用。Flutter逆向是一个正在不断发展的领域工具和方法都在快速演进。Blutter是一个强大的起点但它不是终点。真正的能力在于你对移动应用运行机制的理解、对汇编代码的阅读能力、以及灵活运用各种静态动态工具解决问题的思维。保持学习多动手实践从像ACTF这样的CTF题目开始逐步挑战更复杂的真实世界应用你的“对抗”能力就会不断提升。
返回列表