【无标题】

为什么使用 Android GPU 驱动程序

虽然该漏洞本身是一个相当标准的使用后释放漏洞,涉及 GPU 驱动程序中的严格竞争条件,并且这篇文章主要关注如何绕过设备而非 GPU 上的许多缓解措施,但仍然值得给出一些动机来解释为什么 Android GPU 成为攻击者的有吸引力的目标。

正如 Maddie Stone 在文章“你知道的越多,你知道的越多,你不知道的越多”中提到的,在 2021 年检测到的七个被利用的 Android 0-day 中,有五个针对的是 GPU 驱动程序。截至撰写本文时,另一个被利用的漏洞CVE-2021-39793(于 2022 年 3 月披露)也针对的是 GPU 驱动程序。

除了大多数 Android 设备使用 Qualcomm Adreno 或 ARM Mali GPU(这使得能够以相对较少的错误获得普遍覆盖)这一事实之外(Maddie Stone 的文章中提到了这一点),GPU 驱动程序也可以从所有 Android 设备中的不受信任的应用沙箱访问,从而进一步减少了完整链中所需的错误数量。GPU 驱动程序具有吸引力的另一个原因是,大多数 GPU 驱动程序还处理 GPU 设备和 CPU 之间相当复杂的内存共享逻辑。这些通常涉及相当复杂的内存管理代码,这些代码容易出现错误,可被滥用以实现对物理内存的任意读写或绕过内存保护。由于这些错误使攻击者能够滥用 GPU 内存管理代码的功能,因此其中许多错误也无法检测到内存损坏,并且不受现有缓解措施的影响,这些缓解措施主要旨在防止控制流劫持。一些例子是Guan GongBen Hawkes的工作,他们利用处理 GPU 操作码时的逻辑错误来获得任意内存读写。

漏洞

该漏洞是在 Qualcomm msm 5.4 内核的 5.4 分支中引入的,当时引入了新的kgsl 时间线功能以及一些与之相关的新 ioctl。msm 5.4 内核对内核图形支持层 (kgsl) 驱动程序(位于drivers/gpu/msm下,这是 Qualcomm 的 GPU 驱动程序)进行了一些相当大的重构,并引入了一些新功能。这些新功能和重构都导致了许多回归和新的安全问题,其中大多数问题都是在内部发现和修复的,然后在公告中作为安全问题公开披露(赞扬 Qualcomm 没有默默修补安全问题),其中一些问题看起来相当容易被利用

kgsl_timeline`可以通过 ioctl`IOCTL_KGSL_TIMELINE_CREATE`和创建和销毁对象`IOCTL_KGSL_TIMELINE_DESTROY`。对象在字段中`kgsl_timeline`存储对象列表。ioctl和可用于将对象添加到此列表。添加的对象是引用计数对象,并使用标准方法减少其引用计数。`dma_fence``fences``IOCTL_KGSL_TIMELINE_FENCE_GET``IOCTL_KGSL_TIMELINE_WAIT``dma_fence``dma_fence``dma_fence_put

有趣的是timeline->fences,它实际上并不为栅栏保留额外的引用计数。相反,为了避免释放dma_fencein ,使用自定义函数在释放之前将其删除。timeline->fences``release``timeline_fence_release``dma_fence``timeline->fences

dma_fence当中存储的的引用计数kgsl_timeline::fences减少为零时,timeline_fence_release将调用 方法来删除 ,dma_fence以便kgsl_timeline::fences它不再能被 引用kgsl_timeline,然后dma_fence_free调用 来释放对象本身:

static void timeline_fence_release(struct dma_fence *fence)
{
    ...
    spin_lock_irqsave(&timeline->fence_lock, flags);

    /* If the fence is still on the active list, remove it */
    list_for_each_entry_safe(cur, temp, &timeline->fences, node) {
        if (f != cur)
            continue;

        list_del_init(&f->node);    //<----- 1. Remove fence
        break;
    }
    spin_unlock_irqrestore(&timeline->fence_lock, flags);
    ...
    kgsl_timeline_put(f->timeline);
    dma_fence_free(fence);     //<-------    2.  frees the fence
}
fence`尽管from的删除`timeline->fences`受到 的正确保护`timeline->fence_lock`,`IOCTL_KGSL_TIMELINE_DESTROY`但可以在其引用计数达到零之后、但在从in 中删除之前获取对`dma_fence`a的引用:`fences``fences``timeline_fence_release
long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
        unsigned int cmd, void *data)
{
    ...
    spin_lock(&timeline->fence_lock);  //<------------- a.
    list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
        dma_fence_get(&fence->base);
    list_replace_init(&timeline->fences, &temp);
    spin_unlock(&timeline->fence_lock);


    spin_lock_irq(&timeline->lock);
    list_for_each_entry_safe(fence, tmp, &temp, node) { //<----- b.
        dma_fence_set_error(&fence->base, -ENOENT);
        dma_fence_signal_locked(&fence->base);
        dma_fence_put(&fence->base);
    }
    spin_unlock_irq(&timeline->lock);
    ...
}

