文章目录
- 1. 前言:为什么企业要检测Frida
- 1.1 Frida的安全威胁:企业检测的核心动因
- 1.2 Frida检测的典型应用场景
- 1.3 学习Frida绕过的意义
- 2. 检测原理:基于/proc/self/maps的Frida特征识别
- 2.1 proc/self/maps的核心作用
- 2.2 基于/proc/self/maps的Frida检测逻辑
- 3. APK的C++检测实现:代码逻辑与人工验证
- 3.1 C++核心检测代码(JNI实现)
- 代码逻辑梳理
- 3.2 人工验证Frida注入与检测效果
- 4. Hook基础分析:通过JADX反编译定位检测逻辑
- 4.1 步骤1:JADX反编译APK,定位JNI调用方法
- 4.2 步骤2:梳理检测结果的展示流程
- 4.3 核心结论:绕过的关键切入点
- 5. 两种Hook绕过思路详解
- 5.1 思路1:Hook libc.so的fopen函数,重定向文件读取
- 5.1.1 原理:C++文件操作的底层依赖
- 5.1.2 实现:replace与attach两种语法的绕过脚本
- 方式1:Interceptor.replace(替换函数实现)
- 方式2:Interceptor.attach(拦截函数调用)
- 5.1.3 两种语法的核心区别
- 5.2 思路2:Hook libc.so的memchr函数,过滤特征字符
- 5.2.1 原理:IDA反编译后的特征匹配逻辑
- 5.2.2 实现:replace与attach两种语法的绕过脚本
- 方式1:Interceptor.replace(替换函数实现)
- 方式2:Interceptor.attach(拦截函数调用)
- 5.2.3 两种语法的核心区别
- 5.3 关键问题:为什么Hook libc.so对C++代码有效?
- 5.4 绕过的核心步骤
- 6. 章节总结
- 6.1 核心知识点梳理
- 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层调用,核心代码如下:
#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为例):adb shellsupidof com.example.securitycheck# 输出进程ID,例如4081查看进程的内存映射文件,筛选Frida特征
执行以下命令,读取/proc/[PID]/maps并过滤包含“frida”的行:cat/proc/4081/maps|grepfrida验证结果
若命令输出包含“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函数完成文件打开操作,调用链如下:
上层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(替换函数实现)
importJavafrom"frida-java-bridge";functionbypassFopen(){try{constlibc=Module.load('libc.so');constfopenPtr=libc.getExportByName('fopen');if(!fopenPtr){console.log('[!] 未找到fopen符号');returnfalse;}constfopen=newNativeFunction(fopenPtr,'pointer',['pointer','pointer']);Interceptor.replace(fopen,newNativeCallback((pathPtr,modePtr)=>{constpath=pathPtr.readCString();if(path?.includes('/proc/self/maps')){console.log('Redirecting /proc/self/maps to /dev/null');returnfopen(Memory.allocUtf8String("/dev/null"),modePtr);}returnfopen(pathPtr,modePtr);},'pointer',['pointer','pointer']));console.log('Frida detection bypass applied.');returntrue;}catch(error){console.error("Bypass执行出错:",error.message);returnfalse;}}Java.perform(()=>{try{bypassFopen()console.log(111);}catch(error){console.error("Hook执行出错:",error.message);}});方式2:Interceptor.attach(拦截函数调用)
importJavafrom"frida-java-bridge";functionbypassFopen2(){try{constlibc=Module.load('libc.so');constfopenPtr=libc.getExportByName('fopen');if(!fopenPtr){console.log('[!] 未找到fopen符号');returnfalse;}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/nullargs[0]=Memory.allocUtf8String("/dev/null");}}});console.log('Frida detection bypass with attach applied.');returntrue;}catch(error){console.error("Bypass执行出错:",error.message);returnfalse;}}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的作用是在指定内存缓冲区中查找指定字符,原型为:
void*memchr(constvoid*s,intc,size_tn);// s:缓冲区;c:目标字符;n:缓冲区长度检测逻辑通过memchr查找'f'(ASCII码102),再验证后续字符是否为“rida”,从而匹配“frida”特征。因此,Hookmemchr函数,使其在找到Frida相关特征时返回NULL,即可阻断匹配逻辑。
5.2.2 实现:replace与attach两种语法的绕过脚本
方式1:Interceptor.replace(替换函数实现)
importJavafrom"frida-java-bridge";functionbypassMemchr(){try{constlibc=Module.load('libc.so');constmemchrPtr=libc.getExportByName('memchr');if(!memchrPtr){console.log('[!] 未找到memchr符号');returnfalse;}Interceptor.replace(memchrPtr,newNativeCallback((s,c,n)=>{constoriginalMemchr=newNativeFunction(memchrPtr,'pointer',['pointer','int','int']);constresult=originalMemchr(s,c,n);if(result!==NULL&&n>4){constcontent=s.readCString(n);if(content&&(content.includes('frida')||content.includes('libfrida')||content.includes('frida-agent'))){// 拦截并返回NULLreturnNULL;}}returnresult;},'pointer',['pointer','int','int']));console.log('memchr bypass applied.');returntrue;}catch(error){console.error("memchr bypass error: ",error.message);returnfalse;}}Java.perform(()=>{try{bypassMemchr()console.log(111);}catch(error){console.error("Hook执行出错:",error.message);}});方式2:Interceptor.attach(拦截函数调用)
importJavafrom"frida-java-bridge";functionbypassMemchr2(){try{constlibc=Module.load('libc.so');constmemchrPtr=libc.getExportByName('memchr');if(!memchrPtr){console.log('[!] 未找到memchr符号');returnfalse;}Interceptor.attach(memchrPtr,{onEnter:function(args){// 原始参数this.buffer=args[0];// 搜索缓冲区this.searchChar=args[1];// 搜索字符this.bufferSize=args[2];// 缓冲区大小},onLeave:function(retval){if(retval!==NULL){// 从缓冲区起始位置读取内容进行检查constcontent=this.buffer.readCString(this.bufferSize.toInt32());if(content?.includes('frida')){// 隐藏frida相关结果retval.replace(NULL);}}}});console.log('memchr attach bypass applied.');returntrue;}catch(error){console.error("memchr attach bypass error: ",error.message);returnfalse;}}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. Hook memchr函数,过滤Frida特征的查找结果 |
6.2 扩展思考
实际应用中的Frida检测逻辑可能更加多样化,例如:
- 检测代码为纯C编写:直接调用
fopen、fgets、strstr、strcmp等函数,而非C++的流操作; - 特征匹配更隐蔽:使用十六进制特征匹配、多字符组合校验,而非直接匹配字符串。
面对这类型场景,核心原则不变:先通过逆向工具找到检测逻辑的核心依赖点(如strstr、open、read等函数),再根据依赖点选择对应的Hook目标,通过修改输入/输出实现绕过。学会分析思路,而非死记脚本,才能应对各种复杂的检测场景。