ARTICLE DETAIL

资讯详情

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

用Flutter打造手机端逆向工具箱:APK解析与哈希计算实践

用Flutter打造手机端逆向工具箱:APK解析与哈希计算实践 如果你做过移动端逆向大概率经历过这个场景临时拿到一个 APK 样本电脑不在身边想看一眼包名、版本号、MD5、dex 数量和 so 架构结果在手机上翻来翻去找不到一个顺手的工具只能等回到工位再说。这个痛点持续了一段时间后我决定用 Flutter 把这些零散的逆向辅助功能收拢成一个 App让“快速看一眼样本”不再依赖电脑。先给结论手机端不适合做重逆向也不该硬塞 IDA、JEB 那种功能。真正适合手机的场景是信息收集、快速验证和现场取证。Flutter 在这类工具开发里是平衡成本、平台能力和分发效率都比较好的选择。这篇文章会从需求判断、技术选型、模块设计、核心代码实现和常见踩坑几个角度完整复盘这个小工具 App 的开发过程。如果你也打算给自己做一个“随身逆向工具箱”可以直接参考里面的思路和代码。1. 为什么逆向工具箱要跑到手机上很多人对移动逆向工具的想象是把电脑上的完整环境搬进手机里。这个出发点从一开始就是错的。逆向分析真正耗时的环节通常不是某个单点操作而是信息收集和交叉验证。拿到一个样本后第一件事往往是算哈希、查包名、看 targetSdk、确认 lib 目录下有哪几个架构的 so 文件。这些操作本身不复杂但需要多个工具配合至少打开终端、文件管理器和 APK 查看器三个窗口。把这些操作放到手机上有几个现实场景在会议现场、客户现场或 CTF 现场手里只有一部手机。临时收到一个链接下载的 APK想先确认是不是官方包。日常逛安全社区时对比不同版本 APK 的哈希差异。在无法展开全套 PC 工具的隔离环境做简单的格式确认。这类场景的共同点是操作频次高、单次耗时短、对深度分析能力要求不高但对“随取随用”的要求很高。这正好是手机 App 的舒适区。它不追求替代 JetBrains、JEB 或 Frida而是把逆向分析之前最繁琐的“确认身份”环节做掉。2. 选型对比为什么是 Flutter 而不是原生、Termux 或 Web在确定用 Flutter 之前我实际比较过三条常见路线。第一条是纯原生 Android 开发。优点是能直接调用系统 API性能和权限体验最好缺点是需要同时维护两套端侧逻辑而且这个工具后续如果还想延伸到 iOS、Windows 或 macOS基本等于重写。第二条是 Termux 加 Python 脚本。好处是贴近命令行习惯很多逆向脚本可以直接复用坏处是交互体验很初级文件选择、结果复制、界面展示都要自己折腾发到别人手机上更是几乎不可能。第三条是 PWA 或 Web 工具。跨平台是强项但受限于浏览器沙箱读取本地大文件、调用系统分享、做深度的原生解析都不顺手很多能力绕一圈回来还是要包一个原生壳。四条路线的对比如下方案跨平台UI 效率原生能力分发成本维护成本原生 Android差中强中高Termux Python差低中高中PWA / Web强中弱低中Flutter强高中低低Flutter 在这件事里的平衡点比较理想。纯 Dart 层就能覆盖哈希、编码、正则、文件解析大部分逻辑换平台基本不用改遇到读取 APK 元数据这类原生能力也能用 MethodChannel 和平台层临时配合。再加上 Material 组件库自带一套能用得过去的 UI工具箱这类“功能列表 工具页”的产品形态天然适合 Flutter。3. 能力边界工具箱该收哪些模块动手写代码之前我先把功能边界画清楚。逆向工具包最容易犯的毛病是什么都想塞最后每个模块都做不深。我的原则是只收“不需要动态分析、不需要长时间运行、不涉及敏感权限”的功能。最终第一版收敛成下面几个模块文件指纹计算单个文件的 MD5、SHA1、SHA256支持流式读取大文件。APK 快速解析读取 APK 包名、版本号、版本名统计 dex 和 so 文件数量。编码转换Base64、Hex 和 URL 编解码支持批量互转。正则测试正则表达式匹配与子串提取结果预览。颜色工具颜色值与 RGB/Hex 互转配合逆向 UI 还原时使用。逆向笔记本地保存分析过程的零散结论支持导出文本。没有包含的内容也很明确不做动态调试、不做脱壳、不做内存 dump。这些功能要么需要 root要么需要绑定 PC 端 agent硬塞进手机 App 只会让工具失去“随手打开”的轻盈感。PC 端很多人习惯“图吧工具箱”那种集合式工具但移动端逆向没有对标的成熟产品。自己做的时候更应该克制先把信息收集做透比堆砌一堆用不上的按钮有价值。3.1 模块划分与工程结构为了让后续扩展新工具时不改动主框架我按页面、服务、模型三层组织代码reverse_toolbox/ ├── lib/ │ ├── main.dart │ ├── pages/ │ │ ├── home_page.dart │ │ ├── file_tools_page.dart │ │ ├── apk_parse_page.dart │ │ └── encode_tools_page.dart │ ├── services/ │ │ ├── hash_service.dart │ │ ├── apk_parser_service.dart │ │ └── platform_channel_service.dart │ ├── models/ │ │ └── apk_info.dart │ └── utils/ │ └── display_utils.dart ├── android/ │ └── app/ │ └── src/main/kotlin/com/example/reverse_toolbox/MainActivity.kt ├── pubspec.yaml └── README.md页面层只负责布局和交互所有计算逻辑放到 service 层这样后续写单元测试也方便。4. 环境准备与项目初始化开发环境按 Flutter 官方稳定版来即可。以 Windows 环境为例初始化项目前先确认下面几项flutter --version flutter doctorflutter doctor会检查 Android SDK、Android Studio、连接设备等基础环境。一个常见问题是刚安装 Flutter 后发现终端里还是提示找不到命令通常是因为 PATH 没有在已打开的终端里刷新重开一个终端窗口即可。确认环境无误后创建项目flutter create --org com.example --platforms android reverse_toolbox然后打开pubspec.yaml加入工具需要的基础依赖# pubspec.yaml name: reverse_toolbox description: 一个用于 Android 逆向辅助的 Flutter 工具箱。 publish_to: none version: 1.0.01 environment: sdk: 3.2.0 4.0.0 dependencies: flutter: sdk: flutter # 以下版本号以 pub.dev 当前稳定版为准本文示例只表示版本段 file_picker: ^8.0.0 crypto: ^3.0.0 archive: ^3.5.0 package_info_plus: ^7.0.0 path_provider: ^2.1.0 share_plus: ^9.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.0 flutter: uses-material-design: true执行依赖安装flutter pub get如果这一步出现 Gradle 同步失败优先检查 Flutter、Gradle 和 Android Gradle Plugin 的版本是否匹配。不要手动把 AGP 版本改到某个网上教程里的固定值Flutter 模板本身会生成一套经过测试的版本组合。5. 文件选择与哈希计算模块工具箱里我第一个实现的是文件哈希模块因为它几乎不依赖原生逻辑但又非常实用。5.1 文件选择使用file_picker插件用户点击按钮后调起系统文件选择器// lib/services/file_choose_service.dart import package:file_picker/file_picker.dart; class FileChooseService { /// 选择任意文件返回本地路径。 static FutureString? pickFile() async { final result await FilePicker.platform.pickFiles( allowMultiple: false, type: FileType.any, ); return result?.files.single.path; } }直接使用系统文件选择器可以规避大部分 Android 分区存储的权限问题。不要在 Manifest 里申请一堆存储权限那样既敏感也不是很有效。5.2 哈希计算的两种实现最直观的哈希写法是读取文件全部字节后再计算代码很短FutureMapString, String _hashWithReadAsBytes(String filePath) async { final file File(filePath); final bytes await file.readAsBytes(); return { md5: md5.convert(bytes).toString(), sha1: sha1.convert(bytes).toString(), sha256: sha256.convert(bytes).toString(), }; }但真实工具不能这么写。逆向场景里经常需要计算几百 MB APK 的 MD5一次性readAsBytes会导致内存暴涨甚至直接 OOM。正确做法是流式计算。crypto包提供了bind流式接口// lib/services/hash_service.dart import dart:io; import package:crypto/crypto.dart; class HashService { /// 流式计算文件哈希适合大文件。 static FutureMapString, String computeFileHash(String filePath) async { final file File(filePath); final md5Digest await md5.bind(file.openRead()).first; final sha1Digest await sha1.bind(file.openRead()).first; final sha256Digest await sha256.bind(file.openRead()).first; return { md5: md5Digest.toString(), sha1: sha1Digest.toString(), sha256: sha256Digest.toString(), }; } }注意这种方式会读取文件三次。如果文件特别大可以在一次遍历中同时喂给多个算法但考虑到手机端的使用频率三次读取已经够用代码也更容易理解。页面层拿到路径后展示一个 loading 状态然后调用computeFileHash把结果用share_plus导出成文本方便发到群里和对方核对。6. APK 快速解析Dart 与原生通道配合APK 解析是工具箱里最有“逆向味”的模块也是第一个需要绕过纯 Dart 限制的功能。6.1 解析思路APK 本质上是一个 ZIP 包所以先用archive包读取压缩包条目统计出classes.dex数量和lib/arm64-v8a、lib/armeabi-v7a等目录下的 so 信息// lib/services/apk_parser_service.dart import package:archive/archive.dart; import package:path/path.dart as p; class ApkParserService { /// 读取 zip 条目统计 dex / so 基本信息。 static MapString, Object inspectArchive(String filePath) { final bytes File(filePath).readAsBytesSync(); final archive ZipDecoder().decodeBytes(bytes); final dexCount archive.files .where((f) f.name.endsWith(.dex)) .length; final soArchs String{}; for (final file in archive.files) { if (file.name.startsWith(lib/) file.name.endsWith(.so)) { final parts file.name.split(/); if (parts.length 2) { soArchs.add(parts[1]); } } } return { dexCount: dexCount, soArchs: soArchs.toList(), isFlutterApk: archive.files.any((f) f.name.startsWith(lib/) f.name.contains(libflutter.so)), }; } }但包名、版本号这些字段在 APK 里的AndroidManifest.xml中。这里的二进制 XML 不是常规 XML 格式用纯 Dart 解析需要实现 AXML 格式解析器工作量不小。实际开发中更快的做法是走原生通道调用 Android 系统的包解析能力。6.2 Flutter 侧平台通道代码// lib/services/platform_channel_service.dart import package:flutter/services.dart; class PlatformChannelService { static const MethodChannel _channel MethodChannel(com.example.reverse_toolbox/apk); static FutureMapdynamic, dynamic parseApkMetaInfo(String path) async { try { final result await _channel.invokeMapMethoddynamic, dynamic( parseApk, {path: path}, ); return result ?? {}; } on PlatformException catch (e) { throw Exception(APK 解析失败: ${e.message}); } } }6.3 Android 原生侧解析代码打开android/app/src/main/kotlin/com/example/reverse_toolbox/MainActivity.ktpackage com.example.reverse_toolbox import android.os.Bundle import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, com.example.reverse_toolbox/apk ).setMethodCallHandler { call, result - when (call.method) { parseApk - { val path call.argumentString(path) if (path.isNullOrEmpty()) { result.error(INVALID_ARGUMENT, path is empty, null) } else { result.success(parseApkWithArchiveInfo(path)) } } else - result.notImplemented() } } } private fun parseApkWithArchiveInfo(path: String): MapString, Any? { return try { val packageManager packageManager // 注意高版本 Android 对该 API 有限制仅适合读取基础字段。 val packageInfo packageManager.getPackageArchiveInfo(path, 0) if (packageInfo null) { mapOf(success to false, message to 无法解析该 APK) } else { mapOf( success to true, packageName to packageInfo.packageName, versionName to packageInfo.versionName, versionCode to packageInfo.versionCode ) } } catch (e: Exception) { mapOf(success to false, message to (e.message ?: unknown error)) } } }这里需要留意一个兼容性问题getPackageArchiveInfo在较新的 Android 版本上并不稳定尤其是针对 targetSdk 较高的应用返回字段可能不完整。这个 API 在部分官方文档里已经标记为受限或 deprecated。所以我的定位是“尽力解析解析不出来就提示用户”不要在工具里承诺任何 APK 都能拿到完整元数据。更完整的方案是解析 AXML 格式的 AndroidManifest.xml但那是另一个工作量很大的方向而且需要在 Dart 层维护字节级解析逻辑。如果后续有精力我会考虑先用原生代码做一个轻量 AXML 解析器再通过通道把结果传回 Flutter。6.4 UI 展示ApkParsePage拿到参数后用卡片形式展示包名、版本名、versionCode、dex 数量、so 架构和是否 Flutter 应用。这样“一个 APK 是不是 Flutter 写的”也能快速判断对日常样本分类很有帮助。7. 编码转换与正则测试这两个模块没有原生依赖是纯 Dart 实现开发速度很快但使用频率极高。7.1 编码转换服务// lib/services/encode_service.dart import dart:convert; class EncodeService { static String base64Encode(String text) { return base64Encode(utf8.encode(text)); } static String base64Decode(String text) { try { return utf8.decode(base64Decode(text)); } catch (e) { return 解码失败请检查输入是否为合法 Base64 字符串; } } static String hexEncode(String text) { return utf8.encode(text).map((byte) byte.toRadixString(16).padLeft(2, 0)).join(); } static String urlEncode(String text) { return Uri.encodeComponent(text); } static String urlDecode(String text) { return Uri.decodeComponent(text); } }代码逻辑不难但实际使用时要考虑边界情况。在线编码转换工具很多但把数据粘贴到浏览器或聊天工具的二次流转过程本身就容易出错本地 App 直接计算反而不容易泄漏敏感数据。7.2 正则测试服务// lib/services/regex_service.dart class RegexService { static ListString matches(String pattern, String input) { if (pattern.isEmpty) { return const []; } try { final regExp RegExp(pattern); return regExp.allMatches(input).map((match) match.group(0) ?? ).toList(); } catch (e) { return [正则表达式错误: $e]; } } }这个模块在分析 JS 加密函数或解析接口返回参数时很实用。比如拿到一段混淆参数想快速验证 URL 里的签名格式直接在手机上粘贴进去就能看到匹配结果不用专门开一个在线正则网站。其实做这事的过程中我意识到工具箱里很多功能单独看都非常简单但把它们整合进一个 App 后价值来自工作流效率的提升。8. 运行验证与打包发布开发完成后验证方式并不是“界面能打开”就算通过。我给自己列了一个验证清单8.1 功能验证模块验证操作预期结果文件哈希选择一个 10MB 以上的 APK显示 MD5 / SHA1 / SHA256且和 PC 端本地计算值一致APK 解析选择一个未安装的普通 APK显示包名、版本名、versionCode、dex 数量编码转换输入中文文本转 Base64 再转回结果与原始文本一致正则测试输入 IPv4 地址正则能匹配出目标串逆向笔记新建笔记并导出生成文本文件运行代码flutter run连接真机调试比模拟器更接近真实使用场景。如果设备没有开启开发者模式需要先在系统设置里打开开发者选项并授权 USB 调试。8.2 打包发布发布 Release 包flutter build apk --release产物在build/app/outputs/flutter-apk/app-release.apkDebug 包和 Release 包在部分场景下行为不太一致尤其是文件选择器和平台通道相关功能尽量用 Release 包做最终验收。8.3 常见问题与排查问题现象可能原因排查方式解决方案flutter 命令不存在或版本旧PATH 未刷新新开终端后运行flutter --version重新打开终端或手动刷新环境变量Gradle 同步失败Flutter 模板与 AGP 版本不匹配查看android/settings.gradle等配置文件保持 Flutter 稳定版不要手动修改 AGP 大版本Windows 桌面构建报 CMake generator 错误缺少 VS C 桌面开发组件查看 CMake 日志安装“使用 C 的桌面开发”工作负载文件选择器无法访问文件分区存储限制观察file_picker的 onError 日志确认走系统文件选择器避免直接拼接路径操作大文件计算哈希卡顿或崩溃使用 readAsBytes 一次性读入查看日志中的 OOM改用md5.bind(file.openRead())流式方案getPackageArchiveInfo返回 null高版本 Android 限制先用工具确认 APK 是否可正常解压降级提示或改用 AXML 解析方案设置页弹窗主题色和主界面不一致Material 3 下showLicensePage走 dialogTheme查看ThemeData配置统一配置dialogTheme和textTheme9. 合规底线与安全边界逆向工具箱这个分类比较敏感有些边界必须写在前面。这个 App 的定位是协助安全分析人员对“自己开发的应用、开源项目、CTF 题目或已获取书面授权的样本”做信息收集与格式确认。不要把它用于分析未授权的商业应用、破解授权、绕过安全校验或传播恶意样本。具体到实现层面有两个设计约束第一工具默认不申请 root 权限也不需要 root。所有功能都基于公开 API 或文件格式本身完成。第二平台通道只做了读取元数据的操作没有 hook 任何系统函数没有修改目标 APK也没有向目标 App 注入代码。这些边界不是限制反而是这个工具能长期用下去的基础。安全意识强的同事拿到这个工具箱第一个问题往往就是“它需要哪些权限”。我的回答是文件读取由系统文件选择器授权其余功能不申请敏感权限。10. 最佳实践与后续优化方向最后沉淀几条工程层面的建议给想自己实现类似工具的人参考。10.1 保持计算逻辑纯 Dart哈希、编码、正则这些能力尽量用 Dart 实现不要图省事全部丢给原生。这样工具后续移植到 iOS、Windows 或 Linux 都不用重写核心逻辑。平台通道只用在真正需要系统 API 的地方比如 APK 元数据解析。10.2 用单个页面承载多个工具入口工具箱类 App 的首页不需要复杂设计。一个ListView或GridView列出工具入口点进去是独立工具页可以快速扩展。新增一个工具时只需要在工具列表注册入口不需要动框架。10.3 为每个工具补充“复制结果”功能逆向场景里结果是要和别人交流的。每个工具页都要留一个复制按钮哪怕功能简单到只有 Base64 转码。这个细节能大幅提升实际使用体验。10.4 后续方向从产品角度看下一步有几个值得扩展的方向离线集成一些常见的加密摘要算法标识识别规则。接入在线查壳、签名信息查询 API前提是服务方稳定且合规。把 APK 的 AXML 解析做成纯 Dart 实现彻底摆脱平台限制。增加批量文件指纹计算方便对比同一应用的多个版本。考虑把正则工具升级为常用规则库内置 HTTPS 链接、手机号、IP、关键参数名的快捷模板。这些方向里我对 AXML 解析的优先级评价最高。因为一旦实现APK 模块就不再依赖 Android 系统 API 的兼容性跨平台能力也会完全打开。工程量不小但值得慢慢投入。如果你也想做一个顺手的小工具建议从文件哈希和编码转换这两个模块开始。它们实现成本低、日常使用频率高跑通一个最小闭环后再逐步加入 APK 解析和正则工具。工具的边界和使用者的边界同样重要希望这个思路也能在你的逆向工作流里找到合适的位置。
返回列表