1、目的
探索学习app接口的加解密机制,并通过api模拟调用的方式,发起业务请求。仅供学习。
2、工具准备
样本App版本:v5.0.80,v5.0.90
设备:Oppo R9s(Android7.1.1)+ MacOS Big Sur(Intel)
注入框架:xposed、frida(hluda 15.2.2)
反编译&其他:JEB、jadx、Charles
3、过程
大致分为抓包、脱壳、反编译、动态调试/加解密算法探索,构造模拟请求几个步骤,每个步骤都可能有不同的异常出现,本文主要记录在过程中的主体脉络和流程,过程中会附上关键代码。
3.1 抓包
首先尝试在手机上配置wifi代理,但Charles中无法看到相应的包记录。猜测是因为App屏蔽了网络代理,因此改用其他方式。手机上安装Drony,并开启手机全局网络代理(类型选择:socks5),代理地址指向Chares,此时就可以愉快的看到请求记录了。
如果是通过iOS抓包,直接通过小火箭抓包也是灰常方便。另外下载Drony App可能需要TZ,解决无法访问的问题。
在抓到的报文中,可以看到每次请求中,都包含了一些奇怪的header,比如t、spv、n、st,这些字段大概率与api接口的加密与签名有关。接下来,需要结合代码进一步分析。
3.2 脱壳&反编译
直接通过Xposed + 反射大师App,即可做到轻松脱壳,App未针对Xposed做检测。脱壳后得到7个dex文件,使用python脚本合并,将7个dex文件利用jadx全部反编译成Java文件到同一目录,即可直接翻阅App反编译后的源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
这时直接在反编译的结果中搜索关键词"spv",却发现找不到。难道这些字段都隐藏到so中了,那就麻烦了。这时使用JEB再次反编译试试看,再次搜索"spv",找到了。
这里,要提醒一下:针对反编译,同样的dex文件,用不同的反编译工具,结果也会不一样,可读性差异很多,因此当使用一种工具反编译失败的话,可以尝试用不同的工具,比如,通用一段代码的反编译结果,使用jadx时,提示反编译失败,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
但是使用JEB时,结果则基本可用,如下:
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 |
|
3.3 动态调试分析
拿到反编译源码后,接下来就需要结合frida动态分析代码调用链,找到api调用的核心算法逻辑并加以验证。
在App最新版本v5.0.90上,连接frida客户端。frida注入失败。随后换了hluda、xcube等方案均以失败告终,看了下app的加固方案,使用的腾讯的加固方案,对应的壳文件是libshell-super.cn.xxxxclub.app.so
,尝试绕过壳的反注入逻辑,也没有效果。
这时偶然看到旧版本的app使用的壳文件是libshell-super.2019.so
,灵光一闪,感觉旧版本的app上应该有机会,于是下载安装v5.0.80,frida注入成功了。app上开启了强制更新,于是在Charles上hook重写了app检查更新接口的返回结果,让app检查不到新版本,app仍然可以继续使用(后续有风险,历史接口可能下线)。
旧版本app上也可以使用frida工具集:Objection,通过调试和代码比对,基本确认了核心的算法签名逻辑位置:
签名的传入参数为分别为:t
- 时间戳、data_json
- 按json序列化后的业务对象参数、n
- 去掉"-"符号后的uuid(32位字符串)、auth_token
- 登录后用户令牌,按照如下规则排列所得:
1 |
|
返回字符串即为签名结果 - st
该签名算法有使用native方法,具体算法逻辑应该需要反汇编相应的so文件了。签名规则已经基本明确了,直接调用java层方法,走RPC调用即可得到我们想要的结果。偷懒了,就不去深挖汇编代码了,笔者也不确认一定能找到结果-_-||
3.4 RPC调用
1)创建js文件app_inject.js
,声明rpc接口
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 |
|
2)创建frida客户端,声明rpc调用。文件名:frida_client.py
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 |
|
3)构造参数,发起RPC调用。文件名:demo.py
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
|
再看看结果,已经成功得到响应数据了。大功告成!
3.5 踩坑说明
在执行frida js注入时,Java.enumerateClassLoaders()
仅支持Android 7.0及以上系统,若使用低版本的Android系统,如Android 6.1,则需要使用send(),进行消息异步通知。当采用异步通知时,在Python客户端的编码中,需要定义消息回调函数,同时将异步调用封装成同步调用,方便上游调用使用。对应的js代码和python代码如下:
app_inject_for_android_6.0.js
:
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 |
|
frida_client_for_android_6.0.js
:
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
|
3.6 备注
通过测试验证,可以发现两个版本v5.0.80,v5.0.90的签名算法是一致的。因此可以直接利用v5.0.80做签名即可。
打完收工!