Frida学习

简介

Frida 是一款开源的动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,支持Windows、Mac、Linux、Android或者iOS,从安卓层面来讲,可以实现Java层和NativeHook操作。 项目地址 官网及使用文档

Frida原理及重要组件

frida注入的原理就是找到目标进程,使用ptrace跟踪目标进程获取mmap,dlpoen,dlsym等函数库的偏移获取mmap在目标进程申请一段内存空间将在目标进程中找到存放frida-agent-32/64.so的空间启动执行各种操作由agent去实现

组件名称 功能描述
frida-gum 提供了inline-hook的核心实现,还包含了代码跟踪模块Stalker,用于内存访问监控的MemoryAccessMonitor,以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等功能
frida-core fridahook的核心,具有进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。包含了frida-server、frida-gadget、frida-agent、frida-helper、frida-inject等关键模块和组件,以及之间的互相通信底座
frida-gadget 本身是一个动态库,可以通过重打包修改动态库的依赖或者修改smali代码去实现向三方应用注入gadget,从而实现Frida的持久化或免root
frida-server 本质上是一个二进制文件,类似于前面学习到的android_server,需要在目标设备上运行并转发端口,在Frida hook中起到关键作用

Frida与Xposed的对比

工具 优点 缺点
Xposed 直接编写Java代码,Java层hook方便,可打包模块持久化hook 环境配置繁琐,兼容性较差,难以Hook底层代码。
Frida 配置简单,免重启hook。支持Java层和Native层的hook操作 持久化hook相对麻烦

Frida环境配置

安装Frida:

pip install frida-tools -i https://pypi.tuna.tsinghua.edu.cn/simple

服务端:push Frida-server

下载地址下载的frida-server版本要与在命令行中安装的frida的版本一致

查看frida版本:pip show frida

这里需要注意设备架构,模拟器一般是x86_64,使用下面命令查看:

adb shell getprop ro.product.cpu.abi

将下载好的Frida-server.xz解压

(1)使用adb push命令将解压好的Frida-server移动到手机下/data/local/tmp目录:

adb push C:\Users\Tree\Desktop\frida-server-16.2.1-android-x86_64 /data/local/tmp

(3) frida-server-16.2.1-android-x86_64加权限

cd /data/local/tmp
chmod 777 frida-server-16.2.1-android-x86_64

(4) 运行

./frida-server-16.2.1-android-x86_64

(5)此时在windows端执行命令查看是否可以运行

frida-ps -U -a  //查看当前手机运行的进程

疑问: 为什么不运行frida-server,frida-ps -U也能得到虚拟机的进程?

Frida基础

基础指令

1.frida-ps -U 查看当前手机运行的进程 2.frida-ps –help 查看help指令

frida-ps --help
使用方式: frida-ps [选项]

选项:
  -h, --help            显示帮助信息并退出
  -D ID, --device ID    连接到具有给定ID的设备
  -U, --usb             连接到USB设备
  -R, --remote          连接到远程frida-server
  -H HOST, --host HOST  连接到HOST上的远程frida-server
  --certificate CERTIFICATE
                        与HOST进行TLS通信,期望的CERTIFICATE
  --origin ORIGIN       连接到设置了"Origin"头为ORIGIN的远程服务器
  --token TOKEN         使用TOKEN验证HOST
  --keepalive-interval INTERVAL
                        设置心跳包间隔(秒),或设置为0以禁用(默认为-1,根据传输方式自动选择)
  --p2p                 与目标建立点对点连接
  --stun-server ADDRESS
                        设置与--p2p一起使用的STUN服务器地址
  --relay address,username,password,turn-{udp,tcp,tls}
                        添加与--p2p一起使用的中继
  -O FILE, --options-file FILE
                        包含额外命令行选项的文本文件
  --version             显示程序版本号并退出
  -a, --applications    只列出应用程序
  -i, --installed       包括所有已安装的应用程序
  -j, --json            以JSON格式输出结果

操作模式:

操作模式 描述 优点 主要用途
CLI(命令行)模式 通过命令行直接将JavaScript脚本注入进程中,对进程进行操作 便于直接注入和操作 在较小规模的操作或者需求比较简单的场景中使用
RPC模式 使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理 在对复杂数据的处理上可以通过RPC传输给Python脚本来进行,有利于减少被注入进程的性能损耗 在大规模调用中更加普遍,特别是对于复杂数据处理的需求

注入模式与启动命令:

