Lazy loaded image
Frida学习笔记(十七):反调试与反检测对抗(上)· 网易云音乐易盾实战
Words 8774Read Time 22 min
2026-7-10
2026-7-10
type
Post
status
Published
date
Jul 10, 2026
slug
summary
这篇和第 18 篇是连着的。之前研究过网易云音乐的反调试,绕过还挺顺利——hook 上 serialdata、抓签名 I/O 没费多大劲;本周换了台手机重新开工,同一套招法却处处失灵,才逼出这篇硬核对抗。我拿网易云音乐 9.2.80 的网易易盾防护当样本,跨几十轮单变量实验:目标看着简单——稳定 hook 一个请求签名函数、让 Frida 会话在真机上活过两分钟;实际全部力气都耗在和五个安全库、一个被我一次次推翻又重建的「击杀模型」、以及一个跨 Android 版本会变脸的检测逻辑较劲上。最值钱的不是最后那条 recipe,而是中间一串被自己证伪掉的假设。第 18 篇换招商银行,那边是三套 SDK 并联 + 主动反击。
tags
frida
逆向工程
category
公众号
icon
password
网址
上次编辑时间
Jul 10, 2026 06:42 AM
comment
AI 总结
这篇和第 18 篇是连着的。之前研究过网易云音乐的反调试,绕过还挺顺利——hook 上 serialdata、抓签名 I/O 没费多大劲;本周换了台手机重新开工,同一套招法却处处失灵,才逼出这篇硬核对抗。我拿网易云音乐 9.2.80 的网易易盾防护当样本,跨几十轮单变量实验:目标看着简单——稳定 hook 一个请求签名函数、让 Frida 会话在真机上活过两分钟;实际全部力气都耗在和五个安全库、一个被我一次次推翻又重建的「击杀模型」、以及一个跨 Android 版本会变脸的检测逻辑较劲上。最值钱的不是最后那条 recipe,而是中间一串被自己证伪掉的假设。第 18 篇换招商银行,那边是三套 SDK 并联 + 主动反击。 本篇样本com.netease.cloudmusic 9.2.80(arm64-v8a),Pixel 5 / Android 14 / Magisk root,Frida 16.7.19 + radare2。Java 层是易盾壳 com.netease.nis.wrapper.MyApplication,真正的防护堆在 native 层。目标函数:com.netease.cloudmusic.utils.NeteaseMusicUtils.serialdata设备很关键:同一套 recipe 在 Pixel 6 Pro / Android 13 上整套失败(agent ~1 秒即被杀,详见第 9 节)。这套防护对 Android 版本和设备历史状态都敏感。 配套脚本:本篇产出的整套网易云音乐反调试 / 反 Frida 对抗脚本(signalProbe 过 SIGTRAP 自检 + MSA 12-NOP / 静态补丁 + 同意页 -N attach 驱动 + 重建双层击杀模型的实验矩阵 + winning recipe「只钩非安全库、真机 >2min 在线」+ serialdata native 定位 / libnesec 内存 dump / cronet 明文点定位 + run.md 真机验证清单,30+ 脚本复制即用)已整理打包。关注本公众号后私信回复关键词「脚本」 即可获取,本系列与后续 Unidbg / SO 逆向 / ARM 汇编 等系列脚本会统一在此发放并持续更新。

一、反常现象:进程还在,agent 却被卸载

逆向网易云的请求签名,绕不开一个 native 方法:
喂它一个接口路径 + 一段 JSON 参数,吐出一串十六进制签名。把它 hook 上、抓输入输出本身不难,Java.use(...).serialdata.implementation = ... 几行就行。难的是它活不久
第一次跑就撞上一个反常现象,反常到我盯着日志看了很久:**app 进程还在前台好好跑,Frida 会话却在 +1~90 秒之间被「卸载」——Frida 报 Process terminated,但 adb shell pidof 一看,app 进程根本没退**。
这在反调试里是个很「脏」的信号。常规反调试自杀走 exit() / kill() / raise(SIGABRT)——进程整个没了,你能在 libc 那几个函数上挂 hook 抓现行。但这个不一样:被弄掉的是 Frida 的 agent,不是整个进程。我把 libc 上所有能终止 / 卸载的函数(exit / \_exit / abort / kill / tgkill / munmap / mprotect / madvise)按任意调用方全局挂了一圈——会话被卸之前,零命中
这不是一道普通的反调试题。这是一个会绕过 libc、用某种我看不见的手段把 agent 弄掉的对手。后面几十轮实验,本质都在回答同一个问题:这只看不见的手,到底是谁、用什么、在什么时机出的?

