移动安全-Frida 应用分析及案例实践

一、Frida 简介

Frida 是核心引擎使用 C 写的一款动态分析工具,通过 Frida 我们可以把一段 JavaScript、Python 等代码注入到一个正在运行中的应用程序进程中去。

Hook

Frida 注入代码的过程通常被称为 Hook(钩子)。在运行中的应用程序拦截和修改函数或方法的执行流程称为 Hooking 。通过 Hook,我们可以在目标应用程序的指定函数执行前或执行后插入自定义的代码,从而改变函数的行为或捕获关键信息。换个思路理解就类似于我们日常 Burp 抓包修改,但 Hook 需要定位到相应的函数,然后通过编写 Hook 脚本修改其函数的行为或者达到其它目的,通常基本的脚本编写使用 Javascript 或者 Python,但 Frida 还提供了 Node.js、 Swift、.net、Qml 等语言的接口封装,根据个人选择。

Frida 注入代码的过程通常被称为 Hook(钩子)。在运行中的应用程序拦截和修改函数或方法的执行流程称为 Hooking 。通过 Hook,我们可以在目标应用程序的指定函数执行前或执行后插入自定义的代码,从而改变函数的行为或捕获关键信息。换个思路理解就类似于我们日常 Burp 抓包修改,但 Hook 需要定位到相应的函数,然后通过编写 Hook 脚本修改其函数的行为或者达到其它目的,通常基本的编写使用 Javascript 或者 Python,但Frida 还提供了 Node.js、 Swift、.net、Qml 等语言的接口封装,根据个人选择。

相应功能

在实际测试中,Frida 主要有以下几种功能可辅助安全测试人员进行测试。

  • 监视加密API
  • 修改函数的输出
  • 绕过 AES 加密
  • 绕过 SSL Pinning、ROOT 检测
  • 追踪应用代码
  • 脱壳
  • ……

二、工具安装

frida 分为客户端和服务端,服务端一般在目标设备上运行的,客户端则在开发机上运行,如对 Android APP 的调试,服务端放在 Android 设备上,而客户端则在我们自己的电脑上安装即可。(注意:服务端与客户端的 frida 版本需要一致,否则可能会出现报错)

1、服务端,下载地址:https://github.com/frida/frida,选择 android-arm64.xz 版本

image-20231125132155193

使用 adb 上传至手机中,adb 下载地址:https://developer.android.google.cn/studio/releases/platform-tools?hl=zh-cn(在进行 Android APP 调试,测试机记得 ROOT,这边不赘述), 以下执行可知,adb shell 进入后为 $ ,即默认权限为普通权限。

image-20231125131931077

本人上传的为 data 目录,需要 root 权限,所以在执行上传文件前,先执行 adb root ,然后手机上点击允许调试。

1
adb root

image-20231125134232951

1
adb push D:\app\test\frida-server-16.1.6-android-arm64 /data/app_test

image-20231125134927921

进入 adb shell 将上传的 frida 给予运行权限

1
2
3
adb shell
cd /data/app_test
chmod 777 frida-server-16.1.6-android-arm64

image-20231125134816019

2、客户端,客户端 frida 安装较为简单,直接使用 pip3 进行安装即可,但需要 python3.x 环境,这边使用 python 3.9 进行安装。

1
2
3
pip3 install frida	// 安装 frida
pip3 install frida==16.1.6 //指定版本安装
pip3 install frida-tools // 安装 frida cli 工具,可运行 frida 某些命令

image-20231125135652353

3、验证,先在 Android 设备进入 frida-server 所在目录运行 frida-server 启动服务端(服务端最好修改一下名称,部分安全加固会将frida 字段进行安全检测)

1
./frida-server-16.1.6-android-arm64

image-20231125135907946

在电脑上使用 frida-tools 自带命令,查看是否已经和手机连接了,下图使用 frida-ps -Uia 命令查看手机上正在运行的程序,证明服务端与客户端通讯正常

1
frida-ps -Uia

image-20231125140543748

