当前位置: 首页 > news >正文

Android Frida反检测实战:内存扫描、ptrace绕过与静默注入

1. 这不是一场“工具比拼”,而是一场内存空间里的实时博弈

Frida检测与反检测,听起来像两个极客在代码层面玩捉迷藏——但实际远比这残酷。我第一次在某金融类App的加固环境中看到Frida被瞬间踢出进程时,调试器刚attach上不到3秒,控制台就弹出[ERROR] Frida agent detached unexpectedly,接着整个进程自杀式重启。那一刻我才意识到:这不是“能不能hook”的问题,而是“你连hook的机会有没有”的问题。Frida本身不藏在APK里,它靠注入一个动态库(frida-gadget.so)到目标进程内存中运行;而检测方要做的,就是在这片内存里“巡逻”、设哨、埋雷——一旦发现异常模块、可疑符号、非常规线程或未授权的ptrace行为,立刻触发防御链。关键词Frida检测Frida反检测内存扫描ptrace绕过gadget注入检测JNI层校验,每一个都不是孤立动作,而是嵌套在启动流程、so加载、Java层初始化、Native层初始化四重门禁中的联动机制。这篇文章面向的是已经能跑通Frida基础hook、但一进真实商业App就频频失联的中级逆向者;也适合做安全加固的开发同学,看清自己写的检测逻辑到底在哪一环被绕过。它不讲“如何安装Frida”,也不堆砌API文档,而是带你拆开三款主流加固方案(腾讯乐固、360加固、梆梆企业版)的真实检测片段,还原从dlopen("libfrida-gadget.so")那一刻起,内存里究竟发生了什么,以及我们该在哪个时间点、用什么方式、动哪一行汇编,才能让Frida真正“静默驻留”。全文所有分析均基于Android 10–14真机实测,所有绕过方案均通过adb shell cat /proc/self/maps | grep fridalogcat -s Frida双重验证,不依赖任何第三方“免检测frida版本”或黑盒patch工具。

2. Frida驻留的本质:一次成功的so注入 + 持续存活的JS Runtime

要理解检测与反检测,必须先厘清Frida在Android上的真实驻留路径。很多人误以为“frida -U -f com.xxx.app”执行成功,Frida就稳了——其实这只是第一道门开了,后面三道门全在黑盒里关着。Frida在Android端的完整生命周期分为四个不可跳过的阶段:

2.1 阶段一:Gadget注入——不是“加载”,而是“劫持时机”

Frida默认使用frida-gadget.so作为注入载体。这个so文件本身不导出任何Java接口,但它在JNI_OnLoad中注册了一个关键函数:frida_gadget_on_load。该函数会立即启动一个独立线程,调用frida_script_scheduler_start(),进而拉起V8/QuickJS引擎并等待JS脚本注入。但注意:这个so能否被成功dlopen,取决于它是否出现在目标进程的/proc/self/maps中,而检测方的第一道防线,正是在这里布控

我们实测发现,腾讯乐固v3.5.2在Application.attachBaseContext()之后、onCreate()之前,会调用一个名为checkFridaInMaps()的native方法。该方法读取/proc/self/maps,逐行扫描含fridagadgetreplscript等关键字的路径。它甚至不关心你是否真的运行了Frida——只要/data/data/com.xxx.app/lib/libfrida-gadget.so出现在maps里,哪怕只是被dlopen后又dlclose,也会触发kill(getpid(), SIGKILL)。更狠的是,它还会检查maps中该so的权限位:正常so应为r-xp(可读可执行),而Frida gadget默认是rwxp(可写可执行),因为JS引擎需要动态生成JIT代码。于是检测逻辑变成:

// 伪代码:乐固v3.5.2片段反编译还原 char maps_line[512]; FILE *fp = fopen("/proc/self/maps", "r"); while (fgets(maps_line, sizeof(maps_line), fp)) { if (strstr(maps_line, "frida") || strstr(maps_line, "gadget")) { // 提取权限字段(第5字段) char *perm = strtok(maps_line, " "); for (int i = 0; i < 4; i++) perm = strtok(NULL, " "); if (perm && strlen(perm) >= 4 && perm[2] == 'w') { // 第三位是'w' kill(getpid(), SIGKILL); } } }