二、战场:五个半安全库

先把战场画清楚。我早期视野太窄,以为只有 libnesec 在干活,后来才数清楚——五个安全库,外加一个不算安全库但同样关键的网络栈
图: 五个半安全库各管一段——重点是看清每个库在「击杀」里扮演什么角色,以及它有没有 inline svc(能不能在 libc hook 的视野外行动)。
notion image
五个半安全库火力全景
光这张图就是第一个教训:国产大厂的防护是「叠罗汉」——多个独立 SDK 各管一段、互不知道对方存在,但对逆向者是叠加的火力。我后面踩的最大的坑,几乎都来自「把多个独立机制误当成一个」。
serialdata 在 smali 里是 native 方法:
有意思的是:**所有 so 的导出表里都没有标准 JNI 名 Java_..._serialdata,连解密后的 libnesec 内存镜像、各 so 字符串里都搜不到 "serialdata" 这个串**。说明它是 RegisterNatives 动态注册、名字运行时才解密。这个细节后面会变成一条破局线索——它的真身在 libpoison + 0x7a28c,靠 GetStaticMethodID 拿到 ArtMethod*、读它的 data_ 字段(native 函数指针)定位到(搜不到名字,就从注册结构里挖)。

三、libnesec:逆清「持续猎杀者」

libnesec 是主心骨,磁盘密文、运行时解密。要静态分析,先从活着的进程把它的内存镜像 dump 出来(scripts/frida17/frida/analysis/dump_nesec.js:app 进程写不了 /data/local/tmp,得写自己私有目录再 root 拉):
dump 出来 offset = vaddr,可直接喂 r2 -a arm -b 64。**下文所有偏移都是这一次 dump(加载基址 0x7ab6cb5000)的实测值,会随版本 / 重打包漂移**——换版本得重新 dump 再定位。下面几个机制,每个对应一种反 Frida 手段。

3.1 load 期 SIGTRAP 自检

先讲原理:为什么「给自己发一个 SIGTRAP」就能照出调试器。SIGTRAP断点信号——调试器(ptrace 那一类)正是靠它实现断点的:进程一触发 SIGTRAP,内核会优先把这个信号交给「正在 trace 它的人」。所以同一句 raise(SIGTRAP),两种世界里结局不同:
  • 没人 trace、没人插桩:信号按正常流程派发,进程自己注册的 handler 会被调用
  • 有调试器或 Frida 这类插桩在场:信号被对方的处理逻辑抢先截走,进程自己的 handler 根本轮不到跑。
libnesec 就拿这个差异当探针。它先在 MyJni.load 里注册自己的 SIGTRAP handler(在 0x82998,职责是「把全局脏标志 0xca8a8 清 0、再重装 sigaction」),然后 raise(SIGTRAP),紧接着回读这个标志判定:
  • 标志被清成 0 → 我的 handler 跑过了 → 干净;
  • 标志还脏(非 0)→ handler 没跑 → 信号被人半路截走了 → 判定被调试,走 0x828c4raise + tgkill 自杀。
