源自某次CTF,运行效果如下

一个flutter应用,思考提取flutter应用源码,记得下面这个工具可以

https://github.com/worawit/blutter

该应用是否可以抽取dart源码以及以及是否能直接绕过反逆向技术和原理有待探究

blutter环境配置

参考:

首先我们需要从GitHub上克隆blutter项目:

1
git clone https://github.com/worawit/blutter.git

然后进入blutter目录执行初始化环境的脚本:

1
2
cd blutter/
python scripts/init_env_win.py

此处我的电脑使用anaconda配置python环境(base)

对于具体的逆向工程项目,比如你想逆向一个名为flutter_chall的flutter项目,你需要确保你已经获得了该项目的libapp.so和libflutter.so文件。之后运行下面的命令:

1
python blutter.py C:\path\to\your\so_files\libapp\armeabi-v7a output_folder_name

除了一个flutter自己的库以外还存在一个自己写的库

1
python blutter.py C:\Users\At\Desktop\test\apk\lib\armeabi-v7a C:\Users\At\Desktop\test\output

那在这个文章中有问题,修改如下

1
python blutter.py C:\Users\At\Desktop\test\apk\lib\arm64-v8a C:\Users\At\Desktop\test\output

这次可以跑了,不过跑到一半还是报错了

实际上官方也有说明windows上的配置

不过官方也提到了,在linux上配置更合适

linux上配置:

windows上貌似得在这个窗口运行命令才行,不过这样我得修改目标python了

到此应该是配置好了

1
2
cd D:\工具软件\Reverse\blutter
python blutter.py C:\Users\At\Desktop\test\apk\lib\arm64-v8a C:\Users\At\Desktop\test\output

中途出现文件无法打开的情况,猜测可能是中文路径的原因

不过还原出来的是这个东西

作用是恢复符号链接,也是之前完全没用过

ida分析

这个生成的addNames就是为了给libapp.so恢复符号链接的,首先先把python环境变量改回去

过滤ontap函数也不太行,不知如何下手分析

其实在符号恢复后也看不懂就应该搬出正向大法了,选个一样的编译环境,然后实现差不多的功能自己逆向一下即可

不过这个版本是多少来着,当时没仔细看

只好重新运行一遍,为Dart3.5

dart正向

环境配置

dart3.5开发环境搭建:

我电脑中有flutter,还需要判断一下版本号

Dart版本是3.4.4,还得更新到3.5才合适,这里还可以注意一下sdk的安装路径

更新到3.5 : flutter upgrade

项目初始化

官方演示demo参考:https://codelabs.developers.google.cn/codelabs/flutter-codelab-first?hl\=zh-cn#0

项目我在很久之前就创建过,所以直接拿以前的来用

按照步骤,下一步要替换这个

思考为什么要替换,原本的不行吗,我感觉也差不多

关于flutter开发中的pubspec.yaml配置文件的作用:

包含有关 Flutter 项目的基本信息,例如项目名称、版本号和描述

管理项目所需的依赖库

配置 Flutter 相关的设置,如素材文件、字体、图标等

限制 Dart 和 Flutter SDK 的最低版本要求,确保项目在特定的 SDK 版本上运行

感觉类似于maven中的pom文件

除了要换pubspec.yaml还需要换analysis_options.yaml

dart主函数

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}

class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();

return Scaffold(
body: Column(
children: [
Text('A random idea:'),
Text(appState.current.asLowerCase),
],
),
);
}
}

添加按钮

这里提到了热重载,不需要重启应用也可以应用更改

确实是非常好用的功能

接下来,在 Column​ 底部添加一个按钮,也就是第二个 Text​ 实例的正下方。

MyApp​ 中的代码设置了整个应用,包括创建应用级状态(稍后会详细介绍)、命名应用、定义视觉主题以及设置“主页” widget,即应用的起点。

文档中说到

在构建每一个 Flutter 应用时,widget 都是一个基本要素。如您所见,应用本身也是一个 widget。

这跟安卓中的activity有点相似

MyAppState​ 类定义应用的状态

MyHomePage​ 程序员功能实现主体

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
28
29
30
31
32
// ...

class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2

return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}