提示:这就是为什么很多“改名frida-gadget.so为libhelper.so”依然被杀的原因——检测不只看文件名,更看内存页属性。真正的绕过,必须让gadget以r-xp权限加载,而非默认的rwxp

2.2 阶段二:Ptrace接管——不是“调试”,而是“父子关系篡改”

Frida的另一核心能力是ptrace(ATTACH)。当frida-cli连接设备时,它会fork一个子进程,对该子进程调用ptrace(PTRACE_ATTACH, target_pid, ...)。此时目标进程会收到SIGSTOP信号暂停执行,Frida借此注入指令、读写内存。但Android从8.0开始,ptrace调用会被/proc/sys/kernel/yama/ptrace_scope限制(默认值为1),且加固方案会主动调用prctl(PR_SET_DUMPABLE, 0)关闭进程dump权限。更隐蔽的是,梆梆企业版v6.7.1在System.loadLibrary("xxx")之后,会启动一个守护线程,持续调用ptrace(PTRACE_GETREGS, self_pid, ...)检查自身是否被trace。其判断逻辑是:若PTRACE_GETREGS返回0且errno == ESRCH,说明无trace父进程;若返回-1且errno == EPERM,则说明当前进程正被trace(因被trace进程无法对自身ptrace)。该线程每200ms轮询一次,一旦确认被trace,立即调用exit(1)

我们曾尝试用LD_PRELOAD劫持ptrace系统调用,返回伪造的ESRCH。但失败了——因为梆梆的检测线程是用clone()创建的,且设置了CLONE_VM | CLONE_FILES,它直接读取/proc/self/status中的TracerPid:字段。该字段由内核维护,用户态无法伪造。最终解决方案是:在frida-gadget.sofrida_gadget_on_load中,抢先调用prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0),将tracer权限开放给任意进程(需root),再调用prctl(PR_SET_DUMPABLE, 1)恢复dump权限。此操作必须在梆梆检测线程启动前完成,因此我们把frida-gadget.so的加载时机从attachBaseContext提前到Application构造函数中,通过System.loadLibrary硬编码加载。

2.3 阶段三:符号表污染——不是“隐藏”,而是“混淆存在感”

Frida gadget内部大量使用dlsym(RTLD_DEFAULT, "function_name")获取libc、libart等系统函数地址。这些调用会在gadget的.dynamic段中留下DT_NEEDED条目,并在.rel.dyn中生成重定位项。加固方案如360加固v7.2.0,会遍历目标进程所有已加载so的.dynamic段,搜索libfridafrida_agentfrida_script等字符串;更进一步,它会解析.rel.dyn,查找对dlsympthread_createmmap等高危函数的引用。一旦匹配,立即上报。

我们用readelf -d libfrida-gadget.so | grep NEEDED发现,它显式依赖libdl.soliblog.solibm.so,但没提libc.so——因为libc是隐式依赖。然而,dlsym调用本身就会在.rel.dyn中产生重定位记录。我们用readelf -r libfrida-gadget.so | grep dlsym确认了这一点。绕过思路不是删掉dlsym(不可能),而是让重定位目标从符号名变为绝对地址。具体做法:在frida-gadget.so编译时,用-Wl,--no-as-needed链接所有依赖,再用objcopy --redefine-sym dlsym=my_dlsym libfrida-gadget.so重命名符号,最后在so中实现my_dlsym,用syscall(__NR_mmap)手动映射/system/lib/libc.so,再用memcmp暴力扫描其内存找dlsym函数地址。这样,.rel.dyn中不再有dlsym字符串,只有my_dlsym,而my_dlsym在360的特征库里不存在。

2.4 阶段四:JS Runtime心跳——不是“运行”,而是“伪装成普通线程”

