实战frida hook 安卓app

前言

某天收到一个测试安卓app的项目,因此简单的看了一看。

app 测试

先从项目群下载该app

image

判断是否有壳

这里借助工具PKID去查询,提示可能是无壳或者未知的加壳方式

image

PDID查壳工具地址:http://www.legendsec.org/1888.html

实际上基本也就无壳了,直接丢入jadx了。

当然这里也可以手工判断有无加壳。

可参考文章:https://blog.csdn.net/g5703129/article/details/85054405

反编译

这里就直接借助jadx工具去查看源码了

jadx工具地址:https://github.com/skylot/jadx

image

APP运行和代理配置

这里使用夜神模拟器去运行app

夜神模拟器官网地址:https://www.yeshen.com/

直接将该apk文件拖入到模拟器里面,即可安装成功

image

开始代理配置

点击工具,在设置里面选择WLAN

image

鼠标左键长按点击WiredSSID,再点击修改网络

image

展开高级选项、代理选择手动

输入物理机IP地址,以及监听端口。如我本机是192.168.1.104,监听端口是8080

image

然后在抓包工具burpsuite处设置监听地址和端口

image

直接打开APP,发现数据已经做了传输加密处理,不方便渗透

image

请求数据加密信息搜集

此处可能被加密的参数主要如下:

参数名
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文件

image

简单解释一下该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子节点

image

注:以下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生命周期的示意图。

image

当程序被打开时,最先执行onCreate方法

image

在此处的WelcomActivity执行后,最先执行onCreate方法。并最终根据knockResultEvent.isConnect的布尔值分别进入到goLoginPage方法或showDialog方法。

goLoginPage

image

此处逻辑是判断用户sessionid当前用户账号是否为空,如果为空,进入到LoginMainActivity(登录界面的Activity),如不为空(登录状态),进入到IndexActivity(应用程序主界面的Activity)。

showDialog

image

此处逻辑是,弹出提示框“网络安全连接异常时”,用户如果点击重新加载的按钮,则最终进入到sendUpdMessage方法,如果点击退出按钮,则kill掉当前进程。

继续分析

正常情况下,打开一个app时,应先进入到登录界面,因此这里主要看goLoginPage方法。即当用户session和当前账号为空,进入到LoginMainActivity(登录界面的Activity)

通过上面的activity生命周期示意图可知,先执行onCreate()方法。

image

在这个方法里面,实例化了一个LoginMainFrg对象。鼠标点击,跟进查看。

image

这里主要关注onActivityCreated()方法。

注:此处是先执行onCreate()方法,再执行onActivityCreated()方法

onActivityCreated()方法中有一个onSuccess方法,即当这个activity执行成功后,会去执行该方法,该方法最终调用的是init方法。

该方法中,关键代码为这两部分

image

此处hashMap的值是一个经纬度,该值会被RequestHeadTransport.getHeadPan处理

跟进到RequestHeadTransport.getHeadPan就发现熟悉的东西了,也就是上面请求信息搜集到的。

image

那就在此处利用frida进行简单的hook一下。

frida环境配置

adb

首先需要一个adb连接设备,在夜神模拟器的安装目录的bin目录下即存在该adb文件。这里就直接复制粘贴使用夜神模拟器的adb了。

注:两个adb得版本一致才能连接设备

image

此时可以看见,能正常获取到设备列表信息

1
adb devices

image

查看该安卓系统的内核版本

1
adb.exe shell getprop ro.product.cpu.abi

image

然后去github下载frida server的x86 版本

frida下载地址:https://github.com/frida/frida/releases

image

注:由于我本地之前安装过frida,没做升级,所以frida-server下的是低版本的。

然后传入模拟器

1
adb.exe push frida-server-15.0.18-android-x86 /data/local/tmp

image

frida安装

1
pip3 install frida-tools

image

注:此处本地安装的frida和模拟器传入的frida-server版本应该相同。

测试frida

先连接设备,并给予frida-server执行权限(x),最后运行frida-server
注:截图里面没体现su,要记得su一下权限

1
2
3
4
5
adb.exe shell
su
cd data/local/tmp/
chmod +x frida-server-15.0.18-android-x86
./frida-server-15.0.18-android-x86

可以发现,能成功连接设备,并查看到模拟器上的后台进程

image

frida hook脚本

这里先做流量转发

1
2
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

请求数据的hook脚本

前面已经知道,要在RequestHeadTransport类的getHeadPan方法处进行hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import frida, sys  

jscode = """
Java.perform(function () {
const util = Java.use("gov.xxx.RequestHeadTransport"); util.getHeadPan.implementation = function (str) {
console.log(str); var result = this.getHeadPan(str); console.log(result); return result; }});
"""
def message(message, data):
print(message)

