Frida

  • Frida是一个动态代码执行工具包,通过Frida开源把一段Javascript代码注入到一个进程中去,或者把一个动态库加载到另一个进程中去。

为什么使用 Python API,但使用 JavaScript 调试逻辑?

  • Frida核心引擎是用C写的,并且集成了Google的Javascript引擎V8,把包含V8的代码注入到目标进程之后,通过一个专门的信息通道,目标进程就可以和这个注入进行的Javascript引擎进行通信了,这样通过Javascript代码就可以在目标进程中做很多事情,例如:内存访问、函数Hook、甚至调用目标进程中的原生函数都可以。

Installation

使用Pip进行安装

pip3 install frida-tools

查看下载的frida-tools的版本,下载对应版本的frida-server

alice@mac ~ % pip3 list
Package            Version 
------------------ --------
...
frida              15.1.1  
frida-tools        10.2.2  
...
  • 将frida-server推入设备(设备需要Root)并启动
alice@mac Downloads % adb root
restarting adbd as root
alice@mac Downloads % adb push frida-server /data/local/tmp/
frida-server: 1 file pushed. 29.0 MB/s (46654900 bytes in 1.534s)
alice@mac Downloads % adb shell "chmod 755 /data/local/tmp/frida-server"
alice@mac Downloads % adb shell "/data/local/tmp/frida-server &"
  • 使用frida-ps -U命令连接上去,就可以看到正在运行的进程。
alice@mac ~ % frida-ps -U             
  PID  Name
-----  ---------------------------------------------------
 6830  Google                                             
 2008  My Application                                     
23351  TiltMazes                                          
15329  YouTube                                            
 3578  adbd                                               
 1850  android.hardware.audio@2.0-service                 
 1917  android.hardware.biometrics.fingerprint@2.1-service
 1851  android.hardware.broadcastradio@1.1-service        
 1852  android.hardware.camera.provider@2.4-service       
 1853  android.hardware.cas@1.1-service                   
 1854  android.hardware.configstore@1.1-service           
 1855  android.hardware.drm@1.0-service                   
 1856  android.hardware.drm@1.2-service.clearkey  
 ....

使用Frida来Hook参数、修改结果

  • 编写一个简单的app,功能是想日志输出两个数相加的结果。
package com.example.demo01;

import androidx.appcompat.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));
    }

}
  • 写一段js代码,用frida-server将这段代码还在到com.example.demo01中去,执行其中的hook函数。
Java.perform(function x() {
    console.log("Inside java perform function");
    //定位类
    var my_class = Java.use("com.example.demo01.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;
    }
});
  • 写一个python脚本,将这段js代码传递给安卓系统里正在运行的frida-server
import sys
import frida

device = frida.get_usb_device()
pid = device.spawn(["com.example.demo01"])
device.resume(pid)
session = device.attach(pid)
with open('./s1.js') as f:
    script = session.create_script(f.read())
script.load()
#等待用户输入
sys.stdin.read()
  • 关闭机器的selinux
alice@mac ~ % adb shell
alice@mac ~ % su
alice@mac ~ % setenforce 0
  • 在主机上运行python脚本,可以看到com.example.demo01这个app重启了,然后adb logcat|grep Sum的内容也变了。
08-09 13:19:32.255 10280 10280 D Sum     : 80
08-09 13:19:33.256 10280 10280 D Sum     : 80
08-09 13:19:34.257 10280 10280 D Sum     : 80
08-09 13:19:35.261 10280 10280 D Sum     : 80
08-09 13:19:36.262 10280 10280 D Sum     : 80
08-09 13:19:37.263 10280 10280 D Sum     : 80
08-09 13:19:40.118 10478 10478 D Sum     : 7
08-09 13:19:48.136 10478 10478 D Sum     : 7
08-09 13:19:49.139 10478 10478 D Sum     : 7
08-09 13:19:53.155 10478 10478 D Sum     : 7
08-09 13:19:54.158 10478 10478 D Sum     : 7
  • 在主机上可以观察到
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)

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

  • 在app中添加如下代码:
package com.example.demo01;

