深入解析Pixel 6 Pro Mali GPU漏洞利用:单漏洞实现权限提升

本文详细分析了CVE-2023-48409漏洞在Pixel 6 Pro设备上的利用过程,通过整数溢出实现内核越界写入,结合物理内存喷射和页表操纵技术,最终绕过Android沙箱和SELinux防护获得root权限。

Solo: A Pixel 6 Pro Story (When one bug is all you need)

目录

  • 根本原因分析
    • CVE-2023-48409
    • 什么是CONFIG_HARDENED_USERCOPY?
    • 缺失的部分:CVE-2023-26083
  • 单漏洞获取root权限
    • 物理映射喷射
    • 页表喷射
    • 从物理到虚拟:转换原始能力
  • Android的独特挑战
    • 绕过Android应用沙箱
    • 禁用SELinux强制
  • 意外发现
    • 理解时间线流泄漏
    • 命令流前端
    • 黄金对象:kbase_context
    • 清理:使漏洞利用更实用
  • 结论
  • 参考文献

根本原因分析

CVE-2023-48409

在实习期间,我的任务是分析Pixel 7/8设备上的Mali GPU漏洞利用,并将其适配到另一台设备:Pixel 6 Pro。虽然漏洞利用过程本身相对容易复现(理论上我们只需要找到目标设备的正确符号偏移和签名),但Pixel 6 Pro的有趣之处在于它使用了与Pixel 7/8不同的Mali GPU,缺乏漏洞利用中两个漏洞之一所依赖的功能支持。

但等等,我们真的需要两个漏洞都能工作吗?

这里,我们将首先深入探讨另一个漏洞:CVE-2023-48409,它似乎更容易被利用。这个CVE在2023年12月的Pixel安全公告中有所覆盖;参考内部Bug ID,我们实际上可以找到我们设备的确切补丁,验证了该漏洞确实在2023年12月SPL之后被修复。因此,我们将设备版本回滚到更早的补丁,在我们的案例中是UP1A.231005.007。

从描述中:

在gpu_pixel_handle_buffer_liveness_update_ioctl函数中(位于private/google-modules/gpu/mali_kbase/mali_kbase_core_linux.c),由于整数溢出可能导致越界写入。这可能导致本地权限提升,无需额外的执行权限。利用不需要用户交互。

该漏洞是由整数溢出引起的,当计算在内核对象分配的大小以准备GPU驱动程序中的操作(非常描述性地处理活跃度更新)时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int gpu_pixel_handle_buffer_liveness_update_ioctl(struct kbase_context* kctx,
                                                  struct kbase_ioctl_buffer_liveness_update* update)
{
	int err = 0;
	struct gpu_slc_liveness_update_info info;
	u64* buff;

	/* 计算我们需要从用户空间复制的数组大小 */
	u64 const buffer_info_size = sizeof(u64) * update->buffer_count;			// [1]
	u64 const live_ranges_size =
	    sizeof(struct kbase_pixel_gpu_slc_liveness_mark) * update->live_ranges_count;
	/* 无事可做 */
	if (!buffer_info_size || !live_ranges_size)
		goto done;

	/* 防止空指针 */
	if (!update->live_ranges_address || !update->buffer_va_address || !update->buffer_sizes_address)
		goto done;
	/* 分配我们需要从用户空间复制的内存 */
	buff = kmalloc(buffer_info_size * 2 + live_ranges_size, GFP_KERNEL);			// [2]

	/* 通过指向分配来设置信息结构。全部8字节对齐 */
	info = (struct gpu_slc_liveness_update_info){						// [3]
	    .buffer_va = buff,
	    .buffer_sizes = buff + update->buffer_count,
	    .live_ranges = (struct kbase_pixel_gpu_slc_liveness_mark*)(buff + update->buffer_count * 2),
	    .live_ranges_count = update->live_ranges_count,
	};
	/* 从用户空间复制数据 */
	err =
	    copy_from_user(info.live_ranges, u64_to_user_ptr(update->live_ranges_address), live_ranges_size);
	if (err) {
		dev_err(kctx->kbdev->dev, "pixel: failed to copy live ranges");
		err = -EFAULT;
		goto done;									// [4]
	}
    
...

done:
	kfree(buff);
	return err;
}

