文章目录
-
- [1. 前言:为什么企业要检测Frida](#1. 前言:为什么企业要检测Frida)
-
- [1.1 Frida的安全威胁:企业检测的核心动因](#1.1 Frida的安全威胁:企业检测的核心动因)
- [1.2 Frida检测的典型应用场景](#1.2 Frida检测的典型应用场景)
- [1.3 学习Frida绕过的意义](#1.3 学习Frida绕过的意义)
- [2. 检测原理:基于/proc/self/maps的Frida特征识别](#2. 检测原理:基于/proc/self/maps的Frida特征识别)
-
- [2.1 proc/self/maps的核心作用](#2.1 proc/self/maps的核心作用)
- [2.2 基于/proc/self/maps的Frida检测逻辑](#2.2 基于/proc/self/maps的Frida检测逻辑)
- [3. APK的C++检测实现:代码逻辑与人工验证](#3. APK的C++检测实现:代码逻辑与人工验证)
-
- [3.1 C++核心检测代码(JNI实现)](#3.1 C++核心检测代码(JNI实现))
- [3.2 人工验证Frida注入与检测效果](#3.2 人工验证Frida注入与检测效果)
- [4. Hook基础分析:通过JADX反编译定位检测逻辑](#4. Hook基础分析:通过JADX反编译定位检测逻辑)
-
- [4.1 步骤1:JADX反编译APK,定位JNI调用方法](#4.1 步骤1:JADX反编译APK,定位JNI调用方法)
- [4.2 步骤2:梳理检测结果的展示流程](#4.2 步骤2:梳理检测结果的展示流程)
- [4.3 核心结论:绕过的关键切入点](#4.3 核心结论:绕过的关键切入点)
- [5. 两种Hook绕过思路详解](#5. 两种Hook绕过思路详解)
-
- [5.1 思路1:Hook libc.so的fopen函数,重定向文件读取](#5.1 思路1:Hook libc.so的fopen函数,重定向文件读取)
-
- [5.1.1 原理:C++文件操作的底层依赖](#5.1.1 原理:C++文件操作的底层依赖)
- [5.1.2 实现:replace与attach两种语法的绕过脚本](#5.1.2 实现:replace与attach两种语法的绕过脚本)
- [5.1.3 两种语法的核心区别](#5.1.3 两种语法的核心区别)
- [5.2 思路2:Hook libc.so的memchr函数,过滤特征字符](#5.2 思路2:Hook libc.so的memchr函数,过滤特征字符)
-
- [5.2.1 原理:IDA反编译后的特征匹配逻辑](#5.2.1 原理:IDA反编译后的特征匹配逻辑)
- [5.2.2 实现:replace与attach两种语法的绕过脚本](#5.2.2 实现:replace与attach两种语法的绕过脚本)
- [5.2.3 两种语法的核心区别](#5.2.3 两种语法的核心区别)
- [5.3 关键问题:为什么Hook libc.so对C++代码有效?](#5.3 关键问题:为什么Hook libc.so对C++代码有效?)
- [5.4 绕过的核心步骤](#5.4 绕过的核心步骤)
- [6. 章节总结](#6. 章节总结)
-
- [6.1 核心知识点梳理](#6.1 核心知识点梳理)
- [6.2 扩展思考](#6.2 扩展思考)
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
1. 前言:为什么企业要检测Frida
1.1 Frida的安全威胁:企业检测的核心动因
Frida是一款跨平台的动态插桩工具,能够在不修改目标程序源码、不重新编译的情况下,动态注入脚本拦截函数调用、修改内存数据、篡改业务逻辑。对于企业而言,Frida的滥用会带来以下风险:
- 核心业务逻辑被破解:如金融APP的支付校验、加密算法被逆向,付费软件的授权验证被绕过;
- 敏感数据被窃取:如APP中的用户令牌、加密密钥、隐私数据被Hook获取;
- 业务流程被篡改:如电商APP的订单金额、风控校验被恶意修改;
- 企业内部应用被渗透:企业私有化部署的应用被逆向,导致内部数据泄露。
因此,检测并阻断Frida的注入行为,是企业保障应用安全的重要防线。
1.2 Frida检测的典型应用场景
Frida检测逻辑通常被嵌入在对安全性要求高的应用中,常见场景包括:
- 金融类应用:银行APP、支付APP、证券APP等,涉及用户资金安全;
- 付费/版权类应用:会员制软件、付费内容APP、游戏等,防止破解与盗版;
- 企业级应用:企业内部管理系统、私有化部署的业务应用,防止内部数据泄露;
- 安全防护类应用:杀毒软件、安全加固工具,防止被逆向分析。
1.3 学习Frida绕过的意义
对于安全研究者、渗透测试工程师、逆向分析人员而言,学习Frida绕过并非为了恶意攻击,而是:
- 合规的安全评估:在获得企业授权后,对应用进行安全测试,验证防护机制的有效性;
- 理解攻防对抗本质:通过分析检测逻辑与绕过方法,掌握移动安全的核心攻防思路;
- 提升逆向工程能力:从JNI调用、SO层逻辑、系统函数调用等维度,深化对Android底层的理解。
2. 检测原理:基于/proc/self/maps的Frida特征识别
2.1 proc/self/maps的核心作用
/proc/self/maps是Linux/Android系统中的虚拟文件 ,用于记录当前进程的内存映射信息,包括:
- 内存区域的地址范围、访问权限;
- 映射的文件路径(如加载的SO库、配置文件);
- 内存区域的所属进程与权限属性。
该文件是系统提供的进程内存快照,任何进程都可以读取自身的/proc/self/maps文件。
2.2 基于/proc/self/maps的Frida检测逻辑
当Frida注入目标进程时,可能会在进程中加载frida-agent.so、libfrida-core.so、libfrida.so等相关模块,这些模块的路径与名称会被写入/proc/self/maps文件中。
企业应用的检测逻辑正是利用这一特征:读取/proc/self/maps文件,逐行查找是否包含"frida"相关关键词,若存在则判定为Frida注入。
3. APK的C++检测实现:代码逻辑与人工验证
本章节使用的示例 APK、相关源码如下:
链接: https://pan.baidu.com/s/1Gr0RSnS2DAQ3YeB_FE-DPQ?pwd=vdn7
提取码: vdn7
3.1 C++核心检测代码(JNI实现)
示例APK将检测逻辑下沉到C++层(SO文件),通过JNI供Java层调用,核心代码如下:
c++
#include <jni.h>
#include <string>
#include <fstream>
#include <sstream>
static bool checkFrida() {
// 打开当前进程的/proc/self/maps文件
std::ifstream mapsFile("/proc/self/maps");
if (!mapsFile.is_open()) {
return false;
}
std::string line;
// 逐行读取文件内容
while (std::getline(mapsFile, line)) {
// 检测行内容中是否包含Frida相关特征
if (line.find("frida") != std::string::npos ||
line.find("libfrida") != std::string::npos ||
line.find("frida-agent") != std::string::npos) {
mapsFile.close();
return true; // 检测到Frida,返回true
}
}
mapsFile.close();
return false;
}
// JNI接口方法:供Java层调用,返回检测结果
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_securitycheck_MainActivity_checkSecurity(JNIEnv *env, jobject thiz) {
bool isDetected = checkFrida();
if (isDetected) {
return env->NewStringUTF("检测到使用frida");
} else {
return env->NewStringUTF("未检测到frida");
}
}
代码逻辑梳理
上述代码分为两个核心部分:
checkFrida函数:通过C++的std::ifstream读取/proc/self/maps,逐行匹配frida、libfrida、frida-agent特征字符串;- JNI接口方法:将
checkFrida的布尔结果转换为字符串,返回给Java层展示。
3.2 人工验证Frida注入与检测效果
通过ADB命令可手动验证/proc/self/maps中的Frida特征,步骤如下:
-
注入 Frida 脚本后,启动目标应用,获取进程ID
打开终端执行以下命令,通过
pidof获取应用进程的PID(以示例应用com.example.securitycheck为例):shelladb shell su pidof com.example.securitycheck # 输出进程ID,例如4081 -
查看进程的内存映射文件,筛选Frida特征
执行以下命令,读取
/proc/[PID]/maps并过滤包含"frida"的行:shellcat /proc/4081/maps | grep frida -
验证结果
若命令输出包含"frida"等关键词,说明Frida已成功注入,此时点击应用中的检测按钮,会显示"检测到使用frida"。

测试时可以先如下图操作(示例应用的包名是com.example.securitycheck),先注释掉绕过检测的方法。

4. Hook基础分析:通过JADX反编译定位检测逻辑
要实现绕过,首先需要通过逆向工具定位检测逻辑的调用链,这里使用JADX反编译APK进行分析。
4.1 步骤1:JADX反编译APK,定位JNI调用方法
将APK文件拖入JADX后,在com.example.securitycheck.MainActivity类中,发现checkSecurity方法被声明为native方法(如图所示),说明该方法通过JNI调用C++层的检测逻辑。

4.2 步骤2:梳理检测结果的展示流程
在MainLayout的布局逻辑中,按钮的点击事件会触发checkSecurity方法的调用,并将返回的字符串结果显示在文本控件中。这意味着:只要篡改checkSecurity的底层依赖逻辑,使其返回"未检测到frida",即可完成绕过。

4.3 核心结论:绕过的关键切入点
C++层的检测逻辑依赖两个核心步骤:读取/proc/self/maps文件、匹配特征字符串。因此,我们可以通过Hook这两个步骤的底层函数,阻断检测逻辑的执行。
5. 两种Hook绕过思路详解
本节将详细讲解Hook fopen(重定向文件读取)和Hook memchr(过滤特征字符)两种绕过思路,并对比Interceptor.replace与Interceptor.attach两种语法的区别与适用场景。
5.1 思路1:Hook libc.so的fopen函数,重定向文件读取
5.1.1 原理:C++文件操作的底层依赖
C++的std::ifstream是高层的文件流操作类,其底层最终会调用libc.so (C标准库)的fopen函数完成文件打开操作,调用链如下:
shell
上层C++代码:std::ifstream("/proc/self/maps")
↓
libc++的std::filebuf::open() (C++文件缓冲区的核心方法)
↓
libc的fopen() (Android的C库fopen实现)
↓
Linux系统调用open() (最终触发内核打开文件)
因此,Hook libc.so的fopen函数,可拦截应用对/proc/self/maps的读取请求,将其重定向到空文件(如/dev/null),使检测逻辑读取不到任何内容,从而绕过检测。
5.1.2 实现:replace与attach两种语法的绕过脚本
方式1:Interceptor.replace(替换函数实现)
javascript
import Java from "frida-java-bridge";
function bypassFopen() {
try {
const libc = Module.load('libc.so');
const fopenPtr = libc.getExportByName('fopen');
if (!fopenPtr) {
console.log('[!] 未找到fopen符号');
return false;
}
const fopen = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']);
Interceptor.replace(fopen, new NativeCallback((pathPtr, modePtr) => {
const path = pathPtr.readCString();
if (path?.includes('/proc/self/maps')) {
console.log('Redirecting /proc/self/maps to /dev/null');
return fopen(Memory.allocUtf8String("/dev/null"), modePtr);
}
return fopen(pathPtr, modePtr);
}, 'pointer', ['pointer', 'pointer']));
console.log('Frida detection bypass applied.');
return true;
} catch (error) {
console.error("Bypass执行出错:", error.message);
return false;
}
}
Java.perform(() => {
try {
bypassFopen()
console.log(111);
} catch (error) {
console.error("Hook执行出错:", error.message);
}
});
方式2:Interceptor.attach(拦截函数调用)
javascript
import Java from "frida-java-bridge";
function bypassFopen2() {
try {
const libc = Module.load('libc.so');
const fopenPtr = libc.getExportByName('fopen');
if (!fopenPtr) {
console.log('[!] 未找到fopen符号');
return false;
}
Interceptor.attach(fopenPtr, {
onEnter: function(args) {
this.path = args[0].readCString();
this.shouldRedirect = this.path?.includes('/proc/self/maps');
if (this.shouldRedirect) {
console.log('Redirecting /proc/self/maps to /dev/null');
// 修改参数指向 /dev/null
args[0] = Memory.allocUtf8String("/dev/null");
}
}
});
console.log('Frida detection bypass with attach applied.');
return true;
} catch (error) {
console.error("Bypass执行出错:", error.message);
return false;
}
}
Java.perform(() => {
try {
bypassFopen2()
console.log(111);
} catch (error) {
console.error("Hook执行出错:", error.message);
}
});
5.1.3 两种语法的核心区别
| 语法类型 | 核心逻辑 | 优势 | 适用场景 |
|---|---|---|---|
| Interceptor.replace | 完全替换原函数的实现,需手动调用原始函数处理非目标场景 | 可完全接管函数逻辑,自定义程度高 | 需要修改函数返回值或逻辑的场景 |
| Interceptor.attach | 拦截函数的进入/退出阶段,仅修改参数或返回值,保留原函数的完整逻辑 | 逻辑更简洁,副作用小(不破坏原函数) | 仅需修改参数或返回值的简单场景 |
为什么两种方式都能绕过?
无论是替换函数实现(replace)还是拦截调用修改参数(attach),最终都达成了同一个目标:让应用读取不到/proc/self/maps的真实内容,效果如图所示。

5.2 思路2:Hook libc.so的memchr函数,过滤特征字符
5.2.1 原理:IDA反编译后的特征匹配逻辑
通过IDA反编译目标SO文件(libsecuritycheck.so),发现C++层的特征匹配逻辑最终依赖libc.so的memchr函数 (如图所示)。memchr的作用是在指定内存缓冲区中查找指定字符,原型为:
c
void *memchr(const void *s, int c, size_t n); // s:缓冲区;c:目标字符;n:缓冲区长度
检测逻辑通过memchr查找'f'(ASCII码102),再验证后续字符是否为"rida",从而匹配"frida"特征。因此,Hookmemchr函数,使其在找到Frida相关特征时返回NULL,即可阻断匹配逻辑。


5.2.2 实现:replace与attach两种语法的绕过脚本
方式1:Interceptor.replace(替换函数实现)
javascript
import Java from "frida-java-bridge";
function bypassMemchr() {
try {
const libc = Module.load('libc.so');
const memchrPtr = libc.getExportByName('memchr');
if (!memchrPtr) {
console.log('[!] 未找到memchr符号');
return false;
}
Interceptor.replace(memchrPtr, new NativeCallback((s, c, n) => {
const originalMemchr = new NativeFunction(memchrPtr, 'pointer', ['pointer', 'int', 'int']);
const result = originalMemchr(s, c, n);
if (result !== NULL && n > 4) {
const content = s.readCString(n);
if (content &&
(content.includes('frida') ||
content.includes('libfrida') ||
content.includes('frida-agent'))) {
// 拦截并返回NULL
return NULL;
}
}
return result;
}, 'pointer', ['pointer', 'int', 'int']));
console.log('memchr bypass applied.');
return true;
} catch (error) {
console.error("memchr bypass error: ", error.message);
return false;
}
}
Java.perform(() => {
try {
bypassMemchr()
console.log(111);
} catch (error) {
console.error("Hook执行出错:", error.message);
}
});
方式2:Interceptor.attach(拦截函数调用)
javascript
import Java from "frida-java-bridge";
function bypassMemchr2() {
try {
const libc = Module.load('libc.so');
const memchrPtr = libc.getExportByName('memchr');
if (!memchrPtr) {
console.log('[!] 未找到memchr符号');
return false;
}
Interceptor.attach(memchrPtr, {
onEnter: function (args) {
// 原始参数
this.buffer = args[0]; // 搜索缓冲区
this.searchChar = args[1]; // 搜索字符
this.bufferSize = args[2]; // 缓冲区大小
},
onLeave: function (retval) {
if (retval !== NULL) {
// 从缓冲区起始位置读取内容进行检查
const content = this.buffer.readCString(this.bufferSize.toInt32());
if (content?.includes('frida')) {
// 隐藏frida相关结果
retval.replace(NULL);
}
}
}
});
console.log('memchr attach bypass applied.');
return true;
} catch (error) {
console.error("memchr attach bypass error: ", error.message);
return false;
}
}
Java.perform(() => {
try {
bypassMemchr2()
console.log(111);
} catch (error) {
console.error("Hook执行出错:", error.message);
}
});
5.2.3 两种语法的核心区别
与Hook fopen的逻辑一致,两种语法最终都达成了让memchr无法返回Frida特征的查找结果的目标,因此都能成功绕过检测,效果如图所示。

5.3 关键问题:为什么Hook libc.so对C++代码有效?
很多读者会疑惑:检测代码是C++编写的,为什么Hook C标准库(libc.so)的函数能生效?核心原因有两点:
- C++标准库的底层依赖 :C++的高层封装(如
std::ifstream、std::string::find)并非完全独立实现,而是依赖libc.so提供的底层系统调用(如fopen、memchr、read)。也就是说,C++是"上层封装",libc.so是"底层支撑"。 - IDA中的函数归属 :通过IDA看到的
memchr、fopen等函数,本质上是libc.so导出的函数,C++代码只是调用了这些函数,因此Hook libc.so的函数能直接拦截到C++层的调用。
这也是为什么即使检测逻辑是纯C++编写,Hook libc.so的核心函数依然是最有效的绕过手段。
5.4 绕过的核心步骤
无论采用哪种Hook思路,核心步骤都可总结为:
- 分析检测逻辑的底层依赖 :确定检测代码依赖的核心函数(如
fopen、memchr); - 定位函数的所属库:找到函数所在的库(如libc.so)及导出符号;
- 选择Hook语法 :根据需求选择
replace(自定义逻辑)或attach(简单修改); - 篡改函数的输入/输出:要么修改参数(如重定向文件路径),要么修改返回值(如返回NULL),阻断检测逻辑。
6. 章节总结
6.1 核心知识点梳理
| 维度 | 具体内容 |
|---|---|
| 检测方法 | 读取/proc/self/maps文件,匹配"frida""libfrida"等特征字符串,判断是否存在Frida注入 |
| 分析方法 | 1. 用JADX反编译APK,定位JNI调用的native方法; 2. 用IDA反编译SO文件,还原底层检测逻辑(依赖的核心函数) |
| 绕过方法 | 1. Hookfopen函数,重定向/proc/self/maps的读取请求; 2. Hookmemchr函数,过滤Frida特征的查找结果 |
6.2 扩展思考
实际应用中的Frida检测逻辑可能更加多样化,例如:
- 检测代码为纯C编写 :直接调用
fopen、fgets、strstr、strcmp等函数,而非C++的流操作; - 特征匹配更隐蔽:使用十六进制特征匹配、多字符组合校验,而非直接匹配字符串。
面对这类型场景,核心原则不变:先通过逆向工具找到检测逻辑的核心依赖点(如strstr、open、read等函数),再根据依赖点选择对应的Hook目标,通过修改输入/输出实现绕过。学会分析思路,而非死记脚本,才能应对各种复杂的检测场景。