在 中kgsl_ioctl_timeline_destroy,当销毁时间轴时, 中的栅栏timeline->fences首先被复制到另一个列表,temp然后从 中删除timeline->fences(点 a.)。由于timeline->fences没有保存栅栏的额外引用,因此引用计数会增加以阻止它们在 中被释放temp。同样, 的操作timeline->fences在这里受到保护。但是,如果当达到上面的timeline->fence_lock时栅栏的引用计数已经为零,但还不能将其从 中删除(它还没有到达 中的点 1. 在 中包含的代码片段中),那么将被移动到,尽管它的引用会增加,但已经太晚了,因为当它到达 点 2. 时将释放,而不管引用计数是多少。因此,如果事件按以下顺序发生,则可能会在点 b. 触发释放后使用:a``timeline_fence_release``timeline->fences``timeline_fence_release``dma_fence``temp``timeline_fence_release``dma_fence

img

img

在上图中,红色块表示持有相同锁的代码,这意味着这些块的执行是互斥的。虽然事件的顺序可能看起来有些牵强(当你试图说明竞争条件时,它总是如此),但实际的时间并不难实现。由于 中的timeline_fence_release移除来自 的dma_fence代码timeline->fences无法在 中的代码kgsl_ioctl_timeline_destroy访问时运行timeline->fence(两者都持有),通过向timeline->fence_lock中添加大量,我可以增加运行 中的红色代码块所需的时间。如果我在线程一中的红色代码块运行时将线程二中最后一个 的引用计数减少为零,我可以在增加线程一中这个 的引用计数之前触发。由于线程二中的红色代码块也需要获取,所以它不能在线程一中的红色代码块完成后才移除来自。到那时,所有的都已移动到列表。这也意味着当线程二中的红色代码块运行时,是一个空列表,循环很快完成并继续到。简单来说,只要我向中添加足够多的,我就可以在移动到时创建一个很大的竞争窗口。只要我在这个窗口内减少最后一个 的最后引用计数,我就能触发 UAF 漏洞。dma_fence``timeline->fence``kgsl_ioctl_timeline_destroy``dma_fence``timeline->fences``timeline_fence_release``dma_fence_get``dma_fence``timeline->fence_lock``dma_fence``timeline->fences``dma_fence``timeline->fences``temp``timeline->fences``dma_fence_free``dma_fences``timeline->fences``kgsl_ioctl_timeline_destroy``dma_fences``timeline->fences``temp``dma_fence``timeline->fences

缓解措施

虽然触发该漏洞并不太难,但另一方面,利用它则是完全不同的事情。我用来测试此漏洞和开发漏洞利用的设备是三星 Galaxy Z Flip3。运行内核版本 5.x 的最新三星设备可能具有最多的缓解措施,甚至比 Google Pixels 还要多。虽然运行内核 4.x 的旧设备通常具有缓解措施,例如关闭了kCFI(内核控制流完整性)和变量初始化,但所有这些功能都在 5.x 内核分支中打开,最重要的是,还有三星 RKP(实时内核保护),它保护各种内存区域,例如内核代码和进程凭据,即使实现任意内存读写,也很难执行任意代码。在本节中,我将简要解释这些缓解措施如何影响漏洞利用。

碳氢化合物

kCFI 可以说是最难绕过的缓解措施,尤其是与三星虚拟机管理程序结合使用时,后者可以保护内核中的许多重要内存区域。kCFI 通过使用函数签名限制动态调用站点可以跳转到的位置,从而防止控制流被劫持。例如,在当前漏洞中,在释放后,将调用dma_fence以下函数:dma_fence_signal_locked

