frida_root检测

在hook的时候,要想使用frida进行hook注入,就需要启动frida-server(server注入方式),而该server需要adb具有root权限,所以一些app会防止被hook而做一些frida检测或root检测。

常规的frida检测方法

参考自:安卓逆向之过frida检测总结版_android frida-CSDN博客Frida检测 - huolong blog

1、检测文件和进程名

  • 默认的frida-server会在/data/local/tmp目录下,因此app可以检测该目录下有没有frida的特征文件,对于这种检测可以给frida-server文件重命名,或者hook一些**fopen()strstr()**函数来绕过他比对文件名的代码即可。

  • 较老版本的frida会在运行后会在/data/local/tmp/目录释放一个名为re.frida.server/frida-agent-64.so的文件,不过新版本已经改进了这一问题。

2、检测端口

frida-server启动后默认会在27042端口监听,可以通过检查该端口是否被占用的方式来检测frida,绕过方法就是给frida-server转发到另一个端口。

1
2
./frida-server -l 0.0.0.0:8888
frida -H 手机ip地址:8888 -f 包名 -l hook.js //附加模式

3、ptrace占坑

一个app只能被一个进程附加,app内部可能自己附加了自己或开启了子进程附加自己,让frida没法附加上去。

绕过方式是可以使用spawn方式启动app。

4、双进程检测

采用双进程的方式,对父进程进行保护,基于信号的发送和接收,实现相互的保护防止被动态攻击。简单的双进程保护就是从原进程再fork一个空进程出来,让逆向分析的时候附加到空进程中导致hook不上。

绕过方式也是使用spawn方式启动app,让frida重新打开一个进程而不是附加到当前进程中。

5、检测D-Bus

D-Bus是一种进程间通信(IPC)和远程过程调用(RPC)机制,最初是为Linux开发的,目的是用一个统一的协议替代现有的和竞争的IPC解决方案

遍历连接手机所有端口发送D-bus消息,如果返回”REJECT”这个特征则认为存在frida-server。

内存中存在frida rpc字符串,认为有frida-server。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for(i = 0 ; i <= 65535 ; i++) {
sock = socket(AF_INET , SOCK_STREAM , 0);
sa.sin_port = htons(i);
if (connect(sock , (struct sockaddr)&sa , sizeof sa) != -1) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "FRIDA DETECTION [1]: Open Port: %d", i);
memset(res, 0 , 7);
// send a D-Bus AUTH message. Expected answer is “REJECT"
send(sock, "\x00", 1, NULL);
send(sock, "AUTH\r\n", 6, NULL);
usleep(100);
if (ret = recv(sock, res, 6, MSG_DONTWAIT) != -1) {
if (strcmp(res, "REJECT") == 0) {
/ Frida server detected. Do something… /
}
}
}
close(sock);
}

绕过方法,hook掉strcmp或strstr等字符串比较函数等。

6、检测fd

/proc/pid/fd 目录的作用在于提供了一种方便的方式来查看进程的文件描述符信息,这对于调试和监控进程非常有用。通过查看文件描述符信息,可以了解进程打开了哪些文件、网络连接等,帮助开发者和系统管理员进行问题排查和分析工作。

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
 复制代码 隐藏代码
bool check_fd() {
DIR *dir = NULL;
struct dirent *entry;
char link_name[100];
char buf[100];
bool ret = false;
if ((dir = opendir("/proc/self/fd/")) == NULL) {
LOGI(" %s - %d error:%s", __FILE__, __LINE__, strerror(errno));
} else {
entry = readdir(dir);
while (entry) {
switch (entry->d_type) {
case DT_LNK:
sprintf(link_name, "%s/%s", "/proc/self/fd/", entry->d_name);
readlink(link_name, buf, sizeof(buf));
if (strstr(buf, "frida") || strstr(buf, "gum-js-loop") ||
strstr(buf, "gmain") ||
strstr(buf, "-gadget") || strstr(buf, "linjector")) {
LOGI("check_fd -> find frida:%s", buf);
ret = true;
}
break;
default:
break;
}
entry = readdir(dir);
}
}
closedir(dir);
return ret;
}

