对高通Adreno GPU处理器的攻击研究 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

对高通Adreno GPU处理器的攻击研究

h1apwn 技术 2022-04-10 11:50:00
170796
收藏

导语:这篇文章介绍了一个影响高通 Adreno GPU 的独特且异常强大的安全漏洞。

在编写 Android 漏洞利用程序时,突破应用程序沙箱通常是关键步骤。有各种各样的远程攻击方法可以让你以应用程序的权限执行代码,但仍需要沙盒逃逸才能获得完整的系统访问权限。

这篇文章重点介绍可从 Android 应用程序沙箱访问系统底层的一个有趣的攻击面:图形处理单元 (GPU) 硬件。下面描述了 Qualcomm 的 Adreno GPU 中的一个漏洞,以及如何使用它在 Android 应用程序沙箱中实现内核代码执行。

这项研究是建立在oldfresher的工作基础上的,他在 2019 年 8 月报告了CVE-2019-10567。一年后,也就是 2020 年 8 月上旬,oldfresher发布了一份披露 CVE-2019-10567的漏洞paper,以及一些允许远程攻击者破坏整个系统的其他漏洞。

但是在 2020 年 6 月,我发现 CVE-2019-10567 的补丁不完整,并与高通的安全团队和 GPU 工程师合作,从根本上修复了这个问题。此新问题的补丁 CVE-2020-11179 已发布给 OEM 供应商进行集成。

0x01 Android 攻击面

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

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

层级:Linux生态

描述:影响 Android 生态系统中所有设备的问题。

示例:Linux 内核漏洞,如 Dirty COW,或标准系统服务中的漏洞。

层级:芯片组

描述:影响 Android 生态系统大部分的问题,具体取决于各种 OEM 供应商使用的硬件类型。

示例:Snapdragon SoC 性能计数器漏洞,或 Broadcom WiFi 固件堆栈溢出漏洞。

层级:供应商

说明:影响特定 Android OEM 供应商的大多数或所有设备的问题

示例:三星内核驱动程序漏洞

层级:设备

说明:影响 Android OEM 供应商的特定设备型号的问题

示例:Pixel 4 人脸解锁“attention aware”漏洞

从攻击者的角度来看, Android 漏洞利用能力是一个以尽可能最具成本效益的方式覆盖尽可能广泛的 Android 生态系统的问题。Linux生态层的漏洞会影响许多设备,但与其他层相比,发现漏洞可能成本高昂且利用效果相对短暂。芯片组层通常会存在相当多的漏洞利用的覆盖范围,但不如生态层漏洞影响大。对于某些攻击面,例如基带和 WiFi 攻击,芯片组层是主要选择。供应商和设备层更容易找到漏洞,但需要维护大量单独的漏洞利用。

对于沙盒逃逸,GPU 从芯片组层提供了一个特别有趣的攻击面。由于 GPU 加速在应用程序中被广泛使用,Android 沙盒允许完全访问底层 GPU 设备。此外,只有两种 GPU 硬件在 Android 设备中特别流行:ARM Mali 和 Qualcomm Adreno。

这意味着,如果攻击者能够在这两个 GPU 实现中找到一个可很好利用的漏洞,那么他们就可以有效地保持针对大多数 Android 生态系统的沙盒逃逸漏洞利用能力。此外,由于 GPU 非常复杂,有大量闭源组件、固件、微代码,因此很有可能会找到一个危害极高且长期存在的漏洞。

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

From 0ceb2be799b30d2aea41c09f3acb0a8945dd8711 Mon Sep 17 00:00:00 2001From: Jordan Crouse Date: Wed, 11 Sep 2019 08:32:15 -0600Subject: [PATCH] msm: kgsl: Make the "scratch" global buffer use a random GPU addressSelect a random global GPU address for the "scratch" buffer that is usedby the ringbuffer for various tasks.