每个 widget 均定义了一个 build() 方法,每当 widget 的环境发生变化时,系统都会自动调用该方法,以便 widget 始终保持最新状态。
MyHomePage 使用 watch 方法跟踪对应用当前状态的更改。
每个 build 方法都必须返回一个 widget 或(更常见的)嵌套 widget 树。在本例中,顶层 widget 是 Scaffold。您不会在此 Codelab 中使用 Scaffold,但它是一个有用的 widget。在绝大多数真实的 Flutter 应用中都可以找到该 widget。
Column 是 Flutter 中最基础的布局 widget 之一。它接受任意数量的子项并将这些子项从上到下放在一列中。默认情况下,该列会以可视化形式将其子项置于顶部。您很快就会对其进行更改,使该列居中。
您在第一步中更改了此 Text widget。
第二个 Text widget 接受 appState,并访问该类的唯一成员 current(这是一个 WordPair)。WordPair 提供了一些有用的 getter,例如 asPascalCase 或 asSnakeCase。此处,我们使用了 asLowerCase。但如果您希望选择其他选项,您现在可以对其进行更改。
请注意,Flutter 代码大量使用了尾随逗号。此处并不需要这种特殊的逗号,因为 children 是此特定 Column 参数列表的最后一个(也是唯一一个)成员。不过,在一般情况下,使用尾随逗号是一种不错的选择。尾随逗号可大幅减小添加更多成员的必要性,并且还可以在 Dart 的自动格式化程序中作为添加换行符的提示。如需了解详细信息,请参阅代码格式。
// ...

将text段重构一个Widget出来

当前代码如下

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
//定义函数方法
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(),
//Text(pair.asLowerCase),
//Text(appState.current.asLowerCase),
ElevatedButton(
onPressed: () {
appState.getNext();
print('button pressed!');
},
child: Text('Next'),
),
],
),
),
);
}
}
class BigCard extends StatelessWidget {
const BigCard({
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
var appState = context.watch<MyAppState>();
var pair = appState.current;
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase, style: style),
),
);
}
}

使用gpt给的输入验证demo:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import 'package:flutter/material.dart';

void main() {
runApp(CardVerificationApp());
}

class CardVerificationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CardVerificationPage(),
);
}
}

class CardVerificationPage extends StatefulWidget {
@override
_CardVerificationPageState createState() => _CardVerificationPageState();
}

class _CardVerificationPageState extends State<CardVerificationPage> {
final TextEditingController _controller = TextEditingController();
String _result = '';

// 假设正确的卡密为 '123456'
final String correctCardKey = '123456';

void _verifyCardKey() {
setState(() {
if (_controller.text == correctCardKey) {
_result = '卡密正确';
} else {
_result = '卡密错误';
}
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('卡密验证'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: '请输入卡密',
border: OutlineInputBorder(),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _verifyCardKey,
child: Text('验证'),
),
SizedBox(height: 20),
Text(
_result,
style: TextStyle(
fontSize: 18,
color: _result == '卡密正确' ? Colors.green : Colors.red,
),
),
],
),
),
);
}
}

也许就因为这个小问题,不过要重新配置起来还是相当的麻烦

构建了得有20分钟才好:

好吧就算是自己写的也不能直接看出来什么

工具再跑一下

1
2
3
cd C:\Users\At\Desktop\test\blutter
C:
python blutter.py C:\Users\At\Desktop\test\myflutter\lib\arm64-v8a C:\Users\At\Desktop\test\myflutter\output

跑完然后运行addname.py,这样再搜“123456”能命中

不过无法通过引用定位代码

只有两个ontap函数

1
2
flutter_src_widgets_text_selection_TextSelectionGestureDetectorBuilder::onTapTrackStart_27d9bc
flutter_src_widgets_text_selection_TextSelectionGestureDetectorBuilder::onTapTrackReset_27d974

猜测这个带着onTapTrackStart的是按钮函数开头

但是这个反汇编代码相比之下是真的不太行

难道这里是判断?

1
2
3
4
if ( (__int64)*(int *)(v18 + 19) >> 1 == (__int64)*(int *)(v18 + 23) >> 1 )
v19 = v3 + 48;
else
v19 = v3 + 32;

反证一下,将原来的判断代码进行修改,把原本的==换成!=

离谱,为什么这个能搜出这么多

测试了一下确定是改了的

但是二者确实没区别

使用二进制比对工具发现是这里的36,37不同

找到对应的位置297ce0

然后成功找到判断函数

值的注意的是这个自定义函数的符号都被保留下来了

源代码中我是这么写的

本想将这个函数往上溯源,不过找不到调用,那很有可能不是静态调用,而是动态传参调用,这样一来不是我自己写的话找地方会找不到吧

为什么直接搜还搜不到了,ida的问题?

能搜到,只不过这么搜名字会略有差别

1
namer_app$main__CardVerificationPageState___anon_closure_297c78

思路太乱了,要是有源代码能设置逆向标志位的东西就好,比如printf控制台输出

有点好奇在flutter中写的print函数最终会变成什么

本想偷懒用原来的addname.py,看样子是不行的

没看到print函数,难道是release会去除?

关于字串,在c语言写的程序的,右侧会有引用,且点击字段本身不会跳转,这里是自身架构的动态调用,不能用像C语言逆向那样的技巧

对于不是直接引用字符串地址的程序,字符串并不能有效定位

之前推测print函数可能被优化,gpt表示不确定,这样也不好定位

不对,怎么在这里找到了

在namer_app_main__CardVerificationPageState::_verifyCardKey_297c24@<X0>中找到print

但是在最终的比对中没有

