当前位置: 首页 > 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 frida和logcat -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逐行扫描含frida、gadget、repl、script等关键字的路径。它甚至不关心你是否真的运行了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.so的frida_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段搜索libfrida、frida_agent、frida_script等字符串更进一步它会解析.rel.dyn查找对dlsym、pthread_create、mmap等高危函数的引用。一旦匹配立即上报。我们用readelf -d libfrida-gadget.so | grep NEEDED发现它显式依赖libdl.so、liblog.so、libm.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 dlsymmy_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_start或v8::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.c中mmap调用将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.c的frida_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: 1234frida-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重写dlsym为my_dlsym并用syscall(__NR_openat)打开/system/lib/libc.sosyscall(__NR_read)读取其内容memmem搜索ELF魔数再遍历Program Header找.dynsym节最后memcmp匹配dlsym字符串地址。步骤3用objcopy --strip-symboldlsym --add-symbolmy_dlsym0x12345678,global,func,0x10 libfrida-gadget.so伪造符号表。结果readelf -r libfrida-gadget.so中不再出现dlsym360日志彻底静默。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.so的libart.so!art::ClassLinker::FindClass函数开头插入ret指令用ptrace写内存使其直接返回NULL。我们用frida-trace -i libart.so!art::ClassLinker::FindClass确认该函数被高频调用且参数descriptor含frida字样。实测易盾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.c将mmap(..., PROT_WRITE | PROT_EXEC)改为PROT_READ | PROT_EXECmeson build --buildtypedebug -Dbuild_examplesfalse线程伪装src/agent/agent-main.c替换pthread_setname_np为android.bg插入fake_stack_frame汇编ninja -C build符号混淆src/core/frida-core.c将dlsym重命名为frida_dlsym并在frida-core.c中实现其逻辑objcopy --redefine-sym dlsymfrida_dlsym build/frida-gadget.soptrace预授权src/main.c在frida_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中用WebView或OkHttp请求http://127.0.0.1:8080/script.js将JS内容传给frida_script_scheduler_post_message。这样整个过程无外部进程、无网络外连、无ADB依赖完全静默。4.4 实战效果对比表三款加固App的绕过成功率我们选取三款真实上线App均已脱敏在Pixel 6Android 13、小米12Android 12、华为Mate 40EMUI 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需关闭SELinuxsetenforce 0360加固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_EXECW^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是否在预期范围内你就已经赢了一半。剩下的不过是把prctl、mprotect、syscall这些系统调用像搭积木一样拼成一道对方没想到的墙。最后分享一个小技巧所有绕过方案务必在frida-gadget.so的JNI_OnLoad中加入LOGI(SilentGadget v1.0 loaded);并用__android_log_print输出到logcat。这样当绕过失败时你至少能确认是gadget根本没加载还是加载了但被杀还是加载了但JS没跑起来——定位问题永远比解决问题更重要。
http://www.gsyq.cn/news/1388646.html

相关文章:

  • 链路预测:白盒模型与黑盒算法的性能对比与选型指南
  • 八木天线原理没那么难:用‘滞后相位’和‘感容性’定性理解它的指向性与增益
  • 终极Windows右键菜单清理指南:ContextMenuManager让你3分钟搞定杂乱菜单
  • 千川投手最核心的能力不再是建计划,是用AI拆解“跑量素材”的结构特征——爆款复刻Agent帮你做
  • 高效能个体的日常炼金术:从心流系统到AI外脑的实践指南
  • 避坑指南:在MATLAB里跑通OMP、CoSaMP等压缩感知算法,你可能遇到的5个常见错误
  • 抖音批量下载工具:一键获取用户主页全作品,高效管理海量内容
  • 从梯形图到SCL:在FactoryIO里重构机械手程序,我总结了5个效率翻倍的SCL编程技巧
  • 架构革命:Box64如何重塑ARM平台上的x86_64程序运行生态
  • 程序员打怪升级之路:我是怎么从写bug到画架构图的
  • ARM ETE嵌入式跟踪技术原理与实践指南
  • 深度估计技术:从双像素传感器到DiFuse-Net架构
  • 对话记忆系统实战:从原理到实现,构建连贯智能交互
  • TVA在电子元器件领域的创新应用(4)
  • TVA在电子元器件领域的创新应用(3)
  • 基于LC谐振与自由衰减法的电感变压器快速评估方案
  • 终极免费GTA5线上小助手:让你的洛圣都冒险更简单高效
  • 硬件工程师的EMC避坑指南:直流电机PCB布局与滤波电路设计实战
  • 终极Windows任务栏透明化指南:TranslucentTB完整配置方案
  • 从零构建本地语音AI助手:基于Whisper与Llama的隐私优先智能体实践
  • 单片机密码锁进阶玩法:给你的AT89C51项目添加“输错锁定”和LED状态提示
  • 跨平台游戏模组自由:WorkshopDL让你在Epic/GOG平台也能畅玩Steam创意工坊模组
  • 别再混淆了!5分钟搞懂PCM、LPCM、ADPCM的区别与联系(附实例数据)
  • 告别物理开关!用CD4013和MOSFET给你的单片机项目做个“软”开关(附完整电路图)
  • PCI / PCIe 基础理论与配置空间结构深度剖析
  • QMCDecode终极指南:3步解锁QQ音乐加密文件,重获音乐自由!
  • UABEAvalonia:跨平台Unity资源逆向工程与资产编辑解决方案
  • AI输出安全:构建LLM应用的三层防御体系与实战指南
  • A2A协议:多智能体协同架构的核心与2026年系统设计原则
  • Python情感分析实战:从零构建可复现的朴素贝叶斯分类器