当我们想到向地址添加熵时,通常会想到地址空间布局随机化 (ASLR)。但这里我们谈论的是 GPU 虚拟地址,而不是内核虚拟地址,为什么需要随机分配 GPU 地址?

此提交是 CVE-2019-10567 的安全补丁之一,这些补丁在高通的咨询中有相关链接。此 CVE 还包含一个相关补丁:

From 8051429d4eca902df863a7ebb3c04cbec06b84b3 Mon Sep 17 00:00:00 2001From: Jordan Crouse Date: Mon, 9 Sep 2019 10:41:36 -0600Subject: [PATCH] msm: kgsl: Execute user profiling commands in an IBExecute user profiling in an indirect buffer. This ensures that addressesand values specified directly from the user don't end up in the ringbuffer.

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

在我们进一步讨论之前,让我们退一步描述一下这里涉及的一些基本组件:GPU, ringbuffer, scratch mapping等。

0x02 Adreno GPU 简介

GPU 是现代图形计算的主要组件,大多数应用程序都广泛使用 GPU。从应用程序的角度来看,GPU 硬件的具体实现通常由 OpenGL ES 和 Vulkan 等库抽象出来。这些库实现了一个标准 API,用于对常见的 GPU 加速操作进行编程,例如texture mapping 和 running shaders。然而,在底层,此功能是通过与内核空间中运行的 GPU 设备驱动程序交互来实现的。

image-20220324233623933.pngimage-20220324233623933

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

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

sargo:/ # cat /system/vendor/ueventd.rc | grep kgsl-3d0/dev/kgsl-3d0             0666   system     system

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

sargo:/ # ls -Zal /dev/kgsl-3d0crw-rw-rw- 1 system system u:object_r:gpu_device:s0 239, 0 2020-07-21 15:48 /dev/kgsl-3d0hawkes@glaptop:~$ adb pull /sys/fs/selinux/policy/sys/fs/selinux/policy: 1 file pulled, 0 skipped. 16.1 MB/s ...hawkes@glaptop:~$ sesearch -A -s untrusted_app policy | grep gpu_deviceallow untrusted_app gpu_device:chr_file { append getattr ioctl lock mapopen read write };

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

0x03 GPU 共享映射

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

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

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

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

image-20220324234002661image-20220324234002661.png

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

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

在被root的 Android 设备上,我们可以使用以下命令dump全局映射及其 GPU 虚拟地址:

sargo:/ # cat /sys/kernel/debug/kgsl/globals0x00000000fc000000-0x00000000fc000fff             4096 setstate0x00000000fc001000-0x00000000fc040fff           262144 gpu-qdss0x00000000fc041000-0x00000000fc048fff            32768 memstore0x00000000fce7a000-0x00000000fce7afff             4096 scratch0x00000000fc049000-0x00000000fc049fff             4096 pagetable_desc0x00000000fc04a000-0x00000000fc04afff             4096 profile_desc0x00000000fc04b000-0x00000000fc052fff            32768 ringbuffer0x00000000fc053000-0x00000000fc053fff             4096 pagetable_desc0x00000000fc054000-0x00000000fc054fff             4096 profile_desc0x00000000fc055000-0x00000000fc05cfff            32768 ringbuffer0x00000000fc05d000-0x00000000fc05dfff             4096 pagetable_desc0x00000000fc05e000-0x00000000fc05efff             4096 profile_desc0x00000000fc05f000-0x00000000fc066fff            32768 ringbuffer0x00000000fc067000-0x00000000fc067fff             4096 pagetable_desc0x00000000fc068000-0x00000000fc068fff             4096 profile_desc0x00000000fc069000-0x00000000fc070fff            32768 ringbuffer0x00000000fc071000-0x00000000fc0a0fff           196608 profile0x00000000fc0a1000-0x00000000fc0a8fff            32768 ucode0x00000000fc0a9000-0x00000000fc0abfff            12288 capturescript0x00000000fc0ac000-0x00000000fc116fff           438272 capturescript_regs0x00000000fc117000-0x00000000fc117fff             4096 powerup_register_list0x00000000fc118000-0x00000000fc118fff             4096 alwayson0x00000000fc119000-0x00000000fc119fff             4096 preemption_counters0x00000000fc11a000-0x00000000fc329fff          2162688 preemption_desc0x00000000fc32a000-0x00000000fc32afff             4096 perfcounter_save_restore_desc0x00000000fc32b000-0x00000000fc53afff          2162688 preemption_desc0x00000000fc53b000-0x00000000fc53bfff             4096 perfcounter_save_restore_desc0x00000000fc53c000-0x00000000fc74bfff          2162688 preemption_desc0x00000000fc74c000-0x00000000fc74cfff             4096 perfcounter_save_restore_desc0x00000000fc74d000-0x00000000fc95cfff          2162688 preemption_desc0x00000000fc95d000-0x00000000fc95dfff             4096 perfcounter_save_restore_desc0x00000000fc95e000-0x00000000fc95efff             4096 smmu_info

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