process = frida.get_remote_device().attach("磐x")
script = process.create_script(jscode)
script.on('message', message)
script.load()
sys.stdin.read()

将字符串打印后,可以看见是成功hook到了。其中请求数据的明文,直接在控制台的第一行就被输出了。

image

简单的处理一下,方便浏览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(function () {  
const util = Java.use("gov.zwfw.iam.tacsdk.rpc.transport.RequestHeadTransport");
util.getHeadPan.implementation = function (str) {
console.log("[*] 请求体明文数据:"+str);
var result = this.getHeadPan(str);
const mEncryptBody = result.mEncryptBody.value;
console.log("[*] 请求体密文数据:" + mEncryptBody);
var hashMap = result.hashMap.value;
var keys = hashMap.keySet().toArray();
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = hashMap.get(key);
console.log("[*] " + key + ":" + value);
}
return result;
}
})
;

也就将上述所有的数据都拿到了。

image

继续分析LoginMainFrg

上面的分析,只是拿到了数据,而还没有开始发送数据包。因此继续分析。

image

生成好的参数,会被传递到init方法中去。跟进到该方法。

跟进以后,就发现,这个是抓包过程中发现的http接口地址。

image

分析RxUtil发现响应信息解密

此时进入到RxUtil中分析

image

Util先初始化TacSdkService在初始化 Retrofit

image

此时初始化TacSdkService时,执行init方法,init方法调用了RetrofitgetRetrofit方法,并最终调用了lambda$getRetrofit$1

阅读该方法处的代码,通过变量命名似乎发现了我所需要的东西,即对响应数据解密。

image

响应数据的hook脚本

1
2
3
4
5
6
7
8
9
10
11
12
var RxUtil = Java.use("gov.xxx.utils.RxUtil");  
RxUtil.lambda$getRetrofit$1.implementation = function (chain) {
console.log("-------------------------响应数据信息-------------------------")
var result = this.lambda$getRetrofit$1(chain);
var proceed = chain.proceed(chain.request());
var sm2Decrypt = Java.use("com.crypto.sm.SMHelper").sm2Decrypt("64cb2ea5725919a8697487ffabf2298fc4dee2903a7f80b80ee4f6c160b5f519", proceed.header("key"));
var body = proceed.body().string();
console.log("[*] 响应数据解密前:" + body);
var sm4Decrypt = Java.use("com.crypto.sm.SMHelper").sm4Decrypt(sm2Decrypt, body);
console.log("[*] 响应数据解密后:" + sm4Decrypt);
return result;
}

可以看见,脚本成功hook,数据被成功解密。

image

最终版hook脚本

通过总结可知,以上的分析是通过对WelcomActivity分析时加载的第一个接口的数据加解密的分析,似乎是只针对这一个接口生效。

通过分析可知,请求数据加密和响应数据解密都是由com.crypto.sm.SMHelper来完成的。因此这里可以直接hook SMHelpersm4Encryptsm4Decrypt方法。

image

frida hook脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Java.perform(function () {  
var SMHelper = Java.use("com.crypto.sm.SMHelper");
//加密算法
SMHelper.sm4Encrypt.overload('java.lang.String', 'java.lang.String').implementation = function (str1, str2) {
console.log("-------------------------------------------------------------------------------------------------------------------");
console.log("[*] 请求数据被加密前:" + str2);
console.log("[*] 请求数据加密密钥:" + str1);
var result = this.sm4Encrypt(str1, str2);
console.log("[*] 请求数据被加密后:" + result);
return result;
};
//解密算法
SMHelper.sm4Decrypt.overload('java.lang.String', 'java.lang.String').implementation = function (str1, str2) {
console.log("-------------------------------------------------------------------------------------------------------------------");
console.log("[*] 响应数据被解密前:" + str2);
console.log("[*] 响应数据解密密钥:" + str1);
var result = this.sm4Decrypt(str1, str2);
console.log("[*] 响应数据被解密后:" + result);
return result;
}
});

image

可以看见,只要有流量产生,脚本就帮你hook。

image

既然数据加解密搞定了,其他服务端的漏洞也都能相对的测试一下了。难度不大。

总结

frida hook挺有意思的。之前没有学过,这次恰好遇到了,也就直接现学现用。这个还真是不错。

应该能hook掉这个app的活体检测,等下再研究看看。

image

注:此处能通过关键字搜索去定位加密函数,一般有加密函数的地方,也就有解密函数。因此不用像我这么麻烦从程序入口开始分析。

Author: jdr
Link: https://jdr2021.github.io/2023/04/01/实战frida-hook-安卓app/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.