根因分析
该漏洞源于kbase_jit_grow函数中的竞争条件。当调用者请求的物理页数量超过kctx内存池(mempool)中的页数时,会暂时释放锁以便内核重新填充内存池。在锁被释放的竞争窗口期间,如果通过写入指令引发页面错误来增长JIT内存区域,实际的后备页面数量(reg->gpu_alloc->nents)将大于之前缓存的old_size值。在重新获取锁之后,kbase_mem_grow_gpu_mapping函数仍使用旧的old_size值进行计算,导致部分已映射和备份的页面未被正确映射。
后备页面
kbase_jit_grow调用kbase_alloc_phy_pages_helper_locked来添加后备页面。传入的delta参数是竞争窗口前缓存的旧值。该函数使用新的reg->gpu_alloc->nents值作为起始偏移量,从偏移nents到nents + delta分配物理后备页面。
映射页面
随后,kbase_jit_grow调用kbase_mem_grow_gpu_mapping来映射新页面。该函数将delta计算为info->commit_pages - old_size,并从old_size开始映射delta个页面。由于该函数不知道区域的实际nents已增加,它将无法映射最后的FAULT_SIZE个页面,从而留下一个部分未映射但由物理页面支持的内存区域状态。
利用
为了使漏洞可被利用,需要理解Mali驱动程序如何处理内存区域的收缩和释放。目标是创建一个部分未映射而周围区域仍被映射的内存区域。这种配置导致收缩例程跳过该特定部分的解除映射,即使其支持页面被标记为释放。
为此,可以在现有内存区域末尾附近引入第二次故障以满足条件。随后,收缩内存区域会导致GPU从final_size之后开始解除映射。Mali驱动程序将跳过解除PTE1中的映射,因为它是未映射且无效的。当到达PTE2时,由于第一个条目因对应地址未映射而无效,kbase_mmu_teardown_pgd_pages将跳过解除接下来512个虚拟页面的映射,然而其中仍有需要解除映射的有效PTE。
任意读/写
在这一点上,我们拥有两个引用同一物理页面的内存区域(write_addr & reused_addr)。下一步是将这个重叠页面转化为更强的任意读写原语。
原始的利用方法通过喷洒Mali GPU的PGD来重用被释放的页面。由于我们能够读写被释放的页面,我们可以控制新分配的PGD内的一个PTE。这允许我们将预留缓冲区的后备页面更改为指向任何内存区域,随后用它来修改内核函数。
其他路径
我们也可以利用页面UAF原语探索其他内核利用技术。为此,首先需要将被释放的页面从GPU的控制中移出。我们可以修改原始利用,排出2 * MAX_POOL_SIZE个页面,填满上下文和设备内存池。这导致后续释放的页面直接返回给内核伙伴分配器,而不是保留在Mali驱动程序中。
现在我们可以使用通常的Linux内核利用技术来喷洒内核对象。我尝试喷洒pipe_buffer,但未能可靠地重用UAF页面。随后发现了Dirty Pagetable技术,它运作良好。该技术与原始利用中使用的技术非常相似,只是在GPU外部操作。
权限提升
为了完成利用,需要将SELinux设置为宽容状态并获取root权限。我选择重用原始利用中现有的方法。
绕过SELinux
绕过此设备上SELinux的最简单方法是将state->enforcing值覆盖为false。为此,我们可以覆盖内核文本中的avc_denied函数,使其始终授予所有权限请求,即使原本应该被拒绝。
Root
与许多其他Linux利用一样,我们可以通过调用commit_creds(&init_cred)来获取root权限。由于在读取/sys/fs/selinux/enforce时可以调用sel_read_enforce,我们使用shellcode覆盖该函数。
‘修复’延迟
为了理解为什么利用有10分钟的延迟,我们可以检查kmsg中的日志。有许多具有相同堆栈转储的警告被抛出。警告在mmu_insert_pages_no_flush函数中的循环内触发。该警告在给定虚拟地址的页表条目(target)已经有效时触发,这意味着代码正在向已映射的地址范围插入新映射。
为什么这只发生在我们的利用中?回顾第一个竞争条件,如果竞争条件成功,页面错误处理程序将已经在old_size之后映射了FAULT_SIZE个页面。然而,由于kbase_mem_grow_gpu_mapping使用缓存的old_size作为起始偏移量来映射delta个页面,会有FAULT_SIZE个页面的重叠,从而触发警告。记录每个警告的成本显著减慢了我们的利用速度。
我无法找到仅通过利用来跳过检查的方法。然而,我能够编写一个可加载内核模块来跳过警告以加速测试。我们可以使用kprobe跳过分支到0x84C2C,这有效地完全消除了延迟。
在内核模块运行的情况下,利用能够在<5秒内运行。仅仅一行代码就能将利用速度减慢120倍,真是惊人!
漏洞修复
提交0ff2be2a2bb33093a47fcc173fa92c97dad3fe38中引入的补丁添加了一个检查来修复该漏洞。它首先检查竞争窗口后实际的后备页面数量是否大于请求的commit_pages。如果不需要增长,函数将提前返回,跳过映射delta页面的函数。它还在竞争窗口后重新计算old_size,而不是使用缓存值。
参考文献
- https://github.blog/security/vulnerability-research/gaining-kernel-code-execution-on-an-mte-enabled-pixel-8/
- https://ptr-yudai.hatenablog.com/entry/2023/12/08/093606
- https://github.com/star-sg/OBO/blob/main/2024/Day%201/GPUAF%20-%20Using%20a%20general%20GPU%20exploit%20tech%20to%20attack%20Pixel8.pdf