frida脚本入门

Frida脚本概念

  Frida脚本就是利用Firda动态插桩框架,使用Frida导出的API和方法对内存空间里的对象方法进行监视、修改或者替换的一段代码。Frida的API是用JavaScript实现的,所以可以充分利用JavaScript的匿名函数优势以及大量的Hook(钩子函数)和回调函数的API。

Firda版本的“Hello World”

1
2
3
4
5
6
setTimeout(
function (){
Java.perform(function (){
console.log("hello world!")
})
})

代码从外层向里看,首先我们把一个匿名函数作为参数传递给了setTimeout()函数,在这个匿名函数中体中调用了Frida的API函数Java.perform(),这个API函数本身又接受了一个匿名函数作为参数,该匿名函数最终调用console.log()函数来打印“hello world!”字符串。
这里调用setTimeout方法是因为该方法将函数注册到JavaScript运行库中,然后在JavaScript运行库中调用Java.perform()将函数注册到App的Java运行库中并在其中执行该函数。

上面的命令中,-U参数是指USB设备,-l参数用于指定注入脚本所在的路径,后面跟上要注入的脚本,最后的android.process.media则为Android设备上正在运行的进程名。在Android中,App进程的名字都是App的包名,这是App进程的唯一标志。

Java层Hook基础

学习Frida在Android Java中的Hook功能。为了清除了解App的运行内容,这里先编写一个简单的App用于练习。创建App的过程在Android开发中,这里直接给处App中MainActivity代码

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
27
28
29
30
31
32
33
34
package com.r0ysue.demo02;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;


import android.os.Handler;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {
private Timer mTimer;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mTimer = new Timer();
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 这里调用你的函数
fun(50, 30);
}
}, 0, 1000); // 0 毫秒后开始执行,然后每隔 1000 毫秒执行一次
}
void fun(int x, int y) {
Log.d("r0ysue.sum", String.valueOf(x + y));
}
}

App逻辑是每次间隔1秒打印一次fun(50,30)函数的运行结果。运行结果显示在控制台中。

确认app正常运行且函数fun被调用,编写frida脚本,目标是编写Hook fun()函数并打印处fun()函数的参数值。Frida脚本的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function main(){
console.log("Script loaded succeessfully");
Java.perform(function (){
console.log("Inside java perform function");
var MainActivity = Java.use('com.r0ysue.demo02.MainActivity');
console.log("Jaav.Use.Successfully!") //定位类成功!
MainActivity.fun.implementation = function(x,y){
console.log("x=>",x,"y=>",y);
var ret_value = this.fun(x, y);
return ret_value;
}
})
}
setImmediate(main);

通过命令使用Frida的CLI模式以attach模式注入app
frida -U -l demo02.js com.r0ysue.demo02

从运行结果可以看到参数值打印出来了。
  分析脚本内容:使用function关键字顶一个了一个main()函数,用于存放Hook脚本,然后调用Frida的API函数Java.perform()将脚本中的内容注入到Java运行库,API参数是一个匿名函数,函数内容是监控和修改Java函数逻辑的主体内容。Java.perform()函数很重要,任何对App中Java层的操作都必须包裹在这个函数中。
  在Java.perform()函数包裹的匿名函数中,首先调用了Frida的API函数Java.use(),这个函数的参数是Hook的函数所在类的类名,参数的类型是一个字符串类型。函数的返回值动态地为相应Java类获取一个JavaScript Wrapper,通俗理解就是Javascript对象。
  获取到JavaScript对象后,通过“.”符号连接fun这个对应的函数名,然后加上implementation关键字表示实现MainActivity对象的fun()函数,最后通过“=”连接一个匿名函数,参数内容和原Java的内容一致。不同的是,Javascript是一个弱类型的语言,不需要指明参数类型。此时针对MainActivity类的fun函数的Hook框架就完成了。
  匿名函数内容取决于逆向开发和分析人员想修改这个被Hook的函数的哪些运行逻辑。简言之,就是重新运行原函数内容,最后将这个函数的返回值直接通过return指令返回。
  也可以传递不同的参数,简单修改程序逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function main(){
console.log("Script loaded succeessfully");
Java.perform(function (){
console.log("Inside java perform function");
var MainActivity = Java.use('com.r0ysue.demo02.MainActivity');
console.log("Jaav.Use.Successfully!") //定位类成功!
MainActivity.fun.implementation = function(x,y){
console.log("x=>",x,"y=>",y);
var ret_value = this.fun(2,5);
return ret_value;
}
})
}
setImmediate(main);

  重新保存脚本内容,由于Frida脚本是即时生效的,因此不需要重新执行注入命令。查看日志

  setImmediate(Frida的API函数)函数传递的参数是要被执行的函数,比如main参数,表示Frida注入App后立即执行main()函数。
