0x1 简介

Frida就是一个「脚本化、可移植、免费且非常流行」的动态插桩工具。

对于知道插桩是什么意思的朋友来说,不必再过多解释。而如果你不知道插桩是什么意思,大概介绍下,你可以理解为:将你的代码,插入到源程序的某个地方,最终一起执行,而「不必改变源程序的代码」。

不理解也没关系,忽略它,不影响后面的内容

项目地址:https://github.com/frida/frida

环境配置:

1
pip install frida==16.1.3 frida-tools==12.3.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
1
frida-server-16.1.3-android-arm64

https://github.com/frida/frida/releases

https://github.com/hzzheyang/strongR-frida-android

0x2 注入模式

spawn

将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App

优点:hook时机早,可以在程序刚启动就执行。

比如:

  • JNI_OnLoad
  • Application.attach
  • Activity.onCreate
  • 反调试代码
  • root检测
  • anti-frida

缺点:有些 App 检测 spawn 行为

例如:

  • anti-debug
  • anti-frida
  • ptrace 检测
1
frida -U -f 包名 -l hook.js

attach

在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作

优点:稳定

缺点:早期函数 hook 不到

例如:

  • Application.attach
  • JNI_OnLoad
  • native 初始化
  • 早期检测函数

这些可能已经执行完了。

1
frida -U -n 包名 -l hook.js

0x3 常用api

模板:

1
2
3
4
5
6
7
8
9
Java.perform(function() {

var <class_reference> = Java.use("<package_name>.<class>");
<class_reference>.<method_to_hook>.implementation = function(<args>) {


}

})
  • Java.performFrida 中的 context 函数用于为脚本创建一个特殊的上下文,以便与 Android 应用程序中的 Java 代码进行交互。它就像打开一扇门,让你能够访问和操作应用程序内部运行的 Java 代码。进入此上下文后,可以执行诸如钩取方法或访问 Java 类之类的操作,从而控制或观察应用程序的行为。

  • Frida 在 Android 上提供的 Java API(如 Java.use()、Java.perform() 等)实际上就是:

    在 Native 层调用 ART/Dalvik 的内部接口,实现类似 Java 反射的能力

  • var <class_reference> = Java.use("<package_name>.<class>");

    在这里,声明一个变量<class_reference>来表示目标 Android 应用程序中的一个 Java 类。需要指定要与该函数一起使用的类Java.use,该函数以类名作为参数。<package_name>``<package_name>表示 Android 应用程序的包名,<class_name>``<class>表示您要与之交互的类。

  • <class_reference>.<method_to_hook>.implementation = function(<args>) {}

    在选定的类中,可以使用 hook 语法访问要挂钩的方法,并指定要挂钩的方法<class_reference>.<method_to_hook>。可以在此处定义在调用被挂钩方法时要执行的自定义逻辑。arguments``<args>表示传递给函数的参数。

Java hook

1.Hook普通方法、打印参数和修改返回值,替换参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hookTest1(){
Java.perform(function(){
//获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
var utils = Java.use("类名");
//修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
utils.$init.overload.......
utils.method.overload('int','int').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;
//替换参数
this.method(xxx,xxx);
}
})
}

2.Hook静态方法和字段

1
2
3
4
5
6
7
//静态方法的主动调用
function hookTest9(){
Java.perform(function(){
var ClassName=Java.use("类名");
ClassName.privateFunc("6666");
})
}
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("类名");
//修改类的静态字段"flag"的值
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);
//非静态字段的修改
// 使用`Java.choose()`枚举类的所有实例
Java.choose("类名", {
onMatch: function(obj){
//修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。
obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){

}
});
});

}

3.主动调用

非静态方法

1
2
3
4
5
6
7
8
9
10
11
function hooktest(){
Java.perform(function() {

var check = Java.use("类名");
var check_obj = check.$new(); // Class Object
var res = check_obj.get_flag('value'); // Calling the method
console.log("FLAG " + res);

})

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hookTest10(){
Java.perform(function () {
var ret = null;
Java.choose("类名",{ //要hook的类
onMatch:function(instance){
ret=instance.privateFunc("aaaaaaa"); //要hook的方法
},
onComplete:function(){
//console.log("result: " + ret);
}
});
})
//return ret;
}

4.hook内部类

1
2
3
4
5
6
7
8
9
10
11
12
function hookTest6(){
Java.perform(function(){
//内部类
var innerClass = Java.use("类名$InnerClass");//$拼接内部类名
console.log(innerClass);
innerClass.$init.implementation = function(){//获取到内部类
//后续操作
console.log("eeeeeeee");
}

});
}

native hook

so文件加载与JNI注册:https://bbs.kanxue.com/thread-285950.htm

如何获取Frida中特定函数的地址,有很多方法可以做到这一点

  • 使用 Frida API:Module.enumerateExports()

  • 使用 Frida API: Module.getExportByName()

  • 使用 Frida API:Module.findExportByName()

  • 计算偏移量并将add()其映射到 Module.getBaseAddress()地址

  • 使用 Frida API:Module.enumerateImports()

Native函数的基础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
function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("so文件名","函数名");
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
retval.replace('value');
}
})
}
})
}

