跳转至

CVE-2022-38181

1217 个字 53 行代码 预计阅读时间 7 分钟

原文链接:Pwning the all Google phone with a non-Google bug

Abstrct

Pixel 6 是第一台号称“全谷歌”的手机,但它存在一个“非谷歌”漏洞 —— Arm Mali GPU CVE-2022-38181,通过这个漏洞可以实现任意内核代码执行和获取 root 权限,本文将介绍这个漏洞的利用方法。

The Arm Mali GPU

Arm Mali GPU 被广泛集成于各类设备中。安卓手机的 GPU Driver 是一个易于受到攻击的目标,因为它们可以直接被不可信 app 访问并且所有的安卓设备 GPU 均为 Qualcomm's Adreno Arm Mali GPU,这意味着任何微小的 bug 都将覆盖大量设备。

事实上,2021 年爆出的 7 Android 0-day 漏洞中有 5 个是针对 GPU 的,最近的在野利用 bug CVE-2021-39793,于 2022 3 月披露。包括本漏洞在内,Qualcomm Adreno Arm Mali GPU 各被爆出过 3 个漏洞。

由于用户空间应用和 GPU 间内存管理的复杂性,在 Arm Mali GPU 的内存管理代码中有许多隐患。本漏洞涉及到 GPU 内存中一个特殊类型:JIT memory

这里的 JIT 不是指编译里提到的即时编译,因为它是作为 non-excutable memory 创建的,被用作 GPU kernel Driver memory cache,能够即时与用户程序共享。当内存不足时,它会返回给 kernel 使用。

许多其它类型的 GPU memory 都是通过 ioctl call(例如KBASE_IOCTL_MEM_IMPORT) 直接创建的。但 JIT memory region 并非如此,它通过使用KBASSE_IOCTL_JOB_SUBMIT ioctl 提交一个特殊的 GPU 指令来创建。

KBASE_IOCTL_JOB_SUBMIT ioctl 可以被用来提交 "job chain" GPU 处理。"job chain" 是包含多个 job 的列表,job 为不透明数据结构(opaque data structure,头部的 job header 后跟随包含特定的指令的 payload。尽管 KBASE_IOCTL_JOB_SUBMIT 通常用于向 GPU 发送指令,但也有一些 job kernel 处理并运行在 host CPU 上。这些 software jobs(softjobs) 中包括 JIT memory allocate free(BASE_JD_REQ_SOFT_JIT_ALLOC and BASE_JD_REQ_SOFT_JIT_FREE)

The life cycle of JIT memory

KBASE_IOCTL_JOB_SUBMIT 是一个通用的 ioctl 调用,包含许多负责处理不同类型 GPU jobs 的路径。BASE_JD_REQ_SOFT_JIT_ALLOC本质上通过调用KBASE_JIT_allocate_process,调用 KBASE_ JIT_allogate_process来创建 JIT 内存区域。为了说明 JIT 内存的生命周期和使用情况,以下介绍一些相关概念。

当使用 Mali GPU 驱动时,用于程序首先需要创建并初始化 kbase_context 内核对象。涉及用户程序打开驱动文件并使用得到的文件描述符调用一系列 ioctl call。每个 file handle 都有各自的kbase_context 对象,负责管理打开的驱动文件的资源。它包括三个负责管理 JIT memory list_head fields: thejit_active_head ,jit_pool_head , jit_destroy_head。正如其名,jit_active_head 包含当前正在使用的 JIT memoryjit_pool_head 包含可被再次使用的 JIT memoryjit_destroy_head 包含待销毁返回 kernel JIT memory

kbase_jit_allocate被调用时,它首先尝试在jit_pool_head中找到合适的区域

    if (info->usage_id != 0)
        /* First scan for an allocation with the same usage ID */
        reg = find_reasonable_region(info, &kctx->jit_pool_head, false);
        ...
    if (reg) {
        ...
        list_move(&reg->jit_node, &kctx->jit_active_head);
    }

如果找到了合适的区域,它会被移动到jit_active_head中,否则会创建新的内存区域并放入jit_active_head中。被kbase_jit_allocate分配的内存区域会通过kbase_jit_allocate_process 存储在kbase_contextjit_alloc数组中。

当用户程序不再需要 JIT memory 时,它会发送BASE_JD_REQ_SOFT_JIT_FREE job GPU,随后该 job 调用kbase_jit_free释放内存。kbase_jit_free并不会将内存区域的页直接返回给 kernel,而是先缩小内存区域到最小值,随后移除所有 CPU 侧的映射,如此一来区域中的页就不再被用户进程的地址空间可达。

void kbase_jit_free(struct kbase_context *kctx, struct kbase_va_region *reg)
{
    ...
    //First reduce the size of the backing region and unmap the freed pages
    old_pages = kbase_reg_current_backed_size(reg);
    if (reg->initial_commit < old_pages) {
        u64 new_size = MAX(reg->initial_commit,
            div_u64(old_pages * (100 - kctx->trim_level), 100));
        u64 delta = old_pages - new_size;
        //Free delta pages in the region and reduces its size to old_pages - delta
        if (delta) {
            mutex_lock(&kctx->reg_lock);
            kbase_mem_shrink(kctx, reg, old_pages - delta);
            mutex_unlock(&kctx->reg_lock);
        }
    }
    ...
    //Remove the pages from address space of user process
    kbase_mem_shrink_cpu_mapping(kctx, reg, 0, reg->gpu_alloc->nents);

注意到区域 (reg) 的内存页在此阶段尚未被完全移除,并且 reg 也并非在这里被 free 的,它将被移入jit_pool_headkbase_contextevict_list

    kbase_mem_shrink_cpu_mapping(kctx, reg, 0, reg->gpu_alloc->nents);
    ...
    mutex_lock(&kctx->jit_evict_lock);
    /* This allocation can't already be on a list. */
    WARN_ON(!list_empty(&reg->gpu_alloc->evict_node));
    //Add reg to evict_list
    list_add(&reg->gpu_alloc->evict_node, &kctx->evict_list);
    atomic_add(reg->gpu_alloc->nents, &kctx->evict_nents);
    //Move reg to jit_pool_head
    list_move(&reg->jit_node, &kctx->jit_pool_head);

kbase_jit_free 完成后,其 caller kbase_jit_free_finish将清除分配内存时存储在jit_alloc中的 reference,即使 reg 在此阶段仍然有效。

static void kbase_jit_free_finish(struct kbase_jd_atom *katom)
{
    ...
    for (j = 0; j != katom->nr_extres; ++j) {
        if ((ids[j] != 0) && (kctx->jit_alloc[ids[j]] != NULL)) {
            ...
            if (kctx->jit_alloc[ids[j]] !=
                    KBASE_RESERVED_REG_JIT_ALLOC) {
                ...
                kbase_jit_free(kctx, kctx->jit_alloc[ids[j]]);
            }
            kctx->jit_alloc[ids[j]] = NULL;    //<--------- clean up reference
        }
    }
    ...
}

正如我们之前看到的那样,jit_pool_head中的内存区域可能在用户分配其他 JIT 区域时被重用。那么jit_destry_head有什么用处呢?当 JIT 内存通过调用kbase_jit_free被释放时,它同时也被放入evict_listevict_list中的内存区域将在内存紧张时被释放。通过将不再使用的 JIT 区域放入evict_listMali 驱动能够保留未使用的 JIT memory 用于快速重新分配,而在资源被需要时再返回给 kernel

Linux 内核提供一种机制用于


最后更新: 2024年8月19日 17:59:19
创建日期: 2024年8月13日 19:15:40

评论