为什么这招专治 Frida:frida-gum 的 exceptor 为了实现自己的异常 / 断点处理,会在进程里装一个优先级更高的信号处理器,把 SIGTRAP 先吃掉。于是 libnesec 的 handler 永远跑不到、脏标志留着,正好掉进「判定被调试」那条路。注意它检测的不是「有没有 ptrace」,而是「SIGTRAP 有没有被人半路截胡」——Frida 一进场就中招。
图: 下图把上面讲的两种世界并排摆开——左路 handler 跑到、右路被 gum exceptor 截胡,分野只在「信号有没有被半路接走」这一步;底部那条蓝带就是 signalProbe,它做的两件事,正是把右路强行掰回左路。
notion image
SIGTRAP 自检原理:干净世界 handler 跑、Frida 在场信号被截胡,以及 signalProbe 破法
破法叫 signalProbe:hook libc 的 raise,认准「参数是 SIGTRAP(5) 且调用方是 libnesec」这一刻,替它做两件事——① 直接把脏标志 0xca8a8 写 0,伪装成「我的 handler 已经跑过了」,让判定走向「干净」;② 把信号号改成 0(raise(0) 是空操作,根本不真投递 SIGTRAP,连 frida exceptor 出手截胡的机会都不给)。两手叠加,libnesec 这道自检永远读到「干净」:

3.2 线程名扫描:逐字节读破「抹词」

原理这次很直白:Frida 注入进来会带起几条固定名字的线程——gum-js-loop 是 frida-gum 跑 JS 逻辑的那条线程,gmain 是它内部 glib 事件循环的线程。这俩名字几乎等于「这进程里有 Frida」。线程名躺在 /proc/<pid>/task/<tid>/status 的第一行(Name:),任何进程都能读自己的,不需要权限。于是 libnesec 在 0x88514 遍历 /proc/self/task,对每个 <tid>/status 第一行**逐字节 read(fd, 1)** 地读,再 strstr 匹配 gum-js-loopgmain,扫到就判定被注入。
「逐字节读」是个细节杀器:很多人破线程名检测,是 hook read 在缓冲区里抹掉 gum-js-loop。但它一次只读 1 字节,你抹不掉一个跨多次 read 才拼出来的词。破法只能从源头改名——用 patched frida-server,把服务端二进制里的线程名等长替换:
(顺带一个小推理:MSA 那个库里没有 gum-js-loop 串,所以线程名扫描能确定归属给 libnesec。多库混战里,靠「串的有无」做归属很省事。)

3.3 0xa1ab4:检测调度器,与一个会爆的雷

理解这个函数的关键,是先想清楚它的定位:它自己不做任何检测,它是个「检测任务调度器」。前面 3.1 的 SIGTRAP 自检、3.2 的线程名扫描,对 libnesec 来说都只是一个个独立的「检测 feature」;0xa1ab4 干的事,是从一个 job 描述符里取出「这次该跑哪个检测函数、它的参数、这个检测的 id、要重跑几次」,blr 调过去,再看返回值定通过还是检出。208 字节,结构是这样:
判定看的是返回值 w0最低位(这是它内部的编码约定):bit0 是奇数=干净,按 +0x18 那个上限多跑几次防抖动;bit0 是偶数=检出,路由到 verdict 处理器 0xa0e80——一个在全局区置标志的状态机,记下「第几号 feature 报了警」。所以真正的检测逻辑全在被它 blr 调用的那些 feature 函数里,0xa1ab4 只是统一的「调度 + 收口」外壳。
这里埋了个会爆的雷,得说清楚为什么这个方向走不通。0xa1ab4 是「反 Frida 核心调度」、verdict 是它的判定结论——按直觉,最该攻的就是这里:把判定中和掉,或者干脆把跑检测的 worker 端了,libnesec 不就停手了?这也是我早期主攻的方向。结果撞了三堵墙,每堵都单独足以让这条路作废:
  • 改判定,没用:我 hook 0x913f8,把 verdict 里的 flag_A 强置为 0、判定 code 从 4 改到 0,数据流证实确实改对了——可 agent 照样被卸载。这说明真正动手卸载 agent 的那条路,根本不经过 verdict(后面的两层模型会坐实:慢层是 MSA 构造函数自爆、快层是直接用 store 损坏 agent 内存,两者都不读 0xa0e80 这套标志)。verdict 只是 libnesec「判没判我」的记账,不是「怎么卸我」的开关;改记账不影响动手。
  • 端 worker,自残:那直接把跑检测 job 的 worker 线程杀掉呢?agent 确实活下来了,但 ART 被一起卡住——Java.perform 回调永不执行、app 停在启动页。等于为了拦它,把自己也搭进去,什么都干不成。
  • 关键窗口里,够不着:在唯一能干活的「同意页 + 冷启动」窗口里,0xa1ab4 头 30 秒一次都没被调用(多半是经混淆跳表间接调用、或干脆换了条路径)。你想 hook 它做文章,连它的调用点都等不到。
