攻击高通Adreno GPU

2020-09-09 13:10:55

在编写Android漏洞攻击时,突破应用程序沙箱通常是关键的一步。有很多远程攻击可以让您以应用程序(如浏览器或消息传递应用程序)的权限执行代码,但是仍然需要沙盒转义才能获得完全的系统访问权限。

这篇博客关注的是一个有趣的攻击面,可以从Android应用程序沙箱访问:图形处理器(GPU)硬件。我们描述了高通Adreno GPU中的一个不同寻常的漏洞,以及如何利用它在Android应用程序沙箱中实现内核代码执行。

这项研究是在广功(@oldfresh)的工作基础上进行的,他在2019年8月报告了CVE-2019年-10567。一年后,也就是2020年8月上旬,广工发布了一份出色的白皮书,描述了CVE2019-10567和其他一些允许远程攻击者完全危害系统的漏洞。

然而,在2020年6月,我注意到CVE-2019-10567的补丁是不完整的,并与高通的安全团队和图形处理器工程师合作,从根本上解决了这个问题。针对此新问题的补丁CVE-2020-11179已发布给原始设备制造商进行集成。我们的理解是,高通将在2020年11月的公告中公开列出这一点。

提供支持强大安全和隐私的技术是高通技术的首要任务。我们赞扬Google Project Zero的安全研究人员使用行业标准的协调披露实践。关于高通Adreno GPU漏洞,我们没有证据表明它目前正在被利用,高通技术公司于2020年8月向OEM提供了修复程序。我们鼓励最终用户在他们的运营商或设备制造商提供补丁程序时更新他们的设备,并且只安装来自可信位置(如Google Play Store)的应用程序。";

Android应用程序沙箱是SELinux、seccomp BPF过滤器和基于每个应用程序唯一UID的自主访问控制的不断发展的组合。沙箱用于限制应用程序可以访问的资源,减少攻击面。攻击者使用许多众所周知的路线来逃离沙箱,例如:攻击其他应用程序、攻击系统服务或攻击Linux内核。

在更高的层面上,Android生态系统中有几个不同的攻击层。以下是一些重要的问题:

描述:影响Android生态系统很大一部分的问题,基于不同OEM供应商使用的硬件类型。

从攻击者的角度来看,维护Android攻击能力是一个以最具成本效益的方式覆盖尽可能广泛的Android生态系统的问题。无处不在的层中的漏洞特别吸引人,因为它会影响很多设备,但与其他层相比,查找成本可能会很高,而且寿命相对较短。芯片组层通常会为每个漏洞提供相当多的覆盖范围,但不像无处不在的层那么多。对于某些攻击面,例如基带和WiFi攻击,芯片组层是您的主要选择。供应商和设备层更容易发现漏洞,但需要维护更多的单个利用漏洞。

对于沙盒转义,GPU从芯片组层提供了一个特别有趣的攻击面。由于GPU加速在应用中应用广泛,因此Android沙箱允许完全访问底层GPU设备。此外,在Android设备中特别流行的GPU硬件实现只有两种:ARM马里和高通Adreno。

这意味着,如果攻击者可以在这两个GPU实现中找到一个很好的可利用的错误,那么他们就可以有效地维护一个针对Android生态系统的大部分沙盒逃逸攻击能力。此外,由于GPU非常复杂,有大量的封闭源代码组件(如固件/微码),因此很有可能发现功能相当强大且持续时间较长的漏洞。

考虑到这一点,在2020年4月下旬,我在QUALCOMM Adreno内核驱动程序代码中注意到以下提交:

当我们考虑给地址增加熵时,我们通常会想到地址空间布局随机化(ASLR)。但是这里我们谈论的是GPU虚拟地址,而不是内核虚拟地址。这看起来很不寻常,为什么GPU地址需要随机化呢?

要确认此提交是高通咨询中链接的CVE-2019-10567的安全补丁之一,这是相对直截了当的。还包括此CVE的相关修补程序:

因此,问题就变成了,为什么用户内容不会最终出现在环形缓冲区上很重要,这个补丁真的足以防止这种情况发生吗?如果我们可以恢复临时映射的基地址会发生什么呢?至少从表面上看,两者都是可能的,所以这项研究项目有了一个很好的开端。