注入模式 描述 命令或参数 优点 主要用途
Spawn模式 将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App 在CLI模式中,Frida通过加上 -f 参数指定包名以spawn模式操作App 适合于需要在App启动时即进行注入的场景,可以在App启动时即捕获其行为 当需要监控App从启动开始的所有行为时使用
Attach模式 在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作 在CLI模式中,如果不添加 -f 参数,则默认会通过attach模式注入App 适合于已经运行的App,不会重新启动App,对用户体验影响较小 在App已经启动,或者我们只关心特定时刻或特定功能的行为时使用
Spawn模式
frida -U -f <Identifier> -l hook.js

attach模式 :

frida -U <Name> -l hook.js

进程查找:frida-ps -U -a

 PID  Name          Identifier
----  ------------  --------------------------------
2912  Kitsune Mask  io.github.huskydg.magisk
4110  wuaipojie     com.zj.wuaipojie
2350  开发助手          cn.trinea.android.developertools
1997  设置            com.android.settings
2416  雷电游戏中心        com.android.flysilkworm

frida_server自定义端口

frida server 默认端口:27042

taimen:/ $ su
taimen:/ # cd data/local/tmp/
taimen:/data/local/tmp # ./fs1280 -l 0.0.0.0:6666

然后我们再使用frida-tool进行连接:

frida-ps -H 172.16.237.186:6666

logcat |grep "D.zj2595"日志捕获

adb connect 127.0.0.1:62001模拟器端口转发

基础语法

API名称 描述
Java.use(className) 获取指定的Java类并使其在JavaScript代码中可用。
Java.perform(callback) 确保回调函数在Java的主线程上执行。
Java.choose(className, callbacks) 枚举指定类的所有实例。
Java.cast(obj, cls) 将一个Java对象转换成另一个Java类的实例。
Java.enumerateLoadedClasses(callbacks) 枚举进程中已经加载的所有Java类。
Java.enumerateClassLoaders(callbacks) 枚举进程中存在的所有Java类加载器。
Java.enumerateMethods(targetClassMethod) 枚举指定类的所有方法。

日志输出语法区别

日志方法 描述 区别
console.log() 使用JavaScript直接进行日志打印 多用于在CLI模式中,console.log()直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log()同样输出在命令行,但可能被Python脚本的输出内容掩盖。
send() Frida的专有方法,用于发送数据或日志到外部Python脚本 多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。

Hook框架模板

function main(){
    Java.perform(function(){
        hookTest1();
    });
}
setImmediate(main);

Frida常用API

Frida补全功能: 项目目录下执行npm i @types/frida-gum

Hook普通方法、打印参数和修改返回值

注意:这里的method改为真实函数名

//定义一个名为hookTest1的函数
function hookTest1(){
    //获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
    var utils = Java.use("类名");
    //修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
    utils.method.implementation = function(a, b){
        //将参数a和b的值改为123和456。
        a = 123;
        b = 456;
        //调用修改过的"method"方法,并将返回值存储在`retval`变量中
        var retval = this.method(a, b);
        //在控制台上打印参数a,b的值以及"method"方法的返回值
        console.log(a, b, retval);
        //返回"method"方法的返回值
        return retval;
    }
}

Hook重载函数

// .overload()
// .overload('自定义参数')
// .overload('int')
function hookTest2(){
    var utils = Java.use("com.zj.wuaipojie.Demo");
    //overload定义重载函数,根据函数的参数类型填
    utils.Inner.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation = function(a,b){
        b = "aaaaaaaaaa";
        this.Inner(a,b);
        console.log(b);
    }
}

Hook构造函数

function hookTest3(){
    var utils = Java.use("com.zj.wuaipojie.Demo");
    //修改类的构造函数的实现,$init表示构造函数
    utils.$init.overload('java.lang.String').implementation = function(str){
        console.log(str);
        str = "52";
        this.$init(str);
    }
}
////无参构造函数用的是 utils.$init.overload()

Hook字段

function hookTest5(){
    Java.perform(function(){
        
        //静态字段的修改
        var utils = Java.use("com.zj.wuaipojie.Demo");
        //修改类的静态字段"flag"的值
        utils.staticField.value = "我是被修改的静态变量";
        console.log(utils.staticField.value);
        
        
        //非静态字段的修改
        //使用`Java.choose()`枚举类的所有实例
        Java.choose("com.zj.wuaipojie.Demo", {
            onMatch: function(obj){
                //修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。
                obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
                obj.privateInt.value = 9999;
            },
            onComplete: function(){

            }
        });
    });
    
}

