作者:2freeman(姚俊)
原文鏈接:https://vul.360.net/archives/263
綜述
回顧Android內核漏洞史可以發現,大部分Android內核漏洞屬于內存漏洞,而邏輯漏洞相對少見。由于內存漏洞具有典型的漏洞模式、明顯的副作用以及較完善的檢測方法,因此這類漏洞較容易發現。對應地,邏輯漏洞沒有典型的漏洞模式(往往與功能緊密相關)、不確定的副作用以及缺乏普適的檢測方法,因此,挖掘這類漏洞相對困難。正因為如此,邏輯漏洞有它獨特的魅力。
這篇文章將深入分析CVE-2021-28663,它是ARM Mali GPU驅動里的一個邏輯漏洞。這個漏洞堪稱后門:
1 抗碎片化:影響使用聯發科、海思以及獵戶座SoC的手機,近幾年面世的手機幾乎都受影響;
2 攻擊具有隱蔽性:該漏洞的攻擊方式與常見的利用方式有很大不同,據我所知,目前沒有公開資料介紹該漏洞的利用方法;
3 普通APP可以輕易竊取其他APP或者內核運行時數據,甚至修改其他APP的代碼,整個過程不需要獲得任何額外的權限;
4 ROOT提權具有100%的成功率;
下面我將揭開它神秘的面紗。
漏洞影響
由于聯發科、海思以及獵戶座SoC均使用ARM Mali GPU,所以使用這些芯片的手機都可能受影響。我搜集了部分主流芯片或者手機相關源代碼,發現均受影響:
| 時間 | 廠商 | 手機型號 | 芯片型號 | 驅動版本 |
|---|---|---|---|---|
| 2021 | SAMSUNG | S21 | Exynos 2100 | v_r20p0 |
| 2020 | HUAWEI | Mate40 | Kirin 9000 | r23p0-01rel0 |
| 2020 | Redmi | K30U | 天璣1000+ | v_r21p0 |
| 2020 | Redmi | 10X | 天璣820 | v_r21p0 |
| 2020 | SAMSUNG | S20 | Exynos 990 | v_r25p1 |
| 2019 | HUAWEI | Mate30 | Kirin 990 | b-r18p0-01rel0 |
| 2019 | Redmi | Note8 Pro | Helio G90T | b_r20p0 |
| 2019 | SAMSUNG | S10 | Exynos 9820 | b_r16p0 |
| 2018 | HUAWEI | Mate20 | Kirin 980 | b-r18p0-01rel0 |
| 2018 | Redmi | 紅米 6 | Helio P22 | m-r20p0 |
| 2018 | SAMSUNG | S9 | Exynos 9810 | b_r19p0 |
| 2017 | HUAWEI | Mate10 | Kirin 970 | b-r14p0-00cet0 |
| 2017 | LENOVO | K8 Plus | Heli0 P25 | r18p0 |
| 2017 | SAMSUNG | S8 | Exynos 8895 | b_r16p0 |
| 2016 | HUAWEI | Mate9 | Kirin 960 | b-r14p0-00cet0 |
| 2016 | Meizu | M3x | Helio P20 | r12p1 |
| 2016 | SAMSUNG | S7 | Exynos 8890 | r22p0 |
| 2015 | HUAWEI | Mate8 | Kirin 950 | r20p0-01rel0 |
| 2015 | SAMSUNG | S6 | Exynos 7420 | r15p0 |
正如綜述里提到,普通APP可以借助漏洞完成以下攻擊:
1 竊取其他APP運行時內存數據
2 修改其他APP代碼
3 竊取內核運行時內存數據
4 穩定地獲得ROOT權限
相對常見的內核漏洞,這個漏洞不但可以穩定地獲取ROOT權限,而且可以以非常隱蔽的方式獲取其他APP和內核的運行時數據,甚至修改其他APP的代碼,整個過程不需要獲得任何額外的權限。從攻擊過程和結果來看,堪稱后門級漏洞。
漏洞分析
除了CPU,一個SoC上還有很多針對具體業務場景特制的處理器,比如GPU。GPU的主要功能是對圖形進行渲染。在IOMMU的幫助下,GPU可以有自己的虛擬地址空間。通過映射物理頁,GPU和CPU之間可以高效地傳輸數據。而上述功能的實現,依賴于內核驅動。
GPU映射物理頁過程 – 返回假的虛擬地址
具體到ARM設計實現的GPU,它使用的是Mali驅動。Mali驅動的一個重要功能是為GPU維護IOMMU頁表。當應用(運行在CPU上)想要讓GPU為其處理數據或者渲染圖形時,驅動需要幫忙將數據所在的物理頁映射到GPU的地址空間中,這樣,GPU可以立即“看到”這些數據。整個過程沒有額外的數據拷貝操作,從而大大提高處理效率。Mali驅動實現了以下相關操作:
| 序號 | 命令 | 功能 |
|---|---|---|
| 1 | KBASE_IOCTL_MEM_ALLOC | 分配內存區域,內存區域中的頁會映射到GPU中,可選擇同時映射到CPU |
| 2 | KBASE_IOCTL_MEM_QUERY | 查詢內存區域屬性 |
| 3 | KBASE_IOCTL_MEM_FREE | 釋放內存區域 |
| 4 | KBASE_IOCTL_MEM_SYNC | 同步數據,使得CPU和GPU可以及時看到對方操作結果 |
| 5 | KBASE_IOCTL_MEM_COMMIT | 改變內存區域中頁的數量 |
| 6 | KBASE_IOCTL_MEM_ALIAS | 為某個內存區域創建別名,即多個GPU虛擬地址指向同一個區域 |
| 7 | KBASE_IOCTL_MEM_IMPORT | 將CPU使用的內存頁映射到GPU地址空間中 |
| 8 | KBASE_IOCTL_MEM_FLAGS_CHANGE | 改變內存區域屬性 |
表格中提到的內存區域(memory region)實際上是Mali驅動里的一個概念,它包含了實際使用的物理頁。以下分析基于三星A71源代碼
我先介紹下KBASE_IOCTL_MEM_ALLOC命令處理過程。通過這個命令,你可以了解驅動是如何將物理頁映射到進程地址空間(CPU)和GPU地址空間。
這個命令接收的參數如下:
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
183 union kbase_ioctl_mem_alloc {
184 struct {
185 __u64 va_pages;
186 __u64 commit_pages;
187 __u64 extent;
188 __u64 flags;
189 } in;
190 struct {
191 __u64 flags;
192 __u64 gpu_va;
193 } out;
194 };
主要的輸入參數有:
va_pages表示待分配的內存區域最多可以容納多少物理頁,驅動會在GPU空間中留出相應大小的虛擬地址范圍;
commit_pages表示當下驅動需要為這個內存區域分配多少個物理頁,應用可根據自身需要調用KBASE_IOCTL_MEM_COMMIT命令調整頁的數量;
flags表示內存區域屬性,比如是否映射到CPU、是否可讀可寫;
輸出參數有:
gpu_va表示分配的內存區域在GPU空間中的虛擬地址,GPU可以使用該地址訪問相應的物理頁;
具體的分配過程如下:
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
kbase_api_mem_alloc()
|
| BASE_MEM_SAME_VA
|
|-> kbase_mem_alloc()
|
|-> kbase_check_alloc_flags()
|
|-> kbase_alloc_free_region()
|
|-> kbase_reg_prepare_native()
|
|-> kbase_alloc_phy_pages()
|
|-> kctx->pending_regions[cookie_nr] = reg
如果進程是64位,默認使用BASE_MEM_SAME_VA方式創建映射,它的含義是CPU和GPU使用相同的虛擬地址。具體的分配過程由kbase_mem_alloc()實現。
它首先調用kbase_check_alloc_flags()來檢查應用傳入的flags(屬性)是否合法:
drivers/gpu/arm/b_r19p0/mali_kbase_mem.c
2582 bool kbase_check_alloc_flags(unsigned long flags)
2583 {
2592 /* Either the GPU or CPU must be reading from the allocated memory */
2593 if ((flags & (BASE_MEM_PROT_CPU_RD | BASE_MEM_PROT_GPU_RD)) == 0)
2594 return false;
2595
2596 /* Either the GPU or CPU must be writing to the allocated memory */
2597 if ((flags & (BASE_MEM_PROT_CPU_WR | BASE_MEM_PROT_GPU_WR)) == 0)
2598 return false;
2617 /* GPU should have at least read or write access otherwise there is no
2618 reason for allocating. */
2619 if ((flags & (BASE_MEM_PROT_GPU_RD | BASE_MEM_PROT_GPU_WR)) == 0)
2633 }
上述摘錄的代碼片段主要與映射屬性有關,通過代碼可以了解到:
1 內存區域必須映射到GPU中,映射屬性可以是只讀、僅可寫、可讀寫(line 2619);
2 CPU和GPU至少有一方是可以讀內存區域的,否則分配物理頁沒有意義(line 2593);
3 同樣,至少有一方是可以寫內存區域的,否則分配物理頁沒有意義(line 2597);
之后,驅動調用kbase_alloc_free_region()來分配新的內存區域kbase_va_region:
drivers/gpu/arm/b_r19p0/mali_kbase_mem.h
241 struct kbase_va_region {
248 size_t nr_pages;
372 struct kbase_mem_phy_alloc *cpu_alloc; /* the one alloc object we mmap to the CPU when mapping this region */
373 struct kbase_mem_phy_alloc *gpu_alloc; /* the one alloc object we mmap to the GPU when mapping this region */
383 };
我摘錄了相關字段:
nr_pages表示這個區域最多可以包含多少物理頁;
cpu_alloc用于CPU地址空間映射;
gpu_alloc用戶GPU地址空間映射;
kbase_reg_prepare_native()負責初始化reg->cpu_alloc和reg->gpu_alloc:
drivers/gpu/arm/b_r19p0/mali_kbase_mem.h
541 static inline int kbase_reg_prepare_native(struct kbase_va_region *reg,
542 struct kbase_context *kctx, int group_id)
543 {
549 reg->cpu_alloc = kbase_alloc_create(kctx, reg->nr_pages,
550 KBASE_MEM_TYPE_NATIVE, group_id);
551 if (IS_ERR(reg->cpu_alloc))
552 return PTR_ERR(reg->cpu_alloc);
553 else if (!reg->cpu_alloc)
554 return -ENOMEM;
555
556 reg->cpu_alloc->imported.native.kctx = kctx;
557 if (kbase_ctx_flag(kctx, KCTX_INFINITE_CACHE)
558 && (reg->flags & KBASE_REG_CPU_CACHED)) {
566 } else {
567 reg->gpu_alloc = kbase_mem_phy_alloc_get(reg->cpu_alloc);
568 }
578 }
這里我們需要使reg->cpu_alloc和reg->gpu_alloc指向同一個對象(line 567),它們均是kbase_mem_phy_alloc:
drivers/gpu/arm/b_r19p0/mali_kbase_mem.h
128 struct kbase_mem_phy_alloc {
129 struct kref kref;
130 atomic_t gpu_mappings;
131 size_t nents;
132 struct tagged_addr *pages;
133 struct list_head mappings;
134 struct list_head evict_node;
135 size_t evicted;
136 struct kbase_va_region *reg;
137 enum kbase_memory_type type;
177 };
我僅摘錄了相關字段:
kref表示對象的引用次數;
gpu_mappings表示多少虛擬地址映射到該區域(想想前面提到KBASE_IOCTL_MEM_ALIAS命令);
nents表示當前有多少物理頁;
pages表示物理頁數組;
reg指向包含該對象的reg;
type表示內存類型,這里是KBASE_MEM_TYPE_NATIVE;
基本的數據結構已經建立起來,驅動調用kbase_alloc_phy_pages()為reg->cpu_alloc分配物理頁,之后將reg掛載到kctx->pending_regions數組中:
drivers/gpu/arm/b_r19p0/mali_kbase_mem_linux.c
254 struct kbase_va_region *kbase_mem_alloc(struct kbase_context *kctx,
255 u64 va_pages, u64 commit_pages, u64 extent, u64 *flags,
256 u64 *gpu_va)
257 {
376 if (*flags & BASE_MEM_SAME_VA) {
389 /* return a cookie */
390 cookie_nr = __ffs(kctx->cookies);
391 kctx->cookies &= ~(1UL << cookie_nr);
392 BUG_ON(kctx->pending_regions[cookie_nr]);
393 kctx->pending_regions[cookie_nr] = reg;
394
395 /* relocate to correct base */
396 cookie = cookie_nr + PFN_DOWN(BASE_MEM_COOKIE_BASE);
397 cookie <<= PAGE_SHIFT;
398
403 if (kctx->api_version < KBASE_API_VERSION(10, 1) ||
404 kctx->api_version > KBASE_API_VERSION(10, 4)) {
405 *gpu_va = (u64) cookie;
406 kbase_gpu_vm_unlock(kctx);
407 return reg;
408 }
484 }
這里的邏輯很簡單:在kctx->pending_regions數組中找一個空余位置(line 391),然后保存reg(line 393),需要注意的是返回值并非真正的地址(line 405),只是一個臨時值而已(line 396/397),這個值會在后續過程中使用。
至此,kbase_api_mem_alloc()的主要過程我們已經分析完畢:
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
kbase_api_mem_alloc()
|
|-> kbase_mem_alloc()
|
|-> kbase_check_alloc_flags() // 檢查屬性是否合法
|
|-> kbase_alloc_free_region() // 分配reg
|
|-> kbase_reg_prepare_native() // 分配kbase_mem_phy_alloc,reg->cpu_alloc和reg->gpu_alloc指向同一個對象
|
|-> kbase_alloc_phy_pages() // 分配物理頁
|
|-> kctx->pending_regions[cookie_nr] = reg // 返回假的虛擬地址
GPU映射物理頁過程 – 建立CPU及GPU映射
應用該如何使用假的虛擬地址呢?實際上是作為mmap系統調用參數:
gpu_va = mmap(0, MALI_MAP_PAGES * PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, dev, alloc.out.gpu_va);
mmap系統調用最終調用Mali驅動注冊的kbase_mmap(),這個函數具體過程如下:
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
kbase_mmap()
|
|-> kbase_context_mmap()
|
|-> kbase_reg_mmap()
| |
| | reg = kctx->pending_regions[cookie]
| |
| |-> kbase_gpu_mmap()
|
|-> kbase_cpu_mmap()
mmap系統調用正常語義是將物理頁映射到進程的地址空間,由于驅動指定了BASE_MEM_SAME_VA,所以kbase_mmap()在實現正常的映射功能之外,還要將這些物理頁映射到GPU地址空間中。需要注意的是:CPU和GPU映射的虛擬地址是一樣的。
這里僅分析kbase_gpu_mmap():
drivers/gpu/arm/b_r19p0/mali_kbase_mem.c
1174 int kbase_gpu_mmap(struct kbase_context *kctx, struct kbase_va_region *reg, u64 addr, size_t nr_pages, size_t align)
1175 {
1198 err = kbase_add_va_region(kctx, reg, addr, nr_pages, align);
1199 if (err)
1200 return err;
1205 if (reg->gpu_alloc->type == KBASE_MEM_TYPE_ALIAS) {
// 稍后我會分析這里
1235 } else {
1236 err = kbase_mmu_insert_pages(kctx->kbdev,
1237 &kctx->mmu,
1238 reg->start_pfn,
1239 kbase_get_gpu_phy_pages(reg),
1240 kbase_reg_current_backed_size(reg),
1241 reg->flags & gwt_mask,
1242 ctx->as_nr,
1243 group_id);
1244 if (err)
1245 goto bad_insert;
1246 kbase_mem_phy_alloc_gpu_mapped(alloc);
1247 }
1291 }
kbase_gpu_mmap()主要功能是將物理頁映射到IOMMU中,即調用kbase_mmu_insert_pages(),之后將alloc->gpu_mappings引用計數加1。這個引用計數至關重要,驅動通過查看這個引用計數來確定相關操作是否可以應用到相應的內存區域。最終,mmap系統調用返回值就是映射到CPU和GPU的虛擬地址。
綜上所述,GPU映射的典型流程分為兩步:
alloc and map pages for GPU
|
|-> kbase_api_mem_alloc() // 分配reg及物理頁,reg->gpu_alloc->gpu_mappings = 0
|
|-> kbase_mmap() // 將reg中的物理頁映射到CPU和GPU空間,reg->gpu_alloc->gpu_mappings = 1
在分配物理頁時,這些頁面并沒有映射到GPU的虛擬地址空間中,因此,reg->gpu_alloc->gpu_mappings計數為0;當kbase_gpu_mmap()將物理頁映射到GPU空間時,reg->gpu_alloc->gpu_mappings計數加1。從語義上看,這樣做非常合理,gpu_alloc->gpu_mappings準確、及時地表示了內存區域中物理頁的映射狀態。但是,隨著功能的增加,情況變得復雜。
GPU映射物理頁過程 – 別名操作
正如我之前提到,Mali GPU實現了KBASE_IOCTL_MEM_ALIAS命令,它的主要作用是將同一個內存區域映射到多個不同的虛擬地址空間中。整個別名實現過程類似于KBASE_IOCTL_MEM_ALLOC,也是分為兩步:
alias mapping on GPU
|
|-> kbase_api_mem_alias() // 創建新的reg對象,引用需要別名操作的內存區域,返回假的虛擬地址
|
|-> kbase_mmap() // 將內存區域映射到新的虛擬地址
kbase_api_mem_alias()主要邏輯由kbase_mem_alias()完成,其實現如下:
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
1681 u64 kbase_mem_alias(struct kbase_context *kctx, u64 *flags, u64 stride,
1682 u64 nents, struct base_mem_aliasing_info *ai,
1683 u64 *num_pages)
1684 {
1696 *flags &= (BASE_MEM_PROT_GPU_RD | BASE_MEM_PROT_GPU_WR |
1697 BASE_MEM_COHERENT_SYSTEM | BASE_MEM_COHERENT_LOCAL |
1698 BASE_MEM_PROT_CPU_RD | BASE_MEM_COHERENT_SYSTEM_REQUIRED);
1723 if (!kbase_ctx_flag(kctx, KCTX_COMPAT)) {
1726 *flags |= BASE_MEM_NEED_MMAP;
1727 reg = kbase_alloc_free_region(&kctx->reg_rbtree_same, 0,
1728 *num_pages,
1729 KBASE_REG_ZONE_SAME_VA);
1730 }
1743 reg->gpu_alloc = kbase_alloc_create(kctx, 0, KBASE_MEM_TYPE_ALIAS,
1744 BASE_MEM_GROUP_DEFAULT);
1762 for (i = 0; i < nents; i++) {
1763 if (ai[i].handle.basep.handle < BASE_MEM_FIRST_FREE_ADDRESS) {
1773 } else {
1774 struct kbase_va_region *aliasing_reg;
1775 struct kbase_mem_phy_alloc *alloc;
1776
1777 aliasing_reg = kbase_region_tracker_find_region_base_address(
1778 kctx,
1779 (ai[i].handle.basep.handle >> PAGE_SHIFT) << PAGE_SHIFT);
1804 alloc = aliasing_reg->gpu_alloc;
1812 reg->gpu_alloc->imported.alias.aliased[i].alloc = kbase_mem_phy_alloc_get(alloc);
1813 reg->gpu_alloc->imported.alias.aliased[i].length = ai[i].length;
1814 reg->gpu_alloc->imported.alias.aliased[i].offset = ai[i].offset;
1817 }
1818 }
1821 if (!kbase_ctx_flag(kctx, KCTX_COMPAT)) {
1827 /* return a cookie */
1828 gpu_va = __ffs(kctx->cookies);
1829 kctx->cookies &= ~(1UL << gpu_va);
1830 BUG_ON(kctx->pending_regions[gpu_va]);
1831 kctx->pending_regions[gpu_va] = reg;
1832
1833 /* relocate to correct base */
1834 gpu_va += PFN_DOWN(BASE_MEM_COOKIE_BASE);
1835 gpu_va <<= PAGE_SHIFT;
1836 }
1853 return gpu_va;
1873 }
首先,kbase_mem_alias()檢查用戶傳入的flags,從中可以看出(line 1696):別名映射允許CPU只讀,GPU可讀寫。這個條件對利用起到了限制作用,稍后我會分析。之后分配新的reg(line 1727),并為其分配gpu_alloc(line 1743)。這里并沒有直接使用之前分配的reg(回頭看看kbase_mem_alloc()),而是創建一個新的reg。
然后根據用戶傳入的handle找到reg(line 1777),經過一番檢查之后,reg->gpu_alloc->imported.alias.aliased[i].alloc引用了原來的reg。同時,kbase_mem_phy_alloc_get()會將reg->ref加1。
與kbase_mem_alloc()一樣,kbase_mem_alias()將reg掛載到kctx->pending_regions數組中(line 1831),返回假的虛擬地址(line 1853)。
之后,用戶同樣需要調用mmap,kbase_gpu_mmap會根據reg的類型(KBASE_MEM_TYPE_ALIAS)進行相應的處理:
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
kbase_mmap() -> kbase_context_mmap()
|
|-> kbasep_reg_mmap()
|
|-> kbase_gpu_mmap()
drivers/gpu/arm/b_r19p0/mali_kbase_mem.c
1174 int kbase_gpu_mmap(struct kbase_context *kctx, struct kbase_va_region *reg, u64 addr, size_t nr_pages, size_t align)
1175 {
1202 alloc = reg->gpu_alloc;
1203 group_id = alloc->group_id;
1204
1205 if (reg->gpu_alloc->type == KBASE_MEM_TYPE_ALIAS) {
1206 u64 const stride = alloc->imported.alias.stride;
1207
1208 KBASE_DEBUG_ASSERT(alloc->imported.alias.aliased);
1209 for (i = 0; i < alloc->imported.alias.nents; i++) {
1210 if (alloc->imported.alias.aliased[i].alloc) {
1211 err = kbase_mmu_insert_pages(kctx->kbdev,
1212 &kctx->mmu,
1213 reg->start_pfn + (i * stride),
1214 alloc->imported.alias.aliased[i].alloc->pages + alloc->imported.alias.aliased[i].offset,
1215 alloc->imported.alias.aliased[i].length,
1216 reg->flags & gwt_mask,
1217 kctx->as_nr,
1218 group_id);
1219 if (err)
1220 goto bad_insert;
1221
1222 kbase_mem_phy_alloc_gpu_mapped(alloc->imported.alias.aliased[i].alloc);
1223 }
1234 }
1235 }
1291 }
kbase_gpu_mmap()主要邏輯看上去非常簡單:將kbase_mem_alias()收集到的內存區域(line 1210)映射到新的地址空間(line 1211)。如果成功建立映射,將相關的reg->gpu_alloc的gpu_mappings加1。
至此,關于內存區域的兩個重要操作介紹完畢,從上述分析看,相關操作準確、合理,沒有明顯的問題。
GPU映射物理頁過程 – 改變屬性
前面我提到Mali驅動實現了KBASE_IOCTL_MEM_FLAGS_CHANGE命令,該命令可以修改內存區域屬性。相關實現在kbase_api_mem_flags_change()中:
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
kbase_api_mem_flags_change()
|
|-> kbase_mem_flags_change()
drivers/gpu/arm/b_r19p0/mali_kbase_mem_linux.c
838 int kbase_mem_flags_change(struct kbase_context *kctx, u64 gpu_addr, unsigned int flags, unsigned int mask)
839 {
876 reg = kbase_region_tracker_find_region_base_address(kctx, gpu_addr);
877 if (kbase_is_region_invalid_or_free(reg))
878 goto out_unlock;
879
880 /* Is the region being transitioning between not needed and needed? */
881 prev_needed = (KBASE_REG_DONT_NEED & reg->flags) == KBASE_REG_DONT_NEED;
882 new_needed = (BASE_MEM_DONT_NEED & flags) == BASE_MEM_DONT_NEED;
883 if (prev_needed != new_needed) {
884 /* Aliased allocations can't be made ephemeral */
885 if (atomic_read(®->cpu_alloc->gpu_mappings) > 1)
886 goto out_unlock;
887
888 if (new_needed) {
889 /* Only native allocations can be marked not needed */
890 if (reg->cpu_alloc->type != KBASE_MEM_TYPE_NATIVE) {
891 ret = -EINVAL;
892 goto out_unlock;
893 }
894 ret = kbase_mem_evictable_make(reg->gpu_alloc);
895 if (ret)
896 goto out_unlock;
897 } else {
898 kbase_mem_evictable_unmake(reg->gpu_alloc);
899 }
900 }
978 }
這個函數主要功能是支持BASE_MEM_DONT_NEED操作,即應用不再需要某個內存區域上的物理頁了,驅動可以將這些物理頁緩存,待合適時機將其釋放(line 894);同時,驅動也支持反向操作:應用繼續使用這個內存區域,驅動需要將緩存的物理頁找回來,如果已經釋放,可以分配新的物理頁(line 898)。
上述操作的一個前提條件是reg->cpu_alloc->gpu_mappings不能大于1,大于1意味著這些頁映射到多個虛擬地址上。Mali驅動不打算處理這種復雜情形。如果內存區域符合上述條件,kbase_mem_evictable_make()被調用,來進行清理操作:
drivers/gpu/arm/b_r19p0/mali_kbase_mem_linux.c
765 int kbase_mem_evictable_make(struct kbase_mem_phy_alloc *gpu_alloc)
766 {
767 struct kbase_context *kctx = gpu_alloc->imported.native.kctx;
768
769 lockdep_assert_held(&kctx->reg_lock);
770
771 kbase_mem_shrink_cpu_mapping(kctx, gpu_alloc->reg,
772 0, gpu_alloc->nents);
773
774 mutex_lock(&kctx->jit_evict_lock);
775 /* This allocation can't already be on a list. */
776 WARN_ON(!list_empty(&gpu_alloc->evict_node));
777
778 /*
779 * Add the allocation to the eviction list, after this point the shrink
780 * can reclaim it.
781 */
782 list_add(&gpu_alloc->evict_node, &kctx->evict_list);
783 mutex_unlock(&kctx->jit_evict_lock);
784 kbase_mem_evictable_mark_reclaim(gpu_alloc);
785
786 gpu_alloc->reg->flags |= KBASE_REG_DONT_NEED;
787 return 0;
788 }
kbase_mem_evictable_make()首先將之前建立的CPU映射取消(line 771)。此時,應用再也無法通過虛擬地址訪問這些物理頁。之后,將gpu_alloc加入kctx->evict_list鏈表。這個鏈表實際上會被kbase_mem_evictable_reclaim_scan_objects()使用:
drivers/gpu/arm/b_r19p0/mali_kbase_mem_linux.c
627 unsigned long kbase_mem_evictable_reclaim_scan_objects(struct shrinker *s,
628 struct shrink_control *sc)
629 {
638 list_for_each_entry_safe(alloc, tmp, &kctx->evict_list, evict_node) {
639 int err;
640
641 err = kbase_mem_shrink_gpu_mapping(kctx, alloc->reg,
642 0, alloc->nents);
660 kbase_free_phy_pages_helper(alloc, alloc->evicted);
661 freed += alloc->evicted;
662 list_del_init(&alloc->evict_node);
673 }
678 }
kbase_mem_evictable_reclaim_scan_objects()主要作用是遍歷kctx->evict_list鏈表(line 638),將之前建立的GPU映射撤銷(line 641),最后釋放所有的物理頁(line 660)。
至此,物理頁整個生命周期已經分析完畢。漏洞實際上隱藏在KBASE_IOCTL_MEM_ALIAS命令和KBASE_IOCTL_MEM_FLAGS_CHANGE命令中。之前提到kbase_mem_flags_change()有一個前提:reg->cpu_alloc->gpu_mappings不能大于1。而別名操作是分兩步實現的,gpu_mappings引用計數加1是在kbase_gpu_mmap()中。如果我們只調用kbase_mem_alias(),然后緊接著調用kbase_mem_flags_change()會如何?
答案是我們可以映射釋放的頁!
1.1 kbase_api_mem_alloc() // 分配物理頁
1.2 mmap() // 映射到CPU和GPU地址空間
2.1 kbase_mem_alias() // 索引第1步創建的gpu_alloc
3 kbase_mem_flags_change() // 清除第1.2步中建立的CPU映射,gpu_alloc加入kctx->evict_list鏈表,但物理頁沒有被釋放
2.2 mmap() // 將物理頁映射到新的CPU和GPU地址空間
4 kbase_mem_evictable_reclaim_scan_objects() // 清除第1.2步中建立的GPU映射,物理頁被回收,但第2.2步建立的CPU和GPU映射不會清除
利用方法
通過上述調用過程,我們可以將幾乎所有內核可以分配的頁映射到CPU和GPU地址空間。之前提到,別名映射要求是CPU只讀,GPU可讀寫。我們可以在進程的虛擬地址空間中竊取這些頁的內容,但不能修改。而GPU可以讀寫這些頁,因此后面的分析主要集中在如何利用GPU讀寫物理頁。
mesa
針對高通的Adreno GPU,無論是KGSL驅動,還是freeadreno項目,你可以找到大量的GPU私有指令,從而實現GPU讀寫內存。針對ARM的Mali GPU,沒有公開資料介紹它的指令集(商業機密)。唯一的線索是Alyssa Rosenzweig主導的Bifrost和Panfrost項目。我花費了很長時間試圖能夠手寫一段可以直接在Mali GPU上運行的二進制代碼。最后發現這條路困難重重。
如果沒有辦法實現GPU讀寫物理頁,這個漏洞只能實現信息泄露。我們真的無路可走了么?
我們知道大部分的軟件是典型的分層體系結構,通過不斷地抽象,最終完成復雜的功能。具體到GPU,即便我們對指令集一無所知,我們還是可以讓它繪制圖形。這得益于OpenGL,它對底層進行了抽象,屏蔽了硬件之間的不同。但是,OpenGL更多地是面向圖形,比如點、線、投影、剪裁等。我沒有找到接口可以隨意訪問特定位置的內存。
其實,現在的GPU已經不單單是繪制圖形,它還可以用來進行密集計算。而在常規數學運算中,從內存讀取某個變量值(讀內存)和向內存寫入計算結果(寫內存)是基本操作,我們是不是可以通過上層封裝的功能來實現GPU讀寫物理頁?
OpenCL
在瀏覽維基百科關于OpenCL的介紹時,我看到了希望:
OpenCL(Open Computing Language) is a framework for writing programs that execute across heterogeneous platforms consisting of central processing(CPUs), graphics processing units(GPUs), digital signal processors(DSPs), field-programmable gate arrays(FPGAs) and other processors or hardware accelerators.
網上有很多OpenCL代碼示例,這里不做詳細介紹。僅展示下我實現的利用中使用的OpenCL代碼。
片段一:泄露內存地址
char *cl_code =
"__kernel void leak_mem_addr(__global unsigned long *addr) {"
" *addr = (unsigned long)addr;"
"}";
OpenCL庫本身會分配相關內存,我需要知道它分配的內存地址。通過上述代碼,我可以獲取該地址。
片段二:任意地址讀
char *cl_code =
"__kernel void gpu_read(__global unsigned long *addr, int offset) {"
" int idx = get_global_id(0);"
" *(addr+idx) = addr[offset+idx];"
"}";
上述代碼實現了GPU任意地址讀。由于映射的物理頁非常多,我們可以通過并行編程加速這個過程;)
相信你已經深得要領,這里就不展示任意地址寫了。
ROOT提權
由于我們可以映射大量的物理頁,這些頁有可能用于保存應用代碼或者數據,也有可能保存內核代碼或者數據。實際上,內核暴露了大量的數據結構,實現ROOT提權的方法多種多樣。這里就不一一介紹了,下面是我在某手機上實現的本地提權(100%成功率)

