【Flutter逆向】记一次某灰产APP(Sm**th)的逆向学习
作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者无关!如有侵权,请联系作者本人进行删除!
目录
1、逆向原因
2、分析
3、上科技与狠活
5、结语
1、逆向原因
最近听朋友说灰产圈子做app已经全部用flutter这个技术了,因为flutter写的软件不好逆向,给了他们一些可乘之机,以此来对抗安全研究人员的分析,因此我利用一些课余时间(划掉,其实是不信邪,能难到哪里去),最后全部弄完之后发现其实确实稍微有点强度,从五一前逆了半天,然后五一玩完回来研究了一天才把算法全部破解,算法并不难也不陌生,不过对于从来没接触过dart语言的小白和so逆向的安全人员来说确实会造成很大的麻烦,和传统逆向的流程也有很大的区别
2、分析
首先收到朋友给的apk,安装在模拟器上,老规矩,先抓包看一下请求和数据结构,不着急反编译,如果没加密那不就白费功夫嘛(懒),结果发现抓的包全部握手失败,搜了一下才知道flutter这个框架自带SSL校验,事情开始有意思起来了,怪不得灰产都转用flutter了,确实有点东西哈,那么问题也不大,这里提供两种抓包方式
- 将apk解包之后,找到lib文件夹下面的libflutter.so,拖进ida搜索关键字ssl_server,最后使用frida去Hook一下对应校验函数修改返回值,或者使用010 Editor修改so即可,最后配合Postem绕过代理检测抓包即可
- 直接使用reqable抓包就行
抓到包之后可以发现几乎所有的返回值都被加密了,如下:
这时候初步怀疑可能是aes或者des,再看两个接口确认一下,发现格式几乎不变,那么基本可以断定是aes或者des了,我这边就直接拿最复杂的登录接口来做逆向分析,因为其他接口只有返回值是aes加密,入参基本都是明文,而登录接口则是入参和返回都进行了加密,而且入参经过多次抓包发现每次加密的参数结构都不一样,如下:
第一次
第二次
目前根据经验初步怀疑是rsa算法,这时候如果是rsa就有点难搞了,还需要去hook入参,因为rsa是一种非对称加密算法,加密的公钥无法解密,只能通过私钥解密,而通常情况下,开发者只要脑回路正常,就不会把解密的私钥放在apk内,而是将私钥放在服务器进行解密,所以后续还需要去逆出rsa的一个入参,而flutter这项技术反编译之后的文件又会极大的干扰我们的一个分析hook点和判断,有意思了
3、上科技与狠活
将apk解包,得到一个和普通安卓项目类似的一个文件目录:
这时候如果你直接去用jadx分析dex文件会发现里面什么关键字都没有,一搜一个不知道,因为利用 Flutter 框架开发的 app,打包后,会将代码打包成 so,在lib下生成libapp.so和libflutter.so,确实为我们的逆向分析添加了难度,没法直接分析java层了,那么我们直接用ida打开libapp.so,flutter框架写的代码都编译在这个so里面,ida打开之后我们会发现一大堆无法识别的方法,已知的方法也全是看不懂的sub_xxx,但是没关系,先不着急,我们先使用shfit+f12打开字符串搜索rsa关键字:
这时候可以发现盲点,非常显眼的一个文件 \"rsa_public_key.pem\",那么我们合理的猜测一下,是不是开发者把这个公钥放在了资源文件呢,熟悉安卓的大佬们都知道,assets文件夹通常存放的是本地资源,我们一起在解包的apk的assets文件夹中找一下,发现只有这两个目录
怎么没有rsa_key文件夹?不要着急,分析一下,这个apk是使用了flutter框架写的,那么相当于先使用dart语言写了代码之后,再转换为安卓项目,也就是多套了一层,那么flutter源码中的资源文件是不是生成安卓项目之后用一个代表flutter资源文件夹去存放也是非常的合理呢?那么根据文件夹的名字,我们可以发现有一个文件夹叫做flutter_assets,很像我们刚刚分析的吧?点进去看看
看来我们分析的没有问题,里面才是真正的flutter资源路径,我们再按照刚刚ida找到的路径进去看看
成功找到了rsa的加密公钥,好像目前为止并没有很难啊?别急,咱们还有aes的密钥以及rsa的入参没找到呢,我们继续搜一下aes
发现关键字,进去看看,发现符号全部没了
显然,这样子的情况并不利于咱们分析aes秘钥,符号全部消失了,这个时候就要请出flutter逆向神器Blutter了,github上有,拉下来就行,安装环境教程就不多赘述了,网上都有,不过个人建议你们在linux系统上面使用,我个人是使用手机直接装的
装好blutter之后,将libapp.so通过blutter进行反编译就能得到另外一套文件
我们这就先直接使用pp.txt分析,正常来说已经够用,在这里面方法的地址和字符串符号都已经基本还原了,先来打开分析一下,由于aes加密值是接口返回的,那么接口返回值了之后肯定要解密的,我们直接搜接口
找到了,那么我们再根据刚刚分析的返回逻辑往下翻,接口下面几行很明显是读取公钥赋值给publicKeyString变量,我们再往下面翻翻就能看到我们刚刚分析的assets/rsa_key/rsa_public_key.pem这个路径,应该是去读出来了再去给入参加密,进一步证明了我们刚刚找出来的公钥应该是没错的
再往下翻一下,可以看到
函数好像结尾了,再下面就是别的请求了,并没有看到aes有关的东西,好像陷入了僵局?没关系,我们已经知道了准确地址呀,直接掏出frida开始追栈,从这个函数最后结尾的地方往上追,应该就可以把这个函数经过的数据都追出来,毕竟这里面存的只是对象池,逻辑不是很清晰,我们从结尾地址开始追吧,最后一个结尾的地址在这
运行firda,然后打开blutter给我们生成的frida脚本,在进入回调函数中加一行根据上下文打印调用栈,一定要用我这种上下文打印出这个函数的栈,否则使用传统的打印抛出的话只会展示是哪个函数调用了,在这种逻辑不清晰的情况下,打印调用函数根本没意义
console.log(\'输出调用栈:\\n\' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\'\\n\') + \'\\n\');
然后再打印出对应地址的一个栈值,代码如下
console.log(\"============================= 输入 ===============================\",\"\\n\")for(let i = 0; i < 8; i++) { console.log(\"arg\",i,\": \", \"\\n\"); try { console.log(hexdump(args[i],{length: 0x1000}), \"\\r\\n\"); } catch (error) { console.log((args[i]), \"\\r\\n\"); }}
然后把脚本中的fn_addr变量修改为我们刚刚找到的函数结尾地址:0x5c6e44开始追栈
先看到打印的调用栈,重点记一下,待会追栈一个个来
然后翻一下打印的参数,没有什么特殊的值,咱们继续追上一栈:0x98bbd4
还是没什么特殊的,不要着急,继续追,最后也是成功在栈中发现了可疑值: AES/ECB/Pkcs7 256
说明使用了AES的ECB模式,并且pkcs7填充,且秘钥为256位,这个位数的aes比较少见,和常见的16位key不同,256位的密钥长度为32位,而且ecb模式只有一个key并没有iv,那么这个栈里面有没有呢?仔细再翻一下,会发现一个秘钥:GcgzsKdDZTumABNz7uujrCfPIk9TQ355正是32位哈哈哈,python直接解密试一下,得到结果:
{\'code\': 0, \'msg\': \'success\', \'data\': {\'token\': \'81bf1*********************************************44\', \'infoPo\': {\'userId\': \'191983********4640\', \'merchantAcct\': \'sf63\', \'masterAcct\': \'lisasm\', \'agentAcct\': \'lisasm_1\', \'userAcct\': \'A7***6A\', \'acctType\': 3, \'referCode\': None, \'shareCode\': \'A7AH3P6A\', \'isPartner\': 0, \'phoneNumber\': None, \'background\': None, \'headUrl\': \'/emp/head/45d8b46d437047818c919f4a0b403b3a\', \'nickName\': \'A7AH3P6A\', \'signature\': None, \'loginType\': None, \'coinBalance\': 0, \'balance\': 0, \'exp\': 0, \'expLevel\': 0, \'iconFree\': 1, \'vipBegin\': None, \'vipEnd\': None, \'vipFlag\': False, \'vipTitle\': None, \'vipPackageId\': None, \'userStatus\': 0, \'followers\': None, \'followed\': None, \'lastLoginDate\': \'2025-05-07\', \'currentLoginDate\': \'2025-05-08\', \'city\': \'中国\', \'gender\': 0, \'videoFreeBegin\': None, \'videoFreeEnd\': None, \'actorFreeBegin\': None, \'actorFreeEnd\': None, \'expand\': None}}}
第二步完成,那么最后一步入参我们继续追栈,最终再追一个栈后发现
哈哈,参数出来了,最后用python把加密和解密结合,登录成功!
5、结语
总的来说,就是比较新奇,但是在blutter这样强大的反编译工具编译之后,其实考验的就是耐心和基本功了,这个软件在我写这篇文章的时候更新了一次,地址有所变化,如果想自己试试的话可以按照我的思路找一下地址,最后总结,不难,散会