介绍

frida是平台原生appGreasemonkey,说的专业一点,就是一种动态插桩工具,可以插入一些代码到原生app的内存空间去,(动态地监视和修改其行为),这些原生平台可以是WinMacLinuxAndroid或者iOS。而且frida还是开源的。

框架从Java层hook到Native层hook无所不能,虽然持久化还是要依靠Xposedhookzz等开发框架,但是frida的动态和灵活对逆向以及自动化逆向的帮助非常巨大。

安装和部署frida

官方网站:Frida • A world-class dynamic instrumentation framework | Inject JavaScript to explore native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX

安装

1
2
pip install frida-tools
frida --version

部署frida-server

前往github项目Releases · frida/frida

下载相同版本的frida-server发行文件,注意请使用测试机器上的架构,如果是模拟器则是x86的

使用adb连接手机后,将文件上传到手机/data/local/tmp文件夹

1
adb push frida-server /data/local/tmp

对frida-server的端口进行转发

1
adb forward tcp:27042 tcp:27042

使用adb shell进入命令行,最好获取root权限后,进入此文件夹,修改文件权限

1
chmod 755 frida-server

然后运行

1
./frida-server

frida工具命令

查看连接的设备

1
2
3
4
5
6
$ frida-ls-devices
Id Type Name
---------------- ------ ----------------
local local Local System
t49dbutgc6wgizuw usb Redmi Note 8 Pro
socket remote Local Socket

获取设备列表后,在其他命令里可以使用-U来指定usb连接的设备,也可以使用-D <id>来指定id的设备

获取进程列表

1
frida-ps -D <id>	# -ai 精简输出,可能得不到具体的进程名 ":"的含义是指要在当前的进程名前面加上当前的包名,如果当前包名为com.example.jimu。那么这个进程名就应该是com.example.jimu:test。这种冒号开头的进程属于当前应用的私有进程,其他应用的组件不可以和他跑到同一个进程中

frida交互式命令行界面

spawn和attach

spawn方式启动:

1
frida -D <id> -l .\hello.js -f <package name> --no-pause # -o <log filename>

attach模式启动

1
frida -D <id> -l .\hello.js <process name>

快速上手

HOOK参数与结果

先写个简单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
package com.roysue.demo02;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

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

while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
}
}

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

}

可以看到,app的作用就是每1秒种输出50和30的和。

使用adb查看日志:

1
2
3
4
5
6
7
8
$ adb logcat | grep Sum
11-26 21:26:23.234 3245 3245 D Sum : 80
11-26 21:26:24.234 3245 3245 D Sum : 80
11-26 21:26:25.235 3245 3245 D Sum : 80
11-26 21:26:26.235 3245 3245 D Sum : 80
11-26 21:26:27.236 3245 3245 D Sum : 80
11-26 21:26:28.237 3245 3245 D Sum : 80
11-26 21:26:29.237 3245 3245 D Sum : 80

现在我们编写frida的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// s1.js

console.log("Script loaded successfully ");
Java.perform(function x() {
console.log("Inside java perform function");

//定位类
var my_class = Java.use("com.roysue.demo02.MainActivity");
console.log("Java.Use.Successfully!");//定位类成功!

//在这里更改类的方法的实现(implementation)
my_class.fun.implementation = function(x,y){
//打印替换前的参数
console.log( "original call: fun("+ x + ", " + y + ")");
//把参数替换成2和5,依旧调用原函数
var ret_value = this.fun(2, 5);
return ret_value;
}
});

js脚本编写好后,我们可以使用python脚本加载js脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# loader.py

import time
import frida

# 连接安卓机上的frida-server
device = frida.get_usb_device()
# 启动`demo02`这个app
pid = device.spawn(["com.roysue.demo02"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
# 加载s1.js脚本
with open("s1.js") as f:
script = session.create_script(f.read())
script.load()

# 脚本会持续运行等待输入
input()

这样,我们就可以使用python脚本启动frida,并将脚本加载到设备上的frida-server中运行。

然后会发现logcat输出:

1
2
3
4
5
6
7
8
11-26 21:44:47.875  2420  2420 D Sum     : 80
11-26 21:44:48.375 2420 2420 D Sum : 80
11-26 21:44:48.875 2420 2420 D Sum : 80
11-26 21:44:49.375 2420 2420 D Sum : 80
11-26 21:44:49.878 2420 2420 D Sum : 7
11-26 21:44:50.390 2420 2420 D Sum : 7
11-26 21:44:50.904 2420 2420 D Sum : 7
11-26 21:44:51.408 2420 2420 D Sum : 7

这样,我们的HOOK就成功了。

参数构造、方法重载、隐藏函数的处理

修改一下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
package com.roysue.demo02;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

private String total = "@@@###@@@";

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

while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
Log.d("ROYSUE.string" , fun("LoWeRcAsE Me!!!!!!!!!"));
}
}

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

