1. 环境搭建

        1. 准备frida服务端环境

        Releases · frida/frida · GitHub 根据手机具体版本下载对应文件并解压,Android手机一般是arm64架构。将解压后的frida-server推送到手机端的/data/local/tmp目录,并修改该文件的权限为

chmod 777 frida-server

        2. 准备客户端环境(安装在本机window系统上)

pip install frida
pip install frida-tools

        3. 编写实现hook逻辑的JS脚本

        该脚本的具体内容见后续内容,功能为hook住目标函数,输出我们所关心的信息

2. 模式

        frida hook有两种模式,如下

        1. attach模式

        attach到已经存在的进程,核心原理是ptrace修改进程内存。如果此时进程已经处于调试状态(比如做了反调试),则会attach失败。

        2. spawn模式

      启动一个新的进程并挂起,在启动的同时注入frida代码,适用于在进程启动前的一些hook,比如hook RegisterNative函数,注入完成后再调用resume恢复进程。   

3. frida 通过usb连接(结合Python中的frida模块)

import frida


# 连接待调试的usb设备
device = frida.get_usb_device(timeout=3)

# 当使用usb连接了多个设备时,指定调试的设备;其中f59bff09是通过adb devices命令获得的
device = frida.get_device('f59bff09',10)

4. frida 远程连接

        1. 通过命令行连接并进行hook

frida-ps -H 192.168.2.100 -f com.smali.jniexample -l script.js --no-pouse

        其中script.js是为实现了hook逻辑的js脚本

        2. 结合Python中的frida模块进行连接

import frida


remote_ip = "192.168.2.100"
manager = frida.get_device_manager()
device = manager.add_remote_device(remote_ip)

5. Hook JAVA层代码

参考:frida的用法--Hook Java代码篇 - luoyesiqiu - 博客园

Frida开发手册 | 狂人日记

Frida Android hook | Sakuraのblog

        1. 载入类

        Java.use()方法用于加载一个类,相当于Java中的Class.forName().当需要hook一个类A中的x方法时,需要先将类A加载进来,如下

var A = Java.use("A");

        加载A中内部类B,如下

var A_B = Java.use("A$B");

        2. 对要hook的函数进行不同的操作

                1. 函数的参数类型表示

                        1. 对于以下基本类型,只需要写成如下方式即可