三条合起来——改它没用、端它自残、还够不着——所以**硬啃 0xa1ab4/verdict 基本是条岔路**。这也是我第一次「以为找到凶手了,结果改了它凶案照犯」:把对手自己的「判定逻辑」当成了「执行逻辑」。记下来省得后人重走;真正的凶手在别处,后面用单变量实验把它逼出来。

3.4 inline svc 表:libc hook 为何失明

libnesec 有一张自己的内联 syscall 表,根本不调 libc 的包装函数
原理在于 frida 监控系统调用的常规做法:去 hook libc 里 openat / read / exit_group 这些包装函数——因为正常代码发系统调用,都是先调 libc 的包装函数、由它执行 svc。可 libnesec 把 svc #0 直接写进了自己的 .text:系统调用号塞 x8、参数塞 x0-x5,自己执行 svc 进内核。这条调用链上压根不经过 libc,你挂在 libc 包装函数上的 hook,挂在了一个永远不被调用的位置上——这才是「挂了一圈却零命中」的机理。从内核视角看,这次 svc 和 libc 发的没任何区别;差别只在用户态那段调用链被它绕开了。
这个差异后面成了归属的核心杠杆:MSA 没有 inline svc(静态确认 0 个),任何动作只能走 libc;libnesec 有,能在 libc hook 的视野外行动。
想直接 hook 那条 svc 指令?实测 hook libnesec 的热路径 svc 桩(0x27dfc)会因 Interceptor.attach 的指令重定位破坏它的控制流,+13 秒崩。hook libnesec 任意 .text 桩本身就有害。加上它运行时代码是 CFG 平坦化混淆,r2 的自动 xref 直接废掉,静态追扫描器循环比未加密的 MSA 难一个量级。

四、MSA:第二个凶手与 12-NOP

libmsaoaidsec(MSA)是第二个独立反调试单元,未加密,r2 直接啃。机制和 libnesec 完全不同:
  • ptrace 自调试状态机0x1a24c ptrace(SETOPTIONS)、0x1a298 ptrace(CONT)、0x1a2dc ptrace(DETACH),外加 fork 子进程 ptrace 自己(0x1a0d8 是配套的 waitpid 事件循环)。
  • /proc 扫描:maps、task/stat、fd(找 frida socket)、cmdline。
  • 击杀:exit×3、\_exit×4——全走 libc(它没有 inline svc)。
破它用 12-NOP:把所有 exit/\_exit/ptrace 击杀点所在的 12 个函数入口,全替换成「直接 return 0」。这 12 个偏移用 r2 在未加密的 MSA 上反查 exit/\_exit 的 caller、aac 全量验证得到:
图: 这 12 个点不是散落的——图里看得出它们扎堆在两处:so 加载时跑的 _init 构造链,和一个每 4 秒醒一次的轮询循环。要 NOP 的就是这两簇的入口。
notion image
libmsaoaidsec.so 的 MSA 反调试函数:12 个 return-0 NOP 目标,集中在 \_init 链 + 4 秒轮询
⚠️ **NOP 必须 return 0(int/long),不能 void**。void 版会 SIGSEGV——0x1a0d8 的返回值被调用方用 cbz x0 判断,return 0(写 w0 自动清 x0 高位)才能让 cbz x0 命中走非退出分支。
实测 12-NOP 后 MSA 零次 exit、不崩、业务正常。但这里有一笔方法学纠错:我早期武断认为「只有 libnesec 在杀」是错的——MSA 也在独立地杀,只是被我一个 antiExit hook 掩盖了。多个独立机制赛跑,你拦住了一个,会误以为另一个不存在,这是这类对抗最容易犯的归因错误。

五、同意页 attach 窗口

