frida是一款方便而且易用的跨平台Hook工具,使用它不只能够Hook Java写的应用程序,并且还能够Hook原生的应用程序。javascript
frida分客户端环境和服务端环境。在客户端咱们能够编写Python代码,用于链接远程设备,提交要注入的代码到远程,接受服务端的发来的消息等。在服务端,咱们须要用Javascript代码注入到目标进程,操做内存数据,给客户端发送消息等操做。咱们也能够把客户端理解成控制端,服务端理解成被控端。
假如咱们要用PC来对Android设备上的某个进程进行操做,那么PC就是客户端,而Android设备就是服务端。java
本文,服务端在Android平台测试。服务端环境准备步骤以下:python
根据本身的平台下载frida服务端并解压
https://github.com/frida/frida/releases
git
执行如下命令将服务端推到手机的/data/local/tmp目录
adb push frida-server /data/local/tmp/frida-server
github
执行如下命令修改frida-server文件权限
adb shell chmod 777 /data/local/tmp/frida-server
shell
注:Windows系统执行命令能够在CMD中进行;Linux和MacOS执行命令能够在终端中进行。adb是Android一个调试工具,具体安装方法不是本文的重点。数组
在PC上安装Python的运行环境,安装完成后执行下面的命令安装frida缓存
pip install frida
session
下面是frida客户端命令行的参数解释,看一下就好app
Usage: frida [options] target Options: --version show program's version number and exit -h, --help show this help message and exit -D ID, --device=ID connect to device with the given ID -U, --usb connect to USB device -R, --remote connect to remote frida-server -H HOST, --host=HOST connect to remote frida-server on HOST -f FILE, --file=FILE spawn FILE -n NAME, --attach-name=NAME attach to NAME -p PID, --attach-pid=PID attach to PID --debug enable the Node.js compatible script debugger --enable-jit enable JIT -l SCRIPT, --load=SCRIPT load SCRIPT -c CODESHARE_URI, --codeshare=CODESHARE_URI load CODESHARE_URI -e CODE, --eval=CODE evaluate CODE -q quiet mode (no prompt) and quit after -l and -e --no-pause automatically start main thread after startup -o LOGFILE, --output=LOGFILE output to log file
若是将一个脚本注入到Android目标进程
frida -U -l myhook.js com.xxx.xxxx
参数解释:
-p
选项。正在运行的进程能够用frida-ps -U
命令查看frida运行过程当中,执行%resume
从新注入,执行%reload
来从新加载脚本;执行exit
结束脚本注入
Java.use方法用于声明一个Java类,在用一个Java类以前首先得声明。好比声明一个String类,要指定完整的类名:
var StringClass=Java.use("java.lang.String");
修改一个函数的实现是逆向调试中至关有用的。修改一个函数的实现后,若是这个函数被调用,咱们的Javascript代码里的函数实现也会被调用。
不一样的参数类型都有本身的表示方法
基本类型缩写表示表:
基本类型 | 缩写 |
---|---|
boolean | Z |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
例如:int[]
类型,在重载时要写成[I
例如:java.lang.String
例如:[java.lang.String;
修改参数为byte[]类型的构造函数的实现
ClassName.$init.overload('[B').implementation=function(param){ //do something }
注:ClassName是使用Java.use定义的类;param是能够在函数体中访问的参数
修改多参数的构造函数的实现
ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){ //do something }
ClassName.$init.overload().implementation=function(){ //do something }
调用原构造函数
ClassName.$init.overload().implementation=function(){ //do something this.$init(); //do something }
注意:当构造函数(函数)有多种重载形式,好比一个类中有两个形式的func:
void func()
和void func(int)
,要加上overload来对函数进行重载,不然能够省略overload
修改函数名为func,参数为byte[]类型的函数的实现
ClassName.func.overload('[B').implementation=function(param){ //do something //return ... }
ClassName.func.overload().implementation=function(){ //do something }
注: 在修改函数实现时,若是原函数有返回值,那么咱们在实现时也要返回合适的值
ClassName.func.overload().implementation=function(){ //do something return this.func(); }
和Java同样,建立类实例就是调用构造函数,而在这里用$new
表示一个构造函数。
var ClassName=Java.use("com.luoye.test.ClassName"); var instance = ClassName.$new();
实例化之后调用其余函数
var ClassName=Java.use("com.luoye.test.ClassName"); var instance = ClassName.$new(); instance.func();
用Java.cast
方法来对一个对象进行类型转换,如将variable
转换成java.lang.String
:
var StringClass=Java.use("java.lang.String"); var NewTypeClass=Java.cast(variable,StringClass);
这个字段标记Java虚拟机(例如: Dalvik 或者 ART)是否已加载, 操做Java任何东西以前,要确认这个值是否为true
Java.perform(fn)在Javascript代码成功被附加到目标进程时调用,咱们核心的代码要在里面写。格式:
Java.perform(function(){ //do something... });
有了以上的基础知识,咱们就能够进行编写代码了
假设有如下的程序,给isExcellent方法传入两个值,经过计算,返回一个布尔值,表示是否优秀。默认状况下,它是只会显示是否优秀:false
的,由于咱们默认传入的数很小:
public class MainActivity extends AppCompatActivity { private String TAG="Crackme"; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView =findViewById(R.id.tv); textView.setText("是否优秀:"+isExcellent(46,54)); } private boolean isExcellent(int chinese, int math){ if( chinese + math >=180){ return true; } else{ return false; } } }
咱们编写一个脚原本Hook isExcellent函数,使它返回true,显示为是否优秀:true
对于这种简单的场景,直接修改返回值就能够了,由于只有结果是重要的。
想直接返回结果很简单,直接在匿名方法里return便可。
if(Java.available){ Java.perform(function(){ var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity"); MainActivity.isExcellent.implementation=function(){ return true; } }); }
将上面的代码保存为:exp1.js
执行adb shell 'su -c /data/local/tmp/frida-server'
启动服务端
运行目标App
执行frida -U -l exp1.js com.luoyesiqiu.crackme
注入代码
按返回键返回桌面,再从新打开App,发现达到预期
在命令行输入exit
,回车,中止注入代码
注:这里为何要打开两次App?第一打开是为了让frida可以找到进程,第二次打开是为了验证结果,即便Hook成功了,界面是有缓存的,并不能实时显示Hook结果,因此须要从新打开App
假设有如下场景,isExcellent除了返回是否优秀之外,方法的内部还把分数打印出来。
public class MainActivity extends AppCompatActivity { private String TAG="Crackme"; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView =findViewById(R.id.tv); textView.append("是否优秀:"+isExcellent(46,54)+"\n"); } private boolean isExcellent(int chinese, int math){ textView.append("语文+数学总分:"+(chinese+math)+"\n"); if( chinese + math >=180){ return true; } else{ return false; } } }
这种状况下咱们不可能只返回是否优秀吧,显示的总分很低,可是却返回优秀,是很尴尬的...因此咱们要修改isExcellent方法的参数,使其经过计算打印和返回合理的值。
if(Java.available){ Java.perform(function(){ var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity"); MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){ return this.isExcellent(95,96); } }); }
上面的代码,经过overload方法重载参数,修改isExcellent方法实现,并在实现函数里调用原来的方法,获得新的返回值
将上面的代码保存为:exp2.js
执行adb shell 'su -c /data/local/tmp/frida-server'
启动服务端(若是上面启动的服务端还开着可省略这一步)
运行目标App
执行frida -U -l exp2.js com.luoyesiqiu.crackme
注入代码
按返回键,再从新打开App,发现达到预期
在命令行输入exit
,回车,中止注入代码
在本文刚开始的时候说到,咱们能够编写Python代码来配合Javascript代码注入。下面咱们来看看,怎么使用,先看一段代码:
# -*- coding: UTF-8 -*- import frida, sys jscode = """ if(Java.available){ Java.perform(function(){ var MainActivity = Java.use("com.luoyesiqiu.crackme.MainActivity"); MainActivity.isExcellent.overload("int","int").implementation=function(chinese,math){ console.log("[javascript] isExcellent be called."); send("isExcellent be called."); return this.isExcellent(95,96); } }); } """ def on_message(message, data): if message['type'] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) pass # 查找USB设备并附加到目标进程 session = frida.get_usb_device().attach('com.luoyesiqiu.crackme') # 在目标进程里建立脚本 script = session.create_script(jscode) # 注册消息回调 script.on('message', on_message) print('[*] Start attach') # 加载建立好的javascript脚本 script.load() # 读取系统输入 sys.stdin.read()
将上面的代码,保存为exp3.py
执行adb shell 'su -c /data/local/tmp/frida-server'
启动服务端(若是上面启动的服务端还开着可省略这一步)
运行目标App
执行python exp3.py
注入代码
按返回键,再从新打开App,发现达到预期
按Ctrl+C
中止脚本和中止注入代码
上面是一段Python代码,咱们来分析它的步骤:
frida.get_usb_device()
方法来获得一个链接中的USB设备(Device类)实例attach()
方法来附加到目标进程并获得一个会话(Session类)实例,该方法有一个参数,参数是须要注入的进程名或者进程pid。若是须要Hook的代码在App的启动期执行,那么在调用attach方法前须要先调用Device类的spawn()
方法,这个方法也有一个参数,参数是进程名,该方法调用后会重启对应的进程,并返回新的进程pid。获得新的进程pid后,咱们能够将这个进程pid传递给attach()
方法来实现附加。create_script()
方法建立一个脚本,传入须要注入的javascript代码并获得Script类实例on()
方法添加一个消息回调,第一个参数是信号名,乖乖传入message
就行,第二个是回调函数load()
方法来加载刚才建立的脚本。注:若是想在javascript输出日志,能够调用
console.log()
方法。若是想给客户端发送消息,能够在javascript代码里调用send()
方法,并在客户端Python代码里注册一个消息回调来接收服务端发来的消息。
能够看到,结合python代码,使注入更加的灵活了。若是想看Python端frida模块的代码,能够访问:https://github.com/frida/frida-python/blob/master/frida/core.py