为App加点新功能
新的MainActivity类的内容如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.r0ysue.demo02;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;


import android.os.Handler;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {
private Timer mTimer;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mTimer = new Timer();
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 这里调用你的函数
fun(50, 30);
Log.d("r0ysue.string",fun("LoWeRcAsE Me!!!!!"));
}
}, 0, 1000); // 0 毫秒后开始执行,然后每隔 1000 毫秒执行一次
}
void fun(int x, int y) {
Log.d("r0ysue.sum", String.valueOf(x + y));
}

String fun(String x){
return x.toLowerCase();
}
}


fun()方法有了重载,在参数是两个int类型的情况下,返回两个整数之和;当参数是String类型时,返回字符串的小写形式。
如果使用之前的脚本,就会报错,这是函数重载导致Frida不知道具体应该Hook哪一个函数导致,我们做相应的修改就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function main(){
console.log("Script loaded succeessfully");
Java.perform(function (){
console.log("Inside java perform function");
var MainActivity = Java.use('com.r0ysue.demo02.MainActivity');
console.log("Jaav.Use.Successfully!") //定位类成功!
MainActivity.fun.overload('int', 'int').implementation = function(x,y){
console.log("x=>",x,"y=>",y);
var ret_value = this.fun(2,5);
return ret_value;
}
MainActivity.fun.overload('java.lang.String').implementation = function(s){
console.log("s:",s);
var ret_value = this.fun("hello");
return ret_value;
}
})
}
setImmediate(main);

这里我把两个fun函数都Hook了

Java层主动调用

主动调用:主动调用就是强制调用一个函数去执行。
被动调用:是由App按照正常逻辑去执行函数,函数的执行完全依靠与用户交互完成程序逻辑进而间接调用到关键函数,而主动调用则可以直接调用关键函数,主动性更强。
  在Java中,类的函数可以分为两种:类函数和实例方法。通俗讲就是静态方法和动态方法。类函数用static修饰,和对应类是绑定的,如果类函数还被public关键词修饰,在外部就可以直接通过类去调用;实例方法则没有关键字static修饰,在外部只能通过创建对应类的实例再通过这个实例去调用。Frida中主动调用的类型会根据方法类型区分开。
  类函数的主动调用,直接使用Java.use()函数找到类进行调用即可;如果是实例方法的主动调用,则需要找到对应的实例后对方法进行调用。这里用到了Frida中非常重要的API函数Java.choose(),这个函数可以在Java的堆中寻找指定类的实例。
继续为MainActivity添加代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.r0ysue.demo02;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;


import android.os.Handler;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {
private Timer mTimer;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mTimer = new Timer();
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 这里调用你的函数
fun(50, 30);
Log.d("r0ysue.string",fun("LoWeRcAsE Me!!!!!"));
}
}, 0, 1000); // 0 毫秒后开始执行,然后每隔 1000 毫秒执行一次
}
void fun(int x, int y) {
Log.d("r0ysue.sum", String.valueOf(x + y));
}

String fun(String x){
return x.toLowerCase();
}

void secret(){
Log.d("r0ysue.secret","this is secret func");
}

static void staticSecret(){
Log.d("r0ysue","this is static secret func");
}
}

这里添加了两个secret()函数:一个是没有static修饰的secret实例方法,一个是有static关键字修饰的staticSecret类方法。运行:

App中,两个新加的函数并没有被调用。接下来完成两个隐藏函数的主动调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function main(){
console.log("Script loaded sucessfully")
Java.perform(function (){
console.log("Inside java perform fuction")

//静态函数主动调用
var MainActivity=Java.use('com.r0ysue.demo02.MainActivity')
MainActivity.staticSecret()

//动态函数主动调用
Java.choose('com.r0ysue.demo02.MainActivity',{
onMatch: function (instance){
console.log('instance found',instance)
instance.secret()
},
onComplete:function (){
console.log('secrch Complete')
}
})
})
}
setImmediate(main)

将脚本注入App后,App运行日志变成了如下图,可以看到两个secret()函数已经执行了。

可以发现静态的staticSecret()函数和Hook时使用的方式差不多,都是使用Java.use这个API去获取MainActivity类,在获取对应的类对象后通过”.”连接staticSecret方法名,最终以和Java中一样的方式直接调用静态方法staticSecret函数;动态方法secret需要先通过Java.choose这个API从内存中获取相应类的实例对象,然后与才能通过这个实例对象去调用动态的secret()函数。
  如果需要主动调用动态函数,必须确保存在相应类的对象,否则无法进入Java.choose这个API的回调onMatch逻辑中。