signalProbe + MSA 12-NOP + patched frida-server(改线程名)凑齐,已经能在冷启动里抓到 serialdata 突发了——但会话存活是 racy 的,+9 到 +160 秒不等,那只看不见的手还在。
僵持了一阵,是一个观察打开局面的:安全 SDK 受用户同意门控——隐私合规要求点「同意」之前不采集、不全量启动反调试。所以在隐私协议同意页那一刻,app 是「干净态」。
威力体现在一个现象上:frida -U -N com.netease.cloudmusic-N = 按包名 attach)在同意页 attach 成功、而且不被秒杀;而 spawn、或点完同意再 attach,都会被反制。这也解释了长期困扰我的怪事——「Frida 按名枚举不到主进程」,那其实是同意之后的隐藏态
图: 顺着时间轴看中间那段绿色「干净窗口」——它正卡在「同意页出现」和「点同意」之间;下面两张卡对比的,就是在窗口里 attach 先占位、和 spawn / 点完再 attach 硬刚,两条路截然不同的下场。
notion image
同意页 attach 时序:干净窗口 attach 成功,点同意后进入全量反调试与隐藏态
于是流程变成:
这个窗口的意义不止「能抓 serialdata」。它揭示了关键时序:反调试是在「同意之后」才全量启动的;attach 能「先占位、后对抗」,优于 spawn 的「所有检测同时 race」。更重要的是——它给了我一个单变量实验台:在干净窗口 attach,再精确控制「往里加什么」,看什么时候被杀。

六、六个单变量实验:重建击杀模型

都在同意页 -N attach 之后做,单变量:
| # | hook 内容 | 存活 | 现象 | | :-- | :-- | :-- | :-- | | 1 | 纯 Java serialdata,零 native 修改 | +34.5s | 干净 terminated(无 crash),✅ 抓全 5 接口 | | 2 | libc exit/\_exit/abort 中和 + 信号捕获 | +33.0s | 干净 terminated,所有 hook 零命中 | | 3 | \+ hook libnesec .text 的 svc 桩 | +13.1s | 提前崩(指令重定位破坏控制流) | | 4 | libc 中和 + 观测(无安全库 .text 改) | +9.4s | SIGSEGV,PC = frida agent 内部 | | 5 | 零 hook(连 serialdata 都不挂) | +97.7s | SIGSEGV in libmsaoaidsec 构造函数 | | 6 | 纯 Java serialdata,回调零管道活动 | +7.9s | 突发前就被杀 |
几个关键转折:
  • 实验 1 打脸了我的第一个假设「代码篡改自校验是凶手」:纯 Java、零 native .text 修改,照样被杀。基线击杀和 .text 改动无关。
  • 实验 2 用「能力排除法」把矛头指回 libnesec:MSA 没有 inline svc,要杀只能走 libc;我把 libc 全中和了还零命中,所以这次非 libc 的击杀不可能是 MSA,凶手得具备「绕 libc 直接 syscall」的能力。
  • 实验 4 抓到第一具「尸体」:SIGSEGV,#00 pc … /memfd:media-cache-64.so(改名后的 gum agent)。主线程经 hook 进入 agent、执行到被损坏处崩了——某个安全库用普通 store 指令直接改写了 agent 的内存(无 syscall,所以 hook 零命中)。这完美解释了「不可观测」。
  • 实验 5 最有信息量:零 hook 活了 97 秒(远超带 hook 的),然后崩在 MSA 的构造函数里——app 自己 loadLibrary("msaoaidsec") 时,MSA 构造期边扫描边检到 agent,故意 deref 一个毒指针自爆。
  • 实验 6 把我自己提的「快层 ∝ gum 活动量」假设也证伪了:quiet 版比 verbose 崩得更快、且崩在 hook 触发之前——说明是「装 hook 这个动作本身」触发的,与回调是否执行无关。
模型重建成两个独立的层
  • 慢层 = MSA 构造函数检到 agent → deref 毒指针自爆(~+97s,app 加载 MSA 时)。
  • 快层 = 随「装了任何 gum hook」即触发的 agent 内存损坏(racy +8~34s)。凶手 = libnesec / libpoison。
