单漏洞提权:Pixel 6 Pro Mali GPU内核漏洞分析与利用

本文详细分析了Pixel 6 Pro上Mali GPU驱动中的CVE-2023-48409整数溢出漏洞,探讨了如何在仅有一个漏洞的情况下,通过页表喷射、物理映射等技术绕过CONFIG_HARDENED_USERCOPY防护,最终实现从用户空间获取Android内核的完整root权限。

Solo: A Pixel 6 Pro Story (当仅需一个漏洞时) | STAR Labs

June 5, 2025 · 36 min · Lin Ze Wei

目录

Root Cause Analysis

CVE-2023-48409

在12月2023年的Pixel安全公告中提到了该漏洞。通过回滚设备版本至UP1A.231005.007进行验证。漏洞描述为:在gpu_pixel_handle_buffer_liveness_update_ioctl函数中,由于整数溢出可能导致越界写入,本地攻击者无需额外权限即可提权。

漏洞代码位于private/google-modules/gpu/mali_kbase/mali_kbase_core_linux.c。关键问题在于计算要分配的内核对象大小时发生整数溢出:

1
2
3
4
u64 const buffer_info_size = sizeof(u64) * update->buffer_count;
u64 const live_ranges_size =
    sizeof(struct kbase_pixel_gpu_slc_liveness_mark) * update->live_ranges_count;
buff = kmalloc(buffer_info_size * 2 + live_ranges_size, GFP_KERNEL);

攻击者可以控制buffer_countlive_ranges_count,使得分配大小计算溢出,导致live_ranges缓冲区位于分配对象之前,形成缓冲区下溢。

该原语具有两个主要限制:

  1. 分配的对象在触发下溢后立即被释放
  2. 在启用CONFIG_HARDENED_USERCOPY的内核中,直接覆盖SLUB分配器中的对象会被禁止

What is CONFIG_HARDENED_USERCOPY?

CONFIG_HARDENED_USERCOPY是自Linux v4.8引入的内核配置。它在执行copy_to_user()copy_from_user()时检查内存区域,拒绝那些大于指定堆对象、跨多个独立分配页面、不在进程栈中或是内核文本部分的访问。这有效阻止了堆溢出攻击。

具体而言,如果指针位于SLUB对象内,它会确保读/写区域完全落在该对象的可用区域内(基于缓存的object_size)。从v4.16开始,白名单区域进一步限制为对象的usercopy区域。

对于我们的漏洞,这意味着如果没有CONFIG_HARDENED_USERCOPY,我们可以轻易地覆盖任何相邻对象。但启用后,许多越界攻击技术会直接导致内核崩溃。

The Missing Piece: CVE-2023-26083

原始漏洞利用使用第二个漏洞CVE-2023-26083来获取地址泄露。该漏洞涉及驱动中的时间线流(tlstream)功能,它会将某些对象的地址以纯字节形式泄露给用户空间。特别是kbase_kcpu_command_queue对象,可以被自由喷射、定向释放,并将其地址泄露到流中。

然而,在我们的Pixel 6 Pro设备上,这个漏洞无法直接使用,因为相关代码在我们的驱动版本中甚至没有被编译。

One Bug to Root

由于我们只有一个漏洞且受到CONFIG_HARDENED_USERCOPY的限制,我们需要寻找替代方案。我们决定直接攻击页分配器而不是SLAB分配器。

kmalloc请求的大小超过一定阈值(通常是8KB,对应order-1块分配)时,内核会将分配委托给页分配器,从而绕过CONFIG_HARDENED_USERCOPY的保护,但至少会分配一个order-2块(0x4000字节)。

Physmap Spraying

我们使用physmap喷射技术来"猜测"地址。通过耗尽页分配器,我们可以增加目标对象在我们选择的地址上分配的概率。我们编写了一个虚拟驱动程序来强制内核分配尽可能多的页面,并观察分配模式。

通过选择在多次运行中一致出现的地址,我们实际上模拟了一个地址泄露。

Pagetable Spraying

