ARTICLE DETAIL

资讯详情

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

用Flutter打造安卓逆向工具箱:从编码哈希到APK解析

用Flutter打造安卓逆向工具箱:从编码哈希到APK解析 安卓手机上的 Flutter 逆向工具箱我把它做成了 App做逆向分析或安全学习时很多人第一反应是打开电脑装好 APK 解析工具、加解密工具、文本处理工具再插上数据线把目标文件拖到桌面处理。但实际进入工程场景后会发现很多需求发生的地点就是手机本身拿到一个 APK想先看包名、版本、权限在调试接口时想快速算一个 MD5、做一次 Base64 解码或者只是学习 Flutter 应用开发想找一个能练习加解密、编码转换、文本处理和 APK 基础结构解析的综合项目。这些场景都不需要把电脑掏出来更需要一个能装在安卓手机上的“逆向工具箱”。所谓“逆向工具箱”并不是要实现脱壳、Hook、绕过签名这类高风险能力。它更合理的定位是把逆向分析中反复用到的通用工具集中到一个 App 里编码解码、哈希计算、常见加密算法、进制转换、JSON 格式化、正则测试、APK 基础信息解析。这样当你在手机上调试、学习、做安全自查时不用逐个搜索在线转换网站也不用来回切换工具所有能力都在一个 App 内完成。本文会从一个实际可运行的角度出发说明如何用 Flutter 搭建这样一款安卓逆向工具箱。你会看到功能模块如何划分、纯 Dart 实现加解密和哈希怎么组织、Android 平台能力如何通过 MethodChannel 接入、Flutter 主题和通用页面如何复用以及在构建和发布阶段最容易踩到的环境问题。1. 先理解“手机上的逆向工具箱”该做什么、不该做什么1.1 工具箱的合理定位逆向分析的完整流程包含很多环节静态分析、动态调试、协议分析、加解密还原、数据提取。一个手机 App 不可能替代 jadx、IDA、Frida 这类桌面工具也没有必要去替代。手机工具箱更适合承担的是分析流程里“小、频、快”的通用操作。我设计的工具集分成四大类分类功能典型场景编码类Base64、URL 编解码、HTML 实体、Unicode 转换解析接口返回的转义内容、还原混淆字符串哈希类MD5、SHA-1、SHA-256、HMAC校验文件完整性、计算接口签名参数加密类AES、DES、RSA 的常见模式和填充方式分析授权应用或自研协议中的加解密逻辑文本与开发辅助JSON 格式化、进制转换、正则测试、时间戳转换调试接口、阅读日志、辅助脚本生成在实际项目中很多逆向场景并不是“破解”而是自查和复盘。比如想知道自己开发的 APK 是否暴露了不该暴露的权限或者判断某个库是否引入了多余的 so 文件这时候只需要一个能读取 APK 基础信息的工具就能快速完成初步检查。1.2 边界必须清楚因为“逆向”这个词本身容易被误解有必要把这套工具的边界说明白。在合规使用范围内这个工具箱应该只支持以下用途分析自己开发的应用。分析已获得授权的应用。学习和验证公开技术资料中允许分析的示例。对自身应用做安全自查比如检查权限是否过大、是否携带敏感字符串。它不应该支持也不适合做成以下功能脱壳、Hook 运行时方法、绕过签名校验、抓取未授权数据、窃取他人应用核心逻辑。这些功能在多数地区有明显的合规风险而且是新手最容易误入歧途的方向。真正有价值的逆向能力是理解协议、理解加密、理解应用结构而不是绕过限制。1.3 为什么选择 Flutter 实现选择 Flutter有几个很实际的原因。第一Flutter 的 Dart 生态里有成熟、纯 Dart 实现的加密和哈希库比如crypto、pointycastle、convert。这意味着编码、哈希、AES、RSA 等核心逻辑可以完全跨平台运行不依赖 Android 原生代码后续如果要把工具搬到 iOS、桌面端几乎不需要重写算法层。第二Flutter 内置的 Material 组件能快速搭建工具型 App。工具类应用界面结构相似网格入口、输入输出区、结果复制按钮都是高频组件用 Flutter 封装一次就能到处复用。第三当需要读取 APK 的包名、版本、权限等信息时Flutter 可以通过 MethodChannel 调用 Android 原生的 PackageManager 能力。这正好利用了 Flutter 的“UI 原生桥接”混合结构。2. 先准备好 Flutter 开发环境再谈项目结构在写代码前环境要稳定。Flutter 本身不复杂但多数初学者遇到的问题都集中在环境配置阶段。2.1 环境准备清单项目建议版本说明Flutter SDK3.x 稳定版建议从官方渠道下载解压后不要放在带空格或中文的路径Android Studio最新稳定版用于安装 Android SDK、命令行工具和模拟器JavaJDK 17 或 Flutter 提示的版本不同 Flutter 版本对 Gradle 和 JDK 要求不同Android SDKAPI 33 或更高安装 platform-tools、build-tools、platforms测试设备Android 真机或模拟器真机更利于测试文件选择、APK 解析等功能环境装完后先执行下面这组基础命令确认状态flutter doctor flutter --versionflutter doctor输出中如果显示 Android toolchain 正常说明 Flutter 已经能找到 Android SDK。如果显示找不到 Android SDK需要检查 Android Studio 的 SDK 路径或者手动通过flutter config --android-sdk指定路径。2.2 新建项目与目录设计我用下面的命令创建新项目flutter create flutter_reverse_toolbox cd flutter_reverse_toolbox推荐把 lib 目录按功能分层设计这样后面每加一个工具都不会混乱lib/ main.dart core/ tool_executor.dart result_model.dart tools/ codec/ base64_tool.dart url_tool.dart hash/ hash_tool.dart cipher/ aes_tool.dart rsa_tool.dart text/ json_formatter.dart regex_tester.dart number_converter.dart platform/ apk_info_channel.dart ui/ home_page.dart tool_page.dart apk_info_page.dartcore放统一执行逻辑tools放每个工具的具体实现platform放与 Android 原生通信的封装ui放页面。这样算法、界面、原生调用三层完全分离。2.3 环境常见坑Flutter 安装过程有大量环境问题最常见的有三个。问题现象常见原因检查方式处理建议安装后运行flutter提示不是内部或外部命令PATH 未生效输入echo $PATH查看 Flutter bin 目录是否存在重新打开终端或在当前终端执行source ~/.bashrc/export PATH...创建项目后 Gradle 下载极慢或卡住Gradle 下载依赖国外源查看android/gradle/wrapper/gradle-wrapper.properties中的 distributionUrl替换为可用镜像地址或使用本地 Gradle 版本首次构建出现 Gradle 插件命令式应用报错项目结构和 Flutter 版本不匹配检查android/settings.gradle、android/build.gradle内容用flutter create .重新生成 Android 工程再自行修改有一个常见误解Flutter 项目默认结构会自动保持正确。实际上当手动复制别人的项目代码时Android 目录里的 Gradle 插件写法很可能和本机 Flutter 版本不一致。最稳妥的处理方式是自己的项目用flutter create生成后再逐步加入自己的代码而不是直接复制 Android 工程。3. 设计功能模块不要把所有代码堆在页面里工具型 App 最怕的是每个页面写一套自己的逻辑。以 Base64 为例首页写一遍编码页写一遍后面加到十六进制工具里又写一遍代码就会变得不可维护。3.1 用统一执行类管理工具我设计了一个ToolExecutor所有工具都通过统一的调用入口执行class ToolExecutor { static String encode(String toolName, String input, MapString, String params) { switch (toolName) { case base64_encode: return Base64Tool.encode(input); case url_encode: return UrlTool.encode(input); case md5: return HashTool.md5(input); case aes_cbc_encrypt: return AesTool.cbcEncrypt(input, params); default: throw UnsupportedError(未知工具$toolName); } } }这样的好处是页面只负责收集输入和控制按钮状态真正的逻辑全部收敛到tools目录里。新增工具只需要三步在tools下新增实现类在ToolExecutor里增加分支在首页网格中增加一个入口。3.2 结果模型统一格式工具返回结果可能包含文本、耗时、错误信息建议用一个模型统一保存class ToolResult { final String output; final Duration elapsed; final String? error; const ToolResult({ required this.output, required this.elapsed, this.error, }); bool get hasError error ! null; }页面在拿到ToolResult后只负责展示。是否复制成功、是否显示错误状态由页面根据不同字段判断。这样即便以后增加 Base64 图片、文件哈希、网络请求等复杂功能页面结构也不会发生大的变化。4. 用纯 Dart 实现编码、哈希和加密核心逻辑逆向工具箱有很大一部分能力是编码、哈希和经典加密算法。这些算法与平台无关用纯 Dart 实现最省心。4.1 添加依赖先在pubspec.yaml中添加必要依赖dependencies: flutter: sdk: flutter crypto: ^3.0.3 convert: ^3.1.1 pointycastle: ^3.7.3 archive: ^3.4.10 file_picker: ^6.1.1crypto提供 MD5、SHA-1、SHA-256 等哈希算法。convert提供十六进制、Base64、UTF-8 转换。pointycastle提供 AES、RSA 等加密算法实现。archive用于读取 APK 文件作为 ZIP 结构。file_picker用于选择手机中的文件和 APK。执行依赖安装flutter pub get4.2 编码工具实现编码工具是工具箱里的高频功能。Base64 和 URL 编码最典型import dart:convert; class Base64Tool { static String encode(String input) { return base64Encode(utf8.encode(input)); } static String decode(String input) { return utf8.decode(base64Decode(input.trim())); } }这里要注意异常处理。输入不是合法 Base64 时base64Decode会抛出异常不能直接让页面崩溃。应该在ToolExecutor外层统一捕获异常并返回包含错误信息的ToolResultstatic ToolResult safeExecute(Function func) { try { final stopwatch Stopwatch()..start(); final output func().toString(); stopwatch.stop(); return ToolResult(output: output, elapsed: stopwatch.elapsed); } catch (e) { return ToolResult(output: , elapsed: Duration.zero, error: e.toString()); } }4.3 哈希工具实现哈希工具使用crypto包实现import dart:convert; import package:crypto/crypto.dart; class HashTool { static String md5(String input) { return md5.convert(utf8.encode(input)).toString(); } static String sha256(String input) { return sha256.convert(utf8.encode(input)).toString(); } static String hmacSha256(String input, String key) { final hmac Hmac(sha256, utf8.encode(key)); return hmac.convert(utf8.encode(input)).toString(); } }实际项目中哈希是大量接口签名的前置步骤。比如某些接口需要用“时间戳 固定 key”计算 HMAC-SHA256在校验逻辑时可以先用这个工具快速判断自己的参与参数是否正确。4.4 AES 加解密实现AES 是加解密工具里最常用的算法。下面是最常见的 CBC 模式示例import dart:convert; import package:pointycastle/export.dart; class AesTool { static String cbcEncrypt({ required String input, required String key, required String iv, }) { final keyBytes Uint8List.fromList(utf8.encode(key)); final ivBytes Uint8List.fromList(utf8.encode(iv)); final inputBytes Uint8List.fromList(utf8.encode(input)); final cipher CBCBlockCipher(AESEngine()) ..init(true, ParametersWithIV(KeyParameter(keyBytes), ivBytes)); var paddedInput _addPadding(inputBytes); final output Uint8List(paddedInput.length); var offset 0; while (offset paddedInput.length) { cipher.processBlock(paddedInput, offset, output, offset); offset 16; } return base64Encode(output); } static Uint8List _addPadding(Uint8List data) { final padLen 16 - (data.length % 16); final result Uint8List(data.length padLen)..setAll(0, data); for (var i 0; i padLen; i) { result[data.length i] padLen; } return result; } }这段代码的关键在块处理AES 是分组加密算法每组 16 字节数据长度不足时必须按 PKCS7 规则填充。很多新手在这里出错因为原始输入长度正好是 16 的倍数时会漏掉一整块填充导致解密方报错。推荐做法是无论长度多少都强制加一轮填充并在解密时严格对应删除填充字节。4.5 输入边界处理工具型 App 的输入来自用户手工输入你一定不能假设输入合法。至少需要覆盖这几种情况空字符串直接提示“输入不能为空”。编码非法捕获异常并显示具体错误。超大输入如果做文件哈希不要一次性把整个文件读入内存考虑分块读取。加密密钥长度不够AES-128 要求 16 字节AES-256 要求 32 字节工具界面要提前限制输入长度。注意以上 AES 示例用于说明思路实际项目要结合自己的密钥、IV、模式、填充方式统一处理。接口联调时最容易出错的地方不是算法而是编码集不统一一端用 UTF-8另一端用 GBK结果必然不一致。5. 实现 APK 简易信息读取能力工具箱里最有“逆向工具”感觉的功能是直接打开一个 APK 文件并读取基础信息。这个功能在 Android 上要借助原生平台能力完成。5.1 Dart 端封装 MethodChannel先定义 Dart 侧的通道封装import package:flutter/services.dart; class ApkInfoChannel { static const MethodChannel _channel MethodChannel(app.toolbox/apk); static FutureMapString, dynamic? parseApk(String path) async { try { final result await _channel.invokeMethod(parseApkInfo, { path: path, }); return result as MapString, dynamic?; } on PlatformException catch (e) { return { error: e.message ?? 解析失败, }; } } }用户通过file_picker选择 APK 文件后得到文件路径再调用这个方法就能把解析逻辑交给 Android 原生层。5.2 Android 端实现 APK 解析在 Android 的MainActivity中注册通道并通过 PackageManager 的getPackageArchiveInfo解析 APKclass MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel( flutterEngine.dartExecutor.binaryMessenger, app.toolbox/apk ).setMethodCallHandler { call, result - if (call.method parseApkInfo) { val path call.argumentString(path) if (path null) { result.error(400, path is null, null) returnsetMethodCallHandler } result.success(parseApkInfo(path)) } else { result.notImplemented() } } } private fun parseApkInfo(path: String): MapString, Any? { val pm packageManager val pkg pm.getPackageArchiveInfo(path, PackageManager.GET_PERMISSIONS) if (pkg null) { return mapOf(error to 无法解析 APK 文件) } val appInfo pkg.applicationInfo appInfo.sourceDir path appInfo.publicSourceDir path return mapOf( packageName to pkg.packageName, versionName to pkg.versionName ?: , versionCode to pkg.versionCode.toString(), label to pm.getApplicationLabel(appInfo).toString(), permissions to (pkg.requestedPermissions ?: emptyArrayString()) ) } }getPackageArchiveInfo是 PackageManager 提供的公共 API它不需要安装 APK 就能读取包信息。这里的appInfo.sourceDir需要手动指向文件路径否则部分设备上解析应用名会失败。5.3 用 ZIP 结构查看 APK 内部组成APK 本质上是一个 ZIP 压缩包里面包含AndroidManifest.xml、classes.dex、resources.arsc、lib/和assets/。通过archive包可以列出 APK 内文件清单import dart:io; import package:archive/archive_io.dart; class ApkStructure { static ListString listEntries(String path) { final inputStream InputFileStream(path); final archive ZipDecoder().decodeBytes(inputStream.readBytes()); inputStream.close(); return archive.files.map((file) file.name).toList(); } }读取结构后在界面中展示能快速观察几个安全指标lib/下是否存在多余的 so 文件。assets/下是否包含不应该打包的密钥文件。是否有多个classes*.dex文件判断应用是否做了分包或加固。这种“看结构、看权限”的自查方式非常适合放在工具箱里。它不涉及任何违规能力但已经能给开发者提供不少实际价值。注意不同 Android 系统版本对 APK 解析的兼容性有差异。某些国产 ROM 对文件读取权限限制更严格建议让用户把 APK 复制到应用私有目录后再解析避免直接读取公共目录被拒绝。6. 界面主题与通用页面设计工具型 App 页面数量多界面设计必须注意复用。6.1 主题色与黑夜模式Flutter 的 Material 主题可以集中配置final appTheme ThemeData( useMaterial3: true, colorSchemeSeed: const Color(0xFF3F51B5), brightness: Brightness.light, ); final darkTheme ThemeData( useMaterial3: true, colorSchemeSeed: const Color(0xFF3F51B5), brightness: Brightness.dark, );在MaterialApp中通过theme和darkTheme两个属性即可支持系统自动深色模式。有热词提到 Flutter 的 LicensePage 主题颜色问题。当应用内打开showLicensePage时页面会自动继承ThemeData中的主题。如果你发现 LicensePage 的背景颜色和主应用不一致通常是因为没有显式配置theme或者pageTransitionsTheme出现自定义主题覆盖不完整。最直接的做法是在自定义主题中保留copyWith的基本结构避免覆盖掉页面默认背景和 AppBar 样式的默认实现ThemeData( colorSchemeSeed: const Color(0xFF3F51B5), ).copyWith( appBarTheme: const AppBarTheme( centerTitle: true, ), )6.2 通用工具页封装所有编码、哈希、加密工具页面都可以抽象成一个通用页面。输入区、执行按钮、结果区、复制按钮是固定结构class ToolPage extends StatefulWidget { final String toolName; final String Function(String input, MapString, String params) executor; final Widget? extraParamFields; const ToolPage({ super.key, required this.toolName, required this.executor, this.extraParamFields, }); override StateToolPage createState() _ToolPageState(); }每个工具从这里继承传入自己的执行函数和额外参数区。界面上不再出现大面积重复代码。新增一个 URL 编码工具时只需要在首页网格注册入口并传入UrlTool中的对应方法。6.3 APK 信息页APK 信息页是相对独立的页面。它接收file_picker选择的文件路径调用ApkInfoChannel.parseApk然后展示包名、版本、权限列表和文件结构。权限数量比较多时建议用ExpansionTile折叠展示避免页面过长。7. 构建、验证与常见问题排查7.1 运行和构建命令开发调试阶段直接连接安卓设备运行flutter run发布阶段构建正式包flutter build apk --release构建完成后APK 产物在以下目录build/app/outputs/flutter-apk/app-release.apk正式发布前至少执行一次静态分析和单元测试flutter analyze flutter test7.2 高频问题排查表问题现象常见原因检查方式处理建议flutter run提示设备离线USB 调试未开启或驱动异常执行adb devices查看设备状态开启开发者模式和 USB 调试重新插拔数据线构建时报 Gradle 版本不匹配项目 Gradle 版本与本机不一致查看gradle-wrapper.properties的 distributionUrl统一 Gradle 版本清理~/.gradle缓存后重试使用pointycastle后包体积明显增大加密库包含大量算法查看build/app/outputs/.../app-release.apk大小确认构建的是 release 包避免 debug 包体积干扰判断平台上读取 APK 信息返回 null文件路径不可读或权限不足打印完整路径确认文件是真实存在的 APK先把文件复制到应用缓存目录再解析修改原生 Kotlin 代码后不生效Flutter 使用自构建引擎时只编译 Dart观察运行日志是否出现新 kotlin 逻辑执行flutter clean后重新全量构建7.3 工具逻辑验证方法每个工具加入后建议用一组已知结果做基准测试。例如test(base64 encode, () { expect(Base64Tool.encode(hello), aGVsbG8); }); test(md5 encode, () { expect(HashTool.md5(hello), 5d41402abc4b2a76b9719d911017c592); });这些测试既能验证核心算法也能在后续升级依赖时快速发现兼容性问题。8. 安全边界与合规使用建议工具箱里包含加解密、APK 解析、编码转换等能力它们在设计和实现上没有任何问题。但作为公开技术内容必须明确使用边界。这组工具适合以下场景学习 Flutter 和 Dart 的开发项目。对自身开发的 APK 进行权限和结构自查。在获得授权的前提下进行应用安全测试。阅读公开技术资料时用工具验证示例中的编码或加密结果。请勿将其用于破解他人应用、绕过授权校验、抓取未授权数据、修改应用签名或其他违法用途。技术工具的正当性不在工具本身而在使用目的。在实际项目中如果要进一步引入运行时分析、抓包、Hook 等能力请先确认用途合法并确保只在自有设备和授权环境中使用。9. 最佳实践与后续扩展方向9.1 可落地的最佳实践这类工具型 App 有几个可以长期受益的工程实践。第一算法实现与 UI 分离。任何时候都不要在 Widget 里直接写 Base64、AES 逻辑。统一收敛到tools目录新增工具只改一个执行类和一处入口。第二异常处理不能只捕获不提示。编码和解密失败时要返回清晰的错误信息最好带上算法名和原因例如“Base64 解码失败输入包含非法字符”。否则用户无法判断是自己输入错还是工具 Bug。第三所有涉及密钥、IV、盐值参数的页面一律增加输入长度校验。AES 的 Key 长度、HMAC 的 Key 长度、IV 长度都必须在前端提示范围内。第四加解密功能不要在主线程执行。虽然是纯 Dart但在大文本或大文件场景下仍然可能卡住 UI。建议对耗时操作使用compute或Isolatefinal result await compute(_heavyTask, input);9.2 可以继续扩展的方向工具箱做到这里已经是一个完整的练手项目。下一步可以扩展的方向不少。增加文件哈希功能选择任意文件计算 MD5/SHA-256支持分块读取大文件。增加 RSA 密钥生成与加解密在工具中生成密钥对用于自研接口签名调试。增加网络协议辅助构建签名参数时直接把当前时间戳转换、参数拼接、HMAC 计算合在一起形成流程。增加 Rust 高性能算法插件把哈希、加解密逻辑放到 Rust 侧通过 Flutter Rust Bridge 接入体验跨语言调用。增加桌面端支持Flutter 可以让同一套工具逻辑直接跑在 Windows、macOS、Linux 上形成“手机端 桌面端”组合方案。9.3 给新手的练习建议如果第一次做 Flutter 项目建议按下面顺序完成先只做 Base64 和 MD5 两个工具跑通“输入-执行-输出-复制”完整链路。再把所有工具的页面改成统一封装观察自己的代码有多少重复。第三步加入 AES重点理解块加密、填充、IV 和 Base64 输出之间的关系。第四步做 APK 解析掌握 MethodChannel 和 Android 原生调用的基本流程。最后重新整理目录结构把算法、UI、平台通道彻底分离开。这五个阶段全部完成后你既掌握了一个完整的 Flutter 项目也理解了编码、哈希、加密和 APK 结构这些逆向分析基础能力。后续无论往应用开发还是安全方向继续深入这套工具箱都会是一个不错的起点。
返回列表