frida-all-in-one/READMD.md at master · hookmaster/frida-all-in-one
2019-10-29 23:43:17 Author: github.com(查看原文) 阅读量:333 收藏

前阵子受《Xposed模块编写的那些事》这篇文章的帮助很大,感觉有必要写一篇文章来回馈freebuf社区。现在最火爆的又是frida,该框架从Java层hook到Native层hook无所不能,虽然持久化还是要依靠Xposedhookzz等开发框架,但是frida的动态和灵活对逆向以及自动化逆向的帮助非常巨大。

frida是啥?

首先,frida是啥,github目录Awesome Frida这样介绍frida的:

Frida is Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript into native apps that run on Windows, Mac, Linux, iOS and Android. Frida is an open source software.

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

Greasemonkey可能大家不明白,它其实就是firefox的一套插件体系,使用它编写的脚本可以直接改变firefox对网页的编排方式,实现想要的任何功能。而且这套插件还是外挂的,非常灵活机动。

frida也是一样的道理。

frida为什么这么火?

动静态修改内存实现作弊一直是刚需,比如金山游侠,本质上frida做的跟它是一件事情。原则上是可以用frida把金山游侠,包括CheatEngine等“外挂”做出来的。

当然,现在已经不是直接修改内存就可以高枕无忧的年代了。大家也不要这样做,做外挂可是违法行为。

在逆向的工作上也是一样的道理,使用frida可以“看到”平时看不到的东西。出于编译型语言的特性,机器码在CPU和内存上执行的过程中,其内部数据的交互和跳转,对用户来讲是看不见的。当然如果手上有源码,甚至哪怕有带调试符号的可执行文件包,也可以使用gdblldb等调试器连上去看。

那如果没有呢?如果是纯黑盒呢?又要对app进行逆向和动态调试、甚至自动化分析以及规模化收集信息的话,我们需要的是细粒度的流程控制和代码级的可定制体系,以及不断对调试进行动态纠正和可编程调试的框架,这就是frida

frida使用的是pythonJavaScript等“胶水语言”也是它火爆的一个原因,可以迅速将逆向过程自动化,以及整合到现有的架构和体系中去,为你们发布“威胁情报”、“数据平台”甚至“AI风控”等产品打好基础。

官宣屁屁踢甚至将其敏捷开发迅速适配到现有架构的能力作为其核心卖点。

frida实操环境

主机:

Host:Macbook Air CPU: i5 Memory:8G System:Kali Linux 2018.4 (Native,非虚拟机)

客户端:

client:Nexus 6 shamu CPU:Snapdragon 805 Mem:3G System:lineage-15.1-20181123-NIGHTLY-shamu,android 8.1

kali linux的原因是工具很全面,权限很单一,只有一个root,作为原型开发很好用,否则pythonnode的各种权限、环境和依赖实在是烦。用lineage因为它有便利的网络ADB调试,可以省掉一个usb数据线连接的过程。(虽然真实的原因是没钱买新设备,Nexus 6官方只支持到7.1.1,想上8.1只有lineage一个选择。)记得需要刷进去一个lineagesu,获取root权限,frida是需要在root权限下运行的。

首先到官网下载一个platform-tools的linux版本——SDK Platform-Tools for Linux,下载解压之后可以直接运行里面的二进制文件,当然也可以把路径加到环境里去。这样adbfastboot命令就有了。

然后再将frida-server下载下来,拷贝到安卓机器里去,使用root用户跑起来,保持adb的连接不要断开。

$ ./adb root # might be required
$ ./adb push frida-server /data/local/tmp/
$ ./adb shell "chmod 755 /data/local/tmp/frida-server"
$ ./adb shell "/data/local/tmp/frida-server &"