0x4 frida检测与对抗

文件名/端口检测

修改文件名

1
2
3
./fs1 -l 0.0.0.0:6666
adb forward tcp:6666 tcp:6666
frida -H 127.0.0.1:6666 进程名 -l hook.js

使用脚本定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_dlopen() {  var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
console.log("addr_android_dlopen_ext", android_dlopen_ext);
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr != null && pathptr != undefined) {
var path = ptr(pathptr).readCString();
console.log("android_dlopen_ext:", path)


}
},
onLeave: function (retvel) {
}
})
}

setImmediate(hook_dlopen);

检测map

什么是map

/proc/self/maps 是一个特殊的文件,它包含了当前进程的内存映射信息。当你打开这个文件时,它会显示一个列表,其中包含了进程中每个内存区域的详细信息。这些信息通常包括:

  • 起始地址(Start Address)

  • 结束地址(End Address)

  • 权限(如可读、可写、可执行)

  • 共享/私有标志(Shared or Private)

  • 关联的文件或设备(如果内存区域是文件映射的)

  • 内存区域的偏移量

  • 内存区域的类型(如匿名映射、文件映射、设备映射等)
    当注入frida后,在maps文件中就会存在 frida-agent-64.so、frida-agent-32.so 等文件。

    检测逻辑

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
bool check_maps() {
// 定义一个足够大的字符数组line,用于存储读取的行
char line[512];
// 打开当前进程的内存映射文件/proc/self/maps进行读取
FILE* fp = fopen("/proc/self/maps", "r");
if (fp) {
// 如果文件成功打开,循环读取每一行
while (fgets(line, sizeof(line), fp)) {
// 使用strstr函数检查当前行是否包含"frida"字符串
if (strstr(line, "frida") || strstr(line, "gadget")) {
// 如果找到了"frida",关闭文件并返回true,表示检测到了恶意库
fclose(fp);
return true; // Evil library is loaded.
}
}
// 遍历完文件后,关闭文件
fclose(fp);
} else {
// 如果无法打开文件,记录错误。这可能意味着系统状态异常
// 注意:这里的代码没有处理错误,只是注释说明了可能的情况
}
// 如果没有在内存映射文件中找到"frida",返回false,表示没有检测到恶意库
return false; // No evil library detected.
}

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
// 定义一个函数anti_maps,用于阻止特定字符串的搜索匹配,避免检测到敏感内容如"Frida"或"REJECT"
function anti_maps() {
// 查找libc.so库中strstr函数的地址,strstr用于查找字符串中首次出现指定字符序列的位置
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
// 查找libc.so库中strcmp函数的地址,strcmp用于比较两个字符串
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
// 使用Interceptor模块附加到strstr函数上,拦截并修改其行为
Interceptor.attach(pt_strstr, {
// 在strstr函数调用前执行的回调
onEnter: function (args) {
// 读取strstr的第一个参数(源字符串)和第二个参数(要查找的子字符串)
var str1 = args[0].readCString();
var str2 = args[1].readCString();
// 检查子字符串是否包含"REJECT"或"frida",如果包含则设置hook标志为true
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
// 在strstr函数调用后执行的回调
onLeave: function (retval) {
// 如果之前设置了hook标志,则将strstr的结果替换为0(表示未找到),从而隐藏敏感信息
if (this.hook) {
retval.replace(0);
}
}
});

// 对strcmp函数做类似的处理,防止通过字符串比较检测敏感信息
Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
onLeave: function (retval) {
if (this.hook) {
// strcmp返回值为0表示两个字符串相等,这里同样替换为0以避免匹配成功
retval.replace(0);
}
}
});
}

status(线程)检测

1
2
ls ../proc/pid/task 列出线程id
cat /proc/pid/task/线程id/status
  • 在 /proc/pid/task 目录下,可以通过查看不同的线程子目录,来获取进程中每个线程的运行时信息。这些信息包括线程的状态、线程的寄存器内容、线程占用的CPU时间、线程的堆栈信息等。通过这些信息,可以实时观察和监控进程中每个线程的运行状态,帮助进行调试、性能优化和问题排查等工作。
  • 在某些app中就会去读取 /proc/stask/线程ID/status 文件,如果是运行frida产生的,则进行反调试。例如:gmain/gdbus/gum-js-loop/pool-frida等
  1. gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。
  2. gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。
  3. gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
  4. pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。
  5. linjector 是一种用于 Android 设备的开源工具,它允许用户在运行时向 Android 应用程序注入动态链接库(DLL)文件。通过注入 DLL 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。
    PS:由于frida可以随时附加到进程,所以写的检测必须覆盖APP的全周期,或者至少是敏感函数执行前