long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
        unsigned int cmd, void *data)
{
    ...
    spin_lock_irq(&timeline->lock);
    list_for_each_entry_safe(fence, tmp, &temp, node) {
        dma_fence_set_error(&fence->base, -ENOENT);
        dma_fence_signal_locked(&fence->base);       //<---- free'd fence is used
        dma_fence_put(&fence->base);
    }
    spin_unlock_irq(&timeline->lock);
    ...
}

然后,该函数调用列表内元素的dma_fence_signal_locked函数。cur->func``fence->cb_list

int dma_fence_signal_locked(struct dma_fence *fence)
{
    ...
    list_for_each_entry_safe(cur, tmp, &cb_list, node) {
        INIT_LIST_HEAD(&cur->node);
        cur->func(fence, cur);
    }
    ...
}

如果没有 kCFI,现在释放的fence对象就可以用假对象替换,这意味着cb_list及其元素,因此func,都可以被伪造,从而提供一个现成的原语来调用任意函数,其第一和第二个参数都指向受控数据(fencecur都可以被伪造)。一旦 KASLR 被击败,利用漏洞会非常容易(例如,使用单独的漏洞来泄露内核地址,如本漏洞)。但是,由于 kCFI,func现在只能用具有 类型的函数替换dma_fence_func_t,这极大地限制了此原语的使用。

虽然我过去曾写过如何轻松绕过三星的控制流完整性检查(JOPP,面向跳转的编程预防),但绕过 kCFI 却并非易事。绕过 kCFI 的一种常见方法是使用双重释放来劫持释放列表,然后应用内核空间镜像攻击 (KSMA)。这种方法被多次使用,例如在《 Jun Yao 的[Android 内核上空的三片乌云](https://github.com/2freeman/Slides/blob/main/PoC-2020-Three Dark clouds over the Android kernel.pdf)》、 《台风山竹:一键远程通用 root》中,该攻击由Hongli Han、Rong Jian、Xiaodong Wang 和 Peng Zhou 的两个漏洞形成。

而当前的错误在释放dma_fence_put后调用时也给了我一个双重释放原语:fence

long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
        unsigned int cmd, void *data)
{
    ...
    spin_lock_irq(&timeline->lock);
    list_for_each_entry_safe(fence, tmp, &temp, node) {
        dma_fence_set_error(&fence->base, -ENOENT);
        dma_fence_signal_locked(&fence->base); 
        dma_fence_put(&fence->base);       //<----- free'd fence can be freed again
    }
    spin_unlock_irq(&timeline->lock);
    ...
}

上述代码减少了伪造fence对象的引用计数,我可以控制它使其成为一个,这样伪造的栅栏就会再次被释放。然而,这不允许我应用 KSMA,因为它需要覆盖swapper_pg_dir受三星虚拟机管理程序保护的数据结构。

变量初始化

从 Android 11 开始,内核可以通过启用各种内核构建标志来启用自动变量初始化。例如,以下内容取自 Z Flip3 的构建配置:

# Memory initialization
#
CONFIG_CC_HAS_AUTO_VAR_INIT_PATTERN=y
CONFIG_CC_HAS_AUTO_VAR_INIT_ZERO=y
# CONFIG_INIT_STACK_NONE is not set
# CONFIG_INIT_STACK_ALL_PATTERN is not set
CONFIG_INIT_STACK_ALL_ZERO=y
CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y
# CONFIG_INIT_ON_FREE_DEFAULT_ON is not set
# end of Memory initialization

虽然该功能自 Android 11 起可用,但许多运行内核分支 4.x 的设备并未启用该功能。另一方面,运行内核 5.x 的设备似乎已启用这些功能。除了此功能可以防止明显的未初始化变量漏洞之外,它还使对象替换变得更加困难。特别是,不再可能执行部分对象替换,即仅替换对象的前几个字节,而对象的其余部分保持有效。因此,例如,Jann Horn 的“缓解措施也是攻击面”中“堆喷射”一节下的堆喷射技术类型在自动变量初始化下不再可能。在当前错误的背景下,这种缓解措施限制了我拥有的堆喷射选项,在我们讨论漏洞利用时,我将进一步解释。

释放rcu

