前言
很长一段时间对于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 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
}
}
12345678Library:
'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
}
}
12345Library:
'package:crypto/src/md5.dart'
Class: _MD5@
404143612
extends
Hash
{
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:
,
0
1
2
3
4
5
6
7
8
9
A B C D E F
0123456789ABCDEF
10c0afba9
03
6d
00
00
00
00
00
c0 fb
0a
0c
01
00
00
00
00
.m..............
10c0afbb9
00
00
00
14
00
00
00
44
32
33
41
42
43
40
23
35
.......D23ABC@
#5
10c0afbc9
36
00
00
00
00
00
00
00
00
00
00
00
00
00
00
04
6.
..............
。。。。。。
Precompiled_Hmac_convert_37042 onEnter:
,
0
1
2
3
4
5
6
7
8
9
A B C D E F
0123456789ABCDEF
10c0afd79
07
6d
00
00
00
00
00
90
fd
0a
0c
01
00
00
00
00
.m..............
10c0afd89
00
00
00
9a
00
00
00
61
70
69
2e
78
78
78
2e
63
.......api.xxx.c
10c0afd99
6e
2f
78
78
78
78
2f
61
70
69
3f
70
68
6f
6e
65
n
/
xxxx
/
api?phone
10c0afda9
3d
31
33
38
30
30
31
33
38
30
30
30
26
74
78
79
=
13800138000
&txy
10c0afdb9
7a
6d
3d
26
75
72
69
3d
61
70
69
78
78
78
78
2f
zm
=
&uri
=
apixxxx
/
10c0afdc9
61
70
69
2f
75
73
65
72
2f
73
65
6e
64
73
6d
73
api
/
user
/
sendsms
10c0afdd9
63
6f
64
65
00
00
00
00
00
00
00
00
00
00
00
04
code............
。。。。。。
Precompiled____base64Encode_5267 onEnter:
,
0
1
2
3
4
5
6
7
8
9
A B C D E F
0123456789ABCDEF
10c0b0789
03
6d
00
00
00
00
00
a0
07
0b
0c
01
00
00
00
00
.m..............
10c0b0799
00
00
00
28
00
00
00
f6
41
84
fc
8b
76
4a
f3
03
...(....A...vJ..
10c0b07a9
a0 fe d9
2f
e8
5d
85
0f
ee
6d
b2
00
00
00
00
04
...
/
.]...m......
。。。。。。
,Precompiled____base64Encode_5267 onLeave:
,
0
1
2
3
4
5
6
7
8
9
A B C D E F
0123456789ABCDEF
10c0b0859
03
55
00
00
00
00
00
38
00
00
00
00
00
00
00
39
.U.....
8.
......
9
10c0b0869
6b
47
45
2f
49
74
32
53
76
4d
44
6f
50
37
5a
4c
kGE
/
It2SvMDoP7ZL
10c0b0879
2b
68
64
68
51
2f
75
62
62
49
3d
00
00
00
00
00
+
hdhQ
/
ubbI
=
.....
。。。。。。
,Precompiled_Digest_toString_34431 onLeave:
,
0
1
2
3
4
5
6
7
8
9
A B C D E F
0123456789ABCDEF
10c0b0d19
03
55
00
00
00
00
00
40
00
00
00
00
00
00
00
31
.U.....@.......
1
10c0b0d29
30
61
37
64
38
31
61
32
36
34
65
37
39
36
34
30
0a7d81a264e79640
10c0b0d39
63
65
33
34
33
33
66
31
62
30
33
64
39
39
32
00
ce3433f1b03d992.
由于结果有比较明显特征,从日志可以猜出大概算法,有md5(32位字符串或16个字节), base64(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= 结果在这个字符串里), hmac_sha1(40位字符串或20字节)
1md5(
"9kGE/It2SvMDoP7ZL+hdhQ/ubbI="
)
-
>
10a7d81a264e79640ce3433f1b03d992
1234500000000
f6
41
84
fc
8b
76
4a
f3
03
a0 fe d9
2f
e8
5d
85
|öA.ü.vJó. þÙ
/
è].|
00000010
0f
ee
6d
b2 |.î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