检测逻辑

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
bool check_status() {
DIR *dir = opendir("/proc/self/task/");
struct dirent *entry;
char status_path[MAX_PATH];
char buffer[MAX_BUFFER];
int found = false;

if (dir) {
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_DIR) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
snprintf(status_path, sizeof(status_path), "/proc/self/task/%s/status", entry->d_name);
if (read_file(status_path, buffer, sizeof(buffer)) == -1) {
continue;
}
if (strcmp(buffer, "null") == 0) {
continue;
}
char *line = strtok(buffer, "\n");
while (line) {
if (strstr(line, "Name:") != NULL) {
const char *frida_name = strstr(line, "gmain");
if (frida_name || strstr(line, "gum-js-loop") || strstr(line, "pool-frida") || strstr(line, "gdbus")) {
found = true;
break;
}
}
line = strtok(NULL, "\n");
}
if (found) break;
}
}
closedir(dir);
}
return found;
}

anti脚本

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
function replace_str() {
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');

Interceptor.attach(pt_strstr, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1||
str2.indexOf("linjector") !== -1) {
//console.log("strcmp-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
});

Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1||
str2.indexOf("linjector") !== -1) {
//console.log("strcmp-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
})

}

inline hook检测

通过Frida查看一个函数hook之前和之后的机器码,以此来判断是否被Frida的inlinehook注入

检测逻辑

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
#include <jni.h>
#include <string>
#include <dlfcn.h>
#include "dlfcn/local_dlfcn.h"

bool check_inlinehook() {
// 根据系统架构选择对应的libc.so库路径
const char *lib_path;
#ifdef __LP64__
lib_path = "/system/lib64/libc.so";
#else
lib_path = "/system/lib/libc.so";
#endif

// 定义要比较的字节数
const int CMP_COUNT = 8;
// 指定要查找的符号名,这里是"open"函数
const char *sym_name = "open";

// 使用local_dlopen函数打开指定的共享库,并获取操作句柄
struct local_dlfcn_handle *handle = static_cast<local_dlfcn_handle *>(local_dlopen(lib_path));
if (!handle) {
return JNI_FALSE; // 如果无法打开共享库,返回false
}

// 获取"open"函数在libc.so中的偏移量
off_t offset = local_dlsym(handle, sym_name);

// 关闭handle,因为我们接下来使用标准的dlopen/dlsy来获取函数地址
local_dlclose(handle);

// 打开libc.so文件,准备读取数据
FILE *fp = fopen(lib_path, "rb");
if (!fp) {
return JNI_FALSE; // 如果无法打开文件,返回false
}

// 定义一个缓冲区,用于存储读取的文件内容
char file_bytes[CMP_COUNT] = {0};
// 读取指定偏移量处的CMP_COUNT个字节
fseek(fp, offset, SEEK_SET);
fread(file_bytes, 1, CMP_COUNT, fp);
fclose(fp);

// 使用dlopen函数打开libc.so共享库,并获取操作句柄
void *dl_handle = dlopen(lib_path, RTLD_NOW);
if (!dl_handle) {
return JNI_FALSE; // 如果无法打开共享库,返回false
}

// 使用dlsym函数获取"open"函数的地址
void *sym = dlsym(dl_handle, sym_name);
if (!sym) {
dlclose(dl_handle);
return JNI_FALSE; // 如果无法找到符号,返回false
}

// 比较原libc.so中的"open"函数内容与通过dlsym获取的"open"函数内容是否一致
int is_hook = memcmp(file_bytes, sym, CMP_COUNT) != 0;

// 关闭dlopen打开的共享库句柄
dlclose(dl_handle);

// 返回比较结果,如果函数被hook则返回JNI_TRUE,否则返回JNI_FALSE
return is_hook ? JNI_TRUE : JNI_FALSE;
}

anti脚本

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 hook_memcmp_addr(){
//hook反调试
var memcmp_addr = Module.findExportByName("libc.so", "fread");
if (memcmp_addr !== null) {
console.log("fread address: ", memcmp_addr);
Interceptor.attach(memcmp_addr, {
onEnter: function (args) {
this.buffer = args[0]; // 保存 buffer 参数
this.size = args[1]; // 保存 size 参数
this.count = args[2]; // 保存 count 参数
this.stream = args[3]; // 保存 FILE* 参数
},
onLeave: function (retval) {
// 这里可以修改 buffer 的内容,假设我们知道何时 fread 被用于敏感操作
console.log(this.count.toInt32());
if (this.count.toInt32() == 8) {
// 模拟 fread 读取了预期数据,伪造返回值
Memory.writeByteArray(this.buffer, [0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6]);
retval.replace(8); // 填充前8字节
console.log(hexdump(this.buffer));
}
}
});
} else {
console.log("Error: memcmp function not found in libc.so");
}
}