Crane => Blog

猫系代码少年Crane

0%

frida辅助android app测试

这篇是在公司实习时候做的技术分享,语言比较官方=_=

总之就是介绍了一下frida分享APP的方法

前言

在最近的渗透测试中,很多目标都只提供了APP客户端,而没有提供web端,这就导致我们很难从web上面抓取接口进行测试,因此只能去抓取APP的流量包,再进行测试。但是如果APP的流量使用了加密或者签名技术,我们再去修改或者伪造流量包就会变得非常困难。

这时候就需要一些hook工具去辅助分析APP的执行过程,分析加密或签名的算法,提取密钥。现在比较流行的hook框架有:Xposed、Frida,Xposed作为传统的Android JAVA层hook框架,功能非常强大,也出现了大量依赖此框架的各类插件,比如曾经风靡一时的抢红包插件。但是Xposed无法对native层进行hook,而且现在越来越多的APP都开始检测Xposed框架存在而拒绝执行,Xposed本质上还是适合开发一些小插件常驻手机内使用,并不适合调试分析这种工作。

最近两年出现了一个新的hook框架Frida,从Native hook到java hook,从windows到android真可谓无所不能,它现在也是最热门的hook工具。本文就从Frida开始,以两个Android APP分析的例子,简单的介绍一下这款工具的功能。

初识Frida

Frida是啥

Frida是一套动态插桩工具集,也就是hook框架。他可以插入一些代码到原生APP中,从而监测或者修改APP的行为。简单的来说,Frida可以检查或者修改一个函数的参数或者他的返回值。

Frida有以下的特点

  • 脚本化

    开发者可以使用Frida支持的Java Script脚本,动态的注入一个无论是否有源码的进程中,Frida支持实时的重载代码,可以很方便的调试观察输入

  • 跨平台

    Frida可以运行在Windows, macOS, GNU/Linux, iOS, Android, QNX平台上,有各种语言的绑定

  • 免费开源

    Frida是一款自由软件,他的代码开源在github上:https://github.com/frida/frida

  • 非常实用

    已经很多的安全工程师使用Frida分析APP程序,创造了大量的实用分析脚本

Frida提供了丰富的文档和实例,可以在官网找到:

https://frida.re/docs/home/

为什么需要Frida

动态的观察修改函数行为一直是逆向分析的刚需,传统的动态调试工具需要频繁的人工参与,无法实现自动化,这就导致在面对众多的需要分析的APP的情况下,将耗费大量逆向分析人员大量的精力。

Frida的脚本化给了逆向分析人员快速开发patch脚本修改内存的能力,免去了重复劳动,节省了大量的时间。

Frida还提供了RPC调用,可以实现脚本的交互功能,比如调用APP中的加密模块,还可以暴露web接口,协作使用。

Frida安装

这里我们主要介绍Frida在Android上面的使用

在Android上的安装步骤

  1. 用adb push将下载好的frida-server传到android中
  2. 用adb shell进入Android,执行frida-server
  3. 截图中用的是mumu模拟器,所以用的是x86的,如果用实体机,使用arm平台的frida-sever

adb shell中的操作

安装完成之后在PC上执行frida-ps –U,可以看到android进程,到这里就全部装好了

列出进程

从零开始的Android HOOK

一个简单的Android APP

这里提供了一个简单的APP的例子,方便大家进行hook练习

这个APP的基本功能是每隔半秒在log中打印50+30的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {

public void onCreate(Bundle savedInstanceState) {
MainActivity.super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
fun(50, 30);
}
}

public void fun(int x, int y) {
Log.d("Sum", String.valueOf(x + y));
}
}

运行结果:

运行结果

我们现在开始对这个APP进行hook,完成两个目标:

  1. 在Frida的终端中输出原本fun函数的参数

  2. 替换fun调用时的参数,修改为fun(2,3)

编写一段Frida脚本完成上述功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(function(){
// 当JAVA环境存在时回调函数
var MainActivity = Java.use("com.example.myapplication.MainActivity");
// 获取MainActivity这个类
MainActivity.fun.implementation = function(x,y){
// 修改MainActivity类中fun这个函数的实现
console.log("fun("+x+", "+y+")");
// 在控制台中打印一条日志,内容为原来的参数
var ret = this.fun(2,3);
// 调用自身,将参数修改为2和3,并获取返回值
// 这里的this是指MainActivity这个类
return ret;
// 传送返回值
}
})

将脚本保存为test1.js,在命令行中运行frida -U [进程名] -l .\test1.js

frida控制台输出

可以观察到命令行中的输出是原本的函数参数,而abd logcat的输出是我们已经修改掉的值

adb logcat输出

一个复杂的例子

这个例子来自比较常见的通信加密,APP会产生一个随机密钥,可以把这个随机密钥理解为一个跟服务器协商好的随机的会话密钥,同时会产生一个secret,可以理解为要通信的明文。

用随机密钥加密的secret保存为密文,显示在文本框内,要求在输入框内输入secret(明文),相当于解密这个密文。

工作逻辑