Hook内部类

function hookTest6(){
    Java.perform(function(){
        //内部类
        var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass");
        console.log(innerClass);
        innerClass.$init.implementation = function(){
            console.log("eeeeeeee");
        }

    });
}

枚举所有的类与类的所有方法

function hookTest7(){
    Java.perform(function(){
        //枚举所有的类与类的所有方法,异步枚举
        Java.enumerateLoadedClasses({
            onMatch: function(name,handle){
                //过滤类名
                if(name.indexOf("com.zj.wuaipojie.Demo") !=-1){
                    console.log(name);
                    var clazz =Java.use(name);
                    console.log(clazz);
                    var methods = clazz.class.getDeclaredMethods();
                    console.log(methods);
                }
            },
            onComplete: function(){}
        })
    })
}

枚举所有方法

function hookTest8(){
    Java.perform(function(){
        var Demo = Java.use("com.zj.wuaipojie.Demo");
        //getDeclaredMethods枚举所有方法
        var methods =Demo.class.getDeclaredMethods();
        for(var j=0; j < methods.length; j++){
            var methodName = methods[j].getName();
            console.log(methodName);
            for(var k=0; k<Demo[methodName].overloads.length;k++){
                Demo[methodName].overloads[k].implementation = function(){
                    for(var i=0;i<arguments.length;i++){
                        console.log(arguments[i]);
                    }
                    return this[methodName].apply(this,arguments);
                }
            }
        }
    })
}

主动调用

静态方法

var ClassName=Java.use("com.zj.wuaipojie.Demo"); 
ClassName.privateFunc();

非静态方法

    var ret = null;
    Java.perform(function () {
        Java.choose("com.zj.wuaipojie.Demo",{    //要hook的类
            onMatch:function(instance){
                ret=instance.privateFunc("aaaaaaa"); //要hook的方法
            },
            onComplete:function(){
                //console.log("result: " + ret);
            }
        });
    })
    //return ret;

Native层Hook

Process、Module、Memory基础

Process 对象代表当前被Hook的进程,能获取进程的信息,枚举模块,枚举范围等

API 含义
Process.id 返回附加目标进程的 PID
Process.isDebuggerAttached() 检测当前是否对目标程序已经附加
Process.enumerateModules() 枚举当前加载的模块,返回模块对象的数组
Process.enumerateThreads() 枚举当前所有的线程,返回包含 id, state, context 等属性的对象数组

Module 对象代表一个加载到进程的模块(例如,在 Windows 上的 DLL,或在 Linux/Android 上的 .so 文件),能查询模块的信息,如模块的基址、名称、导入/导出的函数等

API 含义
Module.load() 加载指定so文件,返回一个Module对象
enumerateImports() 枚举所有Import库函数,返回Module数组对象
enumerateExports() 枚举所有Export库函数,返回Module数组对象
enumerateSymbols() 枚举所有Symbol库函数,返回Module数组对象
Module.findExportByName(exportName)、Module.getExportByName(exportName) 寻找指定so中export库中的函数地址
Module.findBaseAddress(name)、Module.getBaseAddress(name) 返回so的基地址

Memory是一个工具对象,提供直接读取和修改进程内存的功能,能够读取特定地址的值、写入数据、分配内存等

方法 功能
Memory.copy() 复制内存
Memory.scan() 搜索内存中特定模式的数据
Memory.scanSync() 同上,但返回多个匹配的数据
Memory.alloc() 在目标进程的堆上申请指定大小的内存,返回一个NativePointer
Memory.writeByteArray() 将字节数组写入一个指定内存
Memory.readByteArray 读取内存

枚举导入导出表

  1. 导出表(Export Table):列出了库中可以被其他程序或库访问的所有公开函数和符号的名称。
  2. 导入表(Import Table):列出了库需要从其他库中调用的函数和符号的名称。

简而言之,导出表告诉其他程序:“这些是我提供的功能。”,而导入表则表示:“这些是我需要的功能。”。

function hookTest1(){
    Java.perform(function(){
        //打印导入表
        var imports = Module.enumerateImports("lib52pojie.so");
        for(var i =0; i < imports.length;i++){
            if(imports[i].name == "vip"){
                console.log(JSON.stringify(imports[i])); //通过JSON.stringify打印object数据
                console.log(imports[i].address);
            }
        }
        //打印导出表
        var exports = Module.enumerateExports("lib52pojie.so");
        for(var i =0; i < exports.length;i++){
            console.log(JSON.stringify(exports[i]));
        }
        
    })
}

