开篇汇总

frida的学习离不开如下几个网站:

  1. frida官方文档

  2. frida官方脚本库

  3. 看雪android论坛

开始此篇之前,需要说明一些frida入门学习需要注意的问题:

1、cmd命令行无法使用“frida”,报错“'frida' 不是内部或外部命令,也不是可运行的程序或批处理文件。”;遇到此报错,如果是因为pip没有装frida库,那回头装一个就好,但是装了frida库还是会有该报错,那么还需要装一个“frida-tools”的库,此时才可以在命令行使用frida命令;

2、“pip install frida”报“ERROR:filed building wheel for frida”,此报错主要是由于构建frida时,缺少了其egg文件导致,解决方案见本文末“踩坑排错 - unable to download it within 20 seconds”即可解决;

3、编写frida JavaScript脚本时,编译器不会提示相关的代码引用,此时只需要“npm install @types/frida-gum -g”即可

frida hook Java

 frida在日常渗透过程中,可以做到hook Java层代码、native层代码等诸多功能,本文依据frida 官方文档给出的API配合日常渗透经验来讲解hook Java层代码可实现的操作。

frida 主动调用动态函数

frida 主动调用函数作为frida最基础的功能,此处使用一个LoginActivity的小demo来做演示

android LoginActivity code

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public void onCreate(Bundle bundle) {

super.onCreate(bundle);

this.mContext = this;

setContentView(R.layout.activity_login);

final EditText editText = (EditText) findViewById(R.id.username);

final EditText editText2 = (EditText) findViewById(R.id.password);

((Button) findViewById(R.id.login)).setOnClickListener(new View.OnClickListener() {

@Override // android.view.View.OnClickListener

public void onClick(View view) {

String obj = editText.getText().toString();

String obj2 = editText2.getText().toString();

if (TextUtils.isEmpty(obj) || TextUtils.isEmpty(obj2)) {

Toast.makeText(LoginActivity.this.mContext, "用户名或密码错误", 1).show();

} else if (!LoginActivity.a(obj, obj).equals(obj2)) {

Toast.makeText(LoginActivity.this.mContext, "登录失败", 1).show();

} else {

LoginActivity.this.startActivity(new Intent(LoginActivity.this.mContext, FridaLoginDemo.class));

LoginActivity.this.finishActivity(0);

}

}

});

}


private static String a(byte[] bArr) {

StringBuilder sb = new StringBuilder();

for (int i = 0; bArr != null && i < bArr.length; i++) {

String hexString = Integer.toHexString(bArr[i] & 255);

if (hexString.length() == 1) {

sb.append('0');

}

sb.append(hexString);

}

return sb.toString().toLowerCase();

}


public static String a(String str, String str2) {

try {

SecretKeySpec secretKeySpec = new SecretKeySpec(str2.getBytes(), "HmacSHA256");

Mac mac = Mac.getInstance("HmacSHA256");

mac.init(secretKeySpec);

return a(mac.doFinal(str.getBytes()));

} catch (Exception e) {

e.printStackTrace();

return BuildConfig.FLAVOR;

}

}

 上面的代码展示了app LoginActivity 的部分代码,相关代码逻辑梳理之后为:用户访问LoginActivity,调用onCreate方法后,app接受用户输入的用户名以及密码,if条件语句判断了用户输入的obj经过a方法运算后,是否等于obj2,如果不相等,则直接提示登录失败,如果相等,那么代码步入FridaLoginDemo

 依据上述梳理的逻辑,不难得出结论:只要我们输入的密码等于某个特定的字符串,那么即可登录成功,下面是依据该逻辑编写的frida脚本

1
2
3
4
5
6
7
8
9
10
11
function hook_java() {
Java.perform(function () {
var LoginActivity = Java.use("com.shui.fridademo.Activity.LoginActivity");
console.log("frida hook class:LoginActivity");
LoginActivity.a.implementation = function (str, str2) {
var result = this.a(str, str2); //hook函数,调用原有函数,获得执行结果
console.log("LoginActivity.a函数入参值、返回值为:", str, str2, result);
return result;
};
});
}

将上述js代码通过frida注入到app后,绝对会出现如下报错

该报错的原因是由于hook脚本主动调用了LoginActivity类的a方法,但是此类下存在俩个a方法,直接调用frida也很懵,所以我们需要指定主动调用的方法,这里引出了Java的重载理念,此处就不深入讲解;

