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(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.so
、frida-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() { char line[512]; FILE* fp = fopen("/proc/self/maps", "r"); if (fp) { while (fgets(line, sizeof(line), fp)) { if (strstr(line, "frida") || strstr(line, "gadget")) { fclose(fp); return true; } } fclose(fp); } else { } return false; }
|
绕过方法,也是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 || 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) { 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) { 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(){ 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]; this.size = args[1]; this.count = args[2]; this.stream = args[3]; }, onLeave: function (retval) { console.log(this.count.toInt32()); if (this.count.toInt32() == 8) { Memory.writeByteArray(this.buffer, [0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6]); retval.replace(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()); }, 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){ 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){ console.log(args[2], Process.findModuleByAddress(args[2]).name); 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字符串相关函数strstr
和strcmp
等来绕过。
常规的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
|
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
|
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的效果。