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

2021-08-02T07:29:53.png

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 界面如下:

2021-08-02T07:48:04.png

2.2 hook计算器app

使用frida 进行动态hook的步骤为:

  1. 使用附加的api将frida客户端绑定应用进程
  2. 找出包含你要分析或者修改的方法所在的进程,用frida-ps -U查看。
  3. 找出你要hook的api或者方法,通过adb shell dumpsys activity top,获取当前Activity。
  4. 创建js脚本,调用create_script将脚本推送到进程
  5. 使用script.load方法将js代码推送到进程
  6. 触发代码,并查看结果

下面我们尝试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

2021-08-02T08:08:15.png

2021-08-02T08:10:04.png

可以看到,已经使用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中的基本类型缩写(定义数组时使用)
booleanZ
byteB
charC
doubleD
floatF
intI
longJ
shortS

在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实战的文章,敬请期待。

1 + 6 =
快来做第一个评论的人吧~