前言
很长一段时间对于Flutter的app逆向都比较头疼,它不像纯Java app那样可以使用jadx-gui看源码,也不能像原生native那样可以用ida看字符串交叉引用。
分析flutter的时候,没有交叉引用,那真是想它他拖进回收站。
最近又遇到了几个flutter开发的app,对它进行了一些研究,总结了以下的分析流程。
本文以iOS app为例子作为讲解,Android 的flutter app和iOS 的flutter app分析方法类似。
1. 简述流程
抓包,使用frida-ios-dump进行砸壳后,得到目标app的ipa文件,reFlutter对ipa重打包后得到release.RE.ipa,使用ios-app-signer对release.RE.ipa进行自签名,安装到手机上并运行,得到dump.dart文件,根据dump.dart里的类名、函数名、函数相对_kDartIsolateSnapshotInstructions的偏移,配合ida静态分析+frida动态分析,分析出加密算法。
2. 前期准备
2.1 抓包,确定需要分析的signsafe算法
2.2 frida-ios-dump砸壳
python dump.py app名
打开ipa能看到Frameworks目录下有App.framework, Flutter.framework两个框架,表示这个app是flutter开发的。
2.3 reFlutter重打包
reflutter XXXX.ipa
2.4 ios-app-signer 重签名
如果没有iOS付费开发者账号,使用免费开发者账号的话,需要先使用xcode新建一个demo工程,并运行在iOS上,然后才能使用ios-app-signer重签名
2.5 xcode安装重打包的ipa
xcode -> Window -> Devices and Simulators -> INSTALLED APPS,有个+号,可以把上一步重签名的ipa安装到手机上
2.6 查看dump.dart日志
Devices and Simulators 界面有个Open Console按钮,可以打开控制台,查看dump.dart的路径
reFlutter dump file: /private/var/mobile/Containers/Data/Application/F4F2810A-C863-4732-B871-480BFD1C101B/Documents/dump.dart
使用scp命令把dump.dart 拷贝到本地,可以看到里面包含了类名,函数名,相对_kDartIsolateSnapshotInstructions的偏移
2.7 ida加载App.framework下的App文件
使用ida加载App文件,看Exports窗口,有如下几个导出符号,而且运气比较好,很多函数都有符号,Precompiled_xxx
3. signsafe详细分析流程
去dump.dart搜索signsafe字符串,没有搜索到
根据前面抓包signsafe的内容10a7d81a264e79640ce3433f1b03d992,这是一个32位的字符串,猜的可能是md5相关的算法
于是去dump.dart里搜索md5字符串, 搜索到3处md5相关的类
123456789101112131415161718Library:'package:pointycastle/digests/md5.dart'Class: MD5Digest extends MD4FamilyDigest implementsType: Digest {FactoryConfig factoryConfig=sentinel ;Function'get:algorithmName': getter const. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x0000000000cc8a1c}Function'get:digestSize': getter const. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x0000000000ccd5f4}Function'MD5Digest.': constructor. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x0000000000002828}Function'resetState':. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x0000000000cc4d98}Function'processBlock':. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x0000000000ce4ea0}}
12345678Library:'package:crypto/src/md5.dart'Class: _MD5Sink@404143612extends HashSink {Function'_MD5Sink@404143612.': constructor. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x000000000026309c}Function'updateHash':. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x0000000000c649f8}}
12345Library:'package:crypto/src/md5.dart'Class: _MD5@404143612extendsHash{Function'startChunkedConversion':. String: null {Code Offset: _kDartIsolateSnapshotInstructions+0x0000000000c4f2d4}}
对三个md5相关的函数,进行hook,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function print_native_stack(addr, context) { return ('\r\n[' + addr + '] called from:\n' + Thread.backtrace(context.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join('\n') + '\n');}function hook_md5() { let kDartIsolateSnapshotInstructions = Module.findExportByName("App", "kDartIsolateSnapshotInstructions") console.log(kDartIsolateSnapshotInstructions); let processBlock = kDartIsolateSnapshotInstructions.add(0x0000000000ce4ea0); let updateHash = kDartIsolateSnapshotInstructions.add(0x0000000000c649f8); let startChunkedConversion = kDartIsolateSnapshotInstructions.add(0x0000000000c4f2d4); Interceptor.attach(processBlock, { onEnter(args) { console.log("processBlock:", print_native_stack("processBlock", this)); } }) Interceptor.attach(updateHash, { onEnter(args) { console.log("updateHash:", print_native_stack("updateHash", this)); } }) Interceptor.attach(startChunkedConversion, { onEnter(args) { console.log("startChunkedConversion:", print_native_stack("startChunkedConversion", this)); } })
|
手机上操作一下,可以看到updateHash被调用
去ida中看看PrecompiledapiSign_7813函数
对上图画红框的5个函数进行hook
12345678910111213141516171819202122232425262728293031323334353637function hook_addr(addr, name) {Interceptor.attach(addr, {onEnter(args) {this.log=[]this.log.push(name+" onEnter:\r\n")for(let i=0; i <8; i++) {try{this.log.push(hexdump(args[i]),"\r\n");} catch (error) {this.log.push((args[i]),"\r\n");}}}, onLeave(retval) {this.log.push(name+" onLeave:\r\n")try{this.log.push(hexdump(retval),"\r\n");} catch (error) {this.log.push((retval),"\r\n");}this.log.push("=======================")console.log(this.log);}})}function hook_apisign() {let Precompiled_Hmac_Hmac__7814=DebugSymbol.fromName("Precompiled_Hmac_Hmac__7814")let Precompiled_Hmac_convert_37042=DebugSymbol.fromName("Precompiled_Hmac_convert_37042")let Precompiled____base64Encode_5267=DebugSymbol.fromName("Precompiled____base64Encode_5267")let Precompiled_Hash_convert_37041=DebugSymbol.fromName("Precompiled_Hash_convert_37041")let Precompiled_Digest_toString_34431=DebugSymbol.fromName("Precompiled_Digest_toString_34431")hook_addr(Precompiled_Hmac_Hmac__7814.address, Precompiled_Hmac_Hmac__7814.name);hook_addr(Precompiled_Hmac_convert_37042.address, Precompiled_Hmac_convert_37042.name);hook_addr(Precompiled____base64Encode_5267.address, Precompiled____base64Encode_5267.name);hook_addr(Precompiled_Hash_convert_37041.address, Precompiled_Hash_convert_37041.name);hook_addr(Precompiled_Digest_toString_34431.address, Precompiled_Digest_toString_34431.name);}
得到以下日志,省略了部分无关的日志,以……代替
1234567891011121314151617181920212223242526272829303132333435363738Precompiled_Hmac_Hmac__7814 onEnter:,0123456789A B C D E F0123456789ABCDEF10c0afba9036d0000000000c0 fb0a0c0100000000.m..............10c0afbb900000014000000443233414243402335.......D23ABC@#510c0afbc9360000000000000000000000000000046...............。。。。。。Precompiled_Hmac_convert_37042 onEnter:,0123456789A B C D E F0123456789ABCDEF10c0afd79076d000000000090fd0a0c0100000000.m..............10c0afd890000009a0000006170692e7878782e63.......api.xxx.c10c0afd996e2f787878782f6170693f70686f6e65n/xxxx/api?phone10c0afda93d313338303031333830303026747879=13800138000&txy10c0afdb97a6d3d267572693d617069787878782fzm=&uri=apixxxx/10c0afdc96170692f757365722f73656e64736d73api/user/sendsms10c0afdd9636f6465000000000000000000000004code............。。。。。。Precompiled____base64Encode_5267 onEnter:,0123456789A B C D E F0123456789ABCDEF10c0b0789036d0000000000a0070b0c0100000000.m..............10c0b079900000028000000f64184fc8b764af303...(....A...vJ..10c0b07a9a0 fe d92fe85d850fee6db20000000004.../.]...m......。。。。。。,Precompiled____base64Encode_5267 onLeave:,0123456789A B C D E F0123456789ABCDEF10c0b085903550000000000380000000000000039.U.....8.......910c0b08696b47452f49743253764d446f50375a4ckGE/It2SvMDoP7ZL10c0b08792b686468512f756262493d0000000000+hdhQ/ubbI=.....。。。。。。,Precompiled_Digest_toString_34431 onLeave:,0123456789A B C D E F0123456789ABCDEF10c0b0d1903550000000000400000000000000031.U.....@.......110c0b0d29306137643831613236346537393634300a7d81a264e7964010c0b0d3963653334333366316230336439393200ce3433f1b03d992.
由于结果有比较明显特征,从日志可以猜出大概算法,有md5(32位字符串或16个字节), base64(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= 结果在这个字符串里), hmac_sha1(40位字符串或20字节)
1md5("9kGE/It2SvMDoP7ZL+hdhQ/ubbI=")->10a7d81a264e79640ce3433f1b03d992
1234500000000f64184fc8b764af303a0 fe d92fe85d85|öA.ü.vJó. þÙ/è].|000000100fee6db2 |.îm²|进行 base64 可以得到9kGE/It2SvMDoP7ZL+hdhQ/ubbI=
1hmac_sha1("D23ABC@#56","api.xxx.cn/xxxx/api?phone=13800138000&txyzm=&uri=apixxxx/api/user/sendsmscode")//已打码xxxx
4. 总结
分析以上的例子,因为运气好,dump.dart里面有很多业务相关的函数符号,所以降低了很大的难度。
对抗以上的逆向方法也很简单,flutter官方就有解决方案。
加上 –obfuscate –split-debug-info两个参数就能抹去这些类名和函数名
参考工具
https://github.com/AloneMonkey/frida-ios-dump
https://github.com/Impact-I/reFlutter
https://github.com/DanTheMan827/ios-app-signer
