这根本不是一个安全缓解措施,但在这里提到它仍然很有趣,因为它与一些已提出的 UAF 缓解措施具有类似的效果。fence此错误中的 UAF 对象在dma_fence_free调用时被释放,它kfree使用而不是正常的kfree_rcu。简而言之,kfree_rcu不会立即释放对象,而是安排在满足某些条件时释放它。这有点像延迟释放,会在释放对象的时间上引入不确定性。有趣的是,这种效果与 Scudo 分配器( Android 用户空间进程的默认分配器)中使用的 UAF 缓解非常相似,它在实际释放释放的对象之前隔离它们以引入不确定性。有人为Linux 内核提出了类似的建议(但后来被拒绝)。除了在对象替换中引入不确定性之外,延迟释放还可能在竞争窗口紧密的情况下导致 UAF 问题。因此,从表面上看,使用kfree_rcu对于利用当前错误来说相当成问题。但是,有许多原语可以操纵竞争窗口的大小,例如在与时间赛跑 - 命中一个微小的内核竞争窗口和[利用 [古老] Linux 上的竞争条件](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019 - Exploiting race conditions on Linux.pdf)中详细介绍的原语(均由 Jann Horn 撰写,较旧的技术用于利用当前的错误),任何紧密的竞争窗口都可以足够大,以允许延迟kfree_rcu以及随后的对象替换。至于不确定性,这似乎也不会造成很大的问题。在利用这个错误时,我实际上必须执行kfree_rcu两次对象替换,第二次甚至不知道释放将在哪个 CPU 核心上发生,但即使有了这个和所有其他移动部件,一个相当未优化的漏洞仍然在测试的设备上以合理的可靠性(~70%)运行。虽然我相信第二次对象替换kfree_rcu(释放对象的 CPU 不确定)可能是不可靠性的主要来源,但我认为可靠性损失更多地归因于缺乏 CPU 知识,而不是延迟释放。在我看来,当存在允许操纵调度程序的原语时,延迟释放可能不是一种非常有效的 UAF 缓解措施。

三星 RKP(实时内核保护)

Samsung RKP 保护内存的各个部分不被写入。这可以防止进程覆盖自己的凭据以成为 root,也可以保护 SELinux 设置不被覆盖。它还可以防止内核代码区域和其他重要对象(如内核页表)被覆盖。但在实践中,一旦实现了任意内核内存读写(受 RKP 限制),就有办法绕过这些限制。例如,可以通过覆盖 avc 缓存来修改 SELinux 规则(例如,请参阅 Valentina Palmiotti 的此漏洞),而获取 root 可以通过劫持以 root 身份运行的其他进程来实现。在当前漏洞的背景下,Samsung RKP 主要与 kCFI 配合使用,以防止调用任意函数。

在这篇文章中,我将利用启用了所有这些缓解措施的漏洞。

利用漏洞

现在我将开始研究该漏洞的利用。这是一个相当典型的释放后使用漏洞,涉及竞争条件和可能相当强大的原语,既有可能进行任意函数调用,又有可能进行双重释放,这并不罕见。除此之外,这是一个典型的漏洞,就像内核中发现的许多其他 UAF 一样。因此,使用这个漏洞来衡量这些缓解措施如何影响标准 UAF 漏洞的开发似乎是合适的。

添加dma_fencetimeline->fences

在“漏洞”一节中,我解释了该漏洞依赖于将dma_fence对象添加到对象fences的列表中kgsl_timeline,然后在kgsl_timeline销毁时将其引用计数减少到零。有两种方法可以将dma_fence对象添加到kgsl_timeline,第一种是使用IOCTL_KGSL_TIMELINE_FENCE_GET

long kgsl_ioctl_timeline_fence_get(struct kgsl_device_private *dev_priv,
        unsigned int cmd, void *data)
{
    ...
    timeline = kgsl_timeline_by_id(device, param->timeline);
    ...
    fence = kgsl_timeline_fence_alloc(timeline, param->seqno); //<----- dma_fence created and added to timeline
    ...
    sync_file = sync_file_create(fence);
    if (sync_file) {
        fd_install(fd, sync_file->file);
        param->handle = fd;
    }
    ...
}

这将创建一个dma_fence并将kgsl_timeline_fence_alloc其添加到。然后,调用者获取与相对应的 的timeline文件描述符。当关闭 时, 的引用计数将减少为零。sync_file``dma_fence``sync_file``dma_fence

第二种选择是使用IOCTL_KGSL_TIMELINE_WAIT