我们采用了"脏页表"技术。页表本身是由页分配器分配的普通order-0块。通过写原语,我们可以修改页表项,使得映射的用户空间虚拟页面反映几乎任何任意的物理页面。

这种方法比pipe_buffer更具可喷射性,每个进程可以分配大量页表。我们需要同时检查两个地址(因为mmap页面和页表页面具有不同的迁移类型),以可靠地找到页表。

通过喷射页表,我们还能耗尽order-2空闲列表,使得后续分配更容易获得连续的order-2块。

From Physical to Virtual: Converting Our Primitive

我们通过以下步骤建立完整的利用链:

  1. 分配两个连续的order-2 pipe_buffer数组
  2. 释放第一个数组,并喷射更多页表对象
  3. 释放第二个数组,并触发漏洞分配恶意对象,覆盖页表项
  4. 通过修改页表项,我们可以实现任意物理地址的读/写

然后,我们可以在页表喷射期间散布有用的对象,如pipe_buffer,从而泄露内核文本指针并获取虚拟地址的任意读/写能力。

Android’s Unique Challenges

Bypassing Android’s App Sandbox

在Termux环境中,当我们尝试调用setuid(0)时,程序会因seccomp过滤器而崩溃。Android默认阻止了某些系统调用,包括setuidsetgid

有趣的是,setresuid是明确允许的。我们可以将setuid(0)替换为setresuid(0, 0, 0)来绕过这个限制。对于set*gid系统调用,我们可以通过修补允许的系统调用(如getuid)来重定向到setuid(0)setgid(0)

Disabling SELinux Enforcement

在Android环境中,我们还需要绕过SELinux。一旦我们拥有内核写原语,我们可以清除selinux_state.enforcing位来完全禁用SELinux。

A Surprise Discovery

Understanding the Timeline Stream Leak

当我更仔细地研究CVE-2023-26083时,发现时间线流漏洞实际上在我们的设备上存在。问题在于原始漏洞利用试图泄露kbase_kcpu_command_queue对象的地址,而这是一个CSF特性,在Pixel 6 Pro上不可用。

时间线流本身可以被非特权进程访问,并且确实会泄露内核地址。但我们需要寻找不同的对象来泄露其地址。

Command Stream Frontend

Pixel 6 Pro使用Mali-G78 GPU,这是第二代Valhall GPU,不支持CSF。CSF支持由MALI_USE_CSF配置控制。由于我们的设备没有CSF,相关代码没有被编译。

然而,时间线流获取的ioctl调用仍然可以工作,只要我们不设置CSF特定的标志。

The Golden Object: kbase_context

通过搜索源代码,我找到了一个不同的低门槛目标:kbase_context对象。这个对象的地址会在上下文创建时立即泄露,并且它包含一个指向我们当前任务的task_struct指针。

kbase_context代表进程与驱动程序交互期间的会话信息。我们可以在触发漏洞之前轻松获取这个对象的地址。

Cleaning Up: Making the Exploit Practical

利用这个泄露的地址,我们可以:

  1. 通过kbase_context泄露当前任务的task_struct
  2. 获取内核文本指针(如restart_block.fn
  3. 禁用SELinux
  4. 获取特权凭据指针(通过kthreadd_task
  5. 覆盖当前任务的凭据指针

为了链接多个下溢操作,我们利用了页分配器空闲列表的LIFO特性。我们可以通过调整管道大小来释放和重新分配order-2块,从而控制漏洞触发的位置。

最终,我们实现了从用户空间到Android内核的完整提权,仅使用一个漏洞。

Conclusion

这项研究成功证明Pixel 6 Pro可以仅使用一个漏洞被利用,挑战了传统上认为需要CVE-2023-48409和CVE-2023-26083两个漏洞的认知。

Android有其独特的特点,既加强了防御,也(可能无意中)削弱了某些保护措施。这项研究展示了简单弱点如何在复杂的内核驱动中被发现,以及它们可能造成的损害。

References

  • GitHub: 0x36/Pixel_GPU_Exploit
  • 源码: elixir.bootlin.com/linux, android.googlesource.com
  • CVE详情: CVE-2023-48409, CVE-2023-26083
  • 相关技术文档和演示
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计