最后在kali linux里安装好frida即可,在kali里安装frida真是太简单了,一句话命令即可,保证不出错。(可能会需要先安装pip,也是一句话命令:curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py

pip install frida-tools

然后用frida-ps -U命令连上去,就可以看到正在运行的进程了。

root@kali:~# frida-ps -U
Waiting for USB device to appear...
 PID  Name
----  -----------------------------------------------
 431  ATFWD-daemon
3148  adbd
 391  adspd
2448  android.ext.services
 358  android.hardware.cas@1.0-service
 265  android.hardware.configstore@1.0-service
 359  android.hardware.drm@1.0-service
 360  android.hardware.dumpstate@1.0-service.shamu
 361  android.hardware.gnss@1.0-service
 266  android.hardware.graphics.allocator@2.0-service
 357  android.hidl.allocator@1.0-service
 ...
 ...

基本能力Ⅰ:hook参数、修改结果

先自己写个app

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));
    }

}

原理上很简单,就是间隔一秒在控制台输出一下fun(50,30)函数的结果,fun()这个函数的作用是求和。那最终结果在控制台如下所示。

$ 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

现在我们来写一段js代码,并用frida-server将这段代码加载到com.roysue.demo02中去,执行其中的hook函数。

$ nano 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;
    }
});

然后我们在kali主机上使用一段python脚本,将这段js脚本“传递”给安卓系统里正在运行的frida-server

$ nano 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()

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

然后得保证frida-server正在运行,方法可以是在kali主机输入frida-ps -U命令,如果安卓机上的进程出现了,则frida-server运行良好。

还需要保证selinux是关闭的状态,可以在adb shell里,su -获得root权限之后,输入setenforce 0命令来获得,在Settings→About Phone→SELinux status里看到Permissive,说明selinux关闭成功。

然后在kali主机上输入python loader.js,可以观察到安卓机上com.roysue.demo02这个app马上重启了。然后$ adb logcat|grep Sum里的内容也变了。

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
11-26 21:44:51.921  2420  2420 D Sum     : 7
11-26 21:44:52.435  2420  2420 D Sum     : 7
11-26 21:44:52.945  2420  2420 D Sum     : 7
11-26 21:44:53.459  2420  2420 D Sum     : 7
11-26 21:44:53.970  2420  2420 D Sum     : 7
11-26 21:44:54.480  2420  2420 D Sum     : 7

kali主机上可以观察到:

$ python loader.py
Script loaded successfully
Inside java perform function
Java.Use.Successfully!
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)
original call: fun(50, 30)

说明脚本执行成功了,代码也插到com.roysue.demo02这个包里去,并且成功执行了,s1.js里的代码成功执行了,并且把交互结果传回了kali主机上。

基本能力Ⅱ:参数构造、方法重载、隐藏函数的处理

我们现在把app的代码稍微写复杂一点点:

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;
    }
}

app运行起来后在使用logcat打印出来的日志如下:

$ adb logcat |grep ROYSUE
11-26 22:22:35.689  3051  3051 D ROYSUE.Sum: 80
11-26 22:22:35.689  3051  3051 D ROYSUE.string: lowercase me!!!!!!!!!
11-26 22:22:36.695  3051  3051 D ROYSUE.Sum: 80
11-26 22:22:36.696  3051  3051 D ROYSUE.string: lowercase me!!!!!!!!!
11-26 22:22:37.696  3051  3051 D ROYSUE.Sum: 80
11-26 22:22:37.696  3051  3051 D ROYSUE.string: lowercase me!!!!!!!!!
11-26 22:22:38.697  3051  3051 D ROYSUE.Sum: 80
11-26 22:22:38.697  3051  3051 D ROYSUE.string: lowercase me!!!!!!!!!
11-26 22:22:39.697  3051  3051 D ROYSUE.Sum: 80
11-26 22:22:39.698  3051  3051 D ROYSUE.string: lowercase me!!!!!!!!!

可以看到fun()方法有了重载,在参数是两个int的情况下,返回两个int之和。在参数为String类型之下,则返回字符串的小写形式。

另外,secret()函数为隐藏方法,在app里没有被直接调用。

这时候如果我们直接使用上一节里面的js脚本和loader.js来加载的话,肯定会崩溃。为了看到崩溃的信息,我们对loader.js做一些处理。

def my_message_handler(message , payload): #定义错误处理
	print message
	print payload
...
script.on("message" , my_message_handler) #调用错误处理
script.load()