long kgsl_ioctl_timeline_wait(struct kgsl_device_private *dev_priv,
        unsigned int cmd, void *data)
{
    ...
    fence = kgsl_timelines_to_fence_array(device, param->timelines,
        param->count, param->timelines_size,
        (param->flags == KGSL_TIMELINE_WAIT_ANY));     //<------ dma_fence created and added to timeline
    ...
    if (!timeout)
        ret = dma_fence_is_signaled(fence) ? 0 : -EBUSY;
    else {
        ret = dma_fence_wait_timeout(fence, true, timeout);   //<----- 1.
        ...
    }

    dma_fence_put(fence);
    ...
}

这将dma_fence使用 创建对象kgsl_timelines_to_fence_array并将它们添加到timeline。如果timeout指定了值,则调用将进入dma_fence_wait_timeout(路径标记为 1),它将等待,直到超时到期或线程收到中断。dma_fence_wait_timeout完成后,dma_fence_put将调用以dma_fence将 的引用计数减少为零。因此,通过指定较大的超时,dma_fence_wait_timeout将阻塞直到它收到中断,然后释放dma_fence已添加到 的timeline

虽然IOCTL_KGSL_TIMELINE_FENCE_GET乍一看似乎更容易使用和控制,但在实践中,关闭 产生的开销sync_file使得 的销毁时机dma_fence不太可靠。因此,对于漏洞,我使用IOCTL_KGSL_TIMELINE_FENCE_GET创建并添加持久dma_fence对象来填充timeline->fences列表以扩大竞争窗口,而用于 UAF 漏洞的最后一个dma_fence对象是使用IOCTL_KGSL_TIMELINE_WAIT和 添加的,当我向调用 的线程发送中断信号时,它会被释放IOCTL_KGSL_TIMELINE_WAIT

扩大微小的比赛窗口

回顾一下,为了利用该漏洞,我需要从以下代码块中标记的第一个竞争窗口内的a 列表dma_fence中删除 a 的引用计数:fences``kgsl_timeline

long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
        unsigned int cmd, void *data)
{
    //BEGIN OF FIRST RACE WINDOW
    spin_lock(&timeline->fence_lock);
    list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
        dma_fence_get(&fence->base);
    list_replace_init(&timeline->fences, &temp);
    spin_unlock(&timeline->fence_lock);
    //END OF FIRST RACE WINDOW
    //BEGIN OF SECOND RACE WINDOW
    spin_lock_irq(&timeline->lock);
    list_for_each_entry_safe(fence, tmp, &temp, node) {
        dma_fence_set_error(&fence->base, -ENOENT);
        dma_fence_signal_locked(&fence->base);
        dma_fence_put(&fence->base);
    }
    spin_unlock_irq(&timeline->lock);
    //END OF SECOND RACE WINDOW
    ...
}

如前所述,可以通过向中添加大量对象来扩大第一个竞争窗口dma_fencetimeline->fences从而很容易触发该窗口内的引用计数减少。然而,要利用该漏洞,必须在第二个竞争窗口结束之前完成以下代码以及对象替换:

    spin_lock_irqsave(&timeline->fence_lock, flags);
    list_for_each_entry_safe(cur, temp, &timeline->fences, node) {
        if (f != cur)
            continue;
        list_del_init(&f->node);
        break;
    }
    spin_unlock_irqrestore(&timeline->fence_lock, flags);
    trace_kgsl_timeline_fence_release(f->timeline->id, fence->seqno);
    kgsl_timeline_put(f->timeline);
    dma_fence_free(fence);

如前所述,由于spin_lock,上面的代码在第一个竞争窗口结束之前无法启动,但在运行此代码时timeline->fences已被清空,因此循环将快速运行。但是,由于dma_fence_free使用kfree_rcu,实际释放fence被延迟。除非我们操纵调度程序,否则无法在第二个竞争窗口结束之前替换释放的栅栏。为此,我将使用“利用[[古老] Linux 上的竞争条件](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019 - Exploiting race conditions on Linux.pdf)”中的一种技术,我也在另一个Android 漏洞中使用过该技术来扩大此竞争窗口。

我将在这里为不熟悉该技术的读者重述该技术的精髓。