import androidx.appcompat.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("Demo.String", fun("LoWeRcAsE Me!!!!!!!!!"));
        }
    }

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

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

    String secret(){
        return total;
    }
}
  • app运行起来后,使用logcat打印日志:
08-18 20:06:21.529 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:22.530 24734 24734 D Demo.Sum: 80
08-18 20:06:22.532 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:23.533 24734 24734 D Demo.Sum: 80
08-18 20:06:23.534 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:24.549 24734 24734 D Demo.Sum: 80
08-18 20:06:24.549 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:25.552 24734 24734 D Demo.Sum: 80
08-18 20:06:25.552 24734 24734 D Demo.String: lowercase me!!!!!!!!!
08-18 20:06:26.553 24734 24734 D Demo.Sum: 80
  • 上述func()函数有了重载之后,在参数是两个int的情况下,返回两个int只和,在参数为String类型之下,则返回字符串的小写形式。
  • secret()方法为隐藏方法,在app里面没有直接调用。
  • 直接使用上一节里面的js脚本和loader.js来加载的话,肯定会崩溃,为了看到崩溃信息,我们对loader.py做一些处理。
#定义错误处理
def my_message_handler(message , payload): 
	print message
	print payload
#调用错误处理
script.on("message" , my_message_handler)
  • 在运行loader.py的话,会看到如下错误信息返回:
$ python3 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代码修改如下:
console.log("Script loaded successfully ");
Java.perform(function x() {
    console.log("Inside java perform function");
    var my_class = Java.use("com.example.demo01.MainActivity");
    my_class.fun.overload("int", "int").implementation = function (x, y) { //hooking the old function
        console.log("original call: fun(" + x + ", " + y + ")");
        var ret_value = this.fun(2, 5);
        return ret_value;
    };
    var string_class = Java.use("java.lang.String");
    my_class.fun.overload("java.lang.String").implementation = function (x) { //hooking the new function
        console.log("*************************************")
        var my_string = string_class.$new("My TeSt String#####");
        console.log("Original arg: " + x);
        var ret = this.fun(my_string);
        console.log("Return value: " + ret);
        console.log("*************************************")
        return ret;
    };
});
  • 再次运行loader.py脚本:
Script loaded successfully 
Inside java perform function
original call: fun(50, 30)
*************************************
Original arg: LoWeRcAsE Me!!!!!!!!!
Return value: my test string#####
*************************************
  • 观看打印的logcat
08-18 20:16:29.165 25089 25089 D Demo.String: my test string#####
08-18 20:16:30.166 25089 25089 D Demo.Sum: 7
08-18 20:16:30.167 25089 25089 D Demo.String: my test string#####
08-18 20:16:31.169 25089 25089 D Demo.Sum: 7
08-18 20:16:31.170 25089 25089 D Demo.String: my test string#####
08-18 20:16:32.171 25089 25089 D Demo.Sum: 7
08-18 20:16:32.173 25089 25089 D Demo.String: my test string#####
08-18 20:16:33.174 25089 25089 D Demo.Sum: 7
08-18 20:16:33.175 25089 25089 D Demo.String: my test string#####
08-18 20:16:34.176 25089 25089 D Demo.Sum: 7
08-18 20:16:34.177 25089 25089 D Demo.String: my test string#####
08-18 20:16:35.179 25089 25089 D Demo.Sum: 7
  • 对于隐藏方法的调用,可以直接到内存里去寻找方法,也就是Java.choose(className, callbacks)函数,通过类名触发回调函数:(note:Java.choose在API 29运行会导致frida.TransportError: the connection is closed)
Java.perform(function x() {
...
Java.choose("com.example.demo01.MainActivity", {
        onMatch: function (instance) {
            console.log("Found instance: " + instance);
            console.log("Result of secret func: " + instance.secret());
        },
        onComplete: function () { }
    });
});
  • 再次运行loader.py脚本
Script loaded successfully 
Inside java perform function
Found instance: com.example.demo01.MainActivity@570c3bc
Result of secret func: @@@###@@@
original call: fun(50, 30)
*************************************

