【Frida Android】实战篇17:Frida检测与绕过——基于inline hook的攻防实战

文章目录

  • [1. inline hook 机器码](#1. inline hook 机器码)
    • [1.1 什么是Frida中的inline hook](#1.1 什么是Frida中的inline hook)
    • [1.2 inline hook的检测原理](#1.2 inline hook的检测原理)
  • [2. 检测源码(基于上一章的扩展)](#2. 检测源码(基于上一章的扩展))
    • [2.1 核心检测函数](#2.1 核心检测函数)
  • [3. Hook 思路(绕过inline hook检测)](#3. Hook 思路(绕过inline hook检测))
    • [3.1 分析基础:为何需要Hook dlsym?](#3.1 分析基础:为何需要Hook dlsym?)
    • [3.2 方法一:动态恢复原始机器码](#3.2 方法一:动态恢复原始机器码)
    • [3.3 方法二:伪造函数地址](#3.3 方法二:伪造函数地址)
    • [3.4 两种方法的核心区别](#3.4 两种方法的核心区别)
  • [4. 本章总结](#4. 本章总结)
    • [4.1 inline hook检测与绕过核心](#4.1 inline hook检测与绕过核心)
    • [4.2 动态调整](#4.2 动态调整)

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

1. inline hook 机器码

1.1 什么是Frida中的inline hook

在Frida的动态调试中,inline hook是Native层钩子(如Interceptor.attach)实现的核心机制。其本质是通过修改目标函数的开头指令,将函数的执行流程劫持到Frida的钩子逻辑中,具体过程如下:

  1. 指令替换 :Frida会在目标函数(如系统库libc.so中的strstrstrcmp)的开头,替换前N字节指令为跳转指令;
  2. 执行流劫持 :当目标函数被调用时,跳转指令会将执行流程导向Frida的钩子函数(onEnter/onLeave),从而实现参数监控、修改或逻辑篡改;
  3. 原函数恢复:钩子函数执行完成后,Frida通过"跳板(Trampoline)"恢复被替换的原始指令,并跳回原函数继续执行剩余逻辑。

1.2 inline hook的检测原理

由于Frida的inline hook必然会篡改函数开头的指令,因此检测逻辑的核心是:检查目标函数开头的机器码是否包含异常跳转指令

当应用检测到函数开头存在Frida特征的跳转指令时,即可判定该函数被inline hook,进而推断可能存在Frida注入。

2. 检测源码(基于上一章的扩展)

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

链接: https://pan.baidu.com/s/1dpbagjFPJVouisb2_AW3eg?pwd=b4e6

提取码: b4e6

上一章的检测逻辑主要通过扫描进程线程的status文件,识别与Frida相关的线程名(如fridagmain等)。本章在此基础上,新增了对inline hook的检测,核心代码逻辑如下:

2.1 核心检测函数

  • isHooked函数 :根据架构(ARM64/x86_64)检查函数开头的机器码是否包含Frida特征的跳转指令(如ARM64的0x140000000x94000000等跳转指令掩码,x86_64的0xE9(jmp)、0xFF(间接跳转)等);
  • checkInlineHooks函数 :通过dlsym获取strstrstrcmp(高频被Hook的系统函数)的地址,调用isHooked检查这两个函数是否被篡改;
  • checkFrida函数 :保留上一章的线程名检测逻辑,通过扫描/proc/[pid]/task/[tid]/status中的Name字段识别Frida相关线程;
  • Java_com_example_securitycheck_MainActivity_checkSecurity函数 :综合checkFrida(线程名检测)和checkInlineHooks(inline hook检测)的结果,返回最终检测结论。
c++ 复制代码
#include <jni.h>
#include <string>
#include <fstream>
#include <sstream>
#include <dirent.h>
#include <unistd.h>
#include <cstring>
#include <dlfcn.h>

#if defined(__aarch64__)
bool isHooked(void* func) {
    if (!func) {
        return false;
    }

    uint32_t* instructions = (uint32_t*)func;
    uint32_t instruction = *instructions;

    if ((instruction & 0xFC000000) == 0x14000000) {
        return true;
    }

    if ((instruction & 0xFC000000) == 0x94000000) {
        return true;
    }

    if ((instruction & 0xFFFF001F) == 0xD61F0000) {
        return true;
    }

    return false;
}
#elif defined(__x86_64__)
bool isHooked(void* func) {
    if (!func) {
        return false;
    }

    unsigned char* bytes = (unsigned char*)func;
    unsigned char buffer[2] = {0};
    memcpy(buffer, bytes, sizeof(buffer));

    if (buffer[0] == 0xE9) {
        return true;
    }

    if (buffer[0] == 0xFF) {
        unsigned char modrm = buffer[1];
        if (((modrm >> 3) & 0x7) == 0x4) {
            return true;
        }
    }

    return false;
}
#else
// 默认实现,防止未定义错误
bool isHooked(void* func) {
    (void)func;
    return false;
}
#endif

static bool checkInlineHooks() {
    void* strstr_addr = dlsym(RTLD_DEFAULT, "strstr");
    void* strcmp_addr = dlsym(RTLD_DEFAULT, "strcmp");

    if (!strstr_addr && !strcmp_addr) {
        return false;
    }

    bool strstr_hooked = strstr_addr != nullptr && isHooked(strstr_addr);
    bool strcmp_hooked = strcmp_addr != nullptr && isHooked(strcmp_addr);

    return strstr_hooked || strcmp_hooked;
}

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

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

    taskDir = opendir(taskPath.c_str());
    if (!taskDir) {
        return false;
    }

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

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

        if (statusFile.is_open()) {
            std::string line;
            // 读取status文件中的每一行,查找Name字段
            while (std::getline(statusFile, line)) {
                if (line.find("Name:") != std::string::npos) {
                    // 提取Name字段的值
                    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"));

                        // 使用strcmp进行精确匹配
                        if (strcmp(name.c_str(), "gmain") == 0 ||
                            strcmp(name.c_str(), "gdbus") == 0 ||
                            strcmp(name.c_str(), "frida") == 0 ||
                            strcmp(name.c_str(), "gum-js-loop") == 0) {
                            statusFile.close();
                            closedir(taskDir);
                            return true;
                        }

                        // 使用strstr进行子字符串检测
                        if (strstr(name.c_str(), "gmain") != nullptr ||
                            strstr(name.c_str(), "gdbus") != nullptr ||
                            strstr(name.c_str(), "frida") != nullptr ||
                            strstr(name.c_str(), "gum-js") != nullptr) {
                            statusFile.close();
                            closedir(taskDir);
                            return true;
                        }
                    }
                    break; // 只需要检查Name行
                }
            }
            statusFile.close();
        }
    }

    closedir(taskDir);
    return false;
}

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

    if (fridaDetected && inlineHookDetected) {
        return env->NewStringUTF("检测到status、inline hook");
    } else if (fridaDetected) {
        return env->NewStringUTF("检测到status");
    } else if (inlineHookDetected) {
        return env->NewStringUTF("检测到inline hook");
    } else {
        return env->NewStringUTF("未检测到frida");
    }
}

若仅使用上一章的线程名绕过脚本,会被本章新增的inline hook检测拦截,效果如下:

3. Hook 思路(绕过inline hook检测)

3.1 分析基础:为何需要Hook dlsym?

通过IDA反编译后对比源码分析可知(IDA的伪代码可以使用AI辅助分析),应用通过dlsym(RTLD_DEFAULT, "strstr")dlsym(RTLD_DEFAULT, "strcmp")获取函数地址,再对地址处的机器码进行校验。因此,绕过的核心是干预dlsym返回的地址或该地址对应的机器码,使检测逻辑判断为"未被Hook"。

3.2 方法一:动态恢复原始机器码

核心逻辑
  1. 预先保存原始机器码 :在对strstrstrcmp应用Hook前,保存这两个函数开头的原始字节(根据架构保存8字节或16字节);
  2. 正常Hook目标函数 :对strstrstrcmp应用常规Hook(如过滤Frida相关关键字);
  3. 拦截dlsym调用并恢复 :当dlsym被调用以获取strstrstrcmp地址时,立即将预先保存的原始机器码写回函数开头,使检测逻辑读取到未被篡改的指令;检测完成后,Frida的钩子仍能正常工作(因Frida会自动维护钩子状态)。
实现脚本
javascript 复制代码
const FRIDA_KEYWORDS = ['frida', 'gmain', 'gdbus', 'gum-js-loop'];
const STRSTR_KEYWORDS = ['frida', 'gum-js', 'gmain', 'gdbus', 'tmp'];

// 存储原始函数机器码
const originalCode = new Map();

function saveOriginalCode() {
  try {
    const libc = Module.load('libc.so');
    if (!libc) {
      console.log('[!] 未找到 libc.so');
      return;
    }

    const byteCount = Process.arch === 'arm64' ? 8 : 16;
    console.log(`[+] 当前架构${Process.arch},保存${byteCount}字节机器码`);

    const strstrAddr = libc.getExportByName('strstr');
    const strcmpAddr = libc.getExportByName('strcmp');

    // 保存原始机器码
    if (strstrAddr) {
      originalCode.set('strstr', {
        address: strstrAddr,
        bytes: strstrAddr.readByteArray(byteCount)
      });
      console.log('[+] 保存strstr原始机器码');
    }

    if (strcmpAddr) {
      originalCode.set('strcmp', {
        address: strcmpAddr,
        bytes: strcmpAddr.readByteArray(byteCount)
      });
      console.log('[+] 保存strcmp原始机器码');
    }
  } catch (e) {
    console.log('[!] 保存原始代码出错: ' + e.message);
  }
}

// hook strstr/strcmp
function hookStr() {
  try {
    const libc = Module.load('libc.so');
    if (!libc) return;

    const strstrAddr = libc.getExportByName('strstr');
    const strcmpAddr = libc.getExportByName('strcmp');

    if (strstrAddr) {
      Interceptor.attach(strstrAddr, {
        onEnter: function (args) {
          this.haystack = args[0].readCString();
          this.needle = args[1].readCString();
        },
        onLeave: function (retval) {
          if (STRSTR_KEYWORDS.includes(this.needle)) {
            if (retval !== NULL) {
              retval.replace(NULL);
            }
          }
        }
      });
      console.log('strstr hook applied.');
    }

    if (strcmpAddr) {
      Interceptor.attach(strcmpAddr, {
        onEnter: function (args) {
          this.str1 = args[0].readCString();
          this.str2 = args[1].readCString();
        },
        onLeave: function (retval) {
          if (FRIDA_KEYWORDS.includes(this.str2)) {
            if (this.str1?.includes(this.str2)) {
              retval.replace(ptr(1));
            }
          }
        }
      });
      console.log('strcmp hook applied.');
    }
  } catch (e) {
    console.log('[!] 应用hook出错: ' + e.message);
  }
}

// Hook dlsym函数
function hookDlsym() {
  try {
    const libc = Module.load('libc.so');
    if (!libc) {
      console.log('[!] 未找到 libc.so');
      return;
    }

    const dlsymAddr = libc.getExportByName('dlsym');
    if (!dlsymAddr) {
      console.log('[!] 未找到 dlsym 函数');
      return;
    }

    Interceptor.attach(dlsymAddr, {
      onEnter: function (args) {
        this.symbolName = args[1].readCString();
        console.log(`[+] dlsym 被调用,查找符号: ${this.symbolName}`);
      },
      onLeave: function (retval) {
        // 检查是否在查找我们的目标函数
        if (this.symbolName === 'strstr' || this.symbolName === 'strcmp') {

          // 使用writeByteArray还原函数原始的机器码
          const funcInfo = originalCode.get(this.symbolName);
          funcInfo.address.writeByteArray(funcInfo.bytes);
          console.log(`[+] 检测函数请求 ${this.symbolName},临时恢复原始机器码`);
        }
      }
    });

    console.log('[+] dlsym hook applied');
  } catch (e) {
    console.log('[!] Hook dlsym出错: ' + e.message);
  }
}

Java.perform(() => {
  try {
    // 保存原始代码
    saveOriginalCode();

    // Hook dlsym
    hookDlsym();

    // 初始应用hook
    hookStr();

    console.log('[+] 初始化完成');
  } catch (e) {
    console.log('[!] 初始化出错: ' + e.message);
  }
});

执行效果:成功绕过线程名和inline hook检测。

3.3 方法二:伪造函数地址

核心逻辑
  1. 保存原始机器码 :同方法一,记录strstrstrcmp开头的原始字节;
  2. 创建伪造函数:在内存中分配新的可执行区域,将原始机器码复制到该区域,形成与原始函数开头指令一致的"伪造函数";
  3. 正常Hook目标函数 :对真实的strstrstrcmp应用常规Hook;
  4. 拦截dlsym调用并返回伪造地址 :当dlsym被调用以获取strstrstrcmp地址时,不返回真实地址,而是返回伪造函数的地址。由于伪造函数的机器码未被Hook,检测逻辑会判定为"未被篡改"。
实现脚本
javascript 复制代码
const FRIDA_KEYWORDS = ['frida', 'gmain', 'gdbus', 'gum-js-loop'];
const STRSTR_KEYWORDS = ['frida', 'gum-js', 'gmain', 'gdbus', 'tmp'];

// 存储原始函数地址和伪造函数地址
const originalCode = new Map();
const fakeFunctions = new Map();
let hooksAttached = false;

function saveOriginalCode() {
  try {
    const libc = Module.load('libc.so');
    if (!libc) {
      console.log('[!] 未找到 libc.so');
      return;
    }

    const strstrAddr = libc.getExportByName('strstr');
    const strcmpAddr = libc.getExportByName('strcmp');

    // 保存对应架构的原始字节(ARM64:8字节,X86_64:16字节)
    if (strstrAddr) {
      originalCode.set('strstr', {
        address: strstrAddr,
        bytes: strstrAddr.readByteArray(Process.arch === 'arm64' ? 8 : 16)
      });
      console.log('[+] 保存strstr原始地址');
    }

    if (strcmpAddr) {
      originalCode.set('strcmp', {
        address: strcmpAddr,
        bytes: strcmpAddr.readByteArray(Process.arch === 'arm64' ? 8 : 16)
      });
      console.log('[+] 保存strcmp原始地址');
    }
  } catch (e) {
    console.log('[!] 保存原始地址出错: ' + e.message);
  }
}

// 创建伪造的函数(复制原始函数地址)
function createFakeFunction(funcName) {
  try {
    const libc = Module.load('libc.so');
    if (!libc) return null;

    let originalAddr = null;
    if (funcName === 'strstr') {
      originalAddr = libc.getExportByName('strstr');
    } else if (funcName === 'strcmp') {
      originalAddr = libc.getExportByName('strcmp');
    }

    if (!originalAddr) return null;

    // 分配内存来存储伪造函数
    const fakeFuncMemory = Memory.alloc(Process.pageSize);
    if (!fakeFuncMemory) return null;

    // 复制原始函数代码到伪造函数
    const funcBytes = originalCode.get(funcName).bytes;
    fakeFuncMemory.writeByteArray(funcBytes);

    // 设置内存保护为可执行
    Memory.protect(fakeFuncMemory, Process.pageSize, 'rwx');

    console.log(`[+] 创建伪造的${funcName}地址: ${fakeFuncMemory}`);
    return fakeFuncMemory;
  } catch (e) {
    console.log(`[!] 创建伪造${funcName}地址出错: ` + e.message);
    return null;
  }
}

// hook strstr/strcmp
function hookStr() {
  try {
    const libc = Module.load('libc.so');
    if (!libc) return;

    const strstrAddr = libc.getExportByName('strstr');
    const strcmpAddr = libc.getExportByName('strcmp');

    if (strstrAddr) {
      Interceptor.attach(strstrAddr, {
        onEnter: function (args) {
          this.haystack = args[0].readCString();
          this.needle = args[1].readCString();
        },
        onLeave: function (retval) {
          if (STRSTR_KEYWORDS.includes(this.needle)) {
            if (retval !== NULL) {
              retval.replace(NULL);
            }
          }
        }
      });
      console.log('strstr hook finished.');
    }

    if (strcmpAddr) {
      Interceptor.attach(strcmpAddr, {
        onEnter: function (args) {
          this.str1 = args[0].readCString();
          this.str2 = args[1].readCString();
        },
        onLeave: function (retval) {
          if (FRIDA_KEYWORDS.includes(this.str2)) {
            if (this.str1?.includes(this.str2)) {
              retval.replace(ptr(1));
            }
          }
        }
      });
      console.log('strcmp hook finished.');
    }

    hooksAttached = true;
  } catch (e) {
    console.log('[!] hook str 出错: ' + e.message);
  }
}

// Hook dlsym函数
function hookDlsym() {
  try {
    const libc = Module.load('libc.so');
    if (!libc) {
      console.log('[!] 未找到 libc.so');
      return;
    }

    const dlsymAddr = libc.getExportByName('dlsym');
    if (!dlsymAddr) {
      console.log('[!] 未找到 dlsym 函数');
      return;
    }

    Interceptor.attach(dlsymAddr, {
      onEnter: function (args) {
        // args[0] = handle, args[1] = symbol name
        this.symbolName = args[1].readCString();
        console.log(`[+] dlsym 被调用,查找符号: ${this.symbolName}`);
      },
      onLeave: function (retval) {
        // 检查是否在查找我们的目标函数
        if (this.symbolName === 'strstr' || this.symbolName === 'strcmp') {
          console.log(`[+] 检测函数请求 ${this.symbolName},返回伪造地址`);

          // 返回伪造函数地址
          if (fakeFunctions.has(this.symbolName)) {
            retval.replace(fakeFunctions.get(this.symbolName));
          }
        }
      }
    });

    console.log('[+] dlsym hook finished.');
  } catch (e) {
    console.log('[!] Hook dlsym出错: ' + e.message);
  }
}

Java.perform(() => {
  try {
    // 保存函数原始地址
    saveOriginalCode();

    // 创建伪造函数地址
    const fakeStrstr = createFakeFunction('strstr');
    const fakeStrcmp = createFakeFunction('strcmp');
    if (fakeStrstr) fakeFunctions.set('strstr', fakeStrstr);
    if (fakeStrcmp) fakeFunctions.set('strcmp', fakeStrcmp);

    // Hook dlsym
    hookDlsym();

    // 初始应用hook
    hookStr();

    console.log('[+] 初始化完成');
  } catch (e) {
    console.log('[!] 初始化出错: ' + e.message);
  }
});

执行效果:同样可绕过inline hook检测。

3.4 两种方法的核心区别

维度 方法一(动态恢复原始机器码) 方法二(伪造函数地址)
地址真实性 使用函数真实地址,临时恢复原始机器码 使用全新的伪造地址,与真实地址无关
适用场景 检测逻辑依赖函数真实地址(如后续调用需真实函数) 检测逻辑仅校验地址处的机器码,不依赖后续函数调用
实现复杂度 较低(仅需保存和恢复字节) 较高(需分配可执行内存并复制指令)

4. 本章总结

4.1 inline hook检测与绕过核心

  • 检测方式 :通过检查函数开头的机器码是否包含Frida特征的跳转指令,判断函数是否被inline hook
  • 绕过思路:干预检测逻辑获取的函数地址或地址对应的机器码,使其看到"未被Hook"的状态。具体可通过动态恢复原始机器码(临时还原真实地址的指令)或提供伪造函数地址(返回未被篡改的指令副本)实现。

4.2 动态调整

实际场景中,应用的检测逻辑可能更复杂(如检测更多函数、校验完整指令而非仅开头等)。因此,需根据具体检测手段灵活调整绕过策略:

  • 若检测函数范围扩大(如新增memcpyptrace),需同步扩展Hook和恢复/伪造的目标;
  • 若检测逻辑不仅校验dlsym返回的地址,还直接读取函数内存(如通过固定地址),需额外Hook内存读取函数(如memcpy)以返回原始指令,但是总体思路是一模一样的;
  • 避免僵化使用脚本,需结合逆向分析(如IDA反编译)理解检测逻辑的细节,针对性设计绕过方案。
相关推荐
龙之叶3 小时前
Android如何通过adb命令push文件后在媒体库中显示
android·adb
Just_Paranoid4 小时前
【Settings】Android 设备信息相关参数的获取
android·5g·wifi·ip·sn·network
StarShip4 小时前
SystemServer类 与 system_server进程
android
画个太阳作晴天5 小时前
Android App 跟随系统自动切换白天/黑夜模式:车机项目实战经验分享
android·android studo
成都大菠萝5 小时前
2-2-2 快速掌握Kotlin-语言的接口默认实现
android
代码s贝多芬的音符5 小时前
android webview 打开相机 相册 图片上传。
android·webview·webview打开相机相册
游戏开发爱好者86 小时前
抓包工具有哪些?代理抓包、数据流抓包、拦截转发工具
android·ios·小程序·https·uni-app·iphone·webview
StarShip6 小时前
Android system_server进程介绍
android
StarShip6 小时前
Android Context 的 “上下文”
android