为了确保每个任务(线程或进程)都能公平地共享 CPU 时间,Linux 内核调度程序可以中断正在运行的任务并将其搁置,以便运行另一个任务。这种中断和停止任务的行为称为抢占(被中断的任务被抢占)。任务还可以将自己搁置以允许另一个任务运行,例如当它正在等待某些 I/O 输入时,或者当它调用时sched_yield()。在这种情况下,我们说该任务是自愿被抢占的。抢占也可以发生在系统调用(例如 ioctl 调用)内部,并且在 Android 上,除了某些关键区域(例如持有自旋锁)之外,任务都可以被抢占。可以使用 CPU 亲和性和任务优先级来操纵此行为。

默认情况下,任务以优先级 运行SCHED_NORMAL,但也可以使用调用(或线程)SCHED_IDLE设置较低的优先级。此外,还可以使用 将其固定到 CPU ,这将仅允许它在特定 CPU 上运行。通过将两个任务(一个优先级为 ,另一个优先级为 )固定到同一个 CPU,可以按如下方式控制抢占的时间。sched_setscheduler``pthread_setschedparam``sched_setaffinity``SCHED_NORMAL``SCHED_IDLE

  1. 首先让SCHED_NORMAL任务执行一个系统调用,使其暂停并等待。例如,它可以从没有数据从另一端传入的管道读取数据,然后它会等待更多数据并主动抢占自身,以便任务SCHED_IDLE可以运行。

  2. SCHED_IDLE任务运行时,向任务一直在等待的管道发送一些数据SCHED_NORMAL。这将唤醒该SCHED_NORMAL任务并使其抢占该SCHED_IDLE任务,并且由于任务优先级,该SCHED_IDLE任务将被抢占并搁置。

  3. 然后该SCHED_NORMAL任务可以运行一个忙循环以防止SCHED_IDLE任务被唤醒。

在我们的例子中,对象替换顺序如下:

  1. IOCTL_KGSL_TIMELINE_WAIT在线程上运行以将dma_fence对象添加到kgsl_timeline。将超时设置为较大的值,并使用sched_setaffinity此任务将此任务固定到 CPU,将其称为SPRAY_CPUdma_fence添加对象后,任务将变为空闲状态,直到收到中断。

  2. 设置一个SCHED_NORMAL任务并将其固定到另一个DESTROY_CPU监听空管道的 CPU( )。这将导致此任务最初处于空闲状态,并允许DESTROY_CPU运行优先级较低的任务。一旦空管道收到一些数据,此任务就会运行一个繁忙循环。

  3. 设置一个将运行SCHED_IDLE的任务,以销毁在第一步中添加的时间线。由于第二步中设置的任务正在等待对空管道的响应,因此将首先运行此任务。DESTROY_CPU``IOCTL_KGSL_TIMELINE_DESTROY``dma_fence``DESTROY_CPU

  4. 向正在运行的任务发送中断IOCTL_KGSL_TIMELINE_WAIT。然后,任务将在第一个竞争窗口内解除阻塞并释放正在运行的dma_fence任务。IOCTL_KGSL_TIMELINE_DESTROY

  5. 写入SCHED_NORMAL任务正在监听的空管道。这将导致任务SCHED_NORMAL抢占该SCHED_IDLE任务。一旦成功抢占该任务,DESTROY_CPU将运行忙循环,导致SCHED_IDLE任务被搁置。

  6. 由于SCHED_IDLE正在运行的任务IOCTL_KGSL_TIMELINE_DESTROY被搁置,现在有足够的时间来克服由 引入的延迟,kfree_rcu并允许dma_fence释放和替换步骤四中的 。之后,我可以恢复,IOCTL_KGSL_TIMELINE_DESTROY以便对现在已释放和替换的对象执行后续操作dma_fence

这里需要注意的是,由于当线程持有时不能发生抢占spinlock,因此IOCTL_KGSL_TIMELINE_DESTROY只能在自旋锁之间的窗口期间被抢占(由下面的注释标记):

long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
        unsigned int cmd, void *data)
{
    spin_lock(&timeline->fence_lock);
    list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
      ...
    spin_unlock(&timeline->fence_lock);
    //Preemption window
    spin_lock_irq(&timeline->lock);
    list_for_each_entry_safe(fence, tmp, &temp, node) {
    ...
    }
    spin_unlock_irq(&timeline->lock);
    ...
}

