frida反调试 汇编层过检测 强到离谱

[复制链接]
查看1239 | 回复1 | 2024-12-23 14:03:07 | 显示全部楼层 |阅读模式

立即注册,突破游客限制,与技术大牛交流切磋,畅享海量资源,让你的技术之路不再孤单!

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本帖最后由 逆向君 于 2024-12-23 14:04 编辑



检测
普通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函数。

  1. #include <sys/syscall.h>  //SYS_open SYS_read SYS_close都是syscall.h中的常量
  2.                           //代表系统调用的编号。在Linux系统中,每个系统调用都有一个唯一的编号
  3. #include <unistd.h>
  4. #include <fcntl.h>
  5. int my_open(const char *pathname, int flags) {
  6.     return syscall(SYS_open, pathname, flags);
  7. }
  8. ssize_t my_read(int fd, void *buf, size_t count) {
  9.     return syscall(SYS_read, fd, buf, count);
  10. }
  11. int my_close(int fd) {
  12.     return syscall(SYS_close, fd);
  13. }
  14. // 使用示例
  15. int main() {
  16.     int fd = my_open("/path/to/file", O_RDONLY);  //fd代表文件标识符,代表打开的是哪个文件
  17.     if (fd != -1) {
  18.         char buffer[100];
  19.         ssize_t bytes_read = my_read(fd, buffer, sizeof(buffer));
  20.         my_close(fd);
  21.     }
  22.     return 0;
  23. }
复制代码

实现关键功能的自定义syscall wrapper:

为关键的系统调用创建自己的包装函数

  1. #include <sys/syscall.h>
  2. #include <unistd.h>
  3. #include <fcntl.h>
  4. #include <string.h>
  5. ssize_t secure_read(int fd, void *buf, size_t count) {
  6.     // 完整性检查
  7.     if (syscall(SYS_gettid) != syscall(SYS_getpid)) {
  8.         // 可能正在被调试,终止操作
  9.         return -1;
  10.     }
  11.      
  12.     // 执行实际的读取操作
  13.     ssize_t bytes_read = syscall(SYS_read, fd, buf, count);
  14.      
  15.     // 数据校验 (简单示例,实际应用中可能需要更复杂的校验)
  16.     if (bytes_read > 0) {
  17.         for (ssize_t i = 0; i < bytes_read; i++) {
  18.             ((char*)buf)[i] ^= 0x55;  // 简单的XOR操作
  19.         }
  20.     }
  21.      
  22.     return bytes_read;
  23. }
复制代码

动态生成syscall:
在运行时动态生成syscall指令(直接使用机器码,和下面的汇编差不多)

  1. #include <sys/mman.h>
  2. #include <string.h>
  3. typedef long (*syscall_fn)(long, ...);
  4. syscall_fn generate_write_syscall() {
  5.     // 分配可执行内存
  6.     void* mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  7.      
  8.     // x86-64 架构的 write syscall 机器码
  9.     unsigned char code[] = {
  10.         0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00,  // mov rax, 1 (write syscall number)
  11.         0x0f, 0x05,                                // syscall
  12.         0xc3                                       // ret
  13.     };
  14.      
  15.     // 复制代码到可执行内存
  16.     memcpy(mem, code, sizeof(code));
  17.      
  18.     return (syscall_fn)mem;
  19. }
  20. // 使用示例
  21. int main() {
  22.     syscall_fn my_write = generate_write_syscall();
  23.     const char *msg = "Hello, World!\n";
  24.     my_write(1, msg, strlen(msg));
  25.     return 0;
  26. }
复制代码

使用汇编实现syscall:
直接使用汇编语言实现系统调用
  1. .global my_write
  2. my_write:
  3.     mov x8, #64         // write syscall number for ARM64
  4.     svc #0              // trigger syscall
  5.     ret                 // return to caller
  6. // C代码调用示例
  7. // extern ssize_t my_write(int fd, const void *buf, size_t count);
  8. //
  9. // int main() {
  10. //     const char *msg = "Hello, World!\n";
  11. //     my_write(1, msg, strlen(msg));
  12. //     return 0;
  13. // }
复制代码

最后再举一个实际检测frida的syscall例子:

  1. #include <jni.h>
  2. #include <string.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. JNIEXPORT jboolean JNICALL
  6. Java_com_example_SecurityCheck_detectFrida(JNIEnv *env, jobject thiz) {
  7.     char line[256];
  8.     int fd = syscall(SYS_open, "/proc/self/maps", O_RDONLY);
  9.     if (fd != -1) {
  10.         while (syscall(SYS_read, fd, line, sizeof(line)) > 0) {
  11.             if (strstr(line, "frida") || strstr(line, "gum-js-loop")) {
  12.                 syscall(SYS_close, fd);
  13.                 return JNI_TRUE;
  14.             }
  15.         }
  16.         syscall(SYS_close, fd);
  17.     }
  18.     return JNI_FALSE;
  19. }
复制代码

