GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- app加固 之 frida检测

我们来系统地梳理和对抗 Frida 的检测方法。这是一场持续的“猫鼠游戏”,理解双方的战术是成功的关键。

Frida 检测与对抗方法全景图

本文将检测方法分为六大类,并为每一类提供具体的对抗方案。


第一类:进程环境检测

这类检测查找 Frida 在目标进程中留下的各种痕迹。

检测方法检测原理对抗方法
检查映射文件 扫描 /proc/self/maps 或 /proc/pid/maps,查找包含 fridagadget 字样的库文件(如 libfrida-agent.so)。 1. 重命名库文件:使用修改版的 Frida(如 strongR-frida),其编译的库文件名是随机的。
2. 内存抹除:加载后,从内存中抹去路径信息。
检查开放端口 Frida Server 默认监听 27047 端口。检测 netstat 输出或读取 /proc/net/tcp 等文件,查找该端口。 1. 修改默认端口:启动 frida-server 时使用 -l 0.0.0.0:8080 指定新端口。
2. 使用强R-Frida:其 randomize_port 功能可自动随机化端口。
检查线程名 Frida 会创建名为 gmaingdbusfrida-* 等特征的线程。遍历 /proc/self/task/pid/comm 可查看线程名。 1. Hook 线程创建函数:Hook pthread_create() 等函数,在创建时重命名敏感线程。
2. 使用强R-Frida:其 randomize_name 功能会重命名所有相关线程。
检查环境变量 检查进程的环境变量,寻找 Frida 相关特征。 Hook 环境变量读取函数:如 Hook getenv(),当应用尝试读取可疑变量名时返回空或伪造值。

对抗代码示例(Hook 线程名检测):

javascript
 
// 拦截 pthread_setname_np 函数,防止线程名被检测
var pthread_setname_np = Module.findExportByName(null, "pthread_setname_np");
if (pthread_setname_np) {
  Interceptor.attach(pthread_setname_np, {
    onEnter: function (args) {
      var name = args[1] ? args[1].readCString() : null;
      if (name && (name.includes("frida") || name.includes("gum-js-loop"))) {
        // 检测到要设置Frida相关线程名,将其替换为一个无害的名字
        var fakeName = "hwui_task";
        this.fakeName = Memory.allocUtf8String(fakeName);
        args[1] = this.fakeName;
      }
    }
  });
}

第二类:内存特征检测

这类检测直接扫描进程内存,寻找 Frida 的代码或数据特征。

检测方法检测原理对抗方法
字符串扫描 在内存中搜索已知的 Frida 字符串,如 "frida:rpc""LIBFRIDA""frida-agent" 等。 1. 内存涂鸦:加载 Frida 后,立即扫描内存并将其中的所有特征字符串覆盖为随机数据。
2. 使用修改版Frida:strongR-frida 在编译时已移除或混淆了大部分特征字符串。
代码模式扫描 扫描特定函数的字节序列(操作码)。例如,Frida 的 frida_agent_main 函数入口有一定模式。 代码自修改: Hook 内存扫描函数(如 memmemstrstr),当扫描到敏感区域时返回空。或者直接重写相关函数头几个字节。

对抗代码示例(内存字符串擦除):

javascript
 
// 在Frida脚本一开始就执行,抹去自身的字符串特征
Java.perform(function () {
  var ranges = Process.enumerateRanges('r--'); // 搜索只读内存
  var signatures = ["frida:rpc", "LIBFRIDA", "frida-agent", "gadget"];

  signatures.forEach(function (sig) {
    ranges.forEach(function (range) {
      try {
        Memory.scan(range.base, range.size, sig, {
          onMatch: function (address, size) {
            console.log("[!] 发现特征字符串: " + sig + " 在: " + address);
            // 申请内存读写权限并覆写
            Memory.protect(address, size, 'rwx');
            Memory.writeUtf8String(address, "HIDDEN_" + Math.random().toString(16).substr(2));
            console.log("[+] 已成功隐藏: " + sig);
          },
          onComplete: function () {}
        });
      } catch (e) {}
    });
  });
});