尽管上面的抢占窗口看起来很小,但在实践中,只要任务SCHED_NORMAL试图抢占在第一个被持有期间SCHED_IDLE运行的任务,抢占就会在释放后立即发生,从而更容易在正确的时间成功抢占。IOCTL_KGSL_TIMELINE_DESTROY``spinlock``spinlock``IOCTL_KGSL_TIMELINE_DESTROY

下图说明了理想世界中发生的情况,红色块表示持有spinlock且因此无法抢占的区域,虚线表示空闲的任务。

img

img

下图说明了现实世界中发生的情况:

img

img

对于对象替换,我将使用sendmsg,这是用受控数据替换 Linux 内核中已释放对象的标准方法。由于该方法相当标准,我不会在这里给出详细信息,但请读者参阅上面的链接。从现在开始,我将假设已释放的对象dma_fence被任意数据替换。(使用此方法对前 12 个字节有一些限制,但这不会影响我们的利用。)

假设被释放的dma_fence对象可以被任意数据替换,我们来看看这个假对象是怎么用的。替换之后,它会被如下dma_fence使用:kgsl_ioctl_timeline_destroy

    spin_lock_irq(&timeline->lock);
    list_for_each_entry_safe(fence, tmp, &temp, node) {
        dma_fence_set_error(&fence->base, -ENOENT);
        dma_fence_signal_locked(&fence->base);
        dma_fence_put(&fence->base);
    }
    spin_unlock_irq(&timeline->lock);

三个不同的函数,dma_fence_set_errordma_fence_signal_lockeddma_fence_put将使用参数 进行调用fence。 该函数dma_fence_set_error将向对象写入错误代码fence,这可能对合适的对象替换有用,但对sendmsg对象替换则无用,我不会在这里调查这种可能性。 该函数dma_fence_signal_locked执行以下操作:

int dma_fence_signal_locked(struct dma_fence *fence)
{
    ...
    if (unlikely(test_and_set_bit(DMA_FENCE_FLAG_SIGNALED_BIT,   //<-- 1.
                      &fence->flags)))
        return -EINVAL;

    /* Stash the cb_list before replacing it with the timestamp */
    list_replace(&fence->cb_list, &cb_list);                    //<-- 2.
    ...
    list_for_each_entry_safe(cur, tmp, &cb_list, node) {        //<-- 3.
        INIT_LIST_HEAD(&cur->node);
        cur->func(fence, cur);
    }

    return 0;
}

它首先检查fence->flags(上面的 1.):如果DMA_FENCE_FLAG_SIGNALED_BIT设置了标志,则已fence发出信号,并且函数提前退出。如果fence尚未发出信号,则list_replace调用以移除中的对象fence->cb_list并将它们放置在临时文件中cb_list(上面的 2.)。之后,cb_list调用存储在中的函数(上面的 3.)。如“kCFI”部分所述,由于 CFI 缓解,这只允许我调用某种类型的函数;此外,在这个阶段我不知道函数地址,所以如果我走到这条路,很可能会让内核崩溃。所以,在这个阶段,我别无选择,只能DMA_FENCE_FLAG_SIGNALED_BIT在我的假对象中设置标志,以便dma_fence_signal_locked提前退出。

这给我留下了一个dma_fence_put函数,它会减少引用计数,并在引用计数达到零时fence调用:dma_fence_release

void dma_fence_release(struct kref *kref)
{
    ...
    if (fence->ops->release)
        fence->ops->release(fence);
    else
        dma_fence_free(fence);
}

如果dma_fence_release被调用,那么最终它会检查fence->ops并调用fence->ops->release。这给我带来了两个问题:首先,fence->ops需要指向有效的内存,否则取消引用将失败,即使取消引用成功,也fence->ops->release需要为零,或者必须是适当类型的函数的地址。

所有这些让我面临两个选择。我要么遵循标准路径:尝试用fence另一个对象替换该对象,要么尝试利用dma_fence_putdma_fence_set_error提供的有限写入原语,同时希望我仍然可以控制flags和字段以refcount避免内核崩溃。dma_fence_signal_locked``dma_fence_release

或者,我可以尝试别的方法。

终极的假对象存储

在利用另一个漏洞时,我遇到了软件输入输出转换后备缓冲区 (SWIOTLB),这是一个在启动时很早阶段分配的内存区域。因此,SWIOTLB 的物理地址非常固定,仅取决于硬件配置。此外,由于此内存位于“低内存”区域(Android 设备似乎没有“高内存”区域)而不是内核映像中,因此虚拟地址只是具有固定偏移量的物理地址(对细节感兴趣的读者可以例如关注该kmap函数的实现):