int、short、char、byte、boolean、float、double、long

                        2. 基本类型数组的表示,中括号([)加上基本类型的缩写即可,各类型缩写如下

int:I
short:S
char:C
byte:B
boolean:Z
float:F
double:D
long:J

                        例如,int[]类型的参数,重载时写成[I

                        3. 其他的任意类,直接写完整的类名

                        例如,String:java.lang.String

                        4. 对象数组,用左中括号加上完整类名加上分号

                        例如,String[]:[java.lang.String;

                2. hook 类的构造函数

                注意:当构造函数(函数)有多种重载形式,比如一个类中有两个形式的func:void func()void func(int),要加上overload来对函数进行重载,否则可以省略overload

                类的构造函数名:$init

ClassName.$init.overload('int', 'java.lang.String').implementation = function(arg1, arg2){
	// hook logic
    this.$init(arg1, arg2);
};

                如果构造函数没有参数,直接使用

ClassName.$init.implementation = function(){
	// hook logic
    this.$init();
}

                3. hook一般函数

                注: 在修改函数实现时,如果原函数有返回值,那么我们在实现时也要返回合适的值

ClassName.func.overload('xxx', 'xxx').implementation = function(arg1, arg2){
	// hook logic
	return this.func(arg1, arg2);
}

                4. 调用函数

                和Java一样,创建类示例就是调用构造函数,这里用$new()表示构造函数,实例化对象之后调用类中的函数func()

var ClassName = Java.use("com.cyj.test.ClassName");
var instance = ClassName.$new();
instance.func();

                5. 获取字段

                字段赋值和读取都需要在字段名后加.value,而且如果类中有和字段同名的函数,需要在字段名称前加"_"

package com.cyj.test;
public class Person{
	public String name;
	public int age;
}

                给Person类的各字段进行赋值操作

var personClass = Java.use("com.cyj.test.Person");
var person = personClass.$new();
person.name.value = "cyj";
person.age = 22;

                6. 类型转换

                使用Java.cast()方法对一个对象进行类型转换,将variable 转换成String类型

var String = Java.use("java.lang.String");
var v = Java.cast(variable, String);

                7. 修改函数参数和返回值

Java.perform(function x(){
    var awb = Java.use("com.baidu.webkit.sdk.WebView");
        awb.loadUrl.overload('java.lang.String').implementation = function(arg1){
            console.log("loadUrl:" + arg1);
            if (arg1.includes("https://smartprogram.baidu/docs/html/web-view/web-view.html") && count === 0){
                // 修改参数
                arg1 = "http://gmmmmstat/test.html";
                count = 1;
            }
            this.loadUrl(arg1);
        };
});
// 修改函数addFunc:arg1+arg2的返回值
ClassName.addFunc.implementation = function(arg1, arg2){
	// 修改返回值为10
	return 10;
}

        3. 各种小技巧

                1. 遍历Java数组(array)

ClassName.func.implementation = function(arg1){
	// 假设arg1是一个数组类型的参数
	for(var i=0; i<arg1.length;i++){
		console.log(arg1[i]);
	}
}

                2. 遍历集合(ArrayList/List<>)

// 假设arg1是一个集合
var it = arg1.iterator();
while(it.hasNext()){
	console.log(it.next());
}

                3. 遍历map

// 假设arg1是一个map<>
var keyset = arg1.keySet();
var it = keyset.iterator();
var result = "";
while(it.hasNext()){
	var key = it.next();
	var value = map.get(key);
	result += "key=" + key.toString() + " value=" + value.toString() + "\n";
}
console.log(result);

                4. 打印调用栈

function PrintSrack(){
    console.log("============ stack start ============");
    console.log(Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new()));
    console.log("============ stack end ============");
}

6. Hook Native 层代码

参考:frida hook native -- frida hook so层 实例代码讲解 - 移动安全王铁头 - 博客园

        1. hook有导出的函数

Java.perform(function x(){
    var str_name_so = "libzeuswebviewchromium.so";    //需要hook的so名

    // 需要hook的有导出函数名,可以在Exports表中看到
    var ptr_func = Module.findExportByName(str_name_so, "Java_org_chromium_content_browser_framehost_NavigationControllerImpl_nativeGetPendingEntry");


    Interceptor.attach(ptr_func,{
        //onEnter: 进入该函数前要执行的代码,其中args是传入的参数,一般so层函数第一个参数都是JniEnv,第二个参数是jclass,从第三个参数开始是我们java层传入的参数
        onEnter: function(args) {
            send("*******nativeGetPendingEntry");
            // send("args[2]=" + args[2]); //第一个传入的参数
            // send("args[3]=" + args[3]); //第二个参数
            send("=============================Stack strat=======================");
            send(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n'));
            send("=============================Stack end  =======================");

        },
        onLeave: function(retval){ //onLeave: 该函数执行结束要执行的代码,其中retval参数即是返回值
            send("return:"+retval); //返回值
            // retval.replace(100); //替换返回值为100
        }
    });
});

        2. hook 无导出的函数

Java.perform(function x(){
    var str_name_so = "libzeuswebviewchromium.so";    //需要hook的so名
    var n_addr_func_offset = 0x86DAC8;         //需要hook的函数的偏移 onReceivedError
    var n_addr_so = Module.findBaseAddress(str_name_so); //加载到内存后 函数地址 = so地址 + 函数偏移
    var n_addr_func = parseInt(n_addr_so, 16) + n_addr_func_offset;
    var ptr_func = new NativePointer(n_addr_func);

    Interceptor.attach(ptr_func,{
        //onEnter: 进入该函数前要执行的代码,其中args是传入的参数,一般so层函数第一个参数都是JniEnv,第二个参数是jclass,从第三个参数开始是我们java层传入的参数
        onEnter: function(args) {
            send("*******target func");
            // send("args[2]=" + args[2]); //第一个传入的参数
            // send("args[3]=" + args[3]); //第二个参数
            send("=============================Stack strat=======================");
            send(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n'));
            send("=============================Stack end  =======================");

        },
        onLeave: function(retval){ //onLeave: 该函数执行结束要执行的代码,其中retval参数即是返回值
            send("return:"+retval); //返回值
            // retval.replace(100); //替换返回值为100
        }
    });
});

7. Frida hook 原理

        参考:hook工具frida原理及使用 - 简书

Frida源码分析 | m4bln

        frida使用的是动态二进制插桩技术(DBI)。二进制插桩指的是 将额外的代码注入到二进制可执行文件中,通过修改汇编地址,改变程序运行内容,运行后再返回原来程序运行处,从而实现程序的额外功能。DBI指的则是在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久的改变。

        frida注入基于ptrace实现,frida调用ptrace向目标进程注入了frida-agent-xx.so文件。

        IDA调试也是基于ptrace实现的。但是frida和ida可以结合起来动静调试==>已知一个进程只能被ptrace一次,那为什么frida-server和android-server都能对其进行调试?

        答案:先用frida注入,然后用IDA调试器调试。

        原理:frida在使用完ptrace完成so文件的注入后就释放了,并没有一直占用待调试进程的ptrace状态,所以在frida释放后,后续IDA还可以attach上进程进行调试。

更多推荐

Frida hook零基础教程