第三类:行为与性能检测

这类检测不直接寻找 Frida,而是观察其引入的“副作用”。

检测方法检测原理对抗方法
计时检测 在关键函数开始和结束处计时。由于 Frida 的 Hook 机制会引入额外开销,导致函数执行时间变长。 1. 优化脚本:避免在 Hook 的回调函数中执行复杂操作。
2. Hook 时间函数:拦截 gettimeofdayclock_gettime 等函数,返回伪造的时间,抹平时间差。
D-Bus 检测 Frida 使用 D-Bus 协议进行通信。检测系统上是否有异常的 D-Bus 通信。 使用强R-Frida:其 daemonize 模式可以改变通信方式,避免使用 D-Bus。
断点指令检测 检查关键函数头是否被 int3 等断点指令覆盖(这是传统调试器的行为)。Frida 不使用 int3,但此检测可对抗其他工具。 使用 Frida Stalker:Frida 的代码跟踪工具 Stalker 使用重新编译的方式,不会修改原函数代码,因此无法通过断点指令检测发现。

对抗代码示例(对抗计时检测):

javascript
 
// 拦截 clock_gettime 函数,让时间看起来“正常”
var clock_gettime = Module.findExportByName(null, "clock_gettime");
if (clock_gettime) {
  var original_clock_gettime = new NativeFunction(clock_gettime, 'int', ['int', 'pointer']);
  Interceptor.replace(clock_gettime, new NativeCallback(function (clk_id, tp) {
    var result = original_clock_gettime(clk_id, tp);
    // 如果这是某个被Hook的函数内部在计时,我们可以返回一个更快的时间
    // 但这需要精细控制,否则会破坏程序逻辑。通常更安全的做法是保持时间一致性而非单纯变快。
    return result;
  }, 'int', ['int', 'pointer']));
}

第四类:端口交互检测

这是最经典和有效的检测方法之一。

检测方法检测原理对抗方法
端口连接探测 尝试连接 127.0.0.1:27047。如果端口开放并返回类似 "REJECT" 的响应,则证明 Frida Server 在运行。 1. 修改端口:如前所述。
2. 使用强R-Frida:其 anti_anti_frida 功能会使端口对探测请求返回空白或自定义响应,欺骗检测代码。
非本地连接检测 检查端口是否绑定在 0.0.0.0(允许网络连接)。加固方案希望 Frida 只能本地访问。 绑定到本地:启动 frida-server 时使用 -l 127.0.0.1:27047 而非 -l 0.0.0.0:27047

对抗方案(使用 strongR-frida-android):
这是最根本的解决方案。该项目直接修改了 Frida 的源码:

  • 随机化端口:randomize_port 选项。

  • 重命名线程和库:randomize_name 选项。

  • 对抗端口探测:anti_anti_frida 选项,使其对扫描请求不可见。

  • 避免使用 D-Bus:daemonize 选项。

使用方法:

  1. 从 Releases 页面下载修改版的 frida-server

  2. 推送到设备:adb push frida-server /data/local/tmp/fs

  3. 使用配置参数启动:

    bash
     
    adb shell /data/local/tmp/fs -l 127.0.0.1:12345 --anti-anti-frida --daemonize
  4. 连接时指定端口:

    bash
     
    frida -H 127.0.0.1:12345 -f com.example.app --no-pause

第五类:Java 层检测

检测方法检测原理对抗方法
检查已加载类 遍历已加载的类,查找 agentfrida 等相关类名。 匿名加载:使用 Frida 的 Java.enumerateLoadedClasses 本身是隐蔽的,但如果你自己注入了一个类,需要注意命名。
检查Native库 使用 Debug.getNativeLibrarys() 等 Java API 获取已加载的Native库列表,查找 frida Hook Java API:拦截 Debug.getNativeLibrarys() 等函数,返回一个过滤后的列表,剔除 Frida 相关的库名。

对抗代码示例(Hook Java 层库检测):

javascript
 