在我们继续之前,让我们退后一步,描述一下这里涉及的一些基本组件:GPU、环形缓冲区、暂存映射等等。

GPU是现代图形计算的主力,大多数应用程序都广泛使用GPU。从应用程序的角度来看,GPU硬件的具体实现通常由OpenGL ES和Vulkan等库抽象出来。这些库实现用于编程常见GPU加速操作(如纹理贴图和运行着色器)的标准API。然而,在较低的级别上,此功能是通过与运行在内核空间中的GPU设备驱动程序交互来实现的。

具体而言,对于高通Adreno,/dev/kgsl-3d0设备文件最终用于实现更高级别的GPU功能。在不受信任的应用程序沙箱中可以直接访问/dev/kgsl-3d0文件,因为:

设备文件的文件权限中设置了全局读/写访问权限。权限由ueventd设置:

设备文件的SELinux标签设置为GPU_DEVICE,并且UNTRUSTED_APP SELinux上下文对此标签有特定的允许规则:

这意味着应用程序可以打开设备文件。然后,Adreno&34;KGSL&34;内核设备驱动程序主要通过多个不同的ioctl调用来调用(例如,分配共享内存、创建GPU上下文、提交GPU命令等)。以及mmap(例如将共享存储器映射到用户空间应用)。

在大多数情况下,应用程序使用共享映射将顶点、碎片和着色器加载到GPU中,并接收计算结果。这意味着某些物理内存页面在用户端应用程序和GPU硬件之间共享。

要设置新的共享映射,应用程序将通过调用IOCTL_KGSL_GPUMEM_ALLOC ioctl向KGSL内核驱动程序请求分配。内核驱动程序将准备一个物理内存区域,然后将该内存映射到GPU的地址空间(对于特定的GPU上下文,如下所述)。最后,应用程序将使用从分配ioctl返回的标识符将共享内存映射到用户地址空间。

此时,同一物理内存页面上有两个截然不同的视图。第一个视图来自userland应用程序,它使用虚拟地址访问映射到其地址空间的内存。CPU的内存管理单元(MMU)将执行地址转换以查找适当的物理页。

另一种是来自GPU硬件本身的视图,它使用GPU虚拟地址。GPU虚拟地址由KGSL内核驱动程序选择,该驱动程序使用仅用于GPU的页表结构配置设备的IOMMU(在ARM上称为SMMU)。当GPU尝试读取或写入共享内存映射时,IOMMU会将GPU虚拟地址转换为内存中的物理页面。这类似于在CPU上执行的地址转换,但具有完全不同的地址空间(即应用程序中使用的指针值将不同于GPU中使用的指针值)。

每个Userland进程都有自己的GPU上下文,这意味着当某个应用程序在GPU上运行操作时,GPU将只能访问它与该进程共享的映射。这是必需的,这样一个应用程序就不会要求GPU从另一个应用程序读取共享映射。在实践中,这种分离是通过每当发生GPU上下文切换时改变加载到IOMMU中的页表集合来实现的。只要计划GPU运行来自不同进程的命令,就会发生GPU上下文切换。

但是,所有GPU上下文都使用某些映射,因此可以出现在每组页表中。它们称为全局共享映射,用于GPU和KGSL内核驱动程序之间的各种系统和调试功能。虽然它们从未直接映射到用户端应用程序(例如,恶意应用程序不能直接读取或修改全局映射的内容),但它们会同时映射到GPU和内核地址空间。

在有根的Android设备上,我们可以使用以下命令转储全局映射(及其GPU虚拟地址):

突然,我们的刮擦缓冲区出现了!在左边,我们看到每个全局映射的GPU虚拟地址,然后是大小,然后是分配的名称。通过多次重启设备并检查布局,我们可以看到暂存缓冲区确实是随机的:

相同的测试显示,暂存缓冲区是唯一随机化的全局映射,所有其他全局映射都有一个固定的GPU地址,范围为[0xFC000000,0xFD400000]。这是有道理的,因为CVE2019-10567的补丁只为临时缓冲区分配引入了KGSLMEMDESCRANDOM标志。

因此,我们现在知道暂存缓冲区被正确随机化(至少在一定程度上),并且它是存在于每个GPU上下文中的全局共享映射。但是,暂存缓冲区到底是用来做什么的呢?