由报错可以看出,a方法有俩个重载,“.overload(‘[B’)”、 “.overload(‘java.lang.String’, ‘java.lang.String’)”,而代码块调用的a方法传参是两个,所以此处只需要在原有的hook脚本上对a方法进行重载即可,修改后的脚本如下

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
function change_return() {

Java.perform(function () {

var LoginActivity = Java.use("com.shui.fridademo.Activity.LoginActivity");

console.log("hook class: LoginActivity");

LoginActivity.a.overload('java.lang.String', 'java.lang.String').implementation = function (p1, p2) {

var result = this.a(p1, p2);

console.log("LoginActivity.a: ", p1, p2, result);

return result;

};

})

}

function main() {

change_return();

}

setImmediate(main);

frida 修改返回值

修改返回值绝对是frida hook Java中最经典的几个操作,其依赖的api也极其简单,下面通过一段客户端的Java代码来做hook讲解

FridaLoginDemo code

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public void onCheck() {

try {

if (a(b("请输入密码:")).equals("L4+7byh/WQCFIEzjW9z6jUv/OwlhJURvGQwBq/RTe2nZHz7x0ug==")) {

CheckSuccess();

startActivity(new Intent(this, FridaLoginSuccess.class));

finishActivity(0);

} else {

super.CheckFailed();

}

} catch (Exception e) {

e.printStackTrace();

}

}


public static String a(byte[] bArr) throws Exception {

StringBuilder sb = new StringBuilder();

for (int i = 0; i <= bArr.length - 1; i += 3) {

byte[] bArr2 = new byte[4];

byte b = 0;

for (int i2 = 0; i2 <= 2; i2++) {

int i3 = i + i2;

if (i3 <= bArr.length - 1) {

bArr2[i2] = (byte) (b | ((bArr[i3] & 255) >>> ((i2 * 2) + 2)));

b = (byte) ((((bArr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255) >>> 2);

} else {

bArr2[i2] = b;

b = 64;

}

}

bArr2[3] = b;

for (int i4 = 0; i4 <= 3; i4++) {

if (bArr2[i4] <= 63) {

sb.append(table[bArr2[i4]]);

} else {

sb.append('=');

}

}

}

return sb.toString();

}

该类中的方法很简单,只有一个if判断,在判断条件中,如果输入的密码等于字符串“L4+7byh/WQCFIEzjW9z6jUv/OwlhJURvGQwBq/RTe2nZHz7x0ug==”,那么则直接跳转到startActivity;依据该思路,我们只需要通过frida脚本将函数的返回值永久的修改为固定的字符串即可,脚本如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// frida修改返回值

function change_return() {

Java.perform(function () {

var FridaLoginDemo = Java.use("com.shui.fridademo.Activity.FridaLoginDemo");

console.log("hook class: FridaLoginDemo");

FridaLoginDemo.a.implementation = function (barr) {

console.log("FridaLoginDemo.a");

return "L4+7byh/WQCFIEzjW9z6jUv/OwlhJURvGQwBq/RTe2nZHz7x0ug==";

};

})

}

frida 主动调用静态函数

上文中讲了frida主动去调用动态函数,通过传参的方式获取对应的返回值,既然能调用动态,那么同样的也可以调用静态函数,下面通过一个demo来看一下frida静态调用的方式

FridaLoginSuccess

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
public class FridaLoginSuccess extends BaseFridaActivity {  
private static boolean static_bool_var = false;
private boolean bool_var = false;

@Override
public String getNextCheckTitle() {
return "frida静态调用";
}

private static void setStatic_bool_var() {
static_bool_var = true;
}

private void setBool_var() {
this.bool_var = true;
}

@Override
public void onCheck() {
if (static_bool_var && this.bool_var) {
CheckSuccess();
startActivity(new Intent(this, FridaNext3.class));
finishActivity(0);
return;
}
super.CheckFailed();
}
}

上述代码中,存在俩个boolean变量,“static_bool_var”、“bool_var”,这俩个变量的初始值均为false,而在onCheck中,if的唯一判断条件是如果这俩个变量为true,那么才可以成功执行,startActivity;使得俩个变量为true,则需要调用“setStatic_bool_var”、“setBool_var”方法,但是代码中却没有任意一处对这俩个方法做调用,所以只能通过frida对代码中的静态方法进行调用,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// frida主动调用静态函数

function call_static_methods() {

Java.perform(function () {

var FridaLoginSuccess = Java.use("com.shui.fridademo.Activity.FridaLoginSuccess");

console.log("hook class: FridaLoginSuccess");

FridaLoginSuccess.setStatic_bool_var();

FridaLoginSuccess.setBool_var();

})

}

将上述代码直接植入到app中运行的话,会出现如下报错

该报错的本质是由于上述的俩个方法不都是静态方法

private static void setStatic_bool_var()

private void setBool_var()

在Java中,除了static修饰的方法,其他的均为实例方法,所以frida在调用方法时,如果该方法被static修饰,那么可以调用:FridaActivity2.setStatic_bool_var();,但是如果是实例方法这么调用的话,就会出现上面截图的报错,这个时候用到了frida JavaScript api中的”Java.choose”,该api用于在内存中扫描 Java 堆,枚举 Java 对象(className)实例,官方文档描述如下:

那么frida代码则可以修改为如下

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
// frida主动调用静态函数

function call_static_methods() {

Java.perform(function () {

var FridaLoginSuccess = Java.use("com.shui.fridademo.Activity.FridaLoginSuccess");

console.log("hook class: FridaActivity2");

// 调用static修饰的方法

FridaLoginSuccess.setStatic_bool_var();

// 调用实例方法

Java.choose("com.shui.fridademo.Activity.FridaLoginSuccess", {

onMatch: function (instance) {

instance.setBool_var();

},

onComplete: function () {

}

});

})

}

至此,frida主动调用静态方法以及与区分的实例方法的调用全部完成

frida 修改变量值

上文描述了frida主动调用app内部方法,接下来要讲的是frida如何去修改类中的某个变量,依旧是一个demo来进行操作

FridaNext3

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
public class FridaNext3 extends BaseFridaActivity {  
private static boolean static_bool_var = false;
private boolean bool_var = false;
private boolean same_name_bool_var = false;

@Override
public String getNextCheckTitle() {
return "frida修改变量值";
}

private void same_name_bool_var() {
Log.d("Frida", static_bool_var + " " + this.bool_var + " " + this.same_name_bool_var);
}

@Override
public void onCheck() {
if (static_bool_var && this.bool_var && this.same_name_bool_var) {
CheckSuccess();
startActivity(new Intent(this, FridaNext4.class));
finishActivity(0);
return;
}
super.CheckFailed();
}
}

上述代码中,其实可以按照之前说过的静态方法来类比分类

静态变量:“static_bool_var”

实例变量:“bool_var”、“same_name_bool_var”

对于变量的修改,和主动调用函数类似,static修饰过的变量,frida可以直接对其进行修改,但是如果是实例化的变量,那么就只能通过Java.choose调取当前类之后,在对其进行修改

按照该思路,frida脚本编写如下

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 change_variable() {

Java.perform(function () {

var FridaNext3 = Java.use("com.shui.fridademo.Activity.FridaNext3");

console.log("hook class FridaNext3");

// 修改静态变量

FridaNext3.static_bool_var.value = true;

// 修改动态变量

Java.choose("com.shui.fridademo.Activity.FridaNext3", {

onMatch: function (instance) {

instance.bool_var.value = true;

instance.same_name_bool_var.value = true;
},
onComplete: function () {
}

});

})

}

将该代码注入到app之后,发现frida好像并没有成功hook,打印log之后,我们要修改的值确实都是true

此时,就涉及到了另外一个点 —— 变量名和方法名一致时,如何修改变量的值

上文中“same_name_bool_var”这个字符串,是变量名,同时又是方法名,我们在hook代码中直接将其原始值修改为了true:instance.same_name_bool_var.value = true,但是这种方案是不可行的, 当变量名和方法名一致时,想要修改变量值,需要在字符串前加下划线,及

instance.same_name_bool_var.value = true 应该写成 instance._same_name_bool_var.value = true

这种修改后的hook脚本就是

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 change_variable() {

Java.perform(function () {

var FridaNext3 = Java.use("com.shui.fridademo.Activity.FridaNext3");

console.log("hook class FridaNext3");

// 修改静态变量

FridaNext3.static_bool_var.value = true;

// 修改动态变量

Java.choose("com.shui.fridademo.Activity.FridaNext3", {

onMatch: function (instance) {

instance.bool_var.value = true;

instance._same_name_bool_var.value = true;
},
onComplete: function () {
}

});

})

}

此时,即可成功hook修改FridaNext3的相关操作

frida hook 类的多个函数

在此之前的内容中,frida的操作都是针对单一的函数或者变量来进行的,但是有些场景中往往需要hook诸多的函数,之前就有大佬依据下文将要提到的frida api写的工具ZenTracer

接下来继续以demo为例来引出相关的hook操作

FridaNext4

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
public class FridaNext4 extends BaseFridaActivity {  
@Override
public String getNextCheckTitle() {
return "frida同时hook类下的多个函数";
}

/* loaded from: classes.dex */
private static class InnerClasses {
public static boolean check1() {
return false;
}

public static boolean check2() {
return false;
}

public static boolean check3() {
return false;
}

public static boolean check4() {
return false;
}

public static boolean check5() {
return false;
}

public static boolean check6() {
return false;
}

private InnerClasses() {
}
}

@Override
public void onCheck() {
if (InnerClasses.check1() && InnerClasses.check2() && InnerClasses.check3() && InnerClasses.check4() && InnerClasses.check5() && InnerClasses.check6()) {
CheckSuccess();
startActivity(new Intent(this, FridaNext5.class));
finishActivity(0);
return;
}
super.CheckFailed();
}
}

上述代码中,很直观的能看到onCheck函数的if条件只有一个,那就是代码中所出现的诸多方法都返回true;其实如果按照我们之前的思路这个以如下脚本很简单的实现

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
function hook_InnerClasses() {
Java.perform(function () {
//hook内部类
var InnerClasses = Java.use("com.shui.fridademo.Activity.FridaNext4$InnerClasses");
console.log(InnerClasses);
InnerClasses.check1.implementation = function () {
return true;
};
InnerClasses.check2.implementation = function () {
return true;
};
InnerClasses.check3.implementation = function () {
return true;
};
InnerClasses.check4.implementation = function () {
return true;
};
InnerClasses.check5.implementation = function () {
return true;
};
InnerClasses.check6.implementation = function () {
return true;
};
});
}

class.getDeclaredMethods

就像前面讲过的那样,只需要frida主动调用这些函数,然后对其返回值修改即可实现此次demo想要达成的目的,但是这种一次性调用多次,还都是同一类下的函数同时调用,写成这种排排坐吃果果的形式,未免会影响代码的美观性以及有一点小蠢,所以依赖frida api我们可以写出如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// frida hook 多个函数
function hook_all_methods() {
Java.perform(function () {
//hook 类的多个函数
var class_name = "com.shui.fridademo.Activity.FridaNext4$InnerClasses";
var InnerClasses = Java.use(class_name);
var all_methods = InnerClasses.class.getDeclaredMethods();
for (var i = 0; i < all_methods.length; i++) {
var method = all_methods[i];
var methodStr = method.toString();
var substring = methodStr.substr(methodStr.indexOf(class_name) + class_name.length + 1);
var methodname = substring.substr(0, substring.indexOf("("));
console.log(methodname);

InnerClasses[methodname].implementation = function () {
console.log("hook_mul_function:", this);
return true;
}
}
});
}

Java.enumerateLoadedClasses

上述代码即可完全的hook一个类下的所有方法,不过前面我有提到过大佬写的一个hook脚本ZenTracer,大家如果去审计代码就会发现,同样是hook一个类下的所有方法,但是ZenTracer使用了frida的另一个API:Java.enumerateLoadedClasses,该方法的主要用途是“获取内存中加载的所有类”,下面是frida官方对于该api的描述信息

以此类推,我们还能将frida 的hook 脚本修改为如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// frida hook 多个函数 Java.enumerateLoadedClasses
function hook_loaded_methods() {
Java.perform(function () {
var hook_class = "com.example.androiddemo.Activity.FridaActivity4$InnerClasses";
Java.enumerateLoadedClasses({
onMatch: function (aClass) {
console.log("内存加载出来的所有类有:", aClass)
},
onComplete: function () {

}
});
})
}

通过log的日志打印,可以直观的看到enumerateLoadedClasses加载出来的所有类

但是我们在本次只需要内存中的某一个类,所以需要对其做一个match匹配,然后再hook,而hook类下的其他方法时,依旧回到了之前的“getDeclareedMethods”;所以ZenTracer项目的核心原理就是:先用“enumerateLoadedClasses”捞出所以加载的类,然后再用黑白名单的机制匹配需要hook的类,最后通过“getDeclareedMethods”获取该类下的所有方法,进而hook。

frida hook 动态加载的dex

动态加载dex,作为frida的一个较为高阶用法,其核心API我们上文其实已经提到过了,在此处详细展开说一下,依旧是一个demo的引入

FridaNext5

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
50
51
52
53
54
55
56
57
58
public class FridaNext5 extends BaseFridaActivity {  

private CheckInterface DynamicDexCheck = null;

private void loaddex() {
File cacheDir = getCacheDir();
if (!cacheDir.exists()) {
cacheDir.mkdir();
}
String str = cacheDir.getAbsolutePath() + File.separator + "DynamicPlugin.dex";
File file = new File(str);
try {
if (!file.exists()) {
file.createNewFile();
copyFiles(this, "DynamicPlugin.dex", file);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
this.DynamicDexCheck = (CheckInterface) new DexClassLoader(str, cacheDir.getAbsolutePath(), null, getClassLoader()).loadClass("com.shui.fridademo.Dynamic.DynamicCheck").newInstance();
if (this.DynamicDexCheck != null) {
return;
}
Toast.makeText(this, "loaddex Failed!", 1).show();
} catch (Exception e2) {
e2.printStackTrace();
}
}

public CheckInterface getDynamicDexCheck() {
if (this.DynamicDexCheck == null) {
loaddex();
}
return this.DynamicDexCheck;
}

@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
loaddex();
}

@Override // com.example.androiddemo.Activity.BaseFridaActivity
public void onCheck() {
if (getDynamicDexCheck() != null) {
if (getDynamicDexCheck().check()) {
CheckSuccess();
startActivity(new Intent(this, FridaActivity6.class));
finishActivity(0);
return;
}
super.CheckFailed();
return;
}
Toast.makeText(this, "onClick loaddex Failed!", 1).show();
}
}

上述代码中,onCheck之后,if唯一判断值是getDynamicDexCheck函数返回值不为null,而getDynamicDexCheck方法依赖于loaddex方法加载的dex文件,dex加载后调用getDynamicDexCheck接口的check方法;所以,hook脚本只需要动态hook dex,然后调用“getDynamicDexCheck().check()”即可

hook脚本如下

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
function hook_dyn_dex() {
Java.perform(function () {
var FridaNext5 = Java.use("com.shui.fridademo.Activity.FridaNext5");
Java.choose("com.shui.fridademo.Activity.FridaNext5", {
onMatch: function (instance) {
console.log(instance.getDynamicDexCheck().$className);
}, onComplete: function () {

}
});


//hook 动态加载的dex
Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
if (loader.findClass("com.shui.fridademo.Dynamic.DynamicCheck")) {
console.log(loader);
Java.classFactory.loader = loader; //切换classloader
}
} catch (error) {

}

}, onComplete: function () {

}
});

var DynamicCheck = Java.use("com.shui.fridademo.Dynamic.DynamicCheck");
console.log(DynamicCheck);
DynamicCheck.check.implementation = function () {
console.log("DynamicCheck.check");
return true;
}
});
}

