很长一段时间对于Flutter的app逆向都比较头疼,它不像纯Java app那样可以使用jadx-gui看源码,也不能像原生native那样可以用ida看字符串交叉引用。
分析flutter的时候,没有交叉引用,那真是想它他拖进回收站。
最近又遇到了几个flutter开发的app,对它进行了一些研究,总结了以下的分析流程。
本文以iOS app为例子作为讲解,Android 的flutter app和iOS 的flutter app分析方法类似。
抓包,使用frida-ios-dump进行砸壳后,得到目标app的ipa文件,reFlutter对ipa重打包后得到release.RE.ipa,使用ios-app-signer对release.RE.ipa进行自签名,安装到手机上并运行,得到dump.dart文件,根据dump.dart里的类名、函数名、函数相对_kDartIsolateSnapshotInstructions的偏移,配合ida静态分析+frida动态分析,分析出加密算法。
python dump.py app名
打开ipa能看到Frameworks目录下有App.framework, Flutter.framework 两个框架,表示这个app是flutter开发的。
reflutter XXXX.ipa
如果没有iOS付费开发者账号,使用免费开发者账号的话,需要先使用xcode新建一个demo工程,并运行在iOS上,然后才能使用ios-app-signer重签名
xcode -> Window -> Devices and Simulators -> INSTALLED APPS,有个+号,可以把上一步重签名的ipa安装到手机上
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的偏移
使用ida加载App文件,看Exports窗口,有如下几个导出符号,而且运气比较好 ,很多函数都有符号,Precompiled_xxx
去dump.dart搜索signsafe字符串 ,没有搜索到
根据前面抓包signsafe的内容10a7d81a264e79640ce3433f1b03d992,这是一个32位的字符串,猜的可能是md5相关的算法
于是去dump.dart里搜索md5字符串 , 搜索到3处md5相关的类
对三个md5相关的函数,进行hook,
手机上操作一下,可以看到updateHash被调用
去ida中看看PrecompiledapiSign_7813函数
对上图画红框的5个函数进行hook
得到以下日志,省略了部分无关的日志,以......代替
由于结果有比较明显特征,从日志可以猜出大概算法,有md5(32位字符串或16个字节), base64(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= 结果在这个字符串里), hmac_sha1(40位字符串或20字节)
分析以上的例子,因为运气好,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
https://frida.re
Library:
'package:pointycastle/digests/md5.dart'
Class: MD5Digest extends MD4FamilyDigest implements
Type
: 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
}
}
Library:
'package:pointycastle/digests/md5.dart'
Class: MD5Digest extends MD4FamilyDigest implements
Type
: 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
}
}
Library:
'package:crypto/src/md5.dart'
Class: _MD5Sink@
404143612
extends HashSink {
Function
'_MD5Sink@404143612.'
: constructor. String: null {
Code Offset: _kDartIsolateSnapshotInstructions
+
0x000000000026309c
}
Function
'updateHash'
:. String: null {
Code Offset: _kDartIsolateSnapshotInstructions
+
0x0000000000c649f8
}
}
Library:
'package:crypto/src/md5.dart'
Class: _MD5Sink@
404143612
extends HashSink {
Function
'_MD5Sink@404143612.'
: constructor. String: null {
Code Offset: _kDartIsolateSnapshotInstructions
+
0x000000000026309c
}
Function
'updateHash'
:. String: null {
Code Offset: _kDartIsolateSnapshotInstructions
+
0x0000000000c649f8
}
}
Library:
'package:crypto/src/md5.dart'
Class: _MD5@
404143612
extends
Hash
{
Function
'startChunkedConversion'
:. String: null {
Code Offset: _kDartIsolateSnapshotInstructions
+
0x0000000000c4f2d4
}
}
Library:
'package:crypto/src/md5.dart'
Class: _MD5@
404143612
extends
Hash
{
Function
'startChunkedConversion'
:. String: null {
Code Offset: _kDartIsolateSnapshotInstructions
+
0x0000000000c4f2d4
}
}
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));
}
})
}
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, {
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)