再运行$ python loader.py的话,就会看到如下的错误信息返回:

$ 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

可以看出是一个throwOverloadError,这时候就是因为我们没有处理重载,造成的重载处理错误。这个时候就需要我们来处理重载了,在js脚本中处理重载是这样写的:

my_class.fun.overload("int" , "int").implementation = function(x,y){
...
my_class.fun.overload("java.lang.String").implementation = function(x){

其中参数均为两个int的情况下,上一节已经讲过了。参数为String类的时候,由于String类不是Java基本数据类型,而是java.lang.String类型,所以在替换参数的构造上,需要花点心思。

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;
};

这样我们对于重载函数的处理就算是ok了。我们到实验里来看下:

$ python loader.py
Script loaded successfully
Inside java perform function
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************

然后logcat打出来的结果也变了。

$ adb logcat |grep ROYSUE
11-26 22:23:29.597  3244  3244 D ROYSUE.Sum: 7
11-26 22:23:29.673  3244  3244 D ROYSUE.string: my test string#####
11-26 22:23:30.689  3244  3244 D ROYSUE.Sum: 7
11-26 22:23:30.730  3244  3244 D ROYSUE.string: my test string#####
11-26 22:23:31.740  3244  3244 D ROYSUE.Sum: 7
11-26 22:23:31.789  3244  3244 D ROYSUE.string: my test string#####
11-26 22:23:32.797  3244  3244 D ROYSUE.Sum: 7
11-26 22:23:32.833  3244  3244 D ROYSUE.string: my test string#####

最后再说一下隐藏方法的调用,frida对其的处理办法跟Xposed是非常像的,Xposed使用的是XposedHelpers.findClass("com.example.inner_class_demo.demo",lpparam.classLoader);方法,直接findClass,其实frida也非常类似,也是使用的直接到内存里去寻找的方法,也就是Java.choose(className, callbacks)函数,通过类名触发回掉函数。

Java.choose("com.roysue.demo02.MainActivity" , {
  onMatch : function(instance){ //该类有多少个实例,该回调就会被触发多少次
    console.log("Found instance: "+instance);
    console.log("Result of secret func: " + instance.secret());
  },
  onComplete:function(){}
});

最终运行效果如下:

$ python loader.py
Script loaded successfully
Inside java perform function
Found instance: com.roysue.demo02.MainActivity@92d5deb
Result of secret func: @@@###@@@
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************
original call: fun(50, 30)

这样隐藏方法也被调用起来了。

中级能力:远程调用

上一小节中我们在安卓机器上使用js脚本调用了隐藏函数secret(),它在app内虽然没有被任何地方调用,但是仍然被我们的脚本“找到”并且“调用”了起来

这一小节我们要实现的是,不仅要在跑在安卓机上的js脚本里调用这个函数,还要可以在kali主机上的py脚本里,直接调用这个函数。

也就是使用frida提供的RPC功能(Remote Procedure Call)。

安卓app不需要有任何修改,这次我们要修改的是js脚本和py脚本。

$ nano 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符号,导出名不可以有大写字母或者下划线
};

然后我们可以在kali主机的py脚本里直接调用该函数:

$ nano 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()

然后在kali主机上我们就可以看到以下的输出:

$ 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

这样我们就实现了在kali主机上直接调用安卓app内部的函数的能力。

高级能力:互联互通、动态修改

最后我们要实现的功能是,我们不仅仅可以在kali主机上调用安卓app里的函数。我们还可以把数据从安卓app里传递到kali主机上,在主机上进行修改,再传递回安卓app里面去。

我们编写这样一个app,其中最核心的地方在于判断用户是否为admin,如果是,则直接返回错误,禁止登陆。如果不是,则把用户和密码上传到服务器上进行验证。

package com.roysue.demo04;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Base64;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    EditText username_et;
    EditText password_et;
    TextView message_tv;

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


        password_et = (EditText) this.findViewById(R.id.editText2);
        username_et = (EditText) this.findViewById(R.id.editText);
        message_tv = ((TextView) findViewById(R.id.textView));

        this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if (username_et.getText().toString().compareTo("admin") == 0) {
                    message_tv.setText("You cannot login as admin");
                    return;
                }
                //hook target
                message_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));

            }
        });

    }
}