7、检测maps映射文件

maps文件中存储的是APP运行时加载的依赖

当启动frida后,在maps文件中就会存在 frida-agent-64.sofrida-agent-32.so 文件。

检测maps文件,frida(以64位为例)注入进程后会加载frida-agent-64.so,故进程可以通过访问/proc/pid/maps查找是否加载该库判断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
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.
}

绕过方法,也是hook字符串比较函数来绕过。

8、检测线程status

frida注入进程后会启动多个新线程,通过查询/proc/self/task/number_for_tid/status下的线程信息判断是否被frida注入。

以下为常见的frida会启动的新线程名次:

  • gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。
  • gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。
  • gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
  • pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。
  • linjector:一种用于 Android 设备的开源工具,它允许用户在运行时向 Android 应用程序注入动态链接库(DLL)文件。通过注入 DLL 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。
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;
}

绕过方法,同样可以hook一些str相关函数绕过。

脚本模板:

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 || //tmp在葫芦娃版本中没有,因为葫芦娃会在tmp下生成xxx-xxx-xxx的文件,容易被检测,因此把检测tmp目录的也要过滤掉。
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);
}
}
})
}

或者还会检测proc/pid/stat里,是S表示是正常的。

9、检测inlinehook

inlinehook是一种代码hook技术,通常用于native层代码的控制,原理可以简单概括为修改函数代码开头的几个字节为跳转指令,跳转到一段由攻击者控制的内存执行额外的操作。因此被inlinehook代码的开头几个字节会发生变化,通过判断文件中的代码和内存中的代码开头是否发生变化可以用于检测frida。

so文件底层需要open函数打开并通过fread读取,可以hook相关函数

获取hook前字节码的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
 复制代码 隐藏代码
let bytes_count = 8
let address = Module.getExportByName("libc.so","open")