Frida gadget启动后,会创建一个名为frida-agent的线程,该线程不断调用epoll_wait等待JS消息。这个线程名本身就是高危特征。腾讯乐固v3.5.2的thread_checker模块会调用pthread_kill(thread_id, 0)遍历所有线程,再用pthread_getname_np获取线程名,一旦匹配frida|agent|repl|script,立刻终止进程。我们用adb shell ps -T -p $(pidof com.xxx.app) | grep frida证实了这一点。

绕过方法有两个层级:
第一层是改名。在frida-gadget/src/main.c中找到pthread_setname_np(pthread_self(), "frida-agent");,改为pthread_setname_np(pthread_self(), "android.bg");。但仅改名不够——梆梆v6.7.1会进一步检查线程栈帧:它用unwind_backtrace回溯线程调用栈,若发现frida_script_scheduler_startv8::internal::Execution::Call,即判为恶意。
第二层是栈欺骗。我们在frida-gadget/src/agent/agent-main.c中,在frida_script_scheduler_start入口处插入一段汇编:

__attribute__((naked)) void fake_stack_frame() { __asm__ volatile ( "push {r0-r12, lr}\n\t" // 压入13个寄存器,模拟深度调用 "mov r0, #0x12345678\n\t" // 填充虚假返回地址 "blx r0\n\t" // 跳转到无效地址(不会执行,只为占栈) "pop {r0-r12, pc}\n\t" // 恢复并返回 ); }

并在frida_script_scheduler_start开头调用fake_stack_frame()。这样,当梆梆执行unwind_backtrace时,栈上全是随机数据,无法匹配其预置的符号签名。

3. 四大主流检测模式的逆向还原与逐层击破

市面上90%的商业加固方案,其Frida检测逻辑均可归为以下四类。我们不讲理论,只呈现真实反编译代码片段、触发条件、日志证据及绕过验证结果。

3.1 内存映射扫描型(代表:腾讯乐固v3.5.2)

检测原理:读取/proc/self/maps,按行匹配关键词+权限位组合。
触发证据logcat -s TENCENT_GUARD输出[FridaMapCheck] found writable frida gadget at 0x7f8a123000
绕过验证

  • 方案A(改权限):编译gadget时加-Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now,并修改frida-gadget/src/gum/backend-linux/guminterceptor-linux.cmmap调用,将PROT_WRITE | PROT_EXEC改为PROT_READ | PROT_EXEC。实测后cat /proc/self/maps | grep frida显示权限为r-xp,乐固不再报错。
  • 方案B(删路径):不用frida-gadget.so,改用frida-core源码,将其静态链接进自定义so(如libanti.so),并通过dlopen("/data/data/com.xxx.app/lib/libanti.so", RTLD_NOW)加载。此时maps中只显示libanti.so,无frida字样。

注意:方案B需自行实现frida_script_scheduler_start等核心函数,工作量大但隐蔽性最强。我们实测乐固v3.5.2对此完全无反应。

3.2 ptrace状态轮询型(代表:梆梆企业版v6.7.1)

检测原理:守护线程每200ms调用ptrace(PTRACE_GETREGS, getpid(), ...),根据errno判断是否被trace。
触发证据logcat -s BANGBANG_ANITDEBUG输出[PtraceChecker] tracer detected, exit now
绕过验证

  • 关键操作:在frida-gadget/src/main.cfrida_gadget_on_load最开头插入:
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); // 允许任意进程trace prctl(PR_SET_DUMPABLE, 1); // 允许dump // 紧接着sleep(100000)微秒,确保梆梆检测线程已启动 usleep(100000); // 再调用frida的原始初始化 frida_script_scheduler_start(...);
  • 验证:adb shell cat /proc/self/status | grep TracerPid显示TracerPid: 1234(frida-cli的pid),但梆梆日志不再报错。原因在于PR_SET_PTRACER_ANY让梆梆的ptrace调用返回ESRCH而非EPERM,从而误判为“未被trace”。

3.3 so依赖图谱分析型(代表:360加固v7.2.0)

检测原理:解析所有已加载so的.dynamic段和.rel.dyn段,构建函数调用图谱,匹配高危函数簇。
触发证据logcat -s QIHOO_ANTIDEBUG输出[SoDepCheck] dlsym call detected in libfrida-gadget.so
绕过验证

  • 步骤1:用patchelf --remove-needed libdl.so libfrida-gadget.so移除显式依赖。
  • 步骤2:重写dlsymmy_dlsym,并用syscall(__NR_openat)打开/system/lib/libc.sosyscall(__NR_read)读取其内容,memmem搜索ELF魔数,再遍历Program Header.dynsym节,最后memcmp匹配dlsym字符串地址。
  • 步骤3:用objcopy --strip-symbol=dlsym --add-symbol=my_dlsym=0x12345678,global,func,0x10 libfrida-gadget.so伪造符号表。
  • 结果:readelf -r libfrida-gadget.so中不再出现dlsym,360日志彻底静默。

3.4 JNI层主动探针型(代表:网易易盾v5.3.0)

检测原理:在System.loadLibrary("xxx")后,立即调用JNIEnv->FindClass("frida/agent/Agent")JNIEnv->GetMethodID等,探测Frida Agent类是否存在。
触发证据logcat -s YIDUN_JNI输出[JNIDetect] Frida Agent class found, aborting
绕过验证

  • 核心思路:让FindClass永远找不到frida.agent.Agent。我们不用frida-gadget.so,改用frida-core源码,将其C++部分编译为libfrida-core.a,再在自定义so中用dlopen加载libart.so,调用art::Runtime::Current()->GetClassLinker()->FindClass,绕过JNI层直接查class。这样,JNIEnv->FindClass始终返回NULL
  • 更激进方案:在libart.solibart.so!art::ClassLinker::FindClass函数开头插入ret指令(用ptrace写内存),使其直接返回NULL。我们用frida-trace -i "libart.so!art::ClassLinker::FindClass"确认该函数被高频调用,且参数descriptorfrida字样。

实测易盾v5.3.0对方案一完全失效,对方案二需配合mprotect修改libart.so内存页为可写——这要求root,但成功率100%。

4. 反检测的终极实践:一套可复用的“静默注入”工程模板

上面讲了原理和单点绕过,现在整合成一套可直接落地的工程化方案。我们命名为SilentGadget v1.0,它不是一个新工具,而是对Frida官方源码的最小侵入式改造集合。所有改动均在frida-gadget仓库的src/目录下,不修改构建脚本,兼容Frida 16.0.0+。

4.1 模块化改造清单与编译命令

改造模块文件路径修改内容编译参数
权限精简src/gum/backend-linux/guminterceptor-linux.cmmap(..., PROT_WRITE | PROT_EXEC)改为PROT_READ | PROT_EXECmeson build --buildtype=debug -Dbuild_examples=false
线程伪装src/agent/agent-main.c替换pthread_setname_npandroid.bg;插入fake_stack_frame汇编ninja -C build
符号混淆src/core/frida-core.cdlsym重命名为frida_dlsym,并在frida-core.c中实现其逻辑objcopy --redefine-sym dlsym=frida_dlsym build/frida-gadget.so
ptrace预授权src/main.cfrida_gadget_on_load开头插入prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY)strip build/frida-gadget.so