深入到驱动程序代码中,我们可以清楚地看到驱动程序的探测例程中分配了暂存缓冲区,这意味着设备首次初始化时将分配暂存缓冲区:

*支持GPU和CPU。例如,GPU将使用它来编写。

通过交叉引用内核驱动程序中产生的内存描述符(Device->;Scratch)的所有用法,我们可以找到临时缓冲区的两个主要用法:

抢占恢复缓冲器的GPU地址被转储到临时存储器,这似乎在较高优先级的GPU命令中断较低优先级的命令时使用。

环形缓冲区(RB)的读指针(RPTR)从临时存储器中读取,并在计算环形缓冲区中的空闲空间量时使用。

在这里,我们可以开始把这些点联系起来。首先,我们知道CVE2019-10567的补丁包含对临时缓冲区和环形缓冲区处理代码的更改--这表明我们应该关注上面的第二个用例。

如果GPU将RPTR值写入共享映射(如注释所示),并且内核驱动程序正在从暂存缓冲区读取RPTR值并将其用于分配大小计算,那么如果我们可以让GPU写入无效或不正确的RPTR值,会发生什么情况呢?

要理解无效的RPTR值对于环形缓冲区分配可能意味着什么,我们首先需要描述环形缓冲区本身。当用户端应用程序提交GPU命令(IOCTL_KGSL_GPU_COMMAND)时,驱动程序代码会通过使用生产者-消费者模式的环形缓冲区将命令调度到GPU。内核驱动程序将命令写入环形缓冲区,而GPU将从环形缓冲区读取命令。

这以类似于经典循环缓冲区的方式发生。在较低级别,环形缓冲区是固定大小为32768字节的全局共享映射。维护两个索引以跟踪CPU正在写入的位置(WPTR)和GPU正在读取的位置(RPTR)。要在环形缓冲区上分配空间,CPU必须计算当前WPTR和当前RPTR之间是否有足够的空间。这发生在adreno_ringbuffer_allocspace中:

我们可以看到在[1]处读取的RPTR值,并且它最终来自在[5]处的临时全局共享映射的读取。然后,我们可以看到在与[2]和[3]处的WPTR值进行两次比较时使用的临时RPTR值。第一个比较是针对临时RPTR小于或等于WPTR的情况,这意味着在环形缓冲器的末端或在环形缓冲器的开始处可能有空闲空间。第二个比较是针对擦伤RPTR高于WPTR的情况。如果在WPTR和Scratch RPTR之间有足够的空间,那么我们可以使用该空间进行分配。

那么,如果临时RPTR值由攻击者控制,会发生什么情况呢?在这种情况下,攻击者可以使上述任一条件成功,即使环形缓冲区中实际上没有用于请求的分配大小的空间。例如,我们可以通过人为增加临时RPTR的值,使[3]处的条件在通常不会成功的情况下成功,这会在[4]处导致返回与正确的RPTR位置重叠的部分环形缓冲区。

这意味着攻击者可以用传入的GPU命令覆盖尚未被GPU处理的环形缓冲区命令!或者换句话说,控制暂存RPTR值可能会使CPU和GPU对环形缓冲区布局的理解不同步。那听起来会很有用的!但是,我们如何覆盖临时RPTR值呢?

由于全局共享映射未映射到用户区域,因此攻击者无法直接从其恶意/受损的用户区域进程修改暂存缓冲区。但是,我们知道暂存缓冲区映射到每个GPU上下文中,包括恶意攻击者创建的任何上下文。如果我们可以让GPU硬件代表我们将恶意的RPTR值写入暂存缓冲区,会怎么样?

要实现这一点,有两个基本步骤。首先,我们需要确认映射是否可由用户提供的GPU命令写入。其次,我们需要一种方法来恢复临时映射的GPU基地址。由于最近添加了用于临时映射的GPU地址随机化,因此后一步是必要的。

那么,所有全局共享映射都可以由GPU写入吗?事实证明,并不是每个全局共享映射都可以由用户提供的GPU命令写入,但临时缓冲区可以。我们可以通过使用上面的sysfs调试方法找到临时映射的随机化基础,然后编写一小段GPU命令序列来向临时映射写入一个值来确认这一点:

这里的每个CP_*操作都是在用户空间中构造的,并在GPU硬件上运行。通常,OpenGL库方法和着色器将由供应商支持的库转换为这些原始操作,但攻击者也可以通过设置一些GPU共享内存并调用IOCTL_KGSL_GPU_COMMAND手动构建这些命令序列。然而,这些操作并未记录在案,因此必须通过阅读驱动程序代码和手动测试来推断其行为。一些示例是:1)CP_MEM_WRITE操作将常量值写入GPU地址,2)CP_WAIT_REG_MEM操作暂停执行,直到GPU地址包含某个常量值,以及3)CP_MEM_TO_MEM将数据从一个GPU地址复制到另一个GPU地址。

这意味着我们可以通过检查最终写入是否发生(在正常的用户共享内存映射上)来确保GPU成功地写入暂存缓冲区--如果暂存缓冲区写入没有成功,则CP_WAIT_REG_MEM操作将超时,并且不会写回值。

通过查看全局共享映射的页表是如何在内核驱动程序代码中设置的,也可以确认暂存缓冲区是可写的。具体地说,由于对kgsl_ALLOCATE_GLOBAL的调用没有设置KGSL_MEMFLAGS_GPUREADONLY或KGSL_MEMDESC_PRIVIZED标志,因此生成的映射可由用户提供的GPU命令写入。

但是,如果暂存缓冲区的基址是随机的,我们怎么知道要写到哪里呢?有两种方法可以恢复暂存缓冲区的基址。

第一种方法是简单地使用上面使用的GPU命令来确认暂存缓冲区是可写的,并将其转变为暴力攻击。因为我们知道全局共享映射有一个固定的范围,而且我们知道只有暂存缓冲区是随机的,所以我们要探索的搜索空间非常小。一旦将其他静态全局共享映射位置从考虑中移除,则只有2721个可能的位置用于暂存页。平均而言,在中端智能手机设备上恢复暂存缓冲区地址需要7.5分钟,而且这一时间可能会进一步优化。

第二种方法甚至更好。如上所述,暂存缓冲器也用于抢占。为了使GPU做好抢占的准备,内核驱动程序调用a6xx_preemption_pre_ibmit函数,该函数将一些操作插入到环形缓冲区中。除了6xx_preemption_pre_ibmit将临时缓冲区指针作为CP_MEM_WRITE操作的参数泄漏到环形缓冲区之外,这些操作的详细信息对我们的攻击并不是非常重要。

由于环形缓冲区是全局映射,并且可由用户提供的GPU命令读取,因此可以通过在正确偏移量处使用CP_MEM_TO_MEM命令将临时映射的基础立即提取到环形缓冲区中(即,我们将环形缓冲区的内容复制到攻击者控制的用户共享映射,并且内容包含指向随机临时缓冲区的指针)。

既然我们知道可以可靠地控制临时RPTR值,我们就可以将注意力转向破坏环形缓冲区的内容。环形缓冲区中到底包含了什么,覆盖它给我们带来了什么?

实际上有四个不同的环形缓冲区,每个都用于不同的GPU优先级,但我们只需要一个来应对这种攻击,所以我们选择在现代Android设备上使用最少的环形缓冲区,以避免使用GPU的其他应用程序产生任何噪音(环形缓冲区0,当时Android根本没有使用它)。请注意,环形缓冲区全局共享映射使用KGSL_MEMFLAGS_GPUREADONLY标志,因此攻击者不能直接修改环形缓冲区内容,我们需要使用临时RPTR原语来实现此目的。

回想一下,环形缓冲区用于将命令从CPU发送到GPU。然而,在实践中,用户提供的GPU命令永远不会直接放到环形缓冲区上。这有两个原因:1)环形缓冲区中的空间有限,用户提供的GPU命令可能非常大;2)所有GPU上下文都可以读取环形缓冲区,因此我们希望确保一个进程不能从不同的进程读取命令。

相反,会出现间接层,并且用户提供的GPU命令在发生来自环形缓冲区的间接分支之后运行。从概念上讲,系统级命令直接从环形缓冲区执行,用户级命令在间接分支到GPU共享内存之后运行。一旦用户命令完成,控制流将返回到下一个RiI。

.