源自某次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) { var appState = context.watch<MyAppState>();
return Scaffold( body: Column( children: [ Text('A random AWESOME idea:'), Text(appState.current.asLowerCase), ElevatedButton( onPressed: () { print('button pressed!'); }, child: Text('Next'), ), ], ), ); } }
每个 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(), 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); 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, 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 = '';
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); 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, !