应用安全 --- app加固 之 frida检测
我们来系统地梳理和对抗 Frida 的检测方法。这是一场持续的“猫鼠游戏”,理解双方的战术是成功的关键。
Frida 检测与对抗方法全景图
本文将检测方法分为六大类,并为每一类提供具体的对抗方案。
第一类:进程环境检测
这类检测查找 Frida 在目标进程中留下的各种痕迹。
| 检测方法 | 检测原理 | 对抗方法 |
|---|---|---|
| 检查映射文件 | 扫描 /proc/self/maps 或 /proc/pid/maps,查找包含 frida、gadget 字样的库文件(如 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 会创建名为 gmain、gdbus、frida-* 等特征的线程。遍历 /proc/self/task/pid/comm 可查看线程名。 |
1. Hook 线程创建函数:Hook pthread_create() 等函数,在创建时重命名敏感线程。2. 使用强R-Frida:其 randomize_name 功能会重命名所有相关线程。 |
| 检查环境变量 | 检查进程的环境变量,寻找 Frida 相关特征。 |
Hook 环境变量读取函数:如 Hook getenv(),当应用尝试读取可疑变量名时返回空或伪造值。 |
对抗代码示例(Hook 线程名检测):
// 拦截 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 内存扫描函数(如 memmem, strstr),当扫描到敏感区域时返回空。或者直接重写相关函数头几个字节。 |
对抗代码示例(内存字符串擦除):
// 在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 时间函数:拦截 gettimeofday、clock_gettime 等函数,返回伪造的时间,抹平时间差。 |
| D-Bus 检测 | Frida 使用 D-Bus 协议进行通信。检测系统上是否有异常的 D-Bus 通信。 | 使用强R-Frida:其 daemonize 模式可以改变通信方式,避免使用 D-Bus。 |
| 断点指令检测 | 检查关键函数头是否被 int3 等断点指令覆盖(这是传统调试器的行为)。Frida 不使用 int3,但此检测可对抗其他工具。 |
使用 Frida Stalker:Frida 的代码跟踪工具 Stalker 使用重新编译的方式,不会修改原函数代码,因此无法通过断点指令检测发现。 |
对抗代码示例(对抗计时检测):
// 拦截 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选项。
使用方法:
-
从 Releases 页面下载修改版的
frida-server。 -
推送到设备:
adb push frida-server /data/local/tmp/fs -
使用配置参数启动:
adb shell /data/local/tmp/fs -l 127.0.0.1:12345 --anti-anti-frida --daemonize -
连接时指定端口:
frida -H 127.0.0.1:12345 -f com.example.app --no-pause
第五类:Java 层检测
| 检测方法 | 检测原理 | 对抗方法 |
|---|---|---|
| 检查已加载类 | 遍历已加载的类,查找 agent、frida 等相关类名。 |
匿名加载:使用 Frida 的 Java.enumerateLoadedClasses 本身是隐蔽的,但如果你自己注入了一个类,需要注意命名。 |
| 检查Native库 | 使用 Debug.getNativeLibrarys() 等 Java API 获取已加载的Native库列表,查找 frida。 |
Hook Java API:拦截 Debug.getNativeLibrarys() 等函数,返回一个过滤后的列表,剔除 Frida 相关的库名。 |
对抗代码示例(Hook Java 层库检测):
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 执行完操作后,可以手动恢复原始的信号处理函数。 |
总结与最佳实践
-
首选方案:使用修改版 Frida
strongR-frida-android 是对抗检测的最强力武器,它从根源上解决了大部分问题(端口、线程、文件、字符串、D-Bus)。这应该是你的第一选择。 -
次级方案:主动脚本对抗
如果无法使用修改版,就在你的 Frida 脚本最开始处,集成上述的内存擦除、线程隐藏、API Hook 等对抗代码,先发制人。 -
策略:组合拳
单一的绕过方法很容易被新的检测手段破解。最有效的方法是组合使用多种技术,例如:strongR-frida+自定义对抗脚本。 -
保持更新
这场博弈在不断升级。关注 Frida 和 strongR-frida 的更新,了解最新的检测和反制技术。
最终建议: 对于大多数情况,直接从 strongR-frida-android 下载最新版本的 frida-server,并使用其提供的参数启动,就能绕过 90% 的常见检测。
绕过思路:定位并删除反Hook检测so文件
核心原理
许多大型App(如得物、识货)的安全团队会将核心的安全检测逻辑(如Frida检测、双进程保护、环境检测)编写在C/C++代码中,并编译为 .so 动态链接库。这些so库在App启动时被加载。如果检测到异常(如Frida在运行),则会直接调用 exit() 或 abort() 等函数终止进程。
由于这些so库通常与主业务逻辑无关,仅仅是安全防护模块,因此直接删除它们往往不会影响App的正常运行功能,从而可以实现绕过。
操作步骤
-
解压APK:
将.apk文件重命名为.zip并解压,或在命令行中使用unzip app.apk。进入解压后的lib目录,这里存放着所有so文件,通常按CPU架构分类(arm64-v8a,armeabi-v7a等)。 -
初步筛选:
观察so文件的命名。安全团队开发的so文件命名可能与传统第三方库不同,可能包含诸如security,safe,protect,shield,check,detect,monitor,encrypt等关键字。例如libsecurity.so,libmainprotect.so。 -
暴力删除测试:
这是一种简单粗暴但有效的方法。依次删除你认为可疑的so文件(或整个lib目录下的so文件),然后重新签名安装APK。如果App能正常启动且强制更新消失,说明删除成功。但这种方法效率低,且可能误删业务so导致功能异常。 -
动态加载监控(推荐方法):
使用Frida来监控App运行时加载的所有so文件,从而精确找到在崩溃瞬间被加载的那个so,它就是罪魁祸首。
通用脚本:监控so文件加载
您提到的“通用脚本”其原理是Hook Android底层加载动态库的函数 android_dlopen_ext 或 dlopen。当App加载任何一个so文件时,我们都打印出其路径和名称。当App突然崩溃时,最后一个被加载的so就极有可能是触发检测的so。
以下是这个通用监控脚本的代码:
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库加载监控脚本已启动...");
});
使用方法:
-
将上述代码保存为
monitor_so.js。 -
启动Frida-server。
-
使用 Spawn模式 启动App,以便捕获到最早期的so加载事件:
frida -U -f com.zhihu.android -l monitor_so.js --no-pause -
观察控制台输出。脚本会打印出App启动过程中加载的所有so文件。
-
当App突然崩溃或退出时,最后打印出的那几个so文件就是最大的嫌疑对象。特别是那些名称中包含安全关键词的so。
-
验证并删除:
定位到可疑的so文件后(例如lib/arm64-v8a/libsecurity_detect.so),回到解压的APK目录中,删除它。
然后使用apktool重新打包APK,并使用uber-apk-signer等工具进行签名。
安装修改后的APK,测试是否还能正常启动并绕过强制更新。
注意事项与局限性
-
业务相关性:此方法并非100%有效。有些App的核心业务逻辑也可能用C++编写,删除这些so会导致功能缺失或闪退。务必进行充分测试。
-
完整性校验:高强度的加固方案可能会对APK文件本身或其中的so文件进行完整性校验。如果检测到文件被删除,依然会触发退出机制。
-
对抗升级:安全团队可能会将检测逻辑更深地嵌入到业务so中,或者使用多个so进行交叉校验,使得简单的删除方法失效。
-
法律风险:此操作涉及修改他人的软件,务必在合法授权的范围内进行,仅用于安全学习和研究。
总结
您提供的“定位-删除”法是绕过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
浙公网安备 33010602011771号