CVE-2021-28663¶
原文链接:https://vul.360.net/archives/263
英文版链接:https://www.news4hackers.com/android-kernel-backdoor-vulnerabilities/
1. 背景知识¶
CPU和GPU共用同一块RAM,各自拥有一个MMU(Memory Management Unit)来管理内存。MMU负责将虚拟地址转换为物理地址的映射过程。
Mali GPU驱动的一个重要功能就是去维护GPU的IOMMU页表。
正常内存操作时,CPU分配一段物理内存,并映射给GPU,GPU从这段物理内存中读取数据,并将计算结果写回到这段物理内存中。
1.1 一些IOCTL命令¶
KBASE_IOCTL_MEM_ALLOCATE
:分配内存区域,该内存区域中的内存页会被映射到GPU的地址空间中,同时也可以选择是否将该内存区域映射到CPU的地址空间中。KBASE_IOCTL_MEM_QUERY
:查询内存区域的属性。KBASE_IOCTL_MEM_FREE
:释放内存区域。KBASE_IOCTL_MEM_GET_SYNC
:同步数据,保证CPU和GPU都能及时查看各自的操作结果。KBASE_IOCTL_MEM_COMMIT
:改变内存区域内的内存页数量。KBASE_IOCTL_MEM_ALIAS
:创建内存区域的别名,即有多个GPU虚拟地址映射到同一物理内存。KBASE_IOCTL_MEM_IMPORT
:映射CPU中的内存区域到GPU的地址空间中。KBASE_IOCTL_MEM_FLAGS_CHANGE
:改变内存区域的属性。
1.2 内存区域的分配¶
C | |
---|---|
64位进程默认使用BASE_MEM_SAME_VA,代表GPU和CPU使用同一个虚拟地址。
特定的申请进程由kbase_mem_alloc()
函数来处理。
kbase_mem_alloc()
这个函数首先调用了kbase_check_alloc_flags()
函数来检查内存区域的属性是否合法。
然后调用kbase_alloc_free_region()
函数来分配内存区域kbase_va_region
。
C | |
---|---|
下一步调用kbase_reg_prepare_native()
函数来准备内存区域。这个函数可以初始化reg->gpu_alloc
和reg->cpu_alloc
。
默认情况下,reg->gpu_alloc
和reg->cpu_alloc
都指向同一个内存对象,都是kbase_mem_phy_alloc
。
经过以上步骤,已经形成了一个初步的数据结构,接下来会调用kbase_alloc_phy_pages()
函数为reg->cpu_alloc
分配物理页面,然后将reg
挂载到 kctx->pending_regions
数组中。
kctx->pending_regions
在数组中找到一个空闲位置,然后保存reg
。需要注意的是,返回值并非实际地址,而是一个临时值,此值将在后续处理中使用。
1.3 内存区域的映射¶
调用mmap
的最后会调用kbase_mmap()
函数来映射内存区域。
系统调用的正常语义是将物理页面映射到进程的地址空间。由于驱动程序除了常规的映射功能外,还指定了BASE_MEM_SAME_VA、kbase_mmap(),所以这些物理页面必须映射到GPU地址空间。需要注意的是,CPU和GPU映射的虚拟地址是相同的。
kbase_mmap()
函数会调用kbase_gpu_mmap()
函数来处理GPU的映射,该函数主要功能是将物理页映射到 IOMMU,即调用kbase_mmu_insert_pages()
,然后将alloc>gpu_mappings
的引用计数增加1。这个引用计数非常重要,驱动程序通过查看这个引用计数来确定是否可以对相应的内存区域执行相关操作。最终,系统调用的mmap
返回值是映射到CPU和GPU的虚拟地址。
在分配物理页时,这些页并未映射到 GPU 的虚拟地址空间,因此 reg->gpu_alloc->gpu_mappings
计数为 0;当调用kbase_gpu_mmap()
时,这些物理页被映射到GPU
空间,此时reg->gpu_alloc->gpu_mappings
计数增加1。从语义角度来看,这是非常合理的,因为`gpu_alloc->gpu_mappings
准确且及时地反映了物理页在内存区域中的映射状态。然而,随着功能的增加,情况变得更加复杂。
1.4 内存区域的别名¶
KBASE_IOCTL_MEM_ALIAS
命令用于创建内存区域的别名,即有多个GPU虚拟地址映射到同一物理内存,整个步骤可以分为两步:
- 通过
kbase_api_mem_alias()
函数来创建新的reg对象,引用需要别名操作的内存区域,返回假的虚拟地址。 - 调用
kbase_mmap()
函数来映射新的reg对象。
kbase_api_mem_alias()
函数的主要逻辑部分是kbase_mem_alias()
函数。
首先,kbase_mem_alias()
会检查用户传入的标志。别名映射允许CPU只读,而GPU可读写。
然后分配一个新的reg
,并为其分配gpu_alloc
。这里,之前分配的reg
并未直接使用,而是创建了一个新的reg
。
接着根据用户传入的句柄查找reg
,经过一些检查后,引用了原始reg
的reg->gpu_alloc->imported.alias.aliased[i].alloc
。
同时,1 个kbase_mem_phy_alloc_get()
会将reg->ref
加1。并且kbase_mem_alloc()
与kbase_mem_alias()
一样,都会将reg
挂载到kctx>pending_regions
数组,返回虚拟地址。之后,用户还需要调用mmap
和kbase_gpu_mmap
,并根据reg
的类型(KBASE_MEM_TYPE_ALIAS
)执行相应的处理。
kbase_gpu_mmap()
的主要逻辑是把kbase_mem_alias()
函数中的reg->gpu_alloc->imported.alias.aliased[i].alloc
的物理页映射到新的GPU地址空间。如果成功,reg->gpu_alloc->gpu_mappings
会加1。
仅从上面的描述来看,操作似乎是合理的,没有明显的错误。
但事实上还是有问题的,具体见下面的漏洞分析。
2. CVE-2021-28663漏洞概述¶
该漏洞是一个GPU物理页映射时导致的UAF漏洞。
2.1 利用原理¶
漏洞利用关键在于KBASE_IOCTL_MEM_FLAGS_CHANGE
命令。该命令的主要功能是改变内存区域的属性,与之相关的函数是kbase_api_mem_flags_change()
。
此函数的主要功能是支持BASE_MEM_DONT_NEED
操作,即应用程序不再需要某个内存区域的物理页,驱动程序可以缓存这些物理页并在适当的时候释放它们;同时,驱动程序还支持反向操作:应用程序继续使用此内存区域,而驱动程序需要取回缓存的物理页。如果已被释放,则可以分配新的物理页。
以上操作的实现有一个重要的前提条件:reg->cpu_alloc->gpu_mappings
不能大于1,因为这代表这个物理页被映射到多个GPU地址空间中。
C | |
---|---|
当以上条件得到了满足,驱动程序会调用kbase_mem_evictable_make()
来清理。
kbase_mem_evictable_make()
首先取消CPU的映射(在这之后,CPU或应用程序都无法访问该内存区域)。然后,gpu_alloc->evict_node
会被添加到kctx->evict_list
中。
kctx->evict_list
这个链表会在后续流程中被kbase_mem_evictable_reclaim_scan_objects()
函数使用。
kbase_mem_evictable_reclaim_scan_objects()
这个函数会遍历kctx->evict_list
链表,取消GPU的映射,然后释放掉所有的物理页。
按道理这里应该是不会有问题的。然而我们之前提到过,别名申请时的函数有两个小的组成部分,分别是kbase_mem_alias()
和kbase_gpu_mmap()
。
kbase_mem_alias()
函数会创建一个新的reg
,并将其挂载到kctx->pending_regions
中。kbase_gpu_mmap()
函数会将reg->gpu_alloc->imported.alias.aliased[i].alloc
的物理页映射到新的GPU地址空间,并且reg->gpu_alloc->gpu_mappings
会加1。
也就是说,如果我们只调用kbase_mem_alias()
函数来创建一个新的reg
,这个时候的reg->gpu_alloc->gpu_mappings
计数仍然为1(因为kbase_gpu_mmap()
函数并未被调用)。如果我们此时调用KBASE_IOCTL_MEM_FLAGS_CHANGE
命令,驱动程序会看到reg->cpu_alloc->gpu_mappings
计数为1,允许我们继续操作并把物理页加入到kctx->evict_list
中,并且还有一点很重要的是,reg->gpu_alloc
和reg->cpu_alloc
仍然有效,并没有被释放。
紧接着,我们调用kbase_gpu_mmap()
函数来映射物理页到新的GPU地址空间,这个时候reg
的物理页实际上存在两个映射。
最后我们调用kbase_mem_evictable_reclaim_scan_objects()
函数来释放物理页,这里清除的GPU映射是一开始的reg
,而不是我们刚刚创建的新的reg
。
2.2 漏洞触发流程¶
漏洞触发流程如下:
flowchart TD
A["申请内存区域:kbase_api_mem_alloc"] --> B["映射到CPU和GPU地址(映射1):kbase_mmap"]
B --> C["索引到内存区域:kbase_mem_alias"]
C --> D["清除CPU映射(映射1):kbase_mem_flags_change"]
D --> E["映射到新的CPU和GPU地址(映射2):kbase_gpu_mmap"]
E --> F["清除一开始的GPU映射(映射1):kbase_mem_evictable_reclaim_scan_objects"]
其中kbase_mem_evictable_reclaim_scan_objects()
会在内存不足时被自动调用,即触发shrink机制,因此只要造成内存压力就可以触发该机制。
2.3 漏洞利用方式¶
即使我们触发了漏洞,得到的别名区域还是必须满足申请别名时的条件:即CPU侧只读,GPU侧可读写。如果我们想要利用这个漏洞,只能从GPU侧来读写数据。
不妨使用OPENCL来进行GPU侧的读写操作:
C | |
---|---|
以上代码实现了GPU任意地址读取数据的功能。
2.4 Patch¶
漏洞出现的原因在于,创建别名区域和gpu->mmapings
计数更新之间存在时间差。修改后,驱动程序会在创建别名区域时,直接将gpu->mmapings
计数加1。
3. POC¶
C | |
---|---|
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
|