也可能确实符合,里面还嵌入了一层setState函数

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __usercall namer_app_main__CardVerificationPageState::_verifyCardKey_297c24@<X0>(

{
*(_QWORD *)(v9 - 16) = v13;
*(_QWORD *)(v9 - 8) = v14;
v15 = v9 - 16;
*(_QWORD *)(v9 - 24) = a2;
if ( (unsigned __int64)(v9 - 32) <= v11->stack_limit )
StackOverflowSharedWithoutFPURegsStub_3b7d78(a1, a2, a3, a4, a5, a6, a7, a8, a9);
ContextStub_3b6bd4 = AllocateContextStub_3b6bd4();
v17 = *(_QWORD *)(v15 - 8);
*(_QWORD *)(v15 - 16) = ContextStub_3b6bd4;
*(_DWORD *)(ContextStub_3b6bd4 + 15) = v17;
v24 = dart__internal_::printToConsole_1b52c4(v17, v12->Obj_0x9238, ContextStub_3b6bd4, v18, v19, v20, v21, v22, v23);
ClosureStub_3b6f98 = AllocateClosureStub_3b6f98(v24, v12->Obj_0x9230, *(_QWORD *)(v15 - 16));
flutter_src_widgets_framework_State::setState_1f4048(ClosureStub_3b6f98, *(_QWORD *)(v15 - 8), ClosureStub_3b6f98);// 这里应该是调用源代码setState对应反汇编_anon_closure处
return v10;
}

然后来看看按钮函数逻辑

毕竟这种动态加载的确实是不太好定位

它这里面和周围都不能加print

在build函数中找找

对应源代码

二进制比较大法定位,修改点参数,然后二进制比对看位置

给他加点东西,然后编译比对

我在想这个按钮函数也是热加载,就算定位到了在静态分析下也找不到其要调用的函数,除非动态调试

如何是在解决这题的调用情况下,直接找名字都能定位

从名字也可以看出是从rust中调用代码,还得研究flutter调用rust语言的机制,以及反汇编长什么样

根据前面的自测,这个函数就是按钮函数

甚至这里还有3个toast,应该就算判断是否正确用的

此处底层还算调用的rust

还有个so,加密逻辑在这个里面

反正验证逻辑就只有这一段

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 while ( --v73 )
{
v56 = v55 + 8;
if ( v55 + 8 > v68 )
goto LABEL_89;
if ( v55 > 0x18 )
{
v68 = 32LL;
v56 = 40LL;
LABEL_89:
sub_78DD0(v56, v68, &off_82010);
}
v57 = *(_DWORD *)(v71 + 4 * v55 + 12);
v58 = *(_DWORD *)(v71 + 4 * v55);
v59 = *(_DWORD *)(v71 + 4 * v55 + 4);
v60 = *(_DWORD *)(v71 + 4 * v55 + 16);
v61 = *(_DWORD *)(v71 + 4 * v55 + 8);
v62 = *(_DWORD *)(v71 + 4 * v55 + 24);
v67 = *(_DWORD *)(v71 + 4 * v55 + 20);
v69 = *(_DWORD *)(v71 + 4 * v55 + 28);
if ( v69 + v67 * v57 * v59 - (v60 * v61 + v58 + v62) == *((_DWORD *)&off_1A2A8 + v55) )
{
v63 = v67 * v58;
if ( v57 - v67 * v58 - v60 + v62 + v59 * v69 + v61 == *((_DWORD *)&off_1A2A8 + v55 + 1)
&& v63 - (v60 + v59 * v69) + v61 + v57 * v62 == *((_DWORD *)&off_1A2A8 + v55 + 2) )
{
v64 = v58 * v60;
if ( v57 * v67 * v62 + v59 + v58 * v60 - (v61 + v69) == *((_DWORD *)&off_1A2A8 + v55 + 3)
&& v60 * v61 + v59 + v57 * v67 - (v62 + v69 * v58) == *((_DWORD *)&off_1A2A8 + v55 + 4)
&& v61 + v57 * *(_DWORD *)(v71 + 4 * v55 + 4) + v63 - (v62 + v69 * v60) == *((_DWORD *)&off_1A2A8 + v55 + 5)
&& v67 * v61 + v62 + v69 - v59 - v57 * v64 == *((_DWORD *)&off_1A2A8 + v55 + 6) )
{
v65 = v61 * v62 + v64 + v57 - (v69 + v67 + v59) == *((_DWORD *)&off_1A2A8 + v55 + 7);
v55 += 8LL;
if ( v65 )
continue;
}
}
}
if ( v70 )
sub_3D090(v71, 4 * v70, 4LL);
return 0;
}

解密码不看到此为止,之后还需要补充一下rust调用和flutter-rust调用

这里要是有经验就知道,自动生气的调用so库中存在关键字符串直接定位,例如此处的字符串是

src\\api\\simple.rsHello, !