String fun(String x){
total +=x;
return x.toLowerCase();
}

String secret(){
return total;
}
}

可以发现,fun函数存在重载。我们修改一下原来的python脚本以便观察报错信息

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
import time
import frida

def my_message_handler(message, payload): # 定义错误处理
print(message)
print(payload)

# 连接安卓机上的frida-server
device = frida.get_usb_device()

# 启动`demo02`这个app
pid = device.spawn(["com.example.test"])

print(f"[*] PID: {pid}")

device.resume(pid)

time.sleep(1)
session = device.attach(pid)

# 加载s1.js脚本
with open("s1.js", encoding="utf-8") as f:
script = session.create_script(f.read())

script.on("message", my_message_handler) # 调用错误处理
script.load()

# 脚本会持续运行等待输入
input()

运行后可以看到如下报错:

1
2
3
4
5
6
$ python loader.py
Script loaded successfully
Inside java perform function
Java.Use.Successfully!
{u'columnNumber': 1, u'description': u"Error: fun(): has more than one overload, use .overload(<signature>) to choose from:\n\t.overload('java.lang.String')\n\t.overload('int', 'int')", u'fileName': u'frida/node_modules/frida-java/lib/class-factory.js', u'lineNumber': 2233, u'type': u'error', u'stack': u"Error: fun(): has more than one overload, use .overload(<signature>) to choose from:\n\t.overload('java.lang.String')\n\t.overload('int', 'int')\n at throwOverloadError (frida/node_modules/frida-java/lib/class-factory.js:2233)\n at frida/node_modules/frida-java/lib/class-factory.js:1468\n at x (/script1.js:14)\n at frida/node_modules/frida-java/lib/vm.js:43\n at M (frida/node_modules/frida-java/index.js:347)\n at frida/node_modules/frida-java/index.js:299\n at frida/node_modules/frida-java/lib/vm.js:43\n at frida/node_modules/frida-java/index.js:279\n at /script1.js:15"}
None

这就是处理重载导致的问题,我们处理一下原来的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
console.log("Script loaded successfully ");
Java.perform(function x() {
console.log("Inside java perform function");

//定位类
var my_class = Java.use("com.example.test.MainActivity");
console.log("Java.Use.Successfully!");//定位类成功!

//在这里更改类的方法的实现(implementation)
my_class.fun.overload("int", "int").implementation = function(x, y){
//打印替换前的参数
console.log( "original call: fun("+ x + ", " + y + ")");
//把参数替换成2和5,依旧调用原函数
var ret_value = this.fun(2, 5);
return ret_value;
}

var string_class = Java.use("java.lang.String"); //获取String类型

my_class.fun.overload("java.lang.String").implementation = function(x){
console.log("*************************************");
var my_string = string_class.$new("My TeSt String#####"); //new一个新字符串
console.log("Original arg: " + x );
var ret = this.fun(my_string); // 用新的参数替换旧的参数,然后调用原函数获取结果
console.log("Return value: "+ ret);
console.log("*************************************");
return ret;
};
});

这里处理了两个方法的重载,运行结果:

1
2
3
4
5
6
7
8
08-14 16:09:39.539 10591 10591 D ROYSUE.Sum: 80
08-14 16:09:39.539 10591 10591 D ROYSUE.string: lowercase me!!!!!!!!!
08-14 16:09:40.580 10591 10591 D ROYSUE.Sum: 7
08-14 16:09:40.610 10591 10591 D ROYSUE.string: my test string#####
08-14 16:09:41.653 10591 10591 D ROYSUE.Sum: 7
08-14 16:09:41.658 10591 10591 D ROYSUE.string: my test string#####
08-14 16:09:42.700 10591 10591 D ROYSUE.Sum: 7
08-14 16:09:42.705 10591 10591 D ROYSUE.string: my test string#####