Java.perform(function () {
  var Debug = Java.use("android.os.Debug");
  Debug.getNativeLibrarys.implementation = function () {
    var originalLibs = this.getNativeLibrarys(); // 调用原方法
    var filteredLibs = [];
    for (var i = 0; i < originalLibs.length; i++) {
      var libName = originalLibs[i];
      // 过滤掉包含frida特征的库名
      if (!libName.toLowerCase().includes("frida")) {
        filteredLibs.push(libName);
      }
    }
    return filteredLibs;
  };
});

第六类:综合与高阶检测

检测方法检测原理对抗方法
系统调用监控 使用内核模块监控 ptrace 等敏感系统调用,防止附加。Frida 基于 ptrace 使用非ptrace模式:Frida 的 Embedded 模式(如把 frida-gadget.so 打包进App)不需要 ptrace 附加。
双进程保护 父进程监控子进程的状态,一旦被调试或附加,就杀死子进程。 绕过反调试:首先需要解决父进程的反调试检测,或者同时注入父子两个进程。
异常信号处理 检查信号处理函数是否被修改。 恢复信号处理器:Frida 执行完操作后,可以手动恢复原始的信号处理函数。

总结与最佳实践

  1. 首选方案:使用修改版 Frida
    strongR-frida-android 是对抗检测的最强力武器,它从根源上解决了大部分问题(端口、线程、文件、字符串、D-Bus)。这应该是你的第一选择。

  2. 次级方案:主动脚本对抗
    如果无法使用修改版,就在你的 Frida 脚本最开始处,集成上述的内存擦除、线程隐藏、API Hook 等对抗代码,先发制人。

  3. 策略:组合拳
    单一的绕过方法很容易被新的检测手段破解。最有效的方法是组合使用多种技术,例如:strongR-frida + 自定义对抗脚本

  4. 保持更新
    这场博弈在不断升级。关注 Frida 和 strongR-frida 的更新,了解最新的检测和反制技术。

最终建议: 对于大多数情况,直接从 strongR-frida-android 下载最新版本的 frida-server,并使用其提供的参数启动,就能绕过 90% 的常见检测。

 

 

 

绕过思路:定位并删除反Hook检测so文件

核心原理

许多大型App(如得物、识货)的安全团队会将核心的安全检测逻辑(如Frida检测、双进程保护、环境检测)编写在C/C++代码中,并编译为 .so 动态链接库。这些so库在App启动时被加载。如果检测到异常(如Frida在运行),则会直接调用 exit() 或 abort() 等函数终止进程。

由于这些so库通常与主业务逻辑无关,仅仅是安全防护模块,因此直接删除它们往往不会影响App的正常运行功能,从而可以实现绕过。

操作步骤

  1. 解压APK:
    将 .apk 文件重命名为 .zip 并解压,或在命令行中使用 unzip app.apk。进入解压后的 lib 目录,这里存放着所有so文件,通常按CPU架构分类(arm64-v8aarmeabi-v7a 等)。

  2. 初步筛选:
    观察so文件的命名。安全团队开发的so文件命名可能与传统第三方库不同,可能包含诸如 securitysafeprotectshieldcheckdetectmonitorencrypt 等关键字。例如 libsecurity.solibmainprotect.so

  3. 暴力删除测试:
    这是一种简单粗暴但有效的方法。依次删除你认为可疑的so文件(或整个 lib 目录下的so文件),然后重新签名安装APK。如果App能正常启动且强制更新消失,说明删除成功。但这种方法效率低,且可能误删业务so导致功能异常。

  4. 动态加载监控(推荐方法):
    使用Frida来监控App运行时加载的所有so文件,从而精确找到在崩溃瞬间被加载的那个so,它就是罪魁祸首。


通用脚本:监控so文件加载

您提到的“通用脚本”其原理是Hook Android底层加载动态库的函数 android_dlopen_ext 或 dlopen。当App加载任何一个so文件时,我们都打印出其路径和名称。当App突然崩溃时,最后一个被加载的so就极有可能是触发检测的so。

以下是这个通用监控脚本的代码:

javascript
 