远程调用

  • 上一小节,我们在安卓机器上使用js脚本调用了隐藏函数secret(),它在app内虽然没有被任何地方调用,但是仍然被我们的脚本找到并调用起来。
  • 这一节我们要实现的是,不仅在泡在安卓机上的js脚本调用这个函数, 还要在主机上的py脚本里,直接调用这个函数。
  • 创建s3.js,内容如下:
console.log("Script loaded successfully ");
function callSecretFun() {
    Java.perform(function () {
        Java.choose("com.example.demo01.MainActivity",{
            onMatch:function (instance) {
                console.log("found instance: "+instance);
                console.log("Result of secret func: "+ instance.secret());
            },
            onComplete:function () {

            }
        });
    });
}
rpc.exports = {
  callsecretfunction: callSecretFun
};
  • 创建loader3.py,内容如下:
import sys
import time
import frida


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


device = frida.get_usb_device()
pid = device.spawn(["com.example.demo01"])
device.resume(pid)
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 = input("enter input:")
    if command == "1":
        break
    elif command == "2":
        script.exports.callsecretfunction()
  • 运行loader3.py脚本,输出如下:
Script loaded successfully 
enter input:2
found instance: com.example.demo01.MainActivity@c0ecf45
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
enter input:2
found instance: com.example.demo01.MainActivity@c0ecf45
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE

互通互联、动态修改

-Goal: 不仅仅可以在主机上调用安卓app里的函数,还可以把数据从安卓app里传递到主机上,在主机上进行修改,再传递回安卓app里面去。

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

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    private TextView mMessage_tv;
    private TextView mSubmit;
    private TextView mPassword;
    private TextView mUsername;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mUsername = this.findViewById(R.id.username);
        mPassword = this.findViewById(R.id.password);
        mMessage_tv = this.findViewById(R.id.message_tv);
        mSubmit = this.findViewById(R.id.submit);
        mSubmit.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mUsername.getText().toString().compareTo("admin") == 0) {
                    mMessage_tv.setText("you cannot login as admin");
                    return;
                }
                mMessage_tv.setText("Sending to the server :" + Base64.encodeToString((mUsername.getText().toString() + ":" + mPassword.getText().toString()).getBytes(), Base64.DEFAULT));
            }
        });

    }
}
  • 我们的目标就是自爱主机上得到输入框输入的内容,并且修改其输入的内容,并且传输给安卓机器,使其通过验证,也就是说,哪怕输入admin的账户和密码,也可以绕过本地校验,进行登陆操作。
  • 安卓端的js代码的逻辑就是,截取输入,传输给主机,暂停执行,得到主机传回的数据之后,继续执行,代码如下:
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.overload("java.lang.CharSequence").call(this,"send to server:"+string_to_recv);
    };
});
  • 主机端的流程就是,将接受到的JSON数据解析,提取出密码部分,然后将用户名修改为admin,这样就实现了将admin和pw发送给服务器的效果。
import base64
import codecs

import frida
import time
import base64

def my_message_handler(message, payload):
    print(message)
    print(payload)
    if message['type'] == 'send':
        print(message['payload'])
        data = message['payload'].split(":")[1].strip()
        print('data -> ',data)
        data = base64.b64decode(data.encode('utf-8')).decode('utf-8')
        print('data ->', data)
        user, pw = data.split(":")
        data = base64.b64encode(("admin" + ":" + pw).encode('utf-8')).decode('utf-8')
        print("encoded data:",data)
        script.post({"my_data":data})
        print("Modified data sent")


device = frida.get_usb_device()
pid = device.spawn(["com.example.demo01"])
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()
input()
  • 我们只要输入(非admin)+密码,非admin用户名可以绕过compareTo校验,然后frida会帮助我们将用户名改成admin,最终就是admin:pw的组合发送到服务器
{'type': 'send', 'payload': 'Sending to the server :MTIxOjEyMQ==\n'}
None
Sending to the server :MTIxOjEyMQ==

data ->  MTIxOjEyMQ==
data -> 121:121
encoded data: YWRtaW46MTIx
Modified data sent
string_to_recv: YWRtaW46MTIx

frida rpc 代码 frida-compile_python

  • 到这里,动态修改输入就完成了。