0x00000000fc0df000-0x00000000fc0dffff             4096 scratch...0x00000000fcfc0000-0x00000000fcfc0fff             4096 scratch...0x00000000fc9ff000-0x00000000fc9fffff             4096 scratch...0x00000000fcb4d000-0x00000000fcb4dfff             4096 scratch

同样的测试表明,暂存缓冲区是唯一随机化的全局映射,所有其他全局映射在[0xFC000000, 0xFD400000]范围内都有一个固定的 GPU 地址。这是有道理的,因为 CVE-2019-10567 的补丁只为暂存缓冲区分配引入了 KGSL_MEMDESC_RANDOM 标志。

所以我们现在知道暂存缓冲区至少在某种程度上是正确随机的,并且它是存在于每个 GPU 上下文中的全局共享映射,但是暂存缓冲区到底是做什么用的呢?

0x04 Scratch 缓冲区

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

int adreno_ringbuffer_probe(struct adreno_device *adreno_dev, bool nopreempt){...status = kgsl_allocate_global(device, &device->scratch,                            PAGE_SIZE, 0, KGSL_MEMDESC_RANDOM, "scratch");

我们还发现了下面的注释:

 /* SCRATCH MEMORY: The scratch memory is one page worth of data that *  is mapped into the GPU. This allows for some 'shared' data between *  the GPU and CPU. For example, it will be used by the GPU to write *  each updated RPTR for each RB.

通过在内核驱动程序中交叉引用生成的内存描述符(device->scratch )的所有用法,我们可以找到暂存缓冲区的两个主要用法:

1.抢占恢复缓冲区的 GPU 地址被dump到暂存内存中,如果较高优先级的 GPU 命令中断较低优先级的命令,则会使用该暂存内存。

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

可以开始串联思路。首先,我们知道 CVE-2019-10567 的补丁包括对暂存缓冲区和环形缓冲区处理代码的更改——这表明我们应该关注上面的第二个用例。

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

0x05 环形缓冲区

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

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

unsigned int *adreno_ringbuffer_allocspace(struct adreno_ringbuffer *rb,                unsigned int dwords){        struct adreno_device *adreno_dev = ADRENO_RB_DEVICE(rb);        unsigned int rptr = adreno_get_rptr(rb); [1]        unsigned int ret;        if (rptr _wptr) { [2]                unsigned int *cmds;                if (rb->_wptr + dwords _wptr;                        rb->_wptr = (rb->_wptr + dwords) % KGSL_RB_DWORDS;                        return RB_HOSTPTR(rb, ret);                }                /*                  * There isn't enough space toward the end of ringbuffer. So                 * look for space from the beginning of ringbuffer upto the                 * read pointer.                 */                if (dwords < rptr) {                        cmds = RB_HOSTPTR(rb, rb->_wptr);                        *cmds = cp_packet(adreno_dev, CP_NOP,                                KGSL_RB_DWORDS - rb->_wptr - 1);                        rb->_wptr = dwords;                        return RB_HOSTPTR(rb, 0);                }        }        if (rb->_wptr + dwords < rptr) { [3]                ret = rb->_wptr;                rb->_wptr = (rb->_wptr + dwords) % KGSL_RB_DWORDS;                return RB_HOSTPTR(rb, ret); [4]        }        return ERR_PTR(-ENOSPC);}unsigned int adreno_get_rptr(struct adreno_ringbuffer *rb){        struct adreno_device *adreno_dev = ADRENO_RB_DEVICE(rb);        unsigned int rptr = 0;...                struct kgsl_device *device = KGSL_DEVICE(adreno_dev);                kgsl_sharedmem_readl(&device->scratch, &rptr,                                SCRATCH_RPTR_OFFSET(rb->id)); [5]...        return rptr;}

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

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

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