三、实践案例

追踪程序函数

这边追踪某 APP open()函数,查看其输出情况如何。

1、启动服务端(即手机上运行 frida-server)

1
./frida-server-16.1.6-android-arm64

image-20231125212854538

2、客户端(电脑上)进行追踪 open() 函数,首先查看正在运行的程序获取包名

1
frida-ps -Uia

image-20231125213212970

选择其中一个带有 open() 函数的程序进行追踪。

1
frida-trace -U -i open com.tencent.android.qqdownloader

等待一段时间后,frida 开始 trace 到后台正在运行程序的 open()

image-20231125213641791

打开 APP 程序会发现 frida trace 到大量调用 open() 函数

image-20231125213758207

image-20231125213957887

Bypass SSL Pinning & ROOT

网上 search 的 bypass 脚本,具体地址遗忘了

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
Java.perform(function() {
console.log(' ')
console.log('***********************************')
console.log('* Start Bypass SSL Pinning Nowing *')
console.log('***********************************')

var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var SSLContext = Java.use('javax.net.ssl.SSLContext');

var TrustManager = Java.registerClass({
name: 'com.test.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {
},
checkServerTrusted: function (chain, authType) {
},
getAcceptedIssuers: function () {
return [];
}
}
});

var TrustManagers = [TrustManager.$new()];

var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom'
);
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {
console.log('! Intercepted trustmanager request');
SSLContext_init.call(this, keyManager, TrustManagers, secureRandom);
};

console.log('* Setup custom trust manager');

try {
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function (str) {
console.log('! Intercepted okhttp3: ' + str);
return;
};

console.log('* Setup okhttp3 pinning')
} catch(err) {
console.log('* Unable to hook into okhttp3 pinner')
}

try {
var Activity = Java.use("com.datatheorem.android.trustkit.pinning.OkHostnameVerifier");
Activity.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (str) {
console.log('! Intercepted trustkit{1}: ' + str);
return true;
};

Activity.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (str) {
console.log('! Intercepted trustkit{2}: ' + str);
return true;
};

console.log('* Setup trustkit pinning')
} catch(err) {
console.log('* Unable to hook into trustkit pinner')
}

try {
var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
console.log('! Intercepted TrustManagerImp: ' + host);
return untrustedChain;
}

console.log('* Setup TrustManagerImpl pinning')
} catch (err) {
console.log('* Unable to hook into TrustManagerImpl')
}

try {
var PinningTrustManager = Java.use('appcelerator.https.PinningTrustManager');
PinningTrustManager.checkServerTrusted.implementation = function () {
console.log('! Intercepted Appcelerator');
}

console.log('* Setup Appcelerator pinning')
} catch (err) {
console.log('* Unable to hook into Appcelerator pinning')
}

});

在 APP 启用了 SSL Pinning 时,无法通过 Burp 或 Fiddle 等抓包工具进行直接抓取 Https 的流量包的。

image-20231128201936123

1、启动服务端

image-20231127205755131

编写 frida 脚本,绕过 SSL Pinning。在电脑上启动 frida 客户端,对该 app 注入 SSL Pinning 脚本,绕过其限制。在启动前先执行 frida-ps -Uia 查找正在运行的 APP 找到我们需要绕过的 APP 的包名

1
frida-ps -Uia

微信图片_20240216164737

脚本代码上文已列出,暂命名为 Bypass.js,启动脚本后手机自动启动 APP,然后再运行中注入绕过代码

1
frida -U -f com.app包名.xxxx -l Bypass.js

image-20231128202355240

成功抓取到 Https 流量

image-20231128202609642

追踪加密函数

对某一 APP 加密函数进行追踪,代码如下

image-20231209122317290

直接右键复制为 frida 代码

image-20231209122204941

自动复制的 frida 脚本代码为,注意 m12903a 方法名在程序运行编译时被重载为 a ,由上图 renamed from:a* 可中,所以 frida脚本在编写时,m12903a 需要替换为 a