最终跑起来之后,效果就是这样。

我们的目标就是在kali主机上“得到”输入框输入的内容,并且修改其输入的内容,并且“传输”给安卓机器,使其通过验证。也就是说,我们哪怕输入admin的账户和密码,也可以绕过本地校验,进行登陆的操作。

所以最终安卓端的js代码的逻辑就是,截取输入,传输给kali主机,暂停执行,得到kali主机传回的数据之后,继续执行。形成代码如下:

Java.perform(function () {
    var tv_class = Java.use("android.widget.TextView");
    tv_class.setText.overload("java.lang.CharSequence").implementation = function (x) {
        var string_to_send = x.toString();
        var string_to_recv;
        send(string_to_send); // 将数据发送给kali主机的python代码
        recv(function (received_json_object) {
            string_to_recv = received_json_object.my_data
            console.log("string_to_recv: " + string_to_recv);
        }).wait(); //收到数据之后,再执行下去
        return this.setText(string_to_recv);
    }
});

kali主机端的流程就是,将接受到的JSON数据解析,提取出其中的密码部分,然后将用户名替换成admin,这样就实现了将adminpw发送给“服务器”的结果。

import time
import frida

def my_message_handler(message, payload):
    print message
    print payload
    if message["type"] == "send":
        print message["payload"]
        data = message["payload"].split(":")[1].strip()
        print 'message:', message
        data = data.decode("base64")
        user, pw = data.split(":")
        data = ("admin" + ":" + pw).encode("base64")
        print "encoded data:", data
        script.post({"my_data": data})  # 将JSON对象发送回去
        print "Modified data sent"

device = frida.get_usb_device()
pid = device.spawn(["com.roysue.demo04"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("s4.js") as f:
    script = session.create_script(f.read())
script.on("message", my_message_handler)  # 注册消息处理函数
script.load()
raw_input()

我们只要输入任意用户名(非admin)+密码,非admin的用户名可以绕过compareTo校验,然后frida会帮助我们将用户名改成admin,最终就是admin:pw的组合发送到服务器。

$ python loader4.py
Script loaded successfully
{u'type': u'send', u'payload': u'Sending to the server :YWFhYTpiYmJi\n'}
None
Sending to the server :YWFhYTpiYmJi

message: {u'type': u'send', u'payload': u'Sending to the server :YWFhYTpiYmJi\n'}
data: aaaa:bbbb
pw: bbbb
encoded data: YWRtaW46YmJiYg==

Modified data sent
string_to_recv: YWRtaW46YmJiYg==

动态修改输入内容就这样实现了。

打算做个成套的教程、目录已经想好了

frida『葵花宝典』

第一章.各种环境安装(包括Win、Mac、Ubuntu、ARM机器下的各种环境安装) 第二章.基本案例上手(安卓、iOS、Win、Mac为对象的各种插桩方法) 第三章.frida-tools(frida原生提供的各种工具的使用) 第四章.frida-scripts(各种frida脚本的介绍、使用和总结) 第五章.frida高级应用(安卓hook参数模型的总结、SSL-unpinning模型、iOS应用重打包动态修改等等) 第六章.二次开发基础(frida-API基本使用方法、基于frida的二次开发模型) 第七章.二次开发案例(Fridump、r2frida、brida、Appmon等源码解析和解读)

当然还在酝酿中,大家有想法可以跟我沟通,想要源码的也可以加我。微信&微博:r0ysue

谢谢大家。

参考资料:


文章来源: https://github.com/hookmaster/frida-all-in-one/blob/master/03.%E5%9F%BA%E6%9C%AC%E6%A1%88%E4%BE%8B%E4%B8%8A%E6%89%8B/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E9%A2%86%E6%82%9FFrida%E7%9A%84%E7%B2%BE%E9%AB%93(%E5%9F%BA%E4%BA%8E%E5%AE%89%E5%8D%938.1)/READMD.md
如有侵权请联系:admin#unsafe.sh