自此 frida 动态 hook dex 告一段落。

踩坑排错

unable to download it within 20 seconds

 该问题是由于frida安装时,会依赖于egg文件,但是pip的国内源却没有下载到,就有了如下的报错截图

解决方案也很简单,下述命令敲即可

1
2
3
4
5
6
7
8
9
# frida egg文件下载地址

https://pypi.org/simple/frida/

# 依据自己系统、Python版本、想要安装的frida版本去下载合适的egg文件

easy_install frida-15.1.17-py3.8-macosx-11.0-arm64.egg

# 运行easy_install命令,直至其出现reading官网包即可Ctrl c结束运行

因为会读取官网,所以结束即可,然后正常使用国内源安装frida

1
python3 -m pip install frida -i https://pypi.douban.com/simple

zsh: command not found: easy_install

第一个报错中,使用到了easy_install,但是如果基于brew安装的python之后,大概率会看到标题中这个错误“zsh: command not found: easy_install”;

其实easy_install是pip之前用于安装数据包的插件,默认会安装到python中,其依赖于setuptools,但是如果莽撞的pip安装setuptools的话,100%还是没有easy_install(至少看到本文得你(^▽^));

其实easy_install依赖setuptools这个核心是不变的,但是官网介绍来看,setuptools在版本>51.3.3之后,就不会自带easy_install了,所以想要安装easy_install,只需要

1
2
3
pip3 uninstall setuptools

pip3 install -v setuptools==51.3.3

setuptools移除easy_install的说明如下,官网地址:setuptools update histody

其实,上述俩个错误,都可以通过一个方案解决:降级到3.8……

Error: VM::AttachCurrentThread failed: -1

frida hook app时,可能会出现“Error: VM::AttachCurrentThread failed: -1”这个报错,报错信息如下

该错误的解决方案也很简单

1
2
3
4
5
6
7
8
adb shell

su

setprop persist.device_config.runtime_native.usap_pool_enabled false

getprop | grep usap

grep 结果如下

然后再 hook app 即可正常 attach

解决方案依据:frida issues 1788

参考文献

frida docs