0x06 攻击 Scratch RPTR

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

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

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

 /* write a value to the scratch buffer at offset 256 */    *cmds++ = cp_type7_packet(CP_MEM_WRITE, 3);    cmds += cp_gpuaddr(cmds, SCRATCH_BASE+256);    *cmds++ = 0x41414141;    /* ensure that the write has taken effect */    *cmds++ = cp_type7_packet(CP_WAIT_REG_MEM, 6);    *cmds++ = 0x13;    cmds += cp_gpuaddr(cmds, SCRATCH_BASE+256);    *cmds++ = 0x41414141;    *cmds++ = 0xffffffff;    *cmds++ = 0x1;    /* write 1 to userland shared memory to signal success */    *cmds++ = cp_type7_packet(CP_MEM_WRITE, 3);    cmds += cp_gpuaddr(cmds, shared_mem_gpuaddr);    *cmds++ = 0x1;

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

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

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

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

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

第二种方法更好。如上所述,暂存缓冲区也用于抢占。为了让 GPU 准备好抢占,内核驱动程序调用a6xx_preemption_pre_ibsubmit函数,该函数将一些操作插入到 ringbuffer。除了a6xx_preemption_pre_ibsubmit将一个暂存缓冲区指针作为CP_MEM_WRITE操作的参数溢出到 ringbuffer 之外,这些操作的细节对我们的攻击不是很重要。

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

0x07 覆盖环形缓冲区

现在我们知道我们可以可靠地控制暂存 RPTR 值,我们可以将注意力转向破坏 ringbuffer 的内容。环形缓冲区中究竟包含什么,覆盖它给我们带来了什么?

实际上有四个不同的环形缓冲区,每个都用于不同的 GPU 优先级,但我们只需要一个来进行这种攻击,因此我们选择在现代 Android 设备上使用最少的环形缓冲区,以避免来自使用 GPU 的其他应用程序的干扰。ringbuffer 全局共享映射使用KGSL_MEMFLAGS_GPUREADONLY标志,因此攻击者无法直接修改 ringbuffer 内容,我们需要使用 scratch RPTR 原语来实现这一点。

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

但是,会出现一层间接寻址,用户提供的GPU命令会在来自ringbuffer的“间接分支”发生后运行。从概念上讲,系统级命令直接从环形缓冲区执行,用户级命令在间接分支到 GPU 共享内存后运行。一旦用户命令完成,控制流将返回到下一个环形缓冲区操作。间接分支通过CP_INDIRECT_BUFFER_PFE操作执行,该操作由adreno_ringbuffer_submitcmd插入到环形缓冲区中。这个操作有两个参数,分支目标的 GPU 地址,例如其中包含用户提供的命令的 GPU 共享内存映射和一个大小值。

除了间接分支操作之外,环形缓冲区还包含许多其他 GPU 命令设置和操作,有点像已编译函数的序言和结尾。这包括前面提到的抢占设置、GPU 上下文切换、性能监控Hook、标识符和保护模式操作。当考虑到我们有某种环形缓冲区损坏原语时,保护模式操作肯定是一个潜在的目标区域,所以让我们进一步探讨一下。