補丁
漏洞發生的主要原因是別名操作中對gpu_alloc->gpu_mappings增加計數滯后,導致在mmap系統調用之前,相關的物理頁加入待釋放列表。補丁的思路是將gpu_alloc->gpu_mappings增加計數提前到kbase_mem_alias():
drivers/gpu/arm/b_r19p0/mali_kbase_core_linux.c
1681 u64 kbase_mem_alias(struct kbase_context *kctx, u64 *flags, u64 stride,
1682 u64 nents, struct base_mem_aliasing_info *ai,
1683 u64 *num_pages)
1684 {
1762 for (i = 0; i < nents; i++) {
1763 if (ai[i].handle.basep.handle < BASE_MEM_FIRST_FREE_ADDRESS) {
1773 } else {
1774 struct kbase_va_region *aliasing_reg;
1775 struct kbase_mem_phy_alloc *alloc;
1776
1777 aliasing_reg = kbase_region_tracker_find_region_base_address(
1778 kctx,
1779 (ai[i].handle.basep.handle >> PAGE_SHIFT) << PAGE_SHIFT);
1804 alloc = aliasing_reg->gpu_alloc;
1812 reg->gpu_alloc->imported.alias.aliased[i].alloc = kbase_mem_phy_alloc_get(alloc);
1813 reg->gpu_alloc->imported.alias.aliased[i].length = ai[i].length;
1814 reg->gpu_alloc->imported.alias.aliased[i].offset = ai[i].offset;
+ /* Ensure the underlying alloc is marked as being
+ * mapped at >1 different GPU VA immediately, even
+ * though mapping might not happen until later.
+ *
+ * Otherwise, we would (incorrectly) allow shrinking of
+ * the source region (aliasing_reg) and so freeing the
+ * physical pages (without freeing the entire alloc)
+ * whilst we still hold an implicit reference on those
+ * physical pages.
+ */
+ kbase_mem_phy_alloc_gpu_mapped(alloc);
1817 }
1818 }
1873 }
總結
本文詳細分析位于ARM Mali GPU驅動中的一個邏輯漏洞。這個漏洞可以幫助攻擊者:
1 竊取其他APP運行時內存數據
2 修改其他APP代碼
3 竊取內核運行時內存數據
4 穩定地獲得ROOT權限
在此之前,據我所知,沒有公開資料介紹如何利用該漏洞。而本文指出了一種可行方法:借助OpenCL繞過GPU私有指令集,實現GPU讀寫任意內存。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1690/
暫無評論