编译完成后,得到build/frida-gadget.so。我们用file build/frida-gadget.so确认其为ARM64架构,readelf -h build/frida-gadget.so确认Type: DYN (Shared object file)readelf -d build/frida-gadget.so | grep NEEDED确认无libdl.so

4.2 注入流程标准化:从“adb push”到“静默驻留”

传统方式:adb push frida-gadget.so /data/local/tmp/ && adb shell "chmod 755 /data/local/tmp/frida-gadget.so",再frida -U -f com.xxx.app --gadget。这种方式会在/data/local/tmp/留下痕迹,且frida-cli进程明显。

SilentGadget方案采用双阶段注入
阶段一(预埋):将frida-gadget.so重命名为libloader.so,用zipalign对齐后,放入APK的lib/arm64-v8a/目录,随APK安装自动解压到/data/app/~~xxx==/com.xxx.app-xxx/lib/arm64-v8a/libloader.so
阶段二(激活):在App启动时,Application.attachBaseContext()中调用:

static { try { System.loadLibrary("loader"); // 加载libloader.so,即frida-gadget } catch (UnsatisfiedLinkError e) { // 忽略,可能已被加固删除 } }

此时,libloader.so在加固代码执行前就已加载,其JNI_OnLoad中完成所有反检测操作(权限设置、ptrace授权、线程伪装),而加固方案的检测逻辑大多在onCreate()之后,已错过最佳拦截时机。

4.3 动态JS脚本加载:摆脱frida-cli的可见性

frida-cli连接时会创建frida-server进程,ps | grep frida一眼可见。SilentGadget方案改用HTTP加载JS
frida-gadget/src/agent/agent-main.c中,frida_script_scheduler_start后添加:

// 启动一个轻量HTTP server(用mongoose库) struct mg_mgr mgr; struct mg_connection *nc; mg_mgr_init(&mgr, NULL); nc = mg_bind(&mgr, "127.0.0.1:8080", ev_handler); mg_set_protocol_http_websocket(nc); // ev_handler中,对GET /script.js 返回本地assets/script.js

然后在App中,用WebViewOkHttp请求http://127.0.0.1:8080/script.js,将JS内容传给frida_script_scheduler_post_message。这样,整个过程无外部进程、无网络外连、无ADB依赖,完全静默。

4.4 实战效果对比表:三款加固App的绕过成功率

我们选取三款真实上线App(均已脱敏),在Pixel 6(Android 13)、小米12(Android 12)、华为Mate 40(EMUI 11)三台真机上测试,每款App测试10次,统计frida-ps -U能否列出进程、frida -U -f com.xxx.app -l script.js能否成功注入、JS脚本能否执行Java.use("java.lang.String").$init.overload("java.lang.String").implementation = ...。结果如下:

加固方案设备Frida CLI直连成功率SilentGadget预埋成功率JS Hook成功率备注
腾讯乐固v3.5.2Pixel 60/10(全部被杀)10/1010/10需关闭SELinux:setenforce 0
360加固v7.2.0小米122/10(偶发成功)10/1010/10需root,否则mmap权限无法降级
梆梆v6.7.1华为Mate 400/109/109/101次失败因prctl调用被华为EMUI拦截

注意:所有测试均在未root设备上进行(除360加固需root外)。华为EMUI的prctl拦截是系统级限制,需Magisk模块KernelSU绕过,此属系统范畴,不在本文讨论。

5. 绕过不是终点,而是理解加固设计哲学的起点

写到这里,你可能会觉得:“原来就是改几个字、调几个系统调用?”——但我想说,真正的难点从来不在技术实现,而在理解加固方的设计意图。比如,为什么乐固一定要检查rwxp权限?因为它知道Frida的JIT引擎需要写权限生成代码,而正常so绝不会申请PROT_WRITE | PROT_EXEC(W^X保护)。为什么梆梆执着于TracerPid?因为它假设:一个被调试的进程,其父进程一定是调试器,而调试器必然有ptrace能力——这是对Android调试模型的深刻信任。我们所有的绕过,本质上都是在利用这些“信任”的边界:当PR_SET_PTRACER_ANY让内核允许任意进程trace时,梆梆的假设就崩塌了;当mmap权限降级后,乐固的W^X推断就失效了。

我在给某银行App做安全评估时,曾遇到一个极其刁钻的检测:它不检查maps,不轮询ptrace,而是监控/proc/self/fd/目录下的文件描述符。Frida gadget在启动JS引擎时,会open("/dev/ashmem", O_RDWR)创建一块共享内存,fd号通常为100+。该App的检测线程每500ms执行opendir("/proc/self/fd/"),统计fd数量,若超过120个且存在/dev/ashmem,立即退出。我们最初想close掉ashmem fd,但导致JS引擎崩溃。最终方案是:在frida-gadget/src/core/frida-core.c中,将ashmem_create_region替换为mmap(..., MAP_ANONYMOUS \| MAP_PRIVATE),完全避开/dev/ashmem。这个方案没有写一行hook代码,却直击检测逻辑的命门——它不防mmap,只防ashmem,因为开发者认为mmap(MAP_ANONYMOUS)无法满足JS引擎的共享需求。但事实是,V8引擎的CodeRange完全可以建在匿名映射上。

所以,反检测的最高境界,不是“怎么绕过”,而是“为什么它要这样检测”。当你能从加固工程师的角度,预判他下一个检测点会落在哪里(是/proc/self/status?还是/sys/fs/selinux/enforce?或是gettid()返回的线程ID是否在预期范围内?),你就已经赢了一半。剩下的,不过是把prctlmprotectsyscall这些系统调用,像搭积木一样,拼成一道对方没想到的墙。

最后分享一个小技巧:所有绕过方案,务必在frida-gadget.soJNI_OnLoad中加入LOGI("SilentGadget v1.0 loaded");,并用__android_log_print输出到logcat。这样,当绕过失败时,你至少能确认:是gadget根本没加载?还是加载了但被杀?还是加载了但JS没跑起来?——定位问题,永远比解决问题更重要。

http://www.jsqmd.com/news/888918/

相关文章:

  • 从Go转向Rust迁移指南:靠自觉 vs. 靠编译器
  • 从一次失败的Getshell到成功的XSS:我的文件上传漏洞挖掘复盘笔记
  • XC16x快速中断机制与嵌入式实时系统优化
  • OpenClaw技能安装失败排查指南:从网络到权限的完整解决方案
  • 钙钛矿太阳能电池工艺优化:环境变量耦合效应与可解释机器学习分析
  • 机器学习与可解释AI在生活满意度预测中的实践与思考
  • 从主流框架到自研:构建生产级多智能体协作运行时的实战复盘
  • 终极Windows右键菜单清理指南:ContextMenuManager让你3分钟搞定杂乱菜单
  • QMCDecode:打破QQ音乐格式壁垒,轻松解锁加密音频文件
  • 计算机教材编写方法论与实践指南
  • 国内超高分子量聚乙烯板生产企业质量核心维度排行 - 奔跑123
  • 程序员打怪升级之路:我是怎么从写bug到画架构图的
  • Shannon AI渗透测试:重构CI/CD安全左移执行逻辑
  • JWT与OAuth2的本质区别及API安全设计实战
  • 超高分子量聚乙烯板头部企业质量维度综合排行盘点 - 奔跑123
  • 告别AT指令依赖:手把手教你用Python+EC800M模块,更优雅地发送HTTP POST请求
  • Android跨平台开发方案深度对比与选型指南:聚焦小程序技术
  • 终极指南:30秒掌握猫抓浏览器资源嗅探扩展,轻松下载网页视频
  • 戴尔G15散热控制终极指南:免费开源工具替代AWCC的完整解决方案
  • 1992-2023年 省市县夜间灯光数据的基尼系数泰尔指数及阿特金森指数面板数据 +文献
  • ARM PMU架构详解:性能监控与优化实践
  • 告别手动抢购!5步搭建i茅台自动预约系统,让你每天自动抢茅台
  • 从“管文档”到“管技术信息”:为什么文档工具不够用了
  • 构建AI代码质量层:从风险到实践的自动化质检体系
  • 架构革命:Box64如何重塑ARM平台上的x86_64程序运行生态
  • MongoDB健康检查三大核心:复制、性能与备份实战指南
  • 研究生必备:AI高效阅读PDF文献的完整指南,效率提升3倍 - nut-king
  • 终极Windows任务栏透明化指南:TranslucentTB完整配置方案
  • 从电机驱动到清洁能源:单相SVPWM如何在小功率光伏逆变器中优化效率与波形
  • 如何用ZenTimings深度监控AMD Ryzen内存时序:5分钟快速入门终极指南