CVE-2021-36449¶
原文链接:Rooting with root cause: finding a variant of a Project Zero bug
CVE-2022-36449并不只是一个漏洞,而是一系列漏洞的集合。这里我们会先从编号2327讲起。
Project Zero issue 2327¶
我们已经知道,kbase_va_region
内有两个 kbase_mem_phy_alloc
指针域,分别为 cpu_alloc
和 gpu_alloc
,这两个指针跟踪被映射到GPU的内存页。本情况下,我们只考虑这两个指针指向同一个对象的情况,因此只需要关注 gpu_alloc
。下面是 kbase_mem_phy_alloc
结构体的定义:
C | |
---|---|
其中 pages
保存着GPU映射的物理页的地址。我们可以使用 KBASE_IOCTL_MEM_IMPORT
来把用户空间的内存分享给GPU,所分享的 kbase_va_region
的 kbase_memory_type
会被设置为 KBASE_MEM_TYPE_IMPORTED_USER_BUF
。
对于这种类型的 kbase_va_region
,它的 gpu_alloc
指针指向的 kbase_mem_phy_alloc
结构体的 pages
会通过一个 get_user_pages
的函数来填充。在GPU使用内存页时,get_user_pages
会增加这些内存页的引用计数,并把它们加到 pages
数组;而当GPU不再使用这些内存页,该函数把它们从 pages
数组中移除并减少引用计数。这能够保证GPU使用的内存页不会被释放。
如果在 KBASE_IOCTL_MEM_IMPORT
调用时使用了 KBASE_REG_SHARED_BOTH
标志位,那么用户内存页会在被引入时立即被添加到 pages
数组中,否则会等到GPU第一次访问这些内存页时才添加。
当 KBASE_REG_SHARED_BOTH
标志位没有没设置而且 pages
已经被填充时,内存管理就会出现一些问题。
当 KBASE_MEM_TYPE_IMPORTED_USER_BUF
类型的 pages
在导入时未被填充的时候,可以通过提交一个带有 BASE_JD_REQ_EXTERNAL_RESOURCES
要求的 KBASE_IOCTL_JOB_SUBMIT
指令来提交一个GPU软件作业(“软作业”)来使用该内存。
为了使用这块内存,一个名为 kbase_jd_user_buf_map
,这个函数会调用一个函数 kbase_jd_user_buf_pin_pages
把用户内存页插入到 pages
数组中。
此时,用户页面的引用计数通过 get_user_pages
函数递增,并且其底层内存的物理地址被插入到 pages
数组中。
当软件作业完成后,kbase_jd_user_buf_unmap
会从 pages
数组中删除物理地址,并且减少引用计数。
然而,pages
数组并非访问这些内存页的唯一途径。内核驱动程序还可以为这些页创建内存映射,从而允许从 GPU 和 CPU 访问它们,并且在从 pages
数组中移除这些页之前,应当先移除这些内存映射。例如,作为 kbase_jd_user_buf_unmap
的调用者,kbase_unmap_external_resource
会通过调用 kbase_mmu_teardown_pages
来确保移除GPU中的内存映射。
此外,还可以调用 mmap
创建这些用户空间页面到CPU的映射。当用户空间页面被移除时,应通过调用 kbase_mem_shrink_cpu_mapping
函数来删除这些CPU映射,以防止它们从用户空间地址处被访问。
然而,在从 KBASE_MEM_TYPE_IMPORTED_USER_BUF
类型的内存区域移除用户页面时,并未执行此操作,这意味着,一旦在 kbase_jd_user_buf_unmap
中减少这些用户页面的引用计数,这些页面就可以被释放,而由Mali驱动程序创建的这些页面到CPU的映射仍然可以访问它们,即它们仍然可以从用户应用程序中访问,也就是UAF。
通常,Mali GPU驱动中的共享内存通过 kbase_va_region
的 gpu_alloc
进行管理。在以下两种不同场景中,内存页(backing pages)可能被释放:
如果 gpu_alloc
和 kbase_va_region
本身被释放,则 kbase_va_region
的内存页也会随之释放。为防止此类情况发生,当内核正在使用内存页时,通常会获取对应 gpu_alloc
和 kbase_va_region
的引用,以避免它们被释放。
当通过 kbase_cpu_mmap
创建CPU映射时,会生成一个 kbase_cpu_mapping
结构体,并将其存储于新创建的虚拟内存(vm)区域的 vm_private_data
中。该 kbase_cpu_mapping
会存储并增加 kbase_va_region
和 gpu_alloc
的引用计数(refcount),从而在vm区域使用期间阻止这些对象被释放。
当内存区域类型为 KBASE_MEM_TYPE_NATIVE
时,其内存页(backing pages)由该内存区域自主拥有并维护,同时也可通过收缩后备存储(backing store)的方式释放这些页面。例如:使用 KBASE_IOCTL_MEM_COMMIT
命令时,系统会调用 kbase_mem_shrink
函数移除内存页。
上面的操作是正常的,但对于 KBASE_MEM_TYPE_IMPORTED_USER_BUF
类型且 KBASE_REG_SHARED_BOTH
标志位未设置的内存区域,内存页只会在被调用时才会被添加到 pages
数组中,随后在 kbase_jd_user_buf_unmap
中被移除。
这可能会导致由于需要重新实现复杂的内存管理逻辑而错误地省略了 kbase_mem_shrink
的清理逻辑。
利用流程¶
简要概括POC的利用流程:
graph TD
subgraph "阶段一: 内存准备与双重映射"
A["1. 创建匿名映射<br>mmap(MAP_ANON) -> anon_mapping<br>首次写入 -> 内核分配物理页 P1<br>MM 系统: get_page(P1) -> P1 总引用=1"] --> B("2. 导入内存到 Mali<br>ioctl(MEM_IMPORT, anon_mapping)<br>kbase 记录 P1, 返回句柄 gpu_va_handle")
B --> C("3. 创建 Mali 特殊映射<br>mmap(mali_fd, gpu_va_handle) -> gpu_mapping<br>类型: VM_PFNMAP, 指向 P1<br><b>不增加 P1 MM 引用计数</b><br>P1 总引用仍=1")
C --> D("4. 分配 Job Control 内存 (jc)")
end
subgraph "阶段二: 驱动获取页引用"
D --> E("5. 准备 Atom 1 (WAIT)<br>指定外部资源: gpu_mapping")
E --> F("6. 提交 Atom 1<br>ioctl(JOB_SUBMIT, atom1)")
F --> G("7. 内核处理 Job<br>kbase: 识别 gpu_mapping -> P1<br>kbase: <b>get_page(P1)</b><br>P1 总引用 = 2 (MM=1, kbase=1)")
G --> H("8. 访问 Mali 映射<br>用户: *(gpu_mapping)<br>触发 CPU 缺页 (若PTE未缓存)<br>填充 CPU PTE: gpu_mapping VA -> P1 物理地址")
end
subgraph "阶段三: 引用计数减少与释放"
H --> I("9. 准备 Atom 2 (SET 事件)")
I --> J("10. 提交 Atom 2<br>ioctl(JOB_SUBMIT, atom2)")
J --> K("11. Atom 1 完成<br>kbase: 清理资源<br>kbase: <b>put_page(P1)</b><br>P1 总引用 = 1 (MM=1, kbase=0)")
K --> L("12. 解除匿名映射<br>用户: munmap(anon_mapping)")
L --> M("13. MM 处理 munmap<br>MM 系统: <b>put_page(P1)</b><br>P1 总引用 = 0")
M --> N["14. 物理页 P1 被释放<br>内核将 P1 回收至页分配器"]
end
subgraph "阶段四: Use-After-Free"
N --> O["<b>15. 触发 UAF</b><br>用户: 再次访问 *(gpu_mapping)<br>CPU 访问 gpu_mapping VA<br>PTE 可能仍指向旧 P1 物理地址<br>访问已释放/重用的内存!"]
O --> P["16. 持续观察<br>循环 hexdump 显示 UAF 效果"]
end
简单来说,利用流程如下:
- 准备一块内存:程序先向系统申请一块普通内存 (
anon_mapping
),让系统知道这块内存在用。 - 创建“特殊通道”:通过 Mali 驱动,为这块内存创建一个“特殊”的访问通道 (
gpu_mapping
)。关键是,系统在判断内存是否还在用时,不考虑这个特殊通道。 - Mali 临时使用:让 Mali GPU 驱动通过这个特殊通道临时“借用”一下这块内存(提交一个相关 GPU 作业)。
- 归还与移除:
- 先让 Mali 驱动“还回”这块内存(完成 GPU 作业)。
- 然后,程序告诉系统,它不再需要那块普通内存了(删除
anon_mapping
)。 - 系统回收:系统看到没人(普通通道没了,Mali 也还了)再用这块物理内存了,就把它回收了。
- 触发漏洞 (UAF):此时,虽然物理内存已被回收,但那个“特殊通道” (
gpu_mapping
) 可能还指向原来的位置。程序再次通过这个特殊通道去访问内存,就访问到了已经被释放、内容未知的地方,这就是“释放后使用”(Use-After-Free)。