1
2
3
4
5
let LoginActivity = Java.use("com.方法名.LoginActivity");
LoginActivity["a"].overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function (str, str2, str3) {
console.log(`LoginActivity.m12903a is called: str=${str}, str2=${str2}, str3=${str3}`);
this["a"](str, str2, str3);
};

修改完善代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function () {
console.log("Script Start Nowing");

var LoginActivity = Java.use("com.方法名.LoginActivity");

LoginActivity.a.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function (str, str2, str3) {
console.log("LoginActivity.m12903a results are as follows");
console.log("str: " + str);
console.log("str2: " + str2);
console.log("str3: " + str3);
console.log("---------------------------------");

// 调用原始方法
this.a(str, str2, str3);
};
});

使用脚本进行 Hook,成功输出 m12903a 方法的结果

image-20231209123411245

现已知函数能被正常 Hook,继续进一步追踪程序在调用 m12903a 方法时的调用栈情况

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
Java.perform(function () {
console.log("Script Start Nowing");

var Exception = Java.use("java.lang.Exception");
var LoginActivity = Java.use("com.方法名.LoginActivity");

LoginActivity.a.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function (str, str2, str3) {

console.log("------------------------------------------------------------------------------------------------------------");
console.log("LoginActivity.m12903a results are as follows");
console.log("------------------------------------------------------------------------------------------------------------");
console.log("str: " + str);
console.log("str2: " + str2);
console.log("str3: " + str3);
console.log(" ");

// 调用原始方法
this.a(str, str2, str3);

try {
// 打印调用栈
var stackTrace = Exception.$new().getStackTrace();
console.log("------------------------------------------------------------------------------------------------------------");
console.log("LoginActivity.m12903a call stack results:");
console.log("------------------------------------------------------------------------------------------------------------");

for (var i = 0; i < stackTrace.length; i++) {
console.log(stackTrace[i].toString());
}
} catch (e) {
console.log("Error occurred while printing call stack: " + e);
}
};
});

image-20231209130052729

脱壳

frida-dexdump

frida-dexdump 是一个基于 Frida 的安全工具,可通过查找并转储内存中的dex文件以实现脱壳。测试目标使用 360 加固,尝试利用 frida-dexdump 进行脱壳。项目地址:https://github.com/hluwa/frida-dexdump

image-20231204204118906

1、在安装完 frida 环境的基础上(这一部分就是上面 “工具安装” 章节),然后直接利用 pip 可一键安装 frida-dexdump

1
pip3 install frida-dexdump

image-20231204203656939

2、利用 adb 连接 Android ,然后再启动 frida-server

1
2
3
4
adb root
adb shell
cd data/app_test
./frida-server-16.1.6-android-arm64

image-20231204204558680

3、Android 手机上运行需要测试的 APP,然后利用 frida-tools 查看该程序运行的 PID 号,已知 PID 号为 11256

1
frida-ps -Uia

image-20231204205154000

4、利用 frida-dexdump 进行脱壳处理,命令如下,这里选择 frida-dexdump -U -p 11265

1
frida-dexdump -U -p PID

由于 APP 存在 SSL Pinning 检测,导致脱壳失败,这边需要先进行Bypass SSL Pinning 然后再运行 frida-dexdump 进行脱壳处理

image-20231204210236674

Bypass SSL Pinning ,这边直接使用脚本 Hook ,也可以直接利用 Objection 进行 Hook

1
frida -U -f cn.xx.xxx -l Bypass.js

image-20231204210507093

再次运行 frida-dexdump 进行 dump dex

1
2
frida-ps -Uia
frida-dexdump -U -p PID

image-20231204210719257

最后利用 jadx 进行反编译查看 dex 代码,具体脱壳效果如何,只能祈求上天的恩赐了。

image-20231204210740033

六、参考

1
2
3
4
5
https://www.hackingarticles.in/android-penetration-testing-frida/
https://frida.re/docs/home/
https://github.com/frida/frida
https://codeshare.frida.re/browse
https://github.com/kaungkhantpy