现代 Android 设备的安全防御层次在持续演进,但攻击者的利用手法也在同步升级。2025 年中,Google Project Zero 发布了针对 Pixel 9 的完整零点击漏洞链,展示了如何利用 Dolby 统一解码器(UDC)中的内存破坏漏洞,配合 BigWave 内核驱动中的释放后使用(UAF)缺陷,从一条未读音频消息出发,在无需任何用户交互的情况下实现完整 root 接管。这一成果的发布不仅验证了零点击攻击在现代 Android 安全环境下的实际可行性,也暴露了媒体解码器、驱动程序和沙盒隔离机制中存在的深层安全隐患。
到了 Pixel 10,攻击者面临的局面发生了显著变化。Pixel 10 引入了 RET PAC(返回地址指针认证)作为主要的栈保护机制,这一硬件级缓解措施取代了 Pixel 9 中使用的 -fstack-protector 编译器插桩。这意味着传统的利用 __stack_chk_fail 函数指针进行控制流劫持的策略在 Pixel 10 上不再适用。Project Zero 的研究团队在将漏洞链从 Pixel 9 移植到 Pixel 10 的过程中,必须重新审视整个利用链的构造逻辑,尤其是从 mediacodec 沙盒中的代码执行(mediacodec context RCE)跃迁到内核任意读写(kernel arbitrary read/write)的关键阶段。理解这些技术细节,对于安全工程师评估设备风险、制定防御策略具有重要的参考价值。
零点击入口:Dolby UDC 漏洞的 Pixel 10 适配
Pixel 10 上的零点击入口点仍然来自 Dolby 统一解码器中编号为 CVE-2025-54957 的漏洞。该漏洞根植于 EMDF(Extensible Metadata Delivery Format)容器解析逻辑中的一个整数溢出错误:当解码器处理包含 emdf_payload_size 的 EMDF 有效载荷时,分配大小的计算过程未能正确检查整数溢出。攻击者可以通过构造特殊的杜比数字 Plus(DD+)音频文件,使解码器分配一个远小于预期的堆缓冲区,同时根据原始 payload_length 值向该缓冲区写入超长的数据,从而实现相邻内存区域的越界写入。
在 Pixel 9 的原始利用中,这个越界写入原语被用于覆写 __stack_chk_fail 函数指针。由于 Pixel 9 的 libcodec2_soft_ddpdec.so 库使用 -fstack-protector 编译,编译插入的栈保护失败处理函数位于一个可预测的地址,攻击者可以直接将控制流导向该函数,进而执行任意代码。然而,Pixel 10 采用 RET PAC 作为栈保护方案后,这个函数指针不再存在于可利用的位置。Project Zero 的研究人员在移植过程中发现,Pixel 10 使用 RET PAC 替代了 -fstack-protector,这意味着 __stack_chk_fail 符号根本不可用。
解决这一限制的方案是利用库中其他可覆写的初始化代码段。经过反复试验,研究团队确定 dap_cpdp_init 是一个理想的替代目标。这段代码在解码器初始化时仅被调用一次,之后不会再被执行。更重要的是,它位于库内存空间的一个固定偏移处,且覆写该函数指针不会导致解码器立即崩溃。通过将 dap_cpdp_init 的入口点替换为 ROP gadget,然后借助 DLB_CLqmf_analysisL 函数中的间接调用机制,攻击者可以在 Pixel 10 上重建从越界写入到代码执行的完整路径。整个适配过程主要涉及更新偏移量以匹配 Pixel 10 库文件中的实际地址,核心利用逻辑保持不变。
RET PAC 环境下的代码执行:从函数指针覆写到 ROP 链
RET PAC 的核心机制是对函数返回地址进行加密签名验证。当处理器执行一条返回指令时,它会检查栈上存储的指针是否具有正确的 PAC 签名。如果签名无效,处理器会将指针的最高位设置为 1,导致控制流跳转到一个无效地址而非预期的返回位置。这种机制有效地阻止了传统的栈溢出攻击,因为攻击者即使成功覆写了返回地址,也无法提供有效的 PAC 签名来通过验证。
然而,RET PAC 并不能保护所有形式的控制流转移。在 DLB_CLqmf_analysisL 函数中,存在两个函数调用点,其中一个是间接调用 —— 通过读取内存中的函数指针并调用它。这种间接调用的目标不受 PAC 保护,因为 PAC 签名验证只应用于返回地址,而不包括通过寄存器传递的函数指针。攻击者正是利用了这一特性,通过 WRITE STATIC 原语覆写静态缓冲区中的函数指针,然后触发 DLB_CLqmf_analysisL 中的间接调用,使控制流跳转到选定的 ROP gadget。
在 Pixel 10 上,可用的 ROP gadget 范围受到库加载基址随机化的限制。由于库文件的高位地址(第二个字节的 nibble)被 ASLR 随机化,攻击者必须猜测这个值才能构造有效的 ROP 链。根据 Project Zero 的测试,这个 nibble 的值在 0 到 15 之间均匀分布,这意味着单次尝试的成功概率为 1/16。结合其他 ASLR 相关的概率因素(如动态缓冲区位置的猜测),完整的利用链成功率约为 1/255。虽然这一数字看似很低,但考虑到攻击可以在后台自动重复进行(每次尝试间隔约三秒),平均在六分钟内即可成功完成一次完整的漏洞利用。
沙盒逃逸:BigWave 驱动的释放后使用与内核读写原语
从 mediacodec 沙盒中的代码执行到内核级任意读写,需要跨越 SELinux 强制访问控制和内核内存布局两重障碍。Project Zero 在 Pixel 9 和 Pixel 10 上均使用了 BigWave 驱动程序中编号为 CVE-2025-36934 的漏洞来实现这一步骤。BigWave 是 Pixel 系统芯片(SoC)中用于加速 AV1 视频解码的硬件组件,其对应的内核驱动在 /dev/bigwave 设备节点上实现了 ioctl 接口,该接口可以从 mediacodec SELinux 上下文直接访问。
这个漏洞的根本原因在于驱动中 job 结构的生命周期管理存在竞态条件。当用户空间进程调用 BIGO_IOCX_PROCESS ioctl 提交一个解码任务时,内核会在当前进程上下文中将 job 放入优先级队列,然后进入等待状态,等待 BigWave 工作线程完成任务。如果工作线程因负载过高而延迟(例如队列中积压了大量任务),ioctl 的 16 秒超时期限可能在工作线程实际开始处理该任务时到期。此时,ioctl 代码会认为任务已超时并尝试将其从队列中移除,而工作线程恰好在同一时刻正在使用该 job。更关键的是,当用户进程关闭对应的文件描述符时,inst 结构(包括内联的 job)会被释放,但工作线程仍然持有指向已释放内存的指针。
通过向内核堆喷射大量可控的 kmalloc 分配(例如通过 Unix Domain Socket 消息),攻击者可以控制被释放 job 结构中 regs 字段的内存布局。当 bigo_pull_regs 函数从已释放的 job 结构中读取 regs 指针并调用 memcpy_fromio 时,它实际上是将 BigWave 处理器的寄存器值写入攻击者指定的内核内存位置。这就是半任意写原语(semi-arbitrary write primitive)的来源 —— 攻击者可以控制写入的目标地址和内容(通过在提交任务时预先设置 BigWave 寄存器状态)。
值得注意的是,Pixel 设备上的 kASLR(内核地址空间布局随机化)存在一个已知弱点,Project Zero 在此前的博客文章中已有详细讨论。该弱点允许攻击者使用固定的线性映射地址 0xffffff8000010000 来引用内核数据段中的全局变量,而无需实际泄漏内核基址。这一特性显著简化了漏洞利用的开发工作,并提高了利用的可靠性。
从任意写到可靠读写:VFS 处理函数替换技术
原始的 UAF 写原语存在两个主要缺陷:写入范围过大(2144 字节)可能导致附带内存损坏引发崩溃,且写入操作本身不可靠(取决于堆布局)。为了将这个不稳定的写原语转化为可靠的任意读写能力,攻击者采用了 VFS(虚拟文件系统)处理函数替换技术。
具体而言,攻击者首先利用 UAF 写原语覆写 ashmem_misc 数据结构中的文件操作函数表(fops),将其中部分处理函数替换为 configfs 的对应函数。通过精心构造被覆写数据结构中 private_data 字段的布局,可以创建一个类型混淆:某些 VFS 操作将 private_data 解释为 ashmem_area 结构,而其他操作则将其解释为 configfs 缓冲区。这种设计使攻击者能够通过修改 ASHMEM_SET_NAME ioctl 设置的目标地址,利用 configfs 的读写函数实现对任意内核内存位置的可靠读写。
在此过程中,攻击者还需要绕过 CFI(控制流完整性)保护。由于 Pixel 10 的内核启用了 CFI,函数指针必须指向具有兼容签名的代码位置,不能随意跳转到任意地址。configfs 的 VFS 处理函数恰好具有与 ashmem 兼容的函数签名,因此成为理想的替换目标。
为了获取可靠的任意读能力,攻击者首先将 sel_fs_type 内核对象中的 name 字符串指针覆写为指向攻击者控制的内存区域,然后读取 /proc/self/mounts。由于内核在打印挂载信息时会引用这个字符串指针,攻击者可以通过观察读取到的内容来推断内核内存的值。借助这一信息,攻击者可以读取 ashmem_fops 结构,获取内核代码段中各函数的实际偏移量,从而计算出 kASLR 基址。完成这些信息收集后,攻击者再次使用 UAF 写原语,将 ashmem_misc 结构完整覆写为精心构造的伪造版本,其中包含指向攻击者构建的伪造 fops 表的指针。
最终,借助可靠的任意读写能力,攻击者将 SELinux 状态标志设置为 permissive 模式(从而禁用强制访问控制),然后 fork 一个新进程并利用任意写原语将新进程的 task_struct 中的 cred 指针指向 init_cred。此时,新进程具有 root 身份且 SELinux 被禁用,实现了完整的权限提升。
利用链的成功率与防御启示
整个 Pixel 10 零点击漏洞链的成功率受到多个随机化因素的综合影响。首先,动态缓冲区位置的 ASLR 随机化要求攻击者猜测 4 位 nibble(1/16 概率)。其次,库文件加载基址的高位地址同样被随机化,需要额外猜测一个 nibble(1/16 概率)。第三,堆喷射过程中可能与其他分配发生冲突(约 10% 概率)。综合这些因素,整体成功率约为 1/255。
从防御角度来看,这一研究揭示了多层安全防御中仍然存在的薄弱环节。RET PAC 虽然有效阻止了传统的栈溢出利用,但间接函数调用和全局数据结构仍然可以成为攻击者的突破口。SELinux 虽然限制了 mediacodec 上下文的系统调用权限,但 /proc/self/mem 仍然可以被打开用于注入 shellcode。kASLR 未能保护内核数据段中的固定偏移对象,使得通过线性映射地址访问内核全局变量成为可能。
针对这些攻击向量,有效的缓解措施包括:在媒体解码器中启用 -fbounds-safety 编译器标志(如 Apple 在 macOS/iOS 上的 Dolby UDC 所做的那样),确保 seccomp 策略正确限制 mediacodec 进程的系统调用能力,对内核数据段中的关键对象应用更细粒度的随机化,以及考虑将高风险驱动程序(如 BigWave)重写为 Rust 等内存安全语言。同时,设备制造商应建立快速响应机制,确保关键漏洞的修复补丁能够在合理时间内推送给用户,尤其是对于可通过零点击方式触发的漏洞。
参考资料:
- Project Zero Blog: A 0-click exploit chain for the Pixel 9 Part 1: Decoding Dolby (https://projectzero.google/2026/01/pixel-0-click-part-1.html)
- Project Zero Blog: A 0-click exploit chain for the Pixel 9 Part 2: Cracking the Sandbox with a Big Wave (https://projectzero.google/2026/01/pixel-0-click-part-2.html)
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。