frida初探1:认识frida
文章最后更新时间为:2024年04月08日 17:00:08
Frida是一个很常用的Hook工具,动态代码插桩工具,可以将JavaScript代码注入android、ios等等程序中。它使用了“客户端-服务端”模型,并利用Frida内核和谷歌v8引擎hook进程,具体做法是将QuickJS注入到目标进程中,插入的js可以完全访问内存,函数等。
和xposed不同的是,frida使用方便,既不需要额外的编程,也不需要重启设备。
下面将通过一个小实验认识frida的用法。
1 安装frida
Frida分为2个部分,第一个是位于PC的客户端,第二个是位于手机端的服务端。在安装frida之前需要做一下准备
- 一台 root 后的安卓手机
- PC上安装 python3 环境 + adb 调试工具
1.1 PC上安装客户端
pip3 install frida-tools -i https://pypi.tuna.tsinghua.edu.cn/simple
$ frida --version
15.0.12
1.2 手机上安装server端
1. 从https://github.com/frida/frida/releases下载对应版本的server端,安卓手机就是frida-server-15.0.13-android-arm64.xz,或者adb shell getprop ro.product.cpu.abi查看手机架构
wget https://github.com/frida/frida/releases/download/15.1.17/frida-server-15.1.17-android-arm64.xz
2. 将server解压上传到手机上
adb push frida-server-15.0.13-android-arm64 /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server-15.0.13-android-arm64"
adb shell "su -c /data/local/tmp/frida-server-15.0.13-android-arm64 &"
1.3 测试安装
1. 在手机上启动frida-server(需要su权限)
adb shell
su
cd /data/local/tmp/
./frida-server-15.0.13-android-arm64
2. pc上测试
frida-ps -U
1.4 两种方式注入js
- attach模式:不重启进程,直接注入进程
- spawn模式:重启进程,hook时机较早
# 1. spawn模式注入进程
frida -U -f com.xxx.xxx --no-pause -l index.js
# 2. attach模式注入进程
frida -U com.xxx.xxx -l index.js
frida -UF -l index.js # 直接attach前台应用
2. hook简单实战
2.1 实现一个计算器app
这里我使用android studio实现了一个简单的加法计算器,输入两个数字,点击等于号,会计算出两个数字之和。其中代码为
- MainActivity.java
package com.example.add;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
EditText et_num1;
EditText et_num2;
TextView tv_result;
Button btn_add;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 绑定控件
setContentView(R.layout.activity_main);
et_num1 = findViewById(R.id.et_num1);
et_num2 = findViewById(R.id.et_num2);
tv_result = findViewById(R.id.tv_result);
btn_add = findViewById(R.id.btn_add);
//给等于号普通按钮安装一个监听器,便于点击等于号按钮,和就能求出
btn_add.setOnClickListener(this);
}
@Override
public void onClick(View v) {
int num1, num2;
try {
num1 = Integer.valueOf(et_num1.getText().toString());
} catch (Exception e) {
num1 = 0;
}
try {
num2 = Integer.valueOf(et_num2.getText().toString());
} catch (Exception e) {
num2 = 0;
}
int result = Add(num1, num2);
showSucceed(num1,num2,result);
tv_result.setText(String.valueOf(result));
}
private int Add(int num1, int num2) {
return num1 + num2;
}
private void showSucceed(int num1, int num2,int result){
Toast.makeText(this,String.format("%d+%d=%d",num1,num2,result), Toast.LENGTH_SHORT).show();
}
}
- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/et_num1"
android:layout_width="60dp"
android:layout_height="45dp"
android:layout_marginStart="76dp"
android:layout_marginLeft="76dp"
android:layout_marginTop="140dp"
android:ems="10"
android:hint="数字1"
android:inputType="textShortMessage|numberSigned"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_result"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginTop="132dp"
android:gravity="center"
android:hint="和"
android:textSize="24sp"
app:layout_constraintStart_toEndOf="@+id/btn_add"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="140dp"
android:text="+"
android:textSize="24sp"
app:layout_constraintStart_toEndOf="@+id/et_num1"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_num2"
android:layout_width="60dp"
android:layout_height="45dp"
android:layout_marginTop="140dp"
android:ems="10"
android:hint="数字2"
android:inputType="numberSigned|textPersonName"
app:layout_constraintStart_toEndOf="@+id/textView"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_add"
android:layout_width="50dp"
android:layout_height="60dp"
android:layout_marginTop="132dp"
android:text="="
android:textSize="24sp"
app:layout_constraintStart_toEndOf="@+id/et_num2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
你也可以使用我编译好的apk: https://wwr.lanzoui.com/ibjyus569cf 密码:5jb9
app 界面如下:
2.2 hook计算器app
使用frida 进行动态hook的步骤为:
- 使用附加的api将frida客户端绑定应用进程
- 找出包含你要分析或者修改的方法所在的进程,用
frida-ps -U
查看。 - 找出你要hook的api或者方法,通过
adb shell dumpsys activity top
,获取当前Activity。 - 创建js脚本,调用create_script将脚本推送到进程
- 使用script.load方法将js代码推送到进程
- 触发代码,并查看结果
下面我们尝试hook apk的Add函数,打印出参数和结果,然后使函数返回1234。
先打开app,然后再pc端运行下面这段python代码:
import frida
import sys
jscode = """
Java.perform(function () {
var mainClass = Java.use("com.example.add.MainActivity")
mainClass.Add.implementation = function (a1, a2) {
console.log("a1:" + a1)
console.log("a2:" + a2)
var result = this.Add(a1, a2)
console.log("result:" + result)
return 1234
}
})
"""
# 1. 连接进程
process = frida.get_remote_device().attach('add')
# 2.将脚本推送到进程
script = process.create_script(jscode)
# 3.将js代码推送到进程
script.load()
sys.stdin.read()
- Java.use用来寻找要 hook 的类
- mainClass.Add.implementation是重写该hook类的 Add()方法,如果要 hook 的方法带参数,需要传对应类型。
然后在app上操作,发现无论输入什么,结果都是1234
可以看到,已经使用frida动态插桩成功改变了应用的行为,而无需重启设备。
3. 常见用法
3.1 智能补全
git clone https://github.com/oleavr/frida-agent-example.git
cd frida-agent-example/
npm install
然后使用VSCode等IDE打开此工程,在agent下编写typescript,会有智能提示。
npm run watch会监控代码修改自动编译生成js文件
frida -U -f com.example.android --no-pause -l _agent.js
3.2 frida数据类型
Frida中的基本类型全名 | Frida中的基本类型缩写(定义数组时使用) |
---|---|
boolean | Z |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
在Frida中用[表示数组。例如是int类型的数组,写法为:[I,如果是String类型的数组,则写法为:[java.lang.String; 注意:后面还有个;号。
3.2.1 char[]
const gson = Java.use('com.r0ysue.gson.Gson');
Java.use("java.util.Arrays").toString.overload('[C').implementation = function(charArray){
var result = this.toString(charArray);
console.log("charArray,result:",charArray,result)
console.log("charArray Object Object:",gson.$new().toJson(charArray));
return result;
}
gson的调用查看:https://bbs.pediy.com/thread-259186.htm
3.2.2byte[]
const gson = Java.use('com.r0ysue.gson.Gson');
Java.use("java.util.Arrays").toString.overload('[B').implementation = function(byteArray){
var result = this.toString(byteArray);
console.log("byteArray,result):",byteArray,result)
console.log("byteArray Object Object:",gson.$new().toJson(byteArray));
return result;
}
3.2.3 java array构造
Java.use("java.util.Arrays").toString.overload('[C').implementation = function(charArray){
var newCharArray = Java.array('char', [ '一','去','二','三','里' ]);
var result = this.toString(newCharArray);
console.log("newCharArray,result:",newCharArray,result)
console.log("newCharArray Object Object:",gson.$new().toJson(newCharArray));
var newResult = Java.use('java.lang.String').$new(Java.array('char', [ '烟','村','四','五','家']))
return newResult;
}
3.3 python两种方式调用js
## python 注入 frida js
## python 注入 frida js
import frida
import sys
from loguru import logger
config = {
"handlers": [
{"sink": sys.stdout, "colorize": True, "format": "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level} | {message}",
"level": "INFO"},
{"sink": "file.log", "rotation": "10 MB", "level": "DEBUG"},
],
"extra": {"user": "yanq"}
}
logger.configure(**config)
def on_message(message, data):
if message['type'] == 'send':
logger.info(message["payload"])
else:
print(f"message: {message}")
jscode = open("index.js",encoding="utf-8").read()
device = frida.get_device_manager().enumerate_devices()[-1]
spawn = False # 是否以spawn方式注入进程
package_name = "com.android.settings"
if (spawn):
pid = device.spawn([package_name])
session = device.attach(pid)
else:
# 如果是attach模式的话,需要根据package找出process pid
pid = None
for app in device.enumerate_applications():
if package_name in app.identifier:
pid = app.pid
break
if not pid:
print(f"{package_name} 程序未运行或者不存在")
sys.exit()
session = device.attach(pid)
# 注入js代码
script = session.create_script(jscode)
script.on('message',on_message)
script.load()
if (spawn):
device.resume(pid) # 继续进程
print("frida hook成功...")
sys.stdin.read()
3.4 rpc调用
也就是开放一个rpc接口,给别的程序或者平台去调用。
- js代码
// s3.js
console.log("Script loaded successfully ");
function callSecretFun() { //定义导出函数
Java.perform(function () { //找到隐藏函数并且调用
Java.choose("com.roysue.demo02.MainActivity", {
onMatch: function (instance) {
console.log("Found instance: " + instance);
console.log("Result of secret func: " + instance.secret());
},
onComplete: function () { }
});
});
}
rpc.exports = {
callsecretfunction: callSecretFun //把callSecretFun函数导出为callsecretfunction符号,导出名不可以有大写字母或者下划线
};
- python代码
# loader3.py
import time
import frida
def my_message_handler(message, payload):
print message
print payload
device = frida.get_usb_device()
pid = device.spawn(["com.roysue.demo02"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("s3.js") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()
command = ""
while 1 == 1:
command = raw_input("Enter command:\n1: Exit\n2: Call secret function\nchoice:")
if command == "1":
break
elif command == "2": #在这里调用
script.exports.callsecretfunction()
效果如下:
$ python loader3.py
Script loaded successfully
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.roysue.demo02.MainActivity@2eacd80
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.roysue.demo02.MainActivity@2eacd80
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.roysue.demo02.MainActivity@2eacd80
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:1
4. 总结
这是对frida的入门教程,如有错误,欢迎指正。frida的接口文档https://frida.re/docs/home/比较简略,有些内容还是需要读代码才可以解决,后面应该会写一些app hook实战的文章,敬请期待。