Frida 的默认特征有多明显
Frida 是逆向神器,但它的默认部署在检测方面前几乎是裸奔:
- 进程名:
frida-server写在进程列表里 - 端口:默认监听 27042
- 内存特征:注入的 agent 带
frida字样的内存页、特有的线程名(gum-js-loop等) - 库特征:
frida-agent.so出现在 maps 里
商业 App 的反 Frida 检测就是按这个清单逐项扫的。对抗思路:逐项消除特征。
第一层:改名换端口(5 分钟搞定)
# 1. 二进制改名
adb push frida-server /data/local/tmp/.fs64 # 隐藏文件名
# 2. 换端口 + 只监听本地
adb shell "su -c '/data/local/tmp/.fs64 -l 127.0.0.1:8888 &'"
# 3. 端口转发给电脑
adb forward tcp:8888 tcp:8888
# 4. 客户端连接
frida -H 127.0.0.1:8888 -f com.target.app这一步能过 60% 的检测(大部分 App 只查默认端口和进程名)。
第二层:进程名与线程名伪装
检测方会遍历 /proc/*/cmdline 找 frida 字样,以及扫描本进程的线程名找 gum-js-loop、pool-frida 等特征。
进程名:改名后 frida-server 的 cmdline 就是新名字,已过。
线程名:Frida 注入后在本进程创建的线程名是特征。用自定义编译的 frida(修改源码里的线程名字符串)是根治方案;不重新编译的折中方案是 hook 检测函数本身:
// hook 读取线程名的检测点
var open = Module.findExportByName('libc.so', 'open');
Interceptor.attach(open, {
onEnter: function(args) {
var path = args[0].readCString();
// 检测代码遍历 /proc/self/task/*/comm 读线程名
if (path.indexOf('/proc/self/task/') === 0 && path.endsWith('/comm')) {
this.isCommCheck = true;
}
},
onLeave: function(retval) {
// 让它读到伪造的内容(配合 read hook)
}
});第三层:内存特征消除
检测方扫描自身内存找 frida agent 的特征字符串(frida-agent、gum 等)。对抗:
// 1. 隐藏 maps 里的 frida 条目:hook open 对 /proc/self/maps 的读取
// 读到内容后过滤掉 frida 相关行再返回
// 2. 更彻底:用 frida 的 embed 模式或自定义 gadget,
// 把 agent 编译进自己的 so,内存里不出现 frida 字样自定义 gadget 是工程上的甜点方案:把 frida-gadget 改名打包进 APK(或注入),连接走自定义通道,内存特征完全自定义。
第四层:检测点反制
最高效的对抗不是消除所有特征,而是直接干掉检测函数:
// 逆向找到检测函数后,直接 replace 成无害返回
var checkFrida = Module.findBaseAddress('libsecurity.so').add(0x1234);
Interceptor.replace(checkFrida, new NativeCallback(function() {
return 0; // 永远返回"未检测到 frida"
}, 'int', []));逆向检测函数的方法:jadx/IDA 搜 frida、27042、gum 等字符串,交叉引用定位检测代码。
精髓:对抗的优先级与验证
- 先反制检测函数,再考虑特征消除。改返回值是一行代码的事,消除内存特征是一天的工程——性价比差两个数量级
- 验证用"对比法":同一操作在"无 frida 环境"和"有 frida + 绕过"下各跑一次,对比请求参数和响应结果——一致才算真绕过(防静默标记型检测)
- 检测是组合拳,绕过要有耐心:端口、进程名、线程名、内存、行为时序,逐项排查逐项过。每过一项验证一次,别全改完再测(出问题没法定位是哪项没绕过)
总结
- Frida 默认特征:进程名、27042 端口、线程名、内存字符串
- 改名换端口过 60%,线程/内存特征要 hook 或自定义编译
- 最高效是反制检测函数本身(Interceptor.replace)
- 绕过验证用对比法,防静默标记
交流微信:run1255