另外我们注意,这里还有一个secret隐藏方法,那么我们如何调用这个方法呢?

frida使用的直接到内存里去寻找的方法,也就是Java.choose(className, callbacks)函数,通过类名触发回掉函数。

我们在js脚本结尾添加以下代码:

1
2
3
4
5
6
7
Java.choose("com.example.test.MainActivity", {
onMatch : function(instance){ //该类有多少个实例,该回调就会被触发多少次
console.log("Found instance: " + instance);
console.log("Result of secret func: " + instance.secret());
},
onComplete:function(){}
});

这样,我们可以在MainActivity实例化的时候调用隐藏函数,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ python loader.py
[*] PID: 11467
Script loaded successfully
Inside java perform function
Java.Use.Successfully!
Found instance: com.example.test.MainActivity@f1cb759
Result of secret func: @@@###@@@
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************

RPC远程调用

RPC功能(Remote Procedure Call)可以让我们在python脚本里调用app内部的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log("Script loaded successfully ");

function callSecretFun() { //定义导出函数
Java.perform(function () { //找到隐藏函数并且调用
Java.choose("com.example.test.MainActivity", {
onMatch: function (instance) {
console.log("Found instance: " + instance);
console.log("Result of secret func: " + instance.secret());
},
onComplete: function() {}
});
});
}

rpc.exports = {
//把callSecretFun函数导出为callsecretfunction符号,导出名不可以有大写字母或者下划线
callsecretfunction: callSecretFun
};
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
# coding : utf-8
import time
import frida

def my_message_handler(message, payload):
print(message)
print(payload)

device = frida.get_usb_device()
pid = device.spawn(["com.example.test"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("s3.js", encoding="utf-8") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()

command = ""
while 1 == 1:
command = input("Enter command:\n1: Exit\n2: Call secret function\nchoice:")
if command == "1":
break
elif command == "2": #在这里调用
script.exports.callsecretfunction()

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ python l3.py
Script loaded successfully
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.example.test.MainActivity@f1cb759
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.example.test.MainActivity@f1cb759
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:1

基于frida开发的第三方框架

objection

objection是一个运行时移动探索工具包,由Frida提供支持,旨在帮助您评估移动应用程序的安全状况,而无需越狱。

sensepost/objection: 📱 objection - runtime mobile exploration

1
2
pip install objection
objection version

objection使用

1
2
3
4
# 附加方式
objection -g <process name> explore
# spawn方式
objection -g <process name> explore -c <script file>

运行后进入交互式命令行界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ objection -g com.tencent.mobileqq:MSF explore
Using USB device `Redmi Note 8 Pro`
Agent injected and responds ok!

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.11.0

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
com.tencent.mobileqq on (Redmi: 11) [usb] #

hook类下的方法

1
android hooking <watch/search> class_method <package.to.path.method name> <args>

objection会自动给出命令提示

image-20220730104704117

1
2
3
4
5
6
7
8
9
10
11
# 设置返回值(只支持bool类型)
android hooking set return_value <method name> <false/true>
# 查看内存中加载的库
memory list modules
# 查看库的导出函数
memory list exports <so filename>
# 查看任务列表
jobs list
# 关闭任务
jobs kill <task id>

r0capture

安卓应用层抓包通杀脚本(基于hook技术)

  • 仅限安卓平台,测试安卓7、8、9、10、11 可用 ;
  • 无视所有证书校验或绑定,不用考虑任何证书的事情;
  • 通杀TCP/IP四层模型中的应用层中的全部协议;
  • 通杀协议包括:Http,WebSocket,Ftp,Xmpp,Imap,Smtp,Protobuf等等、以及它们的SSL版本;
  • 通杀所有应用层框架,包括HttpUrlConnection、Okhttp1/3/4、Retrofit/Volley等等;
  • 无视加固,不管是整体壳还是二代壳或VMP,不用考虑加固的事情;

r0ysue/r0capture: 安卓应用层抓包通杀脚本

1
2
3
4
# spawn 模式
python r0capture.py -U -f <process name> -v
# attach 模式
python r0capture.py -U <process name> -v -p <output pcap filename>

参考文章

r0ysue/AndroidSecurityStudy: 安卓应用安全学习

Android多进程刨根问底 - 腾讯云开发者社区-腾讯云

⬆︎TOP