图: 下图上下两条泳道,就是这两个独立的层;对着看「破法」那一栏最关键——慢层有个明确的静态补丁点可下手,快层没有 syscall 能拦、只能绕开它认的那几类插桩。把这两条当成同一个凶手,正是前面一连串误判的总根。
notion image
两层击杀模型:慢层 MSA 构造自爆 + 快层 agent 内存损坏
抓 serialdata 因此是一场赛跑:快层击杀 vs 冷启动 serialdata 突发(+32s)。pm clear 后干净态胜率不低,多跑几次必中——所以「抓算法 I/O」这个目标到这里其实已经达成(含 /api/batch 二十多个子接口)。但「真机 >2min 在线」必须把两层都破掉。
这一节最想强调的方法学:做对抗逆向,你会不断从噪声里提炼出「看起来很顺」的假设,然后必须用单变量对照去杀它。我提了至少四个错误假设(代码篡改自校验、MSA 是运行期凶手、活动量、还有下一节的 maps-scan),每个都被实验证伪。能证伪自己的假设,比能提出假设更重要。

七、慢层:静态补丁破 MSA 自爆

两层分头破(就是上图那两条泳道),先拿好对付的开刀。慢层好啃——MSA 未加密。崩溃栈给了四帧,顺调用链 0x14824 → 0x95f8 → 0x11f64 → 0x20bdc,把崩溃函数 0x20bdc 反汇编,真相浮出来:它在遍历 linker 的 solist(已加载 so 链表),按 Android 版本选 soinfo 偏移,对每个模块名 strstr 匹配注入特征串——是个注入名检测器
崩因清清楚楚:walk 到 frida agent 的 soinfo 时,[x20+0x28] 取到毒指针 0x000b00020000641d0x641d + 0x188 = 0x65a5 → SIGSEGV,和崩溃报告 fault addr 0x000b0002000065a5 完全对上。
这函数本质是「某个名字的 so 加载了吗」的查询。最干净的补丁:让它永远返回 0(等价于「干净设备」,崩溃的 walk 也不再执行):
交付选「免重打包 / 重签」:app 经 System.loadLibrary 加载的是已解压/data/app/.../lib/arm64/libmsaoaidsec.so 的那一份,root 直接把这个文件换成补丁版即可——APK 本体不动,签名校验不受影响,pm clear 也不会重解压、补丁持久。验证:补丁前零 hook +97.7s 撞 MSA 构造崩;补丁后零 hook 满 180s 存活。慢层,破了。

八、快层:检测面与 winning recipe

