【Frida Android】实战篇18:Frida检测与绕过——基于内核指令的攻防实战

文章目录

  • [1. SVC 与系统调用介绍](#1. SVC 与系统调用介绍)
  • [2. 应用源码分析:用指令操作文件的校验逻辑](#2. 应用源码分析:用指令操作文件的校验逻辑)
    • [2.1 核心防绕特点:绕过用户态函数 hook](#2.1 核心防绕特点:绕过用户态函数 hook)
    • [2.2 校验流程回顾](#2.2 校验流程回顾)
  • [3. Hook 绕过:直接拦截系统调用指令](#3. Hook 绕过:直接拦截系统调用指令)
    • [3.1 为何之前的方法失效?](#3.1 为何之前的方法失效?)
    • [3.2 绕过思路:识别并 hook 系统调用指令](#3.2 绕过思路:识别并 hook 系统调用指令)
    • [3.3 完整脚本与流程分析](#3.3 完整脚本与流程分析)
    • [3.4 执行脚本与效果](#3.4 执行脚本与效果)
  • [4. 章节总结](#4. 章节总结)
    • [4.1 指令校验的意义](#4.1 指令校验的意义)
    • [4.2 绕过核心思路](#4.2 绕过核心思路)
    • [4.3 不同架构的适配](#4.3 不同架构的适配)

⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。

1. SVC 与系统调用介绍

对于刚接触底层指令的同学来说,可能会疑惑「SVC」到底是什么?简单说,SVC 是 ARM 架构(包括手机常用的 ARM64 架构)中的一种「特殊指令」,全称是 Supervisor Call(管理程序调用)。它的核心作用是让程序从「用户态」切换到「内核态」------就像给操作系统内核发了一个「请求信号」,让内核帮忙执行一些敏感操作(比如打开文件、读取数据等)。

为什么要用 SVC 呢?因为手机(几乎都是 ARM64 架构)中的应用程序默认运行在「用户态」,没有权限直接操作硬件或核心资源,必须通过 SVC 指令触发「系统调用」,让内核代劳。比如我们想打开一个文件时,应用会通过 SVC 指令告诉内核「我要打开文件,路径是xxx」,内核执行后再把结果返回给应用。

这种方式的特殊之处在于:它绕开了用户态的库函数(比如 libc.so 中的 open 函数),直接通过汇编指令触发系统调用。这就导致之前通过 hook libc.so 中 open、read 等函数的绕过方法失效了------因为程序根本没调用这些函数,而是直接用指令和内核沟通。

本章为了方便演示(模拟器环境),使用的是 x86_64 架构的设备。在 x86_64 中,触发系统调用的指令不是 SVC,而是「syscall」,但核心原理完全一致:都是通过底层指令直接调用内核,避免用户态函数被 hook。

本章节使用的示例 APK、相关源码如下:

链接: https://pan.baidu.com/s/1CSzpLWr7DpY3oStWv5ZV_A?pwd=n2tp

提取码: n2tp

2. 应用源码分析:用指令操作文件的校验逻辑

从源码来看,本次检测 Frida 的核心逻辑和之前类似(检查线程名中的特征关键词),但关键差异在于文件操作的实现方式------通过底层指令直接调用系统调用,而非 libc 函数。

c++ 复制代码
#include <jni.h>
#include <string>
#include <sstream>
#include <dirent.h>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
#include <cstdint>
#include <algorithm>
#include <vector>
#include <android/log.h>
#include <cerrno>
#include <cstdlib>

#define LOG_TAG "FridaCheck"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)

#if defined(__aarch64__) // ARM64架构
// ARM64 syscall号(Android)
#define SYS_OPEN    56
#define SYS_READ    63
#define SYS_CLOSE   57

// SVC指令触发sys_open(内核态打开文件)
static int syscall_open(const char* path, int flags, mode_t mode) {
    int fd;
    __asm__ volatile (
        "mov x8, %1\n"    // x8 = 系统调用号
        "mov x0, %2\n"    // x0 = 文件路径
        "mov x1, %3\n"    // x1 = 打开标志
        "mov x2, %4\n"    // x2 = 文件权限
        "svc #0\n"        // 触发SVC指令,进入内核态
        "mov %0, x0\n"    // 保存返回值(文件描述符)
        : "=r"(fd)
        : "i"(SYS_OPEN), "r"(path), "r"(flags), "r"(mode)
        : "x0", "x1", "x2", "x8", "memory"
    );
    return fd;
}

// SVC指令触发sys_read(内核态读取文件内容)
static ssize_t syscall_read(int fd, void* buf, size_t count) {
    ssize_t ret;
    __asm__ volatile (
        "mov x8, %1\n"
        "mov x0, %2\n"
        "mov x1, %3\n"
        "mov x2, %4\n"
        "svc #0\n"
        "mov %0, x0\n"
        : "=r"(ret)
        : "i"(SYS_READ), "r"(fd), "r"(buf), "r"(count)
        : "x0", "x1", "x2", "x8", "memory"
    );
    return ret;
}

// SVC指令触发sys_close(内核态关闭文件)
static int syscall_close(int fd) {
    int ret;
    __asm__ volatile (
        "mov x8, %1\n"
        "mov x0, %2\n"
        "svc #0\n"
        "mov %0, x0\n"
        : "=r"(ret)
        : "i"(SYS_CLOSE), "r"(fd)
        : "x0", "x8", "memory"
    );
    return ret;
}

#elif defined(__x86_64__) // x86_64架构(模拟器/Android x86_64)
// x86_64 syscall号(Linux/Android)
#define SYS_OPEN    2
#define SYS_READ    0
#define SYS_CLOSE   3

// x86_64 syscall_open实现
static int syscall_open(const char* path, int flags, mode_t mode) {
    long fd;
    __asm__ volatile (
        "movq %1, %%rax\n"  // rax = 系统调用号
        "movq %2, %%rdi\n"  // rdi = 文件路径
        "movq %3, %%rsi\n"  // rsi = 打开标志
        "movq %4, %%rdx\n"  // rdx = 文件权限
        "syscall\n"         // 触发syscall
        "movq %%rax, %0\n"  // 保存返回值
        : "=r"(fd)
        : "i"(SYS_OPEN), "r"((long)path), "r"((long)flags), "r"((long)mode)
        : "rax", "rdi", "rsi", "rdx", "rcx", "r11", "memory"
    );
    // 检查错误返回值
    if (fd >= -4095 && fd < 0) {
        errno = -fd;
        return -1;
    }
    return (int)fd;
}

// x86_64 syscall_read实现
static ssize_t syscall_read(int fd, void* buf, size_t count) {
    long ret;
    __asm__ volatile (
        "movq %1, %%rax\n"  // rax = 系统调用号
        "movq %2, %%rdi\n"  // rdi = 文件描述符
        "movq %3, %%rsi\n"  // rsi = 缓冲区指针
        "movq %4, %%rdx\n"  // rdx = 读取长度
        "syscall\n"         // 触发syscall
        "movq %%rax, %0\n"  // 保存返回值
        : "=r"(ret)
        : "i"(SYS_READ), "r"((long)fd), "r"(buf), "r"((long)count)
        : "rax", "rdi", "rsi", "rdx", "rcx", "r11", "memory"
    );
    // 检查错误返回值
    if (ret >= -4095 && ret < 0) {
        errno = -ret;
        return -1;
    }
    return (ssize_t)ret;
}

// x86_64 syscall_close实现
static int syscall_close(int fd) {
    long ret;
    __asm__ volatile (
        "movq %1, %%rax\n"  // rax = 系统调用号
        "movq %2, %%rdi\n"  // rdi = 文件描述符
        "syscall\n"         // 触发syscall
        "movq %%rax, %0\n"  // 保存返回值
        : "=r"(ret)
        : "i"(SYS_CLOSE), "r"((long)fd)
        : "rax", "rdi", "rcx", "r11", "memory"
    );
    // 检查错误返回值
    if (ret >= -4095 && ret < 0) {
        errno = -ret;
        return -1;
    }
    return (int)ret;
}


#else
// 其他架构默认使用libc函数
#include <cstdio>
#define syscall_open(path, flags, mode) open(path, flags, mode)
#define syscall_read(fd, buf, count) read(fd, buf, count)
#define syscall_close(fd) close(fd)
#endif

// syscall读取(重试应对EAGAIN)
static bool read_file_by_syscall(const std::string& file_path, std::string& content) {
    char buf[4096] = {0};
    int fd = syscall_open(file_path.c_str(), O_RDONLY | O_NONBLOCK, 0); // 增加O_NONBLOCK避免阻塞
    if (fd < 0) {
        LOGE("Open file failed: %s, errno=%d (%s)", file_path.c_str(), errno, strerror(errno));
        return false;
    }

    ssize_t read_len = -1;
    int retry = 3; // 重试3次应对EAGAIN
    while (retry-- > 0 && read_len < 0) {
        read_len = syscall_read(fd, buf, sizeof(buf) - 1);
        if (read_len < 0) {
            if (errno == EAGAIN || errno == EINTR) {
                LOGW("Read retry (%d left) for %s, errno=%d (%s)", retry, file_path.c_str(), errno, strerror(errno));
                usleep(1000); // 休眠1ms重试
                continue;
            }
            break; // 其他错误直接退出
        }
    }

    if (read_len <= 0) {
        LOGE("Read file failed: %s, read_len=%zd, errno=%d (%s)", file_path.c_str(), read_len, errno, strerror(errno));
        syscall_close(fd);
        return false;
    }

    content = std::string(buf, read_len);
    syscall_close(fd);
    LOGI("Read file success: %s, len=%zd", file_path.c_str(), read_len);
    return true;
}

static bool checkFrida() {
    DIR* taskDir;
    struct dirent* entry;

    // Frida特征关键词
    static const std::vector<std::string> frida_keywords = {
            "gmain", "gdbus", "frida", "gum"
    };

    // 获取当前进程ID并构建task目录路径
    pid_t pid = getpid();
    std::string taskPath = "/proc/" + std::to_string(pid) + "/task";

    LOGI("Checking for Frida, PID: %d", pid);

    taskDir = opendir(taskPath.c_str());
    if (!taskDir) {
        LOGE("Open task directory failed: %s, errno=%d (%s)", taskPath.c_str(), errno, strerror(errno));
        return false;
    }

    // 遍历task目录中的每个线程
    while ((entry = readdir(taskDir)) != nullptr) {
        // 跳过.和..目录项
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        LOGI("Processing thread: %s", entry->d_name);

        // 构建线程status文件路径
        std::string statusPath = taskPath + "/" + entry->d_name + "/status";
        std::string file_content;

        // 用syscall读取文件内容
        if (!read_file_by_syscall(statusPath, file_content)) {
            LOGW("Failed to read status file for thread: %s", entry->d_name);
            continue;
        }

        std::istringstream stream(file_content);
        std::string line;
        while (std::getline(stream, line)) {
            // 查找Name字段
            if (line.find("Name:") != std::string::npos) {
                size_t colonPos = line.find(':');
                if (colonPos != std::string::npos) {
                    std::string name = line.substr(colonPos + 1);
                    // 去除前导空格
                    name.erase(0, name.find_first_not_of(" \t"));

                    LOGI("Thread name: %s", name.c_str());

                    // 匹配Frida特征
                    for (const auto& kw : frida_keywords) {
                        if (name.find(kw) != std::string::npos) {
                            closedir(taskDir);
                            LOGI("Detected Frida in thread: %s (keyword: %s)", entry->d_name, kw.c_str());
                            return true;
                        }
                    }
                }
                break;
            }
        }
    }

    closedir(taskDir);
    LOGI("No Frida detected");
    return false;
}

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_securitycheck_MainActivity_checkSecurity(JNIEnv *env, jobject thiz) {
    bool fridaDetected = checkFrida();

    if (fridaDetected) {
        return env->NewStringUTF("检测到frida");
    } else {
        return env->NewStringUTF("未检测到frida");
    }
}

2.1 核心防绕特点:绕过用户态函数 hook

源码中定义了 syscall_opensyscall_readsyscall_close 三个函数,内部通过汇编指令直接触发系统调用:

  • 在 ARM64 架构中,用 svc #0 指令触发,系统调用号存在 x8 寄存器(比如打开文件的调用号是 56);
  • 在 x86_64 架构中,用 syscall 指令触发,系统调用号存在 rax 寄存器(比如打开文件的调用号是 2)。

这种方式完全避开了 libc.so 中的 open、read 等函数,因此之前通过 hook 这些函数来篡改文件路径或内容的方法(比如实战篇15)就失效了------程序根本不经过这些函数,自然无法被 hook 拦截。

2.2 校验流程回顾

检测逻辑依然是遍历当前进程的线程,读取每个线程的 /proc/[pid]/task/[tid]/status 文件,检查「Name」字段是否包含 Frida 特征关键词(gmain、frida、gum 等)。但由于文件读取通过底层指令实现,常规的函数 hook 无法干扰。

3. Hook 绕过:直接拦截系统调用指令

3.1 为何之前的方法失效?

如前所述,由于程序通过 syscall 指令(x86_64)直接调用内核,而非 libc 函数,因此用之前章节 hook libc.so 的 open/fopen 等函数无法拦截文件操作。下图可以看到,即使 hook 了 fopen 也无法影响程序的文件读取:

3.2 绕过思路:识别并 hook 系统调用指令

既然程序用 syscall 指令触发系统调用,那我们就直接针对这些指令进行 hook。核心步骤是:

  1. 找到目标模块(本次是 libsecuritycheck.so)中的可执行代码段;
  2. 在代码段中搜索 syscall 相关的指令特征(比如 x86_64 中打开文件的 mov rax, 2 后跟 syscall);
  3. 对找到的 syscall 指令进行 hook,在执行前修改参数(比如把要读取的文件路径改成无关路径,如 /dev/null)。

3.3 完整脚本与流程分析

以下是针对 x86_64 模拟器的绕过脚本,每一步都有详细注释:

javascript 复制代码
function hookSyscall() {
  // 定位目标模块(libsecuritycheck.so,且位于/data/app路径下,确保是目标应用的模块)
  var targetModule = null;
  var modules = Process.enumerateModules();
  for (var i = 0; i < modules.length; i++) {
    var module = modules[i];
    if (module.name === "libsecuritycheck.so" && module.path.indexOf("/data/app/") !== -1) {
      targetModule = module;
      break;
    }
  }

  if (!targetModule) {
    console.log("[-] 未找到目标模块 libsecuritycheck.so");
    return;
  }
  console.log("[+] 找到目标模块: " + targetModule.name + ",基址: " + targetModule.base);

  // 筛选模块中的可执行段(只有可执行段才会包含指令)
  var execRegions = Process.enumerateRanges('r-x').filter(function (range) {
    return range.base >= targetModule.base && range.base < targetModule.base.add(targetModule.size);
  });
  console.log("[+] 找到可执行段数量: " + execRegions.length);

  // 搜索 open 系统调用的指令特征(x86_64中打开文件的调用号是2,对应指令是mov rax, 2)
  // mov rax, 2 的机器码是 48 c7 c0 02 00 00 00
  var openSyscallPattern = "48 c7 c0 02 00 00 00";
  var hookedCount = 0;

  // 遍历每个可执行段,扫描指令特征
  execRegions.forEach(function (region) {
    console.log("[+] 扫描可执行段: " + region.base + "(大小: 0x" + region.size.toString(16) + ")");
    try {
      // 找到所有匹配的指令位置(即mov rax, 2的位置)
      var matches = Memory.scanSync(region.base, region.size, openSyscallPattern);
      
      matches.forEach(function (match) {
        var movRaxAddr = match.address;
        console.log("[+] 找到 open 系统调用的准备指令: " + movRaxAddr);

        // 在mov rax, 2后面找syscall指令(通常在几条指令内)
        var currentAddr = movRaxAddr;
        var maxSearch = 5; // 最多往后查5条指令
        var found = false;
        
        for (var i = 0; i < maxSearch; i++) {
          try {
            var instruction = Instruction.parse(currentAddr);
            // 找到syscall指令后,对其进行hook
            if (instruction.toString().indexOf('syscall') !== -1) {
              Interceptor.attach(currentAddr, {
                onEnter: function (args) {
                  // 确认当前是open系统调用(rax寄存器的值应为2)
                  if (this.context.rax.toInt32() !== 2) {
                    return;
                  }
                  // 修改文件路径参数(rdi寄存器存储第一个参数,即路径)
                  var fakePath = Memory.allocUtf8String("/dev/null"); // 替换为无效路径
                  this.context.rdi = fakePath;
                  console.log("[+] 已将文件路径修改为 /dev/null");
                }
              });
              console.log("[+] 成功hook syscall指令: " + currentAddr);
              hookedCount++;
              found = true;
              break;
            }
            // 继续查找下一条指令
            currentAddr = currentAddr.add(instruction.size);
          } catch (e) {
            console.log("[-] 解析指令失败: " + e.message);
            break;
          }
        }
        if (!found) {
          console.log("[-] 在mov rax, 2附近未找到syscall指令");
        }
      });
    } catch (e) {
      console.log("[-] 扫描指令失败: " + e.message);
    }
  });

  console.log("[+] 共hook " + hookedCount + "个open系统调用指令,完成");
}

Java.perform(hookSyscall);

3.4 执行脚本与效果

操作步骤

  1. 先打开目标应用;
  2. 执行 frida 命令注入脚本:
shell 复制代码
frida -U -n FridaAPK -l hook.js

绕过效果

脚本成功 hook 了程序中的 syscall 指令,并将文件路径修改为 /dev/null(空设备),导致程序无法读取到真实的线程状态文件,从而无法检测到 Frida。效果如下:

这里注意,要退出 frida 控制台直接通过关闭应用,否则如果使用quit命令会卡。

4. 章节总结

4.1 指令校验的意义

通过 SVC(ARM64)或 syscall(x86_64)等底层指令直接触发系统调用,是一种常见的反调试/反 hook 手段。它绕开了用户态的库函数,让常规的函数 hook 方法失效,从而提升了检测逻辑的安全性。

4.2 绕过核心思路

无论架构如何,绕过的核心是识别并拦截系统调用指令

  1. 找到目标模块中触发系统调用的指令(如 SVC 或 syscall);
  2. 分析指令对应的系统调用号(如 open 操作(openat)在 ARM64 中是 56,x86_64 中是 2);
  3. hook 这些指令,在执行前修改关键参数(如文件路径、读取长度等),让检测逻辑获取到虚假信息。

4.3 不同架构的适配

本文演示了 x86_64 架构的绕过方法,实际在 ARM64 真机上,需调整以下几点:

  • 搜索的指令特征改为 SVC 相关(如 svc #0 的机器码);
  • 系统调用号对应 ARM64 的定义(如 open操作(openat) 是 56);
  • 寄存器参数调整为 ARM64 的规范(如路径存在 x0 寄存器,调用号存在 x8 寄存器)。

原理完全一致,只需根据架构的指令集和寄存器规则修改脚本即可。

相关推荐
阿巴斯甜6 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker7 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95278 小时前
Andorid Google 登录接入文档
android
黄林晴9 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android