RPC及其自动化

  Frida存在两种操作模式,第一种命令行模式,在上面的实践中已经使用了,现在介绍些RPC模式以及使用RPC完成自动化的相关知识。
  Frida中,可以用Python完成JavaScript脚本对进程的注入以及相应的Hook。
先修改demo,修改后的代码如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.r0ysue.demo02;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;


import android.os.Handler;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {
private String total = "hello";
private Timer mTimer;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mTimer = new Timer();
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 这里调用你的函数
fun(50, 30);
Log.d("r0ysue.string",fun("LoWeRcAsE Me!!!!!"));
}
}, 0, 1000); // 0 毫秒后开始执行,然后每隔 1000 毫秒执行一次
}
void fun(int x, int y) {
Log.d("r0ysue.sum", String.valueOf(x + y));
}

String fun(String x){
return x.toLowerCase();
}

void secret(){
total+=" secretFunc";
Log.d("r0ysue.secret","this is secret func");
}

static void staticSecret(){
Log.d("r0ysue","this is static secret func");
}
}

  代码中,增加了一个字符串类型的实例变量total,同时每次调用secret()函数字符串进行扩展。
  我们现在的目的是获取total这个实力变量的值。
  在主动调用时需要注意的是,Java中的变量也存在是否使用static修饰的区别。在用Frida对Java中变量进行处理时也要区分是否使用static修饰:类变量,使用static修饰,可以直接通过类进行获取;实例变量,不使用static修饰,和特定的对象绑定在一起。
js脚本:

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
27
28
29
30
function CallSecretFunc(){
Java.perform(function (){
//动态函数主动调用
Java.choose('com.r0ysue.demo02.MainActivity',{
onMatch: function (instance){
instance.secret()
},
onComplete:function (){
}
})
})
}
function getTotalValue(){
Java.perform(function (){
var MainActivity = Java.use('com.r0ysue.demo02.MainActivity')

//动态函数主动调用
Java.choose('com.r0ysue.demo02.MainActivity',{
onMatch:function (instance){
//instance.secret()
console.log('total value = ',instance.total.value)

},
onComplete:function (){
console.log('seatch Complete')
}
})
})
}
getTotalValue()


实际上只需要在total之后加上一个.value关键词就可以获取变量的值。如果直接使用类去获取实例变量的值,会出错。
如果调用CallSecretFunc()函数,然后再次获取total的值,就会发现total的值变为了hello secretFunc

在确认我们的函数没有写错且功能达到预期后,接下来开始远程调用。将CallSecretFunc()函数和getTotalValue()函数导出,使得外部可以调用,在JavaScript代码末尾加上RPC相关代码:

1
2
3
4
rpc.exports={
callsecretfunc:CallSecretFunc,
gettotalvalue:getTotalValue
}

这部分代码的功能是将CallSecretFunc()函数和getTotalValue()函数分别导出为callsecretfunc和gettotalvalue。需要注意的是,导出名不可以有大写字母或者下划线。接下来在外部就可以调用这两个函数了。
外部调用
python代码如下:

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
import frida,sys
def on_message(message,data):
if message['type']=='send':
print("* {0}".format(message['payload']))
else:
print(message)

device = frida.get_usb_device()
process = device.attach('com.r0ysue.demo02')
with open ('4.js', "r", encoding="utf-8") as f:
jscode=f.read()
script=process.create_script(jscode)

script.on('message',on_message)
script.load()

command=""
while 1==1:
comand=input("\nEnter command:\n1:Exit\n2:Call secret function\n3:Get Total Value\nchoice:")
if comand=="1":
break
elif comand=="2":
script.exports.callsecretfunc()
elif comand=="3":
script.exports.gettotalvalue()


  重新运行App,然后直接运行loader.py,运行结果如上图。和单纯执行JavaScript是一致的。
首先通过frida.get_usb_devices()获取到USB设备句柄;然后通过device.attach(‘com.r0ysue.demo02’) 进程进行注入;接着用creat_script()函数加载编写的JavaScript代码,并使用script.on(‘message’.on_message)注册自己的消息对应的函数,每当JavaScript想要自己输出时都会经过这里指定的on_message进行;最后,也是最重要的RPC调用代码,即通过script.exports访问所有我们在javaScript中定义的导出名,进而调用导出函数。就完成了RPC远程调用,达到主机上可以随意调用App代码的目的。

小结

这里介绍了Frida动态插桩工具、如何使用Frida对函数进行Hook和修改逻辑、主动调用函数相关的知识以及如何获取App进程中一个类的变量值,最后使用Python脚本对JavaScript中定义的导出函数进行远程调用的方式和自动化相关知识。