0x08 保护模式

当用户提供的 GPU 命令在 GPU 上运行时,它会在启用保护模式的情况下运行。这意味着无法访问读取或写入某些全局共享映射和某些 GPU 寄存器范围。事实证明,这对 GPU 架构的安全模型至关重要。

如果我们检查所有禁用保护模式实例的驱动程序代码(使用CP_SET_PROTECTED_MODE操作),我们只会看到少数示例。与抢占、性能计数器和 GPU 上下文切换相关的操作都可能在禁用保护模式的情况下运行。

最后一个操作,GPU 上下文切换,听起来特别有趣。提醒一下,当两个不同的进程使用相同的 ringbuffer 时,会发生 GPU 上下文切换。由于不允许来自一个进程的 GPU 命令对属于另一个进程的共享内存进行操作,因此需要上下文切换来切换 GPU 已加载的页表。

如果我们可以让 GPU 切换到攻击者控制的页表会怎样?我们的 GPU 命令不仅可以读取和写入来自其他进程的共享映射,还可以读取和写入内存中的任何物理地址,包括内核内存!

这是一个有趣的命题,看看内核驱动程序如何在环形缓冲区中设置上下文切换操作,它看起来很有可能。根据对驱动程序代码的检查,GPU 有一个CP_SMMU_TABLE_UPDATE切换页表的操作。选择这种设计可能是出于性能考虑,因为这意味着 GPU 可以执行上下文切换,而无需中断内核并等待 IOMMU 重新配置——它可以简单地重新配置自己。

进一步看,GPU 似乎也将 IOMMU 的“TTBR0”寄存器映射到受保护模式的 GPU 寄存器。通过阅读 ARM地址转换和IOMMU文档,我们可以看到 TTBR0 是用于将 GPU 地址转换为物理内存地址的页表的基地址。这意味着如果我们可以将 TTBR0 指向一组恶意页表,那么我们可以将任何 GPU 地址转换为我们选择的任何物理地址。

我们清楚地了解了 CVE-2019-10567 中的漏洞攻击是如何工作的。回想一下,除了随机化暂存缓冲区位置外,CVE-2019-10567 的补丁还“确保直接从用户指定的地址和值不会出现在环形缓冲区中”。

Guang Gong的攻击成功地使用了 RPTR 破坏技术,当时使用暂存缓冲区的静态地址,通过性能分析命令的参数将操作走私到环形缓冲区中,然后是由于巧妙地与 GPU 上的“真实”RPTR 值对齐而执行。走私操作禁用保护模式并转移到攻击者控制的 GPU 命令,这些命令迅速覆盖 TTBR0 以获得任意物理内存的 R/W 原语。

https://github.com/secmob/TiYunZong-An-Exploit-Chain-to-Remotely-Root-Modern-Android-Devices/blob/master/us-20-Gong-TiYunZong-An-Exploit-Chain-to-Remotely-Root-Modern-Android-Devices-wp.pdf

0x09 Recovering 攻击

由于我们已经绕过了 CVE-2019-10567 补丁的第一部分,为了Recovering 攻击,即能够使用修改后的 TTBR0 写入物理内存,我们只需要绕过第二部分也是如此。

从本质上讲,CVE-2019-10567 补丁的第二部分阻止了攻击者控制的命令被写入环形缓冲区,特别是通过分析子系统。绕过此修复的明显途径是找到一种不同的方式来走私攻击者控制的命令。虽然有一些令人兴奋的途径,例如使用用户提供的 GPU 地址作为命令操作码,但我决定采用不同的方法。

与其将攻击者控制的命令插入环形缓冲区,不如使用 RPTR 环形缓冲区损坏原语来取消同步和重新排序合法的环形缓冲区操作。我们需要的两个基本要素是一个禁用保护模式的操作,以及一个调用间接分支的操作——这两者都在 GPU 内核驱动程序代码中有机地发生。

