Solo: A Pixel 6 Pro Story (当仅需一个漏洞时) | STAR Labs
June 5, 2025 · 36 min · Lin Ze Wei
目录
- Root Cause Analysis
- One Bug to Root
- Android’s Unique Challenges
- A Surprise Discovery
- Conclusion
- References
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。关键问题在于计算要分配的内核对象大小时发生整数溢出:
|
|
攻击者可以控制buffer_count和live_ranges_count,使得分配大小计算溢出,导致live_ranges缓冲区位于分配对象之前,形成缓冲区下溢。
该原语具有两个主要限制:
- 分配的对象在触发下溢后立即被释放
- 在启用
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
我们通过以下步骤建立完整的利用链:
- 分配两个连续的order-2
pipe_buffer数组 - 释放第一个数组,并喷射更多页表对象
- 释放第二个数组,并触发漏洞分配恶意对象,覆盖页表项
- 通过修改页表项,我们可以实现任意物理地址的读/写
然后,我们可以在页表喷射期间散布有用的对象,如pipe_buffer,从而泄露内核文本指针并获取虚拟地址的任意读/写能力。
Android’s Unique Challenges
Bypassing Android’s App Sandbox
在Termux环境中,当我们尝试调用setuid(0)时,程序会因seccomp过滤器而崩溃。Android默认阻止了某些系统调用,包括setuid和setgid。
有趣的是,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
利用这个泄露的地址,我们可以:
- 通过
kbase_context泄露当前任务的task_struct - 获取内核文本指针(如
restart_block.fn) - 禁用SELinux
- 获取特权凭据指针(通过
kthreadd_task) - 覆盖当前任务的凭据指针
为了链接多个下溢操作,我们利用了页分配器空闲列表的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
- 相关技术文档和演示