let before = ptr(address)
console.log("")
console.log(" before hook: ")
console.log(hexdump(before, {
offset: 0,
length: bytes_count,
header: true,
ansi: 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
26
27
28
 复制代码 隐藏代码
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");
}
}

10、扫描内存中是否有Frida库特征出现

例如字符串LIBFRIDA。

11、其他

常见用于检测的系统函数

strstr,strcmp,open,read,fread,readlink,fopen,fgets

葫芦娃的frida-server,是对一些字符串做了处理的版本,去掉了frida的字符串特征。

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

当然还有很多其他的检测,这里就不列举了。

12、小结

app要检测frida,必须在java层或so层。

如果是在so层检测的话,首先需要加载so才能调用so里的代码进行检测,所以比较通用的方法是hook dlopen函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var dlopen = Module.findExportByName(null, "dlopen");
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

Interceptor.attach(dlopen, {
onEnter: function(args){
console.log(args[0].readCString()); //打印so的名字来确定在哪个libc里检测的。
}, onLeave: function(retval){
}
});

Interceptor.attach(android_dlopen_ext, {
onEnter: function(args){
console.log(args[0].readCString());
}, onLeave: function(retval){
}
});

而检测的方式一般是循环检测,通过开启一个子线程来循环检测(如果使用主线程的话一般会卡死)

因此,通用的方式是hook pthread。

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_pthread_create(){
var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
console.log("pthread_create_addr: ", pthread_create_addr);
Interceptor.attach(pthread_create_addr,{
onEnter:function(args){
//打印函数地址和所在的so名字
console.log(args[2], Process.findModuleByAddress(args[2]).name);
},onLeave:function(retval){
console.log("retval is =>",retval)
}
})
}
hook_pthread_create()

绕过方式就是把这些开启pthread线程的函数给干掉,替换成空(这种安全检测函数一般不会和业务逻辑相关联)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_pthread_create(){
var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
console.log("pthread_create_addr: ", pthread_create_addr);
Interceptor.attach(pthread_create_addr,{
onEnter:function(args){
//打印函数地址和所在的so名字
console.log(args[2], Process.findModuleByAddress(args[2]).name);
//找一些包名的so,即非系统so
if(Process.findModuleByAddress(args[2]).name.indexof("libxxx.so")!= -1) {
Interceptor.replace(args[2],new NativeCallback( function() {
console.log("replace success!");
},'void',['void']))
}
},onLeave:function(retval){
console.log("retval is =>",retval)
}
})
}
hook_pthread_create()

但是这种方式也有弊端,hook pthread函数有时候虽然能过frida检测,但是会卡界面。

更加通用的方式是使用第八条里的hook代码,hook字符串相关函数strstrstrcmp等来绕过。

常规的root检测

参考自:Android如何判断系统是否已经被Root_android root检测-CSDN博客

1、判断系统内是否包含su

因为frida-server注入必须要root权限,所以app一般也会检测是否有root环境,一般会定义一些checkSuEnv的方法来检测当前设备下是否有su命令。

因为需要root权限必须要使用su命令进入root中,所以可以通过判断系统内是否包含su来检测root。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 是否存在su命令,并且有执行权限
*
* @return 存在su命令,并且有执行权限返回true
*/
public static boolean isSuEnable() {
File file = null;
String[] paths = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/", "/su/bin/"};
try {
for (String path : paths) {
file = new File(path + "su");
if (file.exists() && file.canExecute()) {
Log.i(TAG, "find su in : " + path);
return true;
}
}
} catch (Exception x) {
x.printStackTrace();
}
return false;
}

绕过方法1:hook

定位到检测代码处,hook掉相关函数,如File的构造函数或file.exists等函数来绕过检测。

绕过方法2:通杀

重编译安卓系统

步骤:

  • 修改安卓系统源码,在编译的时候删除掉out/target/product/sailfish(机型)/system下的su文件
  • 修改build/core/main.mk代码,这个代码中有一段是执行android adb的权限降级的,即adb默认是root权限之后会进行降级到用户权限。具体是修改should_drop_privileges函数,让他返回false即不会降级使用adb root权限。
  • 重编译安卓系统源码,此时adb shell后默认就是root权限,不需要su命令,且系统里没有su命令了。

2、检测特定路径下是否有su文件

1
2
3
4
5
“/data/local/bin/su”,
“/data/local/su”,
“/data/local/xbin/su”,
“/dev/com.koushikdutta.superuser.daemon/”,
“/sbin/su”,

绕过方法,hook掉相关字符串比对函数。

3、判断系统内是否包含 busybox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 是否存在busybox命令,并且有执行权限
*
* @return 存在busybox命令,并且有执行权限返回true
*/
public static boolean isSuEnable() {
File file = null;
String[] paths = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/", "/su/bin/"};
try {
for (String path : paths) {
file = new File(path + "busybox");
if (file.exists() && file.canExecute()) {
Log.i(TAG, "find su in : " + path);
return true;
}
}
} catch (Exception x) {
x.printStackTrace();
}
return false;
}

绕过方法同su。

4、检测系统内是否安装了Superuser.apk之类的App

如magisk等。

1
2
3
4
5
6
“com.noshufou.android.su”,
“com.noshufou.android.su.elite”,
“eu.chainfire.supersu”,
“com.koushikdutta.superuser”,
“com.yellowes.su”,
“com.topjohnwu.magisk”,
1
2
3
4
5
6
7
8
9
10
public static boolean checkSuperuserApk(){
try {
File file = new File("/system/app/Superuser.apk");
if (file.exists()) {
Log.i(LOG_TAG,"/system/app/Superuser.apk exist");
return true;
}
} catch (Exception e) { }
return false;
}

绕过方法同su。

5、判断ro.debuggable属性和ro.secure属性

默认手机出厂后ro.debuggable属性应该为0,ro.secure应该为1。意思就是系统版本要为user版本。

可以通过hook绕过或者直接修改xml文件(一般在没有签名验证的情况下),或者直接在编译安卓系统源码的时候进行修改(使user-debug版本的这些属性值符合user版本的值)。

6、frida-gadget

使用frida-gadget代替frida-server可以使用免root hook的效果。