一、什么是Frida?
Frida
是一款开源的动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,支持Windows、Mac、Linux、Android或者iOS,从安卓层面来讲,可以实现Java
层和Native
层Hook
操作。
项目地址
官网及使用文档
二、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基础知识
(1)基础指令
frida-ps -U
查看当前手机运行的进程。
frida-ps --help
查看help
指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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格式输出结果
|
(2)操作模式
操作模式 |
描述 |
优点 |
主要用途 |
CLI(命令行)模式 |
通过命令行直接将JavaScript脚本注入进程中,对进程进行操作 |
便于直接注入和操作 |
在较小规模的操作或者需求比较简单的场景中使用 |
RPC模式 |
使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理 |
在对复杂数据的处理上可以通过RPC传输给Python脚本来进行,有利于减少被注入进程的性能损耗 |
在大规模调用中更加普遍,特别是对于复杂数据处理的需求 |
(3)注入模式与启动命令
注入模式 |
描述 |
命令或参数 |
优点 |
主要用途 |
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模式
1
| frida -U -f 进程名 -l hook.js
|
Attach模式
Frida_Server自定义端口
1 2 3 4 5
| frida server 默认端口:27042
taimen:/ $ su taimen:/ # cd data/local/tmp/ taimen:/data/local/tmp # ./fs1280 -l 0.0.0.0:6666
|
logcat | grep "D.zj2595"
日志捕获
adb connect 127.0.0.1:62001
模拟器端口转发
(4)基础语法
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) |
枚举指定类的所有方法。 |
(5)日志输出语法区别
日志方法 |
描述 |
区别 |
console.log() |
使用JavaScript直接进行日志打印 |
多用于在CLI模式中,console.log() 直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log() 同样输出在命令行,但可能被Python脚本的输出内容掩盖。 |
send() |
Frida的专有方法,用于发送数据或日志到外部Python脚本 |
多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。 |
(6)Hook框架模版
1 2 3 4 5 6
| function main(){ Java.perform(function(){ hookTest1(); }); } setImmediate(main);
|
四、Frida常用API
(1)Hook普通方法、打印参数和修改返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function hookTest1(){ var utils = Java.use("类名"); utils.method.implementation = function(a, b){ a = 123; b = 456; var retval = this.method(a, b); console.log(a, b, retval); return retval; } }
|
(2)Hook重载参数
函数名称相同,但是函数传入的参数类型不同,这个就是重载。函数的重载导致会导致frida
不知道具体应该Hook
哪个函数而出现的问题。这时候要在代码中加入要Hook
的函数的参数类型,这样就不会报错了。
1 2 3 4 5 6 7 8 9 10 11 12
|
function hookTest2(){ var utils = Java.use("com.zj.wuaipojie.Demo"); utils.Inner.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation = function(a,b){ b = "aaaaaaaaaa"; this.Inner(a,b); console.log(b); } }
|
(3)Hook构造函数
1 2 3 4 5 6 7 8 9
| function hookTest3(){ var utils = Java.use("com.zj.wuaipojie.Demo"); utils.$init.overload('java.lang.String').implementation = function(str){ console.log(str); str = "52"; this.$init(str); } }
|
(4)Hook字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function hookTest5(){ Java.perform(function(){ var utils = Java.use("com.zj.wuaipojie.Demo"); utils.staticField.value = "我是被修改的静态变量"; console.log(utils.staticField.value); Java.choose("com.zj.wuaipojie.Demo", { onMatch: function(obj){ obj._privateInt.value = "123456"; obj.privateInt.value = 9999; }, onComplete: function(){
} }); });
}
|
(5)Hook内部类
1 2 3 4 5 6 7 8 9 10 11
| function hookTest6(){ Java.perform(function(){ var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass"); console.log(innerClass); innerClass.$init.implementation = function(){ console.log("eeeeeeee"); }
}); }
|
(6)枚举所有的类与类的所有方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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(){} }) }) }
|
(7)枚举所有方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function hookTest8(){ Java.perform(function(){ var Demo = Java.use("com.zj.wuaipojie.Demo"); 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); } } } }) }
|
(8)主动调用
1 2
| var ClassName=Java.use("com.zj.wuaipojie.Demo"); ClassName.privateFunc("传参");
|
1 2 3 4 5 6 7 8 9 10 11 12
| var ret = null; Java.perform(function () { Java.choose("com.zj.wuaipojie.Demo",{ onMatch:function(instance){ ret=instance.privateFunc("aaaaaaa"); }, onComplete:function(){ } }); })
|
五、Java层的主动调用
主动调用就是强制调用一个函数去执行。相对地,被动调用是由App按照正常逻辑去执行函数,函数的执行完全依靠与用户交互完成程序逻辑进而间接调用到关键函数,而主动调用则可以直接调用关键函数,主动性更强,甚至可以直接完成关键数据的“自吐”。
在Java
中,类中的函数可以分为两种:类函数和实例方法。通俗的讲,就是静态方法和动态方法。
类函数使用关键字static
修饰,和对应类是绑定的,如果类函数还被public
关键字修饰着,在外部就可以直接通过类去调用。
实例方法则没有关键字static
修饰,在外部只能通过创建对应类的实例再通过这个实例去调用。
在Frida
中主动调用的类型会根据方法类型区分开。如果是类函数的主动调用,直接使用Java.use()
函数找到对应的实例后对方法进行调用。如果是实例方法的主动调用,则需要在找到对应的实例后对方法进行调用。这里用到了Frida
中非常重要的一个API
函数Java.choose()
,这个函数可以在Java
的堆中寻找指定类的实例。
示例代码MainActivity.java
:

Hook
函数的代码如下hook.js
:

从hook.js
这个脚本中,可以发现静态的staticSecret()
函数和Hook
时使用的方式大同小异,都是使用Java.use
这个API
去获取MainActivity
类,在获取对应的类对象后通过"."连接符连接staticSecret
方法名,最终以和Java
中一样的方式直接调用静态方法staticSecret()
函数。
动态方法secret
需要先通过Java.choose
这个API
从内存中获取相应类的实例对象,然后才能通过这个实例对象去调用动态的secret()
函数。
如果需要主动调用动态函数,必须确保存在相应类的对象,否则无法进入Java.choose
这个API
的回调onMatch
逻辑中。比如MainActivity
类对象,由于App
在打开后确实运行在MainActivity
界面上,那么这个对象就一定会存在,这就是所谓的“所见即所得”思想。
参考
https://www.52pojie.cn/thread-1823118-1-1.html
《安卓Frida逆向与抓包实战》 陈佳林/著