前言
某天收到一个测试安卓app的项目,因此简单的看了一看。
app 测试
先从项目群下载该app
判断是否有壳
这里借助工具PKID去查询,提示可能是无壳或者未知的加壳方式
PDID查壳工具地址:http://www.legendsec.org/1888.html
实际上基本也就无壳了,直接丢入jadx了。
当然这里也可以手工判断有无加壳。
可参考文章:https://blog.csdn.net/g5703129/article/details/85054405
反编译
这里就直接借助jadx工具去查看源码了
jadx工具地址:https://github.com/skylot/jadx
APP运行和代理配置
这里使用夜神模拟器去运行app
夜神模拟器官网地址:https://www.yeshen.com/
直接将该apk文件拖入到模拟器里面,即可安装成功
开始代理配置
点击工具,在设置里面选择WLAN
鼠标左键长按点击WiredSSID,再点击修改网络
展开高级选项、代理选择手动
输入物理机IP地址,以及监听端口。如我本机是192.168.1.104,监听端口是8080
然后在抓包工具burpsuite处设置监听地址和端口
直接打开APP,发现数据已经做了传输加密处理,不方便渗透
请求数据加密信息搜集
此处可能被加密的参数主要如下:
| 参数名 | 值 |
|---|---|
| deviceId | a2d2ea2c0014c3b6 |
| appKey | sdfskfjdsklewkrw |
| knockToken | 7e3369d289f52c7800d251e849d5b3bb |
| requestId | 4158ad70433b4bfa84c0a793fb974e88 |
| sign | 304502206a09cc568befa5c2748ac3fc853e8cf39ba864f8a1a829b8ec23b b6c91a549090221008eccaf8e668b2c0b4ff76949159c78ab515627a0185 1e25df1ec9320b1822e30 |
| key | 045b6afabdd0519e757872f42fb976c4f3aff7357ceff2254d428112196942 91931e5a11dc44d00090226675aa0052ab3fd1ad33cbcb39f42645be5cb5 90b9e7d2156b9ed183e851f1cf5cf46b1486aa2eca40eb37ab0fcf8da506a6 de7e5bc166bf212f14465df1c9009b5d078bdd6a51 |
| token | 7e3369d289f52c7800d251e849d5b3bb |
| 请求体数据 | 847216a2b3da8ce1544a73424b14f194228c487063895896ef029c3c1394 ca3ff7604e71a0990b01869c616f0a359262c9e7649ccc7ee48c99f106350d 0c15e2be511bbc240696aefe09e711bc1b0c73 |
数据加密流程分析
AndroidManifest.xml 分析
通过jadx查看apk文件资源文件目录下的AndroidManifest.xml文件
简单解释一下该xml文件的内容
app 权限声明
uses-permission节点
| uses-permission节点 | 代表权限 |
|---|---|
| android:name=”android.permission.ACCESS_COARSE_LOCATION” | 允许应用程序访问设备的粗略位置信息 |
| android:name=”android.permission.ACCESS_FINE_LOCATION” | 允许应用程序访问设备的精确位置信息 |
| android:name=”android.permission.VIBRATE” | 允许应用程序控制设备的振动器 |
| android:name=”android.permission.REQUEST_INSTALL_PACKAGES” | 允许应用程序请求安装包的安装权限 |
| android:name=”android.permission.ACCESS_DOWNLOAD_MANAGER” | 允许应用程序访问系统的下载管理器 |
| android:name=”android.permission.WRITE_EXTERNAL_STORAGE” | 允许应用程序写入外部存储器 |
| android:name=”com.android.launcher.permission.INSTALL_SHORTCUT” | 允许应用程序在主屏幕上安装快捷方式 |
| android:name=”com.android.launcher.permission.READ_SETTINGS” | 允许应用程序读取主屏幕设置信息 |
| android:name=”android.permission.READ_EXTERNAL_STORAGE” | 允许应用程序读取外部存储器上的文件 |
| android:name=”android.permission.INTERNET” | 允许应用程序访问网络 |
| android:name=”android.permission.ACCESS_NETWORK_STATE” | 允许程序可以获得设备的网络状态信息 |
| android:name=”android.permission.MANAGE_EXTERNAL_STORAGE” | 允许应用程序管理外部存储器 |
| android:name=”android.permission.CAMERA” | 允许应用程序访问设备的摄像头 |
| android:name=”android.permission.ACCESS_WIFI_STATE” | 允许应用程序访问设备的 Wi-Fi 状态信息 |
| android:name=”android.permission.CHANGE_WIFI_STATE” | 允许应用程序更改设备的 Wi-Fi 状态 |
| android:name=”android.permission.READ_PHONE_STATE” | 允许应用程序读取设备的电话状态和身份识别码 |
| android:name=”android.permission.USE_BIOMETRIC” | 允许应用程序使用生物识别技术来验证用户身份 |
| android:name=”android.permission.FLASHLIGHT” | 允许应用程序控制设备的闪光灯 |
| android:name=”android.permission.GET_TASKS” | 允许应用程序获取正在运行的任务信息 |
uses-feature 节点
| uses-feature | 功能 |
|---|---|
| android:name=”android.hardware.camera.autofocus” | 相机自动对焦功能 |
| android:name=”android.hardware.camera” android:required=”false” | 允许应用程序访问设备的摄像头。 |
| android:name=”android.hardware.camera.flash” android:required=”false” | 允许应用程序使用设备的闪光灯 |
| android:name=”android.hardware.screen.portrait” | 声明应用程序支持竖屏模式 |
| android:name=”android.hardware.camera.front” android:required=”false” | 允许应用程序访问设备的前置摄像头 |
| android:name=”android.hardware.screen.landscape” android:required=”false” | 声明应用程序支持横屏模式 |
| android:name=”android.hardware.wifi” android:required=”false” | 允许应用程序访问设备的 Wi-Fi 功能 |
application节点
application里面设置了应用程序的类名android:name=com.xxx.base.xxx.app.App,作用是在应用程序启动时执行初始化和设置全局变量等操作。
Activity子节点
注:以下Activity的功能仅从变量命名去猜测功能,且一个Activity代表一个UI界面或者一个特定的功能
| Activity节点 | 功能 |
|---|---|
| AuthActivity | 用户认证登录的Activity |
| ScanSelfActivity | 扫描自己的二维码的Activity,设置启动模式为singleTask |
| ScanQrCodeZxingActivity | 扫描二维码 |
| ShowFrgActivity | |
| TestActivity | 测试用的Activity |
| WelcomActivity | 应用程序的欢迎页面,由于设置了android.intent.action.MAIN,因此程序启动时,先启动WelcomActivity |
| IndexActivity | 应用程序主界面的Activity,设置启动模式为singleTask |
| LoginMainActivity | 登录界面的Activity |
| MainActivity | 应用程序主界面的Activity |
| SelectPicPopupWindow | 选择图片的弹窗活动 |
| CaptureActivity | 扫描二维码的Activity |
| ToCheckLivingActivity | 可能是生物识别有关的Activity |
| CheckLivingActivity | 可能是生物识别有关的Activity |
通过分析发现程序打开时,最先运行WelcomActivity,因此鼠标双击点入跟进分析。
程序执行分析
WelcomActivity
为了方便理解,这里借助该博客https://blog.csdn.net/weixin_44235109/article/details/107600938里的的activity生命周期的示意图。
当程序被打开时,最先执行onCreate方法
在此处的WelcomActivity执行后,最先执行onCreate方法。并最终根据knockResultEvent.isConnect的布尔值分别进入到goLoginPage方法或showDialog方法。
goLoginPage
此处逻辑是判断用户sessionid和当前用户账号是否为空,如果为空,进入到LoginMainActivity(登录界面的Activity),如不为空(登录状态),进入到IndexActivity(应用程序主界面的Activity)。
showDialog
此处逻辑是,弹出提示框“网络安全连接异常时”,用户如果点击重新加载的按钮,则最终进入到sendUpdMessage方法,如果点击退出按钮,则kill掉当前进程。
继续分析
正常情况下,打开一个app时,应先进入到登录界面,因此这里主要看goLoginPage方法。即当用户session和当前账号为空,进入到LoginMainActivity(登录界面的Activity)
通过上面的activity生命周期示意图可知,先执行onCreate()方法。
在这个方法里面,实例化了一个LoginMainFrg对象。鼠标点击,跟进查看。
这里主要关注onActivityCreated()方法。
注:此处是先执行onCreate()方法,再执行onActivityCreated()方法
在onActivityCreated()方法中有一个onSuccess方法,即当这个activity执行成功后,会去执行该方法,该方法最终调用的是init方法。
该方法中,关键代码为这两部分
此处hashMap的值是一个经纬度,该值会被RequestHeadTransport.getHeadPan处理
跟进到RequestHeadTransport.getHeadPan就发现熟悉的东西了,也就是上面请求信息搜集到的。
那就在此处利用frida进行简单的hook一下。
frida环境配置
adb
首先需要一个adb连接设备,在夜神模拟器的安装目录的bin目录下即存在该adb文件。这里就直接复制粘贴使用夜神模拟器的adb了。
注:两个adb得版本一致才能连接设备
此时可以看见,能正常获取到设备列表信息
1 | adb devices |
查看该安卓系统的内核版本
1 | adb.exe shell getprop ro.product.cpu.abi |
然后去github下载frida server的x86 版本
frida下载地址:https://github.com/frida/frida/releases
注:由于我本地之前安装过frida,没做升级,所以frida-server下的是低版本的。
然后传入模拟器
1 | adb.exe push frida-server-15.0.18-android-x86 /data/local/tmp |
frida安装
1 | pip3 install frida-tools |
注:此处本地安装的frida和模拟器传入的frida-server版本应该相同。
测试frida
先连接设备,并给予frida-server执行权限(x),最后运行frida-server。
注:截图里面没体现su,要记得su一下权限
1 | adb.exe shell |
可以发现,能成功连接设备,并查看到模拟器上的后台进程
frida hook脚本
这里先做流量转发
1 | adb forward tcp:27042 tcp:27042 |
请求数据的hook脚本
前面已经知道,要在RequestHeadTransport类的getHeadPan方法处进行hook。
1 | import frida, sys |
将字符串打印后,可以看见是成功hook到了。其中请求数据的明文,直接在控制台的第一行就被输出了。
简单的处理一下,方便浏览
1 | Java.perform(function () { |
也就将上述所有的数据都拿到了。
继续分析LoginMainFrg
上面的分析,只是拿到了数据,而还没有开始发送数据包。因此继续分析。
生成好的参数,会被传递到init方法中去。跟进到该方法。
跟进以后,就发现,这个是抓包过程中发现的http接口地址。
分析RxUtil发现响应信息解密
此时进入到RxUtil中分析
该Util先初始化TacSdkService在初始化 Retrofit 。
此时初始化TacSdkService时,执行init方法,init方法调用了Retrofit的getRetrofit方法,并最终调用了lambda$getRetrofit$1。
阅读该方法处的代码,通过变量命名似乎发现了我所需要的东西,即对响应数据解密。
响应数据的hook脚本
1 | var RxUtil = Java.use("gov.xxx.utils.RxUtil"); |
可以看见,脚本成功hook,数据被成功解密。
最终版hook脚本
通过总结可知,以上的分析是通过对WelcomActivity分析时加载的第一个接口的数据加解密的分析,似乎是只针对这一个接口生效。
通过分析可知,请求数据加密和响应数据解密都是由com.crypto.sm.SMHelper来完成的。因此这里可以直接hook SMHelper的sm4Encrypt和sm4Decrypt方法。
frida hook脚本如下:
1 | Java.perform(function () { |
可以看见,只要有流量产生,脚本就帮你hook。
既然数据加解密搞定了,其他服务端的漏洞也都能相对的测试一下了。难度不大。
总结
frida hook挺有意思的。之前没有学过,这次恰好遇到了,也就直接现学现用。这个还真是不错。
应该能hook掉这个app的活体检测,等下再研究看看。
注:此处能通过关键字搜索去定位加密函数,一般有加密函数的地方,也就有解密函数。因此不用像我这么麻烦从程序入口开始分析。