下面是Activity的代码:

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
public class MainActivity extends AppCompatActivity {
private AesUtils aesUtils;
private String savedPassword;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView password = findViewById(R.id.password);
EditText input = findViewById(R.id.input);
Button button = findViewById(R.id.button);
String secret = AesUtils.getRandom(4);
String key = AesUtils.getRandom(16);

aesUtils = new AesUtils(key);
savedPassword = aesUtils.encrypt(secret);
password.setText(savedPassword);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String testPassword = aesUtils.encrypt(input.getText().toString());
if(savedPassword.equals(testPassword)){
Toast.makeText(getApplicationContext(),"正确!",Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(getApplicationContext(),"错误!",Toast.LENGTH_SHORT).show();
}
}
});
}
}

在onCreate函数中,实例化了一个AesUtils类,产生了一个随机密钥和一个随机secret。

判断密码逻辑是:对输入框输入的内容进行加密,将加密结果与保存的密文对比,如果一致显示“正确!”,不一致显示“错误!”。

下面是AesUtils类:

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
public class AesUtils {
private String key;
public static final String DATA = "0123456789abcdefghijklmnopqrstuvwxyz";
public static Random RANDOM = new Random();

public AesUtils(String key) {
this.key = key;
}

public String encrypt(String text) {
try {
byte[] result = this.encrypt(this.key, text);
return new String(Base64.encode(result, Base64.DEFAULT));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public String decrypt(String text) {
try {
byte[] cipher = Base64.decode(text, Base64.DEFAULT);
byte[] res = this.decrypt(cipher,this.key);
return new String(res, "utf-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static String getRandom(int len) {
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append(DATA.charAt(RANDOM.nextInt(DATA.length())));
}
return sb.toString();
}

private byte[] encrypt(String password, String clear) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(password.getBytes(), "AES/CBC/PKCS5PADDING");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return cipher.doFinal(clear.getBytes("UTF-8"));
}

private byte[] decrypt(byte[] content, String password) throws Exception {
SecretKeySpec key = new SecretKeySpec(password.getBytes(), "AES/CBC/PKCS5PADDING");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(content);
}
}

有一个私有属性保存密钥,两个公开方法加密和解密,静态方法获取随机值。

在这个例子中,我们既不知道密钥,也不知道明文,这看似是一个不可能的任务。

运行截图

下面我们用两种方法对他进行破解:

方法一

我们观察到secret是由getRandom方法产生的,直接通过hook该方法获取加密前的secret。

这是最简单的方法,但是需要Frida去启动这个APP,才能hook住getRandom调用。

1
2
3
4
5
6
7
8
Java.perform(function(){
var AesUtils = Java.use("com.example.myapplication.AesUtils");
AesUtils.getRandom.implementation = function(x){
var ret = this.getRandom(x);
console.log("[call] AesUtils.getRandom("+x+") -> "+ret);
return ret;
}
}

方法一结果

在实战中,这种方法相当于去获取加密之前的明文内容,最简单有效,但是可能无法修改包的内容。

方法二

AesUtils类中保存了加密所用的密钥,我们通过搜索JAVA对象,找到AesUtils实例,再获取该实例的key字段,再去调用AesUtils类中的解密函数,让APP帮我们完成解密工作。

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
Java.perform(function(){
var AesUtils = Java.use("com.example.myapplication.AesUtils");
AesUtils.getRandom.implementation = function(x){
var ret = this.getRandom(x);
console.log("[call] AesUtils.getRandom("+x+") -> "+ret);
return ret;
}

AesUtils.encrypt.overload('java.lang.String').implementation = function(x){
var ret = this.encrypt(x);
console.log("[call] AesUtils.encrypt("+x+") -> "+ret);
return ret;
}


Java.choose("com.example.myapplication.MainActivity", {
onMatch:function(instance){
console.log("[Java.choose MainActivity] "+instance);
var savedPassword = instance.savedPassword.value;
console.log("[savedPassword] "+savedPassword);

Java.choose("com.example.myapplication.AesUtils", {
onMatch:function(instance){
console.log("[Java.choose AesUtils] "+instance);
var key = instance.key.value;
console.log("[AesUtils.key] "+key);
var secret = instance.decrypt(savedPassword);
console.log("[secret] "+secret);

},
onComplete:function() {
console.log("[Java.choose AesUtils] end")
}
});

},
onComplete:function() {
console.log("[Java.choose MainActivity] end")
}
});

})

这样我们直接附加到APP进程上就可以完成上述操作。

方法二结果

在实战中,这种方法相当于去分析APP的通信加密算法,利用Frida去调用APP中的解密函数,可能会比较费时,但是可以完全控制请求包。

一些Frida技巧和工具

objection

基于Frida的内存漫游工具,可以实现批量hook,有很多辅助功能,比如:绕过SSL pinning。

objection

https://github.com/sensepost/objection

FRIDA-DEXDump

基于Frida的脱壳工具,在内存中搜索DEX结构进行脱壳,甚至可以应用在部分的类抽取加固的APP上

FRIDA-DEXDump

https://github.com/hluwa/FRIDA-DEXDump

Brida

一个Frida和Burp Suite连接插件,可以实现在Burp调用Frida提供的RPC接口,同时也提供了一些工具脚本,比如:绕过SSL pinning,绕过root检测

Brida

https://github.com/federicodotta/Brida

总结

APP现在已经成为了互联网的主要入口,大量的企业将主要业务放在APP中,这给渗透测试带来了一定的困难。Frida的出现,降低了APP分析的难度,可以更简单的抓取或者修改请求包,提升了工作效率。