#define __virt_to_phys_nodebug(x) ({                   \
    phys_addr_t __x = (phys_addr_t)(__tag_reset(x));        \
    __is_lm_address(__x) ? __lm_to_phys(__x) : __kimg_to_phys(__x); \
})
#define __is_lm_address(addr)  (!(((u64)addr) & BIT(vabits_actual - 1)))

#define __lm_to_phys(addr) (((addr) + physvirt_offset))

上述定义来自,这是 Android 的相关实现。用于转换地址的arch/arm64/include/asm/memory.h变量是 中的一个固定常量集:physvirt_offset``arm64_memblock_init

void __init arm64_memblock_init(void)
{...
    memstart_addr = round_down(memblock_start_of_DRAM(),
                   ARM64_MEMSTART_ALIGN);
    physvirt_offset = PHYS_OFFSET - PAGE_OFFSET;

 ...
}

除此之外,SWIOTLB 中的内存可以通过adsp不受信任的应用程序访问的驱动程序进行访问,因此这似乎是存储虚假对象和重定向虚假指针的好地方。然而,在 5.x 版本的内核中,只有使用标志编译内核时才会分配 SWIOTLB CONFIG_DMA_ZONE32,而我们的设备并非如此。

然而,还有更好的办法。SWIOTLB 的早期分配为其提供了可预测的地址,这一事实促使我检查启动日志,以查看是否有其他内存区域在启动期间早期分配,结果发现确实有其他内存区域在启动期间很早就分配了。

<6>[    0.000000] [0:        swapper:    0]  Reserved memory: created CMA memory pool at 0x00000000f2800000, size 212 MiB
<6>[    0.000000] [0:        swapper:    0]  OF: reserved mem: initialized node secure_display_region, compatible id shared-dma-pool
...
<6>[    0.000000] [0:        swapper:    0]  OF: reserved mem: initialized node user_contig_region, compatible id shared-dma-pool
<6>[    0.000000] [0:        swapper:    0]  Reserved memory: created CMA memory pool at 0x00000000f0c00000, size 12 MiB

<6>[    0.578613] [7:      swapper/0:    1]  platform soc:qcom,ion:qcom,ion-heap@22: assigned reserved memory node sdsp_region
...
<6>[    0.578829] [7:      swapper/0:    1]  platform soc:qcom,ion:qcom,ion-heap@26: assigned reserved memory node user_contig_region
...

上面的区域Reserved memory似乎是用于分配离子缓冲区的内存池。

在 Android 上,ion_allocator用于分配用于 DMA(直接内存访问)的内存区域,允许内核驱动程序和用户空间进程共享相同的底层内存。不受信任的应用程序可以通过文件访问 ion 分配器/dev/ion,并且ION_IOC_ALLOC可以使用 ioctl 分配 ion 缓冲区。ioctl 会向用户返回一个新的文件描述符,然后可以在mmap系统调用中使用该描述符将 ion 缓冲区的后备存储映射到用户空间。

使用 ion 缓冲区的一个特殊原因是用户可以请求具有连续物理地址的内存。这一点尤其重要,因为有些设备(如硬件上的设备,而不是手机本身)直接访问物理内存,而具有连续的内存地址可以大大提高此类内存访问的性能,而有些设备无法处理不连续的物理内存。

与 SWIOTLB 类似,为了确保有一块具有请求大小的连续物理内存可用,Ion 驱动程序会在启动的早期阶段分配这些内存区域,并将它们用作内存池(“划分区域”),然后在稍后请求时使用这些内存池来分配 Ion 缓冲区。Ion 设备中的内存池并非都是连续内存(例如,通用“系统堆”可能不是物理上连续的区域),但用户可以heap_id_mask在使用时ION_IOC_ALLOC指定具有特定属性的 Ion 堆(例如,连续的物理内存)。

这些内存池在如此早期的阶段分配意味着它们的地址是可预测的,并且仅取决于硬件的配置(设备树、可用内存、内存起始地址、各种启动参数等)。这特别意味着,如果我使用从很少使用的内存池中分配一个 ion 缓冲区ION_IOC

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值