作者: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_allocreg->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_allocreg->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)。

之后,用戶同樣需要調用mmapkbase_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_allocgpu_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(&reg->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%成功率)

img

補丁

漏洞發生的主要原因是別名操作中對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讀寫任意內存。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1690/