【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 寄存器)。

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

相关推荐
码农搬砖_20202 小时前
【一站式学会compose】 Android UI体系之 Text的使用和介绍
android·compose
冬奇Lab2 小时前
Android稳定性基础:系统架构与关键机制
android·系统架构
李坤林2 小时前
Android ION Memory Manager 深度分析
android
Digitally2 小时前
iPhone 无法向安卓设备发送图片:轻松解决
android·ios·iphone
Scholar With Saber2 小时前
kali Linux安装教程,ISO镜像安装(物理机,虚拟机皆可)kali安装2025最新,0基础可用,保姆级图文
linux·运维·网络安全
Digitally2 小时前
如何从 Infinix 手机中删除联系人
android
jingling5552 小时前
uni-app 安卓端完美接入卫星地图:解决图层缺失与层级过高难题
android·前端·javascript·uni-app
草莓熊Lotso2 小时前
C++ 智能指针完全指南:原理、用法与避坑实战(从 RAII 到循环引用)
android·java·开发语言·c++·人工智能·经验分享·qt
Whoami!3 小时前
❾⁄₇ ⟦ OSCP ⬖ 研记 ⟧ 防病毒软件规避 ➱ 本地进程内存注入实践(上)
网络安全·信息安全