文章目录
- 1. Frida注入检测原理:基于/proc文件系统的线程状态分析
- 2. 核心检测源码解析
- 3. 检测效果验证
- 4. 手动查询验证方法
- 5. Hook绕过思路与实现
- 5.1 反编译分析
- 5.2 完整Hook脚本
- 5.3 脚本绕过逻辑分析
- 5.3.1 核心思路
- 5.3.2 bypassStrcmp 函数解析
- 5.3.3 bypassStrstr 函数解析
- 5.4 启动说明
- 5.5 最终效果
- 6. 章节总结
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
1. Frida注入检测原理:基于/proc文件系统的线程状态分析
在Linux系统(包括Android)中,/proc虚拟文件系统是用户态与内核态交互的重要接口,其中包含了当前系统所有进程的详细信息。对于进程注入检测而言,/proc/[进程ID]/task/[线程ID]/status文件是关键分析对象——该文件记录了对应线程的状态信息,其中Name字段会显示线程的名称。
当Frida注入目标进程时,会创建一系列特征线程(如frida、gmain、gdbus、gum-js-loop等),这些线程的名称会被写入其对应的status文件的Name字段。因此,应用程序可以通过遍历自身进程的所有线程,读取其status文件中的Name字段,检测是否包含Frida相关特征关键词,从而判断是否被Frida注入。
本章节使用的示例 APK、相关源码如下:
链接: https://pan.baidu.com/s/1SUOa65u8cDZtFE9ShPxhDg?pwd=4p44
提取码: 4p44
2. 核心检测源码解析
示例代码通过C++实现了Frida注入检测功能,核心逻辑封装在checkFrida函数中,完整检测流程如下:
#include <jni.h> #include <string> #include <fstream> #include <sstream> #include <dirent.h> #include <unistd.h> #include <cstring> static bool checkFrida() { DIR* taskDir; struct dirent* entry; // 获取当前进程ID并构建task目录路径(/proc/[pid]/task,存放当前进程所有线程) pid_t pid = getpid(); std::string taskPath = "/proc/" + std::to_string(pid) + "/task"; taskDir = opendir(taskPath.c_str()); if (!taskDir) { return false; } // 遍历task目录中的所有线程(每个子目录对应一个线程ID) while ((entry = readdir(taskDir)) != nullptr) { // 跳过.和..目录项 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // 构建线程status文件路径(/proc/[pid]/task/[tid]/status) std::string statusPath = taskPath + "/" + std::string(entry->d_name) + "/status"; std::ifstream statusFile(statusPath); if (statusFile.is_open()) { std::string line; // 读取status文件,查找Name字段 while (std::getline(statusFile, line)) { if (line.find("Name:") != std::string::npos) { // 提取Name字段的值(去除冒号和前导空格) size_t colonPos = line.find(":"); if (colonPos != std::string::npos) { std::string name = line.substr(colonPos + 1); name.erase(0, name.find_first_not_of(" \t")); // 关键词检测(两种方式) // 方式一:精确匹配(strcmp)Frida特征线程名 if (strcmp(name.c_str(), "gmain") == 0 || strcmp(name.c_str(), "gdbus") == 0 || strcmp(name.c_str(), "frida") == 0 || strcmp(name.c_str(), "gum-js-loop") == 0) { statusFile.close(); closedir(taskDir); return true; // 检测到Frida } // 方式二:子串匹配(strstr)Frida相关关键词 if (strstr(name.c_str(), "gmain") != nullptr || strstr(name.c_str(), "gdbus") != nullptr || strstr(name.c_str(), "frida") != nullptr || strstr(name.c_str(), "gum-js") != nullptr) { statusFile.close(); closedir(taskDir); return true; // 检测到Frida } } break; // 找到Name字段后无需继续读取 } } statusFile.close(); } } closedir(taskDir); return false; // 未检测到Frida } 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"); } }核心逻辑总结:通过getpid()获取当前进程ID,构建线程目录路径/proc/[pid]/task;遍历所有线程目录,读取每个线程的status文件;提取Name字段的值,分别使用strcmp(精确匹配)和strstr(子串匹配)检测是否包含Frida特征关键词,若命中则返回“检测到使用Frida”。
3. 检测效果验证
当应用启动后,通过Frida命令注入目标进程(如frida -U -n FridaAPK -l hook.js),操作应用按钮触发检测逻辑,应用会显示“检测到使用frida”,验证检测功能有效。
4. 手动查询验证方法
除了应用自动检测,还可以通过adb命令手动验证Frida注入的线程特征,步骤如下:
# 进入设备shell环境adb shell# 获取root权限su# 查找目标应用进程ID(以包名com.example.securitycheck为例)pidof com.example.securitycheck# 输出进程ID,例如4081# 列出该进程的所有线程IDls/proc/4081/task# 查看指定线程的status文件,检查Name字段cat/proc/4081/task/{线程ID}/status# 若某个线程中的Name字段包含gmain、gdbus、frida、gum-js-loop等关键词,则说明存在Frida注入手动查询结果示例如下图:这里不一个个打开看了。
5. Hook绕过思路与实现
5.1 反编译分析
由于检测逻辑实现在SO文件中,通过IDA工具反编译SO文件可清晰看到核心检测逻辑:通过strcmp(精确匹配)和strstr(子串匹配)函数对线程名中的关键词进行校验。因此,绕过检测的核心是Hook这两个函数,篡改其返回结果。
(注:JADX反编译APK的结果与上一章相同,主要逻辑在SO中,故不再重复分析。)
5.2 完整Hook脚本
// 定义需要绕过的检测关键词constFRIDA_KEYWORDS=['frida','gmain','gdbus','gum-js-loop'];constSTRSTR_KEYWORDS=['frida','gum-js','gmain','gdbus','tmp'];// Hook strcmp 函数functionbypassStrcmp(){try{constlibc=Module.load('libc.so');conststrcmpPtr=libc.getExportByName('strcmp');if(!strcmpPtr){console.log('[!] 未找到 strcmp 符号');returnfalse;}Interceptor.attach(strcmpPtr,{onEnter:function(args){this.str1=args[0].readCString();// 待检测的线程名this.str2=args[1].readCString();// 检测关键词},onLeave:function(retval){// 只有当第二个参数是检测关键词时才进行干预if(FRIDA_KEYWORDS.includes(this.str2)){// 如果第一个参数包含敏感词,强制返回不匹配(非0值)if(this.str1?.includes(this.str2)){retval.replace(ptr(1));}}}});console.log('strcmp bypass applied.');returntrue;}catch(error){console.error("strcmp bypass error: ",error.message);returnfalse;}}// Hook strstr 函数functionbypassStrstr(){try{constlibc=Module.load('libc.so');conststrstrPtr=libc.getExportByName('strstr');if(!strstrPtr){console.log('[!] 未找到 strstr 符号');returnfalse;}Interceptor.attach(strstrPtr,{onEnter:function(args){this.haystack=args[0].readCString();// 待搜索的字符串(线程名)this.needle=args[1].readCString();// 搜索关键词},onLeave:function(retval){// 只有当第二个参数是检测关键词时才进行干预if(STRSTR_KEYWORDS.includes(this.needle)){// 如果找到关键词,强制返回NULL(表示未找到)if(retval!==NULL){retval.replace(NULL);}}}});console.log('strstr bypass applied.');returntrue;}catch(error){console.error("strstr bypass error: ",error.message);returnfalse;}}Java.perform(()=>{try{bypassStrcmp();bypassStrstr();console.log(111);}catch(error){console.error("Hook执行出错:",error.message);}});5.3 脚本绕过逻辑分析
5.3.1 核心思路
通过Frida的Interceptor模块Hook系统库libc.so中的strcmp和strstr函数,在函数调用时篡改输入参数或返回值,使检测逻辑无法识别Frida特征关键词。
5.3.2 bypassStrcmp 函数解析
- 语法层面:通过
Module.load('libc.so')加载系统C库,getExportByName('strcmp')获取strcmp函数地址;使用Interceptor.attach挂钩该函数,定义onEnter(函数调用前)和onLeave(函数返回前)回调。 - 执行层面:
onEnter:读取strcmp的两个参数(args[0]为线程名,args[1]为检测关键词),保存到this对象中供onLeave使用。onLeave:若检测关键词(this.str2)属于FRIDA_KEYWORDS列表,且线程名(this.str1)包含该关键词,则通过retval.replace(ptr(1))强制strcmp返回非0值(表示不匹配),绕过精确匹配检测。
5.3.3 bypassStrstr 函数解析
- 语法层面:同样加载
libc.so并获取strstr函数地址,挂钩后定义onEnter和onLeave回调。 - 执行层面:
onEnter:读取strstr的两个参数(args[0]为待搜索字符串,args[1]为搜索关键词)。onLeave:若搜索关键词(this.needle)属于STRSTR_KEYWORDS列表,且原返回值(retval)不为NULL(表示找到关键词),则通过retval.replace(NULL)强制返回NULL(表示未找到),绕过子串匹配检测。
5.4 启动说明
此前章节中常用Python脚本以Spawn模式(启动时注入)启动应用,监控全流程。但在本案例中,由于strcmp和strstr是系统高频使用函数,Spawn模式下Frida初始化过程会与这些函数频繁交互,导致应用卡顿甚至无法启动(冲突问题)。因此需改用Attach模式(应用启动后注入),避免初始化阶段的冲突。
脚本中去掉了import Java from "frida-java-bridge";,因无需额外Java层交互,简化注入流程。
Attach模式注入步骤:
- 手动启动目标应用(如
com.example.securitycheck); - 通过以下方式注入Hook脚本:
# 方式一:通过进程ID注入adb shell pidof com.example.securitycheck# 获取进程ID,例如10479frida -U -p10479-l hook.js# 方式二:通过可读进程名注入frida-ps -U# 查看设备中运行的进程名(如FridaAPK)frida -U -n FridaAPK -l hook.js5.5 最终效果
应用启动后注入Hook脚本,点击检测按钮,应用会显示“未检测到frida”,说明绕过成功。
6. 章节总结
本章核心围绕“基于线程名的Frida注入检测与绕过”展开,重点如下:
检测逻辑核心:通过遍历
/proc/[pid]/task/[tid]/status文件,提取Name字段,使用strcmp(精确匹配)和strstr(子串匹配)检测Frida特征线程名(如frida、gmain等)。绕过思路:针对检测中使用的
strcmp和strstr函数,通过Frida Hook篡改其返回结果——使包含敏感关键词的线程名在检测时被判定为“不匹配”或“未找到”,从而绕过检测。扩展启示:实际场景中,检测工具可能使用其他字符串匹配函数(如
strcasestr、memcmp等),核心绕过方法是分析检测逻辑依赖的底层函数,针对性地Hook并篡改关键参数或返回值。掌握“识别检测函数→挂钩干预”的分析方法,是应对各类注入检测的通用思路。