保护模式操作的典型模式是 1) 放弃保护模式,2) 执行一些特权操作,以及 3) 重新启用保护模式。因此,为了Recovering 攻击,我们可以在步骤 1 和 3 之间执行竞争条件。我们可以开始执行特权操作,例如 GPU 上下文切换,当它仍在禁用保护模式的 GPU 上执行时,我们可以使用 RPTR 环形缓冲区损坏原语覆盖特权操作,本质上将 GPU 上下文切换替换为攻击者控制的间接分支。

image-20220324235938567image-20220324235938567.png

为了获得竞争条件,我们必须将攻击者控制的间接分支写入环形缓冲区中的正确偏移量,例如上下文切换的偏移量,并且我们需要对这个写入操作计时,以便 GPU 命令处理器已经执行禁用保护模式的操作,但尚未执行上下文切换本身。

在实践中,这种竞态条件是高度可行的,时空条件相对稳定。具体来说,通过使用等待命令停止 GPU,然后同步上下文切换和环形缓冲区损坏原语,我们可以建立与等待命令的相对偏移,其中 GPU 将首先观察环形缓冲区损坏的写入。这种离散的边界可能是由缓存行为或固定大小的内部预取缓冲区造成的,它可以直接计算填充、有效负载和上下文切换的正确布局。

竞争条件至少可以在 20% 的时间内获胜,竞争条件可以根据需要重复多次,这意味着只需运行循环利用。

一旦攻击成功,在保护模式被禁用的情况下,就会发生攻击者提供的共享内存的分支。这意味着上面描述的 TTBR0 寄存器可以被修改为指向一个攻击者控制的物理页,该物理页包含一个伪造的页表结构。例如对于 rowhammer 攻击,已知物理地址的设置是通过使用 ION 内存分配器或通过伙伴分配器喷射内存来实现的,后一种方法的成功率很高。

这允许攻击者将 GPU 地址映射到任意物理地址。此时攻击者的 GPU 命令可以覆盖内核代码或数据结构以实现任意内核代码执行,这很简单,因为内核位于 Android 内核上的固定物理地址。

0x10 最后一击

下面链接中提供了一个名为 Adrenaline 的漏洞PoC验证程序。PoC演示了对运行 sargo-user QQ2A.200501.001.B3 内核 4.9.200-gdf3ca60d978c-ab6351706 的 Pixel 3a 设备的攻击。它覆盖内核的 sys_call_table 结构中的一个条目以实现 PC 控制。

https://bugs.chromium.org/p/project-zero/issues/detail?id=2052

除了上面讨论的获得竞争条件的 20% 的成功率之外,PoC还有两个不可靠的地方:1)不能保证 ringbuffer 偏移量在内核版本之间是相同的,但是一旦你使用它就应该能稳定的进行计算,以及 2) 用于在已知物理地址分配攻击者控制的数据的喷射技术非常基础,仅用于演示目的,并且选择的固定页面有可能已经在采用。

0x11 研究总结

报告的漏洞被分配为 CVE-2020-11179,并通过应用 GPU 固件和内核驱动程序更改进行了修补,这些更改对用户提供的 GPU 命令对暂存缓冲区的内存访问实施了新的限制,即在运行间接分支时暂存缓冲区受到保护来自环形缓冲区。

这篇文章介绍了一个影响高通 Adreno GPU 的独特且异常强大的安全漏洞。我们概述了 GPU 和内核驱动程序的设计,以及该设计的一些缺陷,这些缺陷会导致对 GPU 本身的共享内存攻击。这导致了一个相对稳定的竞争条件,绕过了 GPU 保护模式,让攻击者控制的 GPU 命令可以访问任意物理内存。最终,这可用于构建一个 Android 沙盒逃逸漏洞,以执行内核代码。

本文翻译自:https://googleprojectzero.blogspot.com/2020/09/attacking-qualcomm-adreno-gpu.html如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务