检测
普通Frida检测
普通的检测总结以下几种手段:
检测/data/local/tmp下的frida特征文件,frida默认端口27042
双进程检测
检测/proc/pid/maps、/proc/pid/task/tid/stat、/proc/pid/fd中的frida特征
检测D-BUS
安卓系统使用Binder机制来实现进程间通信(IPC),而不是使用D-Bus。检测函数向所有端口发送d-bus消息,如果返回reject就说明fridaserver开启。
自实现Frida检测
我们可以看到,上面使用的绕过技巧,大多都和系统函数有关系,那么如果app中不调用这些系统函数,而是用自实现的函数来进行操作,不就很难hook了吗?
这里就有一种自实现函数的技术:svc。
通过安卓架构的学习,我们知道了安卓从上到下也是由层来分隔的,而层与层之间不能直接交互,而是需要一个中间层来进行操作。我们常见的jni就是Java层和native层的交互。而syscall就是kernel和native之间的中间层。
svc是x86架构中的一个指令,用于在用户模式下发起系统调用。当执行svc指令时,处理器会从用户态转换为内核态,执行内核级别的命令。
开发者可以通过syscall来执行内核函数,而不是直接使用系统函数,下面介绍几种防护手段:
直接使用syscall替代libc函数:
不使用标准C库函数,而是直接调用系统调用。这样可以绕过常见的hook点,因为大多数hook工具主要针对libc函数。
-
- #include <sys/syscall.h> //SYS_open SYS_read SYS_close都是syscall.h中的常量
- //代表系统调用的编号。在Linux系统中,每个系统调用都有一个唯一的编号
- #include <unistd.h>
- #include <fcntl.h>
-
- int my_open(const char *pathname, int flags) {
- return syscall(SYS_open, pathname, flags);
- }
-
- ssize_t my_read(int fd, void *buf, size_t count) {
- return syscall(SYS_read, fd, buf, count);
- }
-
- int my_close(int fd) {
- return syscall(SYS_close, fd);
- }
-
- // 使用示例
- int main() {
- int fd = my_open("/path/to/file", O_RDONLY); //fd代表文件标识符,代表打开的是哪个文件
- if (fd != -1) {
- char buffer[100];
- ssize_t bytes_read = my_read(fd, buffer, sizeof(buffer));
- my_close(fd);
- }
- return 0;
- }
复制代码
实现关键功能的自定义syscall wrapper:
为关键的系统调用创建自己的包装函数
-
- #include <sys/syscall.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include <string.h>
-
- ssize_t secure_read(int fd, void *buf, size_t count) {
- // 完整性检查
- if (syscall(SYS_gettid) != syscall(SYS_getpid)) {
- // 可能正在被调试,终止操作
- return -1;
- }
-
- // 执行实际的读取操作
- ssize_t bytes_read = syscall(SYS_read, fd, buf, count);
-
- // 数据校验 (简单示例,实际应用中可能需要更复杂的校验)
- if (bytes_read > 0) {
- for (ssize_t i = 0; i < bytes_read; i++) {
- ((char*)buf)[i] ^= 0x55; // 简单的XOR操作
- }
- }
-
- return bytes_read;
- }
复制代码
动态生成syscall:
在运行时动态生成syscall指令(直接使用机器码,和下面的汇编差不多)
-
- #include <sys/mman.h>
- #include <string.h>
-
- typedef long (*syscall_fn)(long, ...);
-
- syscall_fn generate_write_syscall() {
- // 分配可执行内存
- void* mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
-
- // x86-64 架构的 write syscall 机器码
- unsigned char code[] = {
- 0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1 (write syscall number)
- 0x0f, 0x05, // syscall
- 0xc3 // ret
- };
-
- // 复制代码到可执行内存
- memcpy(mem, code, sizeof(code));
-
- return (syscall_fn)mem;
- }
-
- // 使用示例
- int main() {
- syscall_fn my_write = generate_write_syscall();
- const char *msg = "Hello, World!\n";
- my_write(1, msg, strlen(msg));
- return 0;
- }
复制代码
使用汇编实现syscall:
直接使用汇编语言实现系统调用
-
- .global my_write
- my_write:
- mov x8, #64 // write syscall number for ARM64
- svc #0 // trigger syscall
- ret // return to caller
-
- // C代码调用示例
- // extern ssize_t my_write(int fd, const void *buf, size_t count);
- //
- // int main() {
- // const char *msg = "Hello, World!\n";
- // my_write(1, msg, strlen(msg));
- // return 0;
- // }
复制代码
最后再举一个实际检测frida的syscall例子:
-
- #include <jni.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- JNIEXPORT jboolean JNICALL
- Java_com_example_SecurityCheck_detectFrida(JNIEnv *env, jobject thiz) {
- char line[256];
- int fd = syscall(SYS_open, "/proc/self/maps", O_RDONLY);
- if (fd != -1) {
- while (syscall(SYS_read, fd, line, sizeof(line)) > 0) {
- if (strstr(line, "frida") || strstr(line, "gum-js-loop")) {
- syscall(SYS_close, fd);
- return JNI_TRUE;
- }
- }
- syscall(SYS_close, fd);
- }
- return JNI_FALSE;
- }
复制代码
绕过
普通检测绕过
网上对frida的检测通常会使用openat、open、strstr、pthread_create、snprintf、sprintf、readlinkat等一系列函数,这里hook了strstr和strcmp函数。
-
- 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);
- }
- }
- })
-
- }
-
复制代码
replace_str(); 一把梭方案
检测点说明:
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 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。
自实现检测绕过
我们通过上面的学习看到,自实现系统函数一个重要的前提就是它们都有标准的系统调用号,标准的机器码。所以我们绕过的时候也可以用同样的思路。
Frida的Memory API可以直接查找整个系统的内存内容,我们直接搜索对应函数的特征码,定位到之后再使用Interceptor进行Hook。(要注意每个架构对应的特征可能不一样)
-
- function hookSysOpen() {
- let SYS_OPEN;
- let SVC_INSTRUCTION_HEX;
- const arch = Process.arch;
-
- if (arch === "arm64") {
- SYS_OPEN = 56; // ARM64架构下open系统调用的编号
- SVC_INSTRUCTION_HEX = "01 00 00 D4"; // ARM64架构下svc指令的十六进制表示
- } else if (arch === "arm") {
- SYS_OPEN = 5; // ARM架构下open系统调用的编号
- SVC_INSTRUCTION_HEX = "00 00 00 EF"; // ARM架构下svc指令的十六进制表示
- } else {
- console.log("不支持的架构: " + arch);
- return;
- }
-
- console.log("当前架构: " + arch);
- console.log("开始搜索SYS_OPEN系统调用...");
-
- //系统调用指令(如svc)通常位于可执行代码段中(r-x)
- Process.enumerateRanges('r-x').forEach(function(range) {
- if (range.file && range.file.path && range.file.path.endsWith(".so")) {
- console.log("搜索模块: " + range.file.path);
-
- Memory.scan(range.base, range.size, SVC_INSTRUCTION_HEX, {
- onMatch: function(address) {
- let sysCallNumber;
- if (arch === "arm64") {
- // 在ARM64中,系统调用号在svc指令之前的指令中
- sysCallNumber = address.sub(4).readU32() & 0xFFFF;
- } else if (arch === "arm") {
- // 在ARM中,系统调用号通常在r7寄存器中,这里我们只能近似处理
- sysCallNumber = address.sub(4).readU16() & 0xFF;
- }
-
- if (sysCallNumber === SYS_OPEN) {
- console.log("找到SYS_OPEN调用,地址: " + address);
-
- Interceptor.attach(address, {
- onEnter: function(args) {
- let fileName;
- if (arch === "arm64") {
- fileName = args[1].readUtf8String();
- } else if (arch === "arm") {
- fileName = args[0].readUtf8String();
- }
- console.log("SYS_OPEN被调用,文件名: " + fileName);
- },
- onLeave: function(retval) {
- console.log("SYS_OPEN返回值: " + retval);
- }
- });
- }
- },
- onComplete: function() {
- console.log("搜索完成");
- }
- });
- }
- });
- }
-
- hookSysOpen();
复制代码