绕过
普通检测绕过
网上对frida的检测通常会使用openat、open、strstr、pthread_create、snprintf、sprintf、readlinkat等一系列函数,这里hook了strstr和strcmp函数。

  1. function replace_str() {
  2.     var pt_strstr = Module.findExportByName("libc.so", 'strstr');
  3.     var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
  4.   
  5.     Interceptor.attach(pt_strstr, {
  6.         onEnter: function (args) {
  7.             var str1 = args[0].readCString();
  8.   
  9.             var str2 = args[1].readCString();
  10.             if (str2.indexOf("tmp") !== -1 ||
  11.                 str2.indexOf("frida") !== -1 ||
  12.                 str2.indexOf("gum-js-loop") !== -1 ||
  13.                 str2.indexOf("gmain") !== -1 ||
  14.                 str2.indexOf("gdbus") !== -1 ||
  15.                 str2.indexOf("pool-frida") !== -1||
  16.                 str2.indexOf("linjector") !== -1) {
  17.                 //console.log("strcmp-->", str1, str2);
  18.                 this.hook = true;
  19.             }
  20.         }, onLeave: function (retval) {
  21.             if (this.hook) {
  22.                 retval.replace(0);
  23.             }
  24.         }
  25.     });
  26.   
  27.     Interceptor.attach(pt_strcmp, {
  28.         onEnter: function (args) {
  29.             var str1 = args[0].readCString();
  30.             var str2 = args[1].readCString();
  31.             if (str2.indexOf("tmp") !== -1 ||
  32.                 str2.indexOf("frida") !== -1 ||
  33.                 str2.indexOf("gum-js-loop") !== -1 ||
  34.                 str2.indexOf("gmain") !== -1 ||
  35.                 str2.indexOf("gdbus") !== -1 ||
  36.                 str2.indexOf("pool-frida") !== -1||
  37.                 str2.indexOf("linjector") !== -1) {
  38.                 //console.log("strcmp-->", str1, str2);
  39.                 this.hook = true;
  40.             }
  41.         }, onLeave: function (retval) {
  42.             if (this.hook) {
  43.                 retval.replace(0);
  44.             }
  45.         }
  46.     })
  47.   
  48. }
  49.   
复制代码

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。(要注意每个架构对应的特征可能不一样)

  1. function hookSysOpen() {
  2.     let SYS_OPEN;
  3.     let SVC_INSTRUCTION_HEX;
  4.     const arch = Process.arch;
  5.     if (arch === "arm64") {
  6.         SYS_OPEN = 56;  // ARM64架构下open系统调用的编号
  7.         SVC_INSTRUCTION_HEX = "01 00 00 D4";  // ARM64架构下svc指令的十六进制表示
  8.     } else if (arch === "arm") {
  9.         SYS_OPEN = 5;  // ARM架构下open系统调用的编号
  10.         SVC_INSTRUCTION_HEX = "00 00 00 EF";  // ARM架构下svc指令的十六进制表示
  11.     } else {
  12.         console.log("不支持的架构: " + arch);
  13.         return;
  14.     }
  15.     console.log("当前架构: " + arch);
  16.     console.log("开始搜索SYS_OPEN系统调用...");
  17.     //系统调用指令(如svc)通常位于可执行代码段中(r-x)
  18.     Process.enumerateRanges('r-x').forEach(function(range) {
  19.         if (range.file && range.file.path && range.file.path.endsWith(".so")) {
  20.             console.log("搜索模块: " + range.file.path);
  21.             
  22.             Memory.scan(range.base, range.size, SVC_INSTRUCTION_HEX, {
  23.                 onMatch: function(address) {
  24.                     let sysCallNumber;
  25.                     if (arch === "arm64") {
  26.                         // 在ARM64中,系统调用号在svc指令之前的指令中
  27.                         sysCallNumber = address.sub(4).readU32() & 0xFFFF;
  28.                     } else if (arch === "arm") {
  29.                         // 在ARM中,系统调用号通常在r7寄存器中,这里我们只能近似处理
  30.                         sysCallNumber = address.sub(4).readU16() & 0xFF;
  31.                     }
  32.                      
  33.                     if (sysCallNumber === SYS_OPEN) {
  34.                         console.log("找到SYS_OPEN调用,地址: " + address);
  35.                         
  36.                         Interceptor.attach(address, {
  37.                             onEnter: function(args) {
  38.                                 let fileName;
  39.                                 if (arch === "arm64") {
  40.                                     fileName = args[1].readUtf8String();
  41.                                 } else if (arch === "arm") {
  42.                                     fileName = args[0].readUtf8String();
  43.                                 }
  44.                                 console.log("SYS_OPEN被调用,文件名: " + fileName);
  45.                             },
  46.                             onLeave: function(retval) {
  47.                                 console.log("SYS_OPEN返回值: " + retval);
  48.                             }
  49.                         });
  50.                     }
  51.                 },
  52.                 onComplete: function() {
  53.                     console.log("搜索完成");
  54.                 }
  55.             });
  56.         }
  57.     });
  58. }
  59. hookSysOpen();
复制代码
这个人还没有签名
savestone | 2024-12-25 10:28:52 | 显示全部楼层
小白完全看不懂,慢慢爬帖学习
这个人还没有签名
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

图文推荐
大神求
大神求
6 天前
热门排行