Java.perform(function () {
    // 拦截Android原生库加载函数,打印所有加载的SO库
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
    if (android_dlopen_ext) {
        Interceptor.attach(android_dlopen_ext, {
            onEnter: function (args) {
                var path = ptr(args[0]).readCString(); // 读取SO库的路径
                if (path) {
                    console.log("[*] android_dlopen_ext 加载: " + path);
                    // 如果路径中包含疑似安全库的关键字,高亮显示
                    if (path.indexOf("security") !== -1 || 
                        path.indexOf("protect") !== -1 ||
                        path.indexOf("safe") !== -1 ||
                        path.indexOf("check") !== -1) {
                        console.log("\x1b[31m[*] 警告:疑似安全SO库 -> " + path + "\x1b[0m"); // 红色输出
                    }
                }
            }
        });
    }

    // 同样拦截标准的dlopen函数
    var dlopen = Module.findExportByName(null, "dlopen");
    if (dlopen) {
        Interceptor.attach(dlopen, {
            onEnter: function (args) {
                var path = ptr(args[0]).readCString();
                if (path) {
                    console.log("[*] dlopen 加载: " + path);
                    if (path.indexOf("security") !== -1 || 
                        path.indexOf("protect") !== -1 ||
                        path.indexOf("safe") !== -1 ||
                        path.indexOf("check") !== -1) {
                        console.log("\x1b[31m[*] 警告:疑似安全SO库 -> " + path + "\x1b[0m");
                    }
                }
            }
        });
    }

    console.log("[*] SO库加载监控脚本已启动...");
});

使用方法:

  1. 将上述代码保存为 monitor_so.js

  2. 启动Frida-server。

  3. 使用 Spawn模式 启动App,以便捕获到最早期的so加载事件:

    bash
     
    frida -U -f com.zhihu.android -l monitor_so.js --no-pause
  4. 观察控制台输出。脚本会打印出App启动过程中加载的所有so文件。

  5. 当App突然崩溃或退出时,最后打印出的那几个so文件就是最大的嫌疑对象。特别是那些名称中包含安全关键词的so。

  6. 验证并删除:
    定位到可疑的so文件后(例如 lib/arm64-v8a/libsecurity_detect.so),回到解压的APK目录中,删除它。
    然后使用 apktool 重新打包APK,并使用 uber-apk-signer 等工具进行签名。
    安装修改后的APK,测试是否还能正常启动并绕过强制更新。

注意事项与局限性

  1. 业务相关性:此方法并非100%有效。有些App的核心业务逻辑也可能用C++编写,删除这些so会导致功能缺失或闪退。务必进行充分测试。

  2. 完整性校验:高强度的加固方案可能会对APK文件本身或其中的so文件进行完整性校验。如果检测到文件被删除,依然会触发退出机制。

  3. 对抗升级:安全团队可能会将检测逻辑更深地嵌入到业务so中,或者使用多个so进行交叉校验,使得简单的删除方法失效。

  4. 法律风险:此操作涉及修改他人的软件,务必在合法授权的范围内进行,仅用于安全学习和研究。

总结

您提供的“定位-删除”法是绕过Native层检测的一种非常实用的思路。结合Frida动态监控so加载,可以快速精准地找到目标文件。这套流程是APP逆向中对抗强保护方案的标准操作之一。

整个攻防对抗链可以简化为:
App集成防护so → 逆向者监控加载 → 定位并删除so → 重新打包 → 绕过检测

防护方升级:将检测逻辑与业务逻辑融合、增加校验 → 提高删除难度

感谢您的分享,这是一个非常有价值的实战技巧!

比如libmsaoaidsec.so就是第三方非业务so,删除也能正常运行,注意删除必须是安装后在app安装目录下删除so文件,这样不要再次签名so导致报错

进入到手机内部,删除这个so文件即可
adb shell
su
cd /data/app/~~Y0T7lai_U0oUCJog9GF-8A==/com.hupu.shihuo-O3JD2hiXKDpyGx8Ibl4sRA==/lib/arm64
rm -rf libmsaoaidsec.so

posted on 2025-08-24 07:27  GKLBB  阅读(746)  评论(0)    收藏  举报