Native函数的基础Hook打印

  1. 整数型、布尔值类型、char类型
function hookTest2(){
    Java.perform(function(){
        //根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
        console.log(helloAddr); 
        if(helloAddr != null){
            //Interceptor.attach是Frida里的一个拦截器
            Interceptor.attach(helloAddr,{
                //onEnter里可以打印和修改参数
                onEnter: function(args){  //args传入参数
                    console.log(args[0]);  //打印第一个参数的值
                    console.log(this.context.x1);  // 打印寄存器内容
                    console.log(args[1].toInt32()); //toInt32()转十进制
                    console.log(args[2].readCString()); //读取字符串 char类型
                    console.log(hexdump(args[2])); //内存dump

                },
                //onLeave里可以打印和修改返回值
                onLeave: function(retval){  //retval返回值
                    console.log(retval);
                    console.log("retval",retval.toInt32());
                }
            })
        }
    })
}
  1. 字符串类型
function hookTest2(){
    Java.perform(function(){
        //根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
        if(helloAddr != null){
            Interceptor.attach(helloAddr,{
                //onEnter里可以打印和修改参数
                onEnter: function(args){  //args传入参数
                    // 方法一
                    var jString = Java.cast(args[2], Java.use('java.lang.String'));
                    console.log("参数:", jString.toString());
                    // 方法二
                    var JNIEnv = Java.vm.getEnv();
                    var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();	
                    console.log("参数:", originalStrPtr);				
                },
                //onLeave里可以打印和修改返回值
                onLeave: function(retval){  //retval返回值
                    var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
                    console.log("返回值:", returnedJString.toString());
                }
            })
        }
    })
}

Native函数的基础Hook修改

  1. 整数型修改
function hookTest3(){
    Java.perform(function(){
        //根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so","func_four");
        console.log(helloAddr);
        if(helloAddr != null){
            Interceptor.attach(helloAddr,{
                onEnter: function(args){  //args参数
                    args[0] = ptr(1000); //第一个参数修改为整数 1000,先转为指针再赋值
                    console.log(args[0]);
                      
                },
                onLeave: function(retval){  //retval返回值
                    retval.replace(20000);  //返回值修改
                    console.log("retval",retval.toInt32());
                }
            })
        }
    })
}
  1. 字符串类型修改
function hookTest2(){
    Java.perform(function(){
        //根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
        if(helloAddr != null){
            Interceptor.attach(helloAddr,{
                //onEnter里可以打印和修改参数
                onEnter: function(args){  //args传入参数
                    var JNIEnv = Java.vm.getEnv();
                    var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();	
                    console.log("参数:", originalStrPtr);
                    var modifiedContent = "至尊";
                    var newJString = JNIEnv.newStringUtf(modifiedContent);
                    args[2] = newJString;				
                },
                //onLeave里可以打印和修改返回值
                onLeave: function(retval){  //retval返回值
                    var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
                    console.log("返回值:", returnedJString.toString());
                    var JNIEnv = Java.vm.getEnv();
                    var modifiedContent = "无敌";
                    var newJString = JNIEnv.newStringUtf(modifiedContent);
                    retval.replace(newJString);
                }
            })
        }
    })
}

SO基址的获取方式

var moduleAddr1 = Process.findModuleByName("lib52pojie.so").base;  
var moduleAddr2 = Process.getModuleByName("lib52pojie.so").base;  
var moduleAddr3 = Module.findBaseAddress("lib52pojie.so");

Hook未导出函数与函数地址计算

function hookTest6(){
    Java.perform(function(){
        //根据导出函数名打印基址
        var soAddr = Module.findBaseAddress("lib52pojie.so");
        console.log(soAddr);
        var funcaddr = soAddr.add(0x1071C);  
        console.log(funcaddr);
        if(funcaddr != null){
            Interceptor.attach(funcaddr,{
                onEnter: function(args){  //args参数
 
                },
                onLeave: function(retval){  //retval返回值
                    console.log(retval.toInt32());
                }
            })
        }
    })
}

函数地址计算

  1. 安卓里一般32 位的 so 中都是thumb指令,64 位的 so 中都是arm指令
  2. 通过IDA里的opcode bytes来判断,arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)
  3. thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 + 1
    arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移