快层是硬骨头。核心技巧是晚挂:前 ~45 秒全程 bare 过冷启动(已知能活),之后才装某一种东西,看会不会被杀、多久——把「冷启动 racy 噪声」和「稳态检测」分离开。
这里先诚实划一下嫌疑范围。我一度想「静态排除 libpoison」——它导入表(不加密的那部分)里没有 pthread_create / ptrace / kill,strings 也搜不到 frida 串,看着像个纯签名校验器。但整理代码包时才反应过来:libpoison 的代码段也是运行时解密的(磁盘密文,和 libnesec 同款),所以「strings 搜不到」根本不成立——是读不出,不是真没有。能确认的只有两条:导入表无 libc 线程 / ptrace(弱证据),以及后面实验 4 崩时只有 libnesec + libpoison 加载。所以快层凶手是 libnesec 或 libpoison 之一,这一轮没法干净排除哪个。好在后面的检测模型和 recipe 都不依赖「到底是这俩里的谁」,结论不受影响——但具体归属要诚实地标成「二选一」。
一个差点把我带沟里的假象:我一度认定快层 = libnesec 的 maps 扫描(插桩后 maps 多出 22~35 行 frida 足迹,看着很顺)。决定性对照实验:bare 过冷启动后,只 mmap 一个匿名 r-x 页、写个 ret 模拟 trampoline,不下任何 hook。结果——maps 行数确实+1,但活满 100 秒。「maps-scan 是杀手」被亲手证伪,连带把「过滤 maps」这个方向一并排除。
然后用一组「晚挂某一种东西」的实验,把检测面一刀刀切出来:
| 晚挂的东西 | 结果 | 推论 | | :-- | :-- | :-- | | native hook 一个从不被调的 libc 函数 catopen | 活满 100s | .text 改动不是全局检测,hook libc 没事 | | native hook libpoison 的 serialdata(+0x7a28c) | +1.3s 被杀 | libpoison 自校验自己的 .text | | Java hook serialdata / 它的调用方 | +0.7~4.4s | 任何 Java/ART hook 都被检,不是 serialdata 专属 | | 硬件断点(零 .text 改,仅置调试寄存器) | +1.5s 被杀,TracerPid=0 | 连硬件断点都被检 |
拼起来,正确的快层模型是:
**libnesec/libpoison 的快层只检测三类,碰即 ~1.5s 被杀:① Java/ART 方法 hook(改 ArtMethod 入口);② 安全库自身 .text 被内联 hook(自校验代码);③ 硬件断点 / 调试寄存器。它不检测:native Interceptor.attach hook 非安全库的 .text;也不检测匿名 r-x 映射本身。**
图: 左栏红的三类,是碰一下就 ~1.5s 被杀的「禁区」;右栏绿的两类是安全区。recipe 说穿了就是把所有落点都搬进右栏——native attach hook 非安全库的 SSL_write
notion image
快层检测面:三类被检 / 两类不检
模型一对(就是上图右栏那两类安全区),破法就自己浮出来了:只用 native Interceptor.attach hook 非安全库,三类被检的全不碰。serialdata 本身在 libpoison(hook 它就被杀),但它的算法 I/O 早抓全了(实验 1/2),我现在只要「>2min 在线 + 抓流量」——那就 hook 一个非安全库里能拿到明文的函数:**conscrypt 的 SSL_write / SSL_read**(在 libssl.so / libjavacrypto.so,承载 OkHttp 走的 API 明文)。
dump() 只是把 buf 截断转成可读字符串打印的辅助函数,连同轮询保活的完整实现见 scripts/frida17/frida/recipe/early_native_ssl.js。)
实测会话满 240 秒不被杀,全程 hooked=true,抓到真实 API 明文(Host: ac.dun.163.com 易盾、gtc.getui.net 个推、Geetest 风控头)。复验三次稳定。「真机 frida 内容页 >2min 在线」——达成。
一句话 recipe:
pm clear → 同意页 -N attach → **只用 native Interceptor.attach hook 非安全库**(SSL_write 抓明文)→ 绝不碰 ①任何 Java/ART hook ②libpoison/libnesec(含 serialdata 本体)的 .text ③硬件断点。三类被检手段全避开,即可长时间存活。
收尾留个尾巴:抓到的 SSL 明文全是 SDK / 风控流量,没有 music.163 的音乐主 API——它走 cronet(HTTP/2 + HPACK 压缩头 + eapi 加密 body 三层封装),就算在线抓到也不是可读参数。而真正有价值的 serialdata 明文 I/O(path/params → 签名)在冷启动突发里早抓全了。所以绕开 cronet 那三层、直接抓 serialdata 入口,才是对的路

九、跨设备:三台机,三种命运