具体来说,我们能够指定两个u64:buffer_count和live_ranges_count,使得驱动程序通过kmalloc(GFP_KERNEL)分配一个对象([2]),该对象应该容纳3个连续的缓冲区,所有这些缓冲区都是用户可控的([3]):

  • buffer_sizes,大小为sizeof(u64) * buffer_count
  • buffer_va,大小为sizeof(u64) * buffer_count
  • live_ranges,大小为sizeof(struct kbase_pixel_gpu_slc_liveness_mark) * update->live_ranges_count(结构体为4字节)

专注于live_ranges缓冲区:

  • 分配的对象大小:8buffer_count2 + 4*live_ranges_count([1]和[2])
  • 从分配对象的偏移:+ 8buffer_count2([1]和[3])

在这种情况下,如果值被精心构造,使得8buffer_count2 + 4live_ranges_count = (1«64) + <object_size>,我们的live_ranges缓冲区将位于分配对象之前4live_ranges_count - <object_size>字节处。幸运的是,由于live_ranges首先被写入,我们可以在写入过程中导致无效内存访问,从而中止整个操作,而不写入其他两个缓冲区(它们有明显的无效边界)([4])。因此,我们可以有效地将漏洞视为一个非常通用的原始能力:一个任意大小的kmalloc(GFP_KERNEL)配对一个任意长度(0x10字节对齐)的缓冲区下溢从分配的对象。

虽然这非常强大,似乎有很多方法可以从此分支,但这个漏洞有两个主要限制:

  1. 分配的对象在下溢后立即被释放。理论上,如果我们只需要下溢一次,这不应该是一个问题,我们也可以尝试找到另一个对象来“暂时”占据位置。
  2. 写入使用copy_from_user,并且下溢的设置方式实际上保证了会溢出受害者对象(进入我们实际分配的对象)。在启用CONFIG_HARDENED_USERCOPY的内核中,这意味着我们直接被禁止破坏SLUB分配器中的任何对象。

但首先,作为入门:

什么是CONFIG_HARDENED_USERCOPY?

CONFIG_HARDENED_USERCOPY,在这里引入,是一个内核配置,首次出现在Linux v4.8中。从Kconfig本身,我们有:

此选项通过在复制内存到/从内核(通过copy_to_user()和copy_from_user()函数)时检查明显错误的内存区域,拒绝大于指定堆对象的内存范围、跨越多个单独分配的页面、不在进程堆栈上或是内核文本一部分的内存范围。这杀死了整个类别的堆溢出漏洞和类似的内核内存暴露。

本质上,在copy_*_user期间,内核调用check_object_size,如果目标指针落在某些突出显示的区域,则执行特定的边界检查,并在违反边界时中止(BUG())。特别是,如果指针在SLUB对象内,它确保至少读/写区域必须完全落在该特定对象的可用区域内(基于缓存的object_size),使大多数对象溢出攻击无效。从v4.16开始,白名单区域进一步限制为仅对象的usercopy区域(基于缓存的useroffset和usersize),尽管对于通用缓存来说,它等同于整个对象区域。

为了理解影响的程度,我们必须首先理解大多数越界攻击,至少在配置引入时,专注于覆盖或泄漏放置在易受攻击对象旁边的单独受害者对象中的关键字段。这种情况下最常用的“技术”是slab分配的对象,尤其是通用对象,由于它们的使用灵活性以满足此类表面常见漏洞的需求。

放在上下文中,没有CONFIG_HARDENED_USERCOPY,我们讨论的漏洞可能非常通用,因为它有潜力由于其灵活的大小覆盖几乎任何我们想要的对象。例如,我们可以通过使用anon_vma_name对象轻松将我们的盲写转换为泄漏:

  1. 喷射anon_vma_name对象,理想情况下填充整个slab以获得更高的可靠性。
  2. 释放其中一个以创建一个洞,并在其位置分配易受攻击的对象,覆盖前面的anon_vma_name对象,使其char缓冲区扩展到后面的anon_vma_name对象。
  3. 如果我们事先标记了对象,我们现在已经找到了前面和后面anon_vma_name对象的id,有效地克服了CONFIG_SLAB_FREELIST_RANDOM:前面的对象将有其名称扩展,而后面的对象的id将出现在扩展的名称中。
  4. 释放后面的anon_vma_name对象并在其位置分配任何合适的对象(匹配大小和类型),我们想要泄漏其第一个字段。

在实践中,由于CONFIG_HARDENED_USERCOPY默认启用,尝试执行上述步骤2会立即使我们的内核崩溃。此功能完全阻止了许多越界漏洞,同时严重限制了其余漏洞的潜在选项。但在我们的案例中,我们仍然有办法。

缺失的部分:CVE-2023-26083

使用严重削弱

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计