开篇说的「之前绕得顺、本周换手机就翻车」,翻车现场就在这一节。同一条 recipe、同一个 patched frida-server,搬到另外两台机上:一台要先重启才肯就范,另一台直接全军覆没。这才看清——recipe 不是对「这个 app」生效,是对「app + 这台机 + 这个 Android 版本 + 设备当前状态」生效
| 设备 | 系统 | 结果 | 说明 | | :-- | :-- | :-- | :-- | | Pixel 5 #1 | Android 14 | 通过(160~240s) | 主力机 | | Pixel 5 #2 | Android 14 | 通过(160s) | 但需先重启设备 | | Pixel 6 Pro | Android 13 | 失败 | agent ~1s 即被杀,对一切手段免疫 |
  • Pixel 6 Pro(Android 13)recipe 不可移植:agent 在 attach/spawn 后 ~0.8~1.0s 即被杀(干净 terminated = inline-svc exit,绕 libc)。还原原版 MSA 后 bare 仍 +0.8s 被杀,证明和 MSA 补丁无关,是纯 libnesec。Android 14 的「同意页干净窗口」是 Android 14 专属;同一个 libnesec 二进制,Android 13 几乎立刻检出 bare agent 的存在本身,比 Android 14 的「快层只认 hook」狠得多。要在 P6 跑通得对 Android 13 重做一轮(规模≈整轮)。
  • Pixel 5 #2 的「退化态」:同型号同系统同 server,初始 bare 也 +1.4s 秒杀。原因是这台机有重度 florida 历史,libnesec 处于「已检测到 frida」的退化态,而且**深到 pm clear 都清不掉**(检测串里有 nedata.db / 163envsec0 这类持久化痕迹)。最后重启设备才恢复。运维结论:跨机验证前先 reboot、先确认 Android 版本——这套防护对「app + Android 版本 + 设备当前状态」三者都敏感。

十、方法学沉淀

这一程最大的价值不在最后那条 recipe,而在中间一串被证伪的假设:
  1. 代码篡改自校验是凶手 —— 实验 1(纯 Java 零 native 修改照样被杀)证伪;
  1. MSA 是运行期凶手 —— 实验 2(MSA 无 inline svc、libc 中和零命中)证伪,MSA 只在构造期自爆,是慢层;
  1. 快层 ∝ gum 活动量 —— 实验 6(quiet 比 verbose 崩得更快、且崩在 hook 触发前)证伪;
  1. maps-scan 是杀手 —— 匿名 r-x 实验(写 ret 模拟 trampoline 活 100s)证伪。
三条可复用的判断:
  • 归属用能力排除法:对手不可观测(inline svc 绕 libc、store 损坏内存于无形)时,你抓不到现行,但能靠「谁有 inline svc 能力 / 谁的 .text 被 hook 会自校验」把嫌疑人逐个排掉。这比「我看见它干了」弱,但在对手不留痕迹时是最强证据。
  • 退出诊断表:排查时先看 Frida / logcat 报的是哪种「没」,再决定改哪一层——
| 现象 | 含义 | 对策 | | :-- | :-- | :-- | | Process terminatedpidof 还在 | agent 被损坏,进程没事 | 快层命中:避开 Java hook / 安全库 .text / 硬件断点 | | 干净 terminated(无 crash),libc hook 零命中 | inline-svc exit,绕 libc | 凶手具备 inline svc 能力(libnesec/libnesdk) | | SIGSEGV in libmsaoaidsec 构造函数 | 慢层 MSA 自爆 | 补丁 0x20bdc → return 0 | | SIGSEGV,PC = memfd agent | agent 内存被 store 损坏 | 同快层 |
  • 环境敏感性:recipe 不是对「这个 app」生效,而是对「app + Android 版本 + 设备当前状态」生效。验证前先 reboot、先确认 Android 版本,是必须的卫生习惯。

📦 获取本篇脚本

本篇整套网易云音乐反调试 / 反 Frida 对抗脚本(signalProbe 过 SIGTRAP 自检 + MSA 12-NOP 与静态补丁 patched_libmsaoaidsec.so + 同意页 -N attach 驱动 + 重建双层击杀模型的实验矩阵(六个单变量 + 晚挂系列)+ winning recipe「只 native 钩非安全库、真机 >2min 在线 + 抓 API 明文」+ serialdata native 定位 / libnesec 内存 dump / cronet 明文点定位 + svc 扫描等静态分析工具 + run.md 真机验证清单,30+ 脚本复制即用)已打包:
  1. 关注本公众号
  1. 私信回复关键词「脚本」
回复内含本系列与其它系列(Unidbg / SO 逆向 / ARM 汇编 ……)的脚本汇总,长期维护更新。
上一篇
Frida学习笔记(十八):反调试与反检测对抗(下)· 招商银行三套 SDK 实战
下一篇
Frida学习笔记(十六):Root 检测绕过

Comments
Loading...