在Android驱动中前进
作者:Seth Jenkins,Google Project Zero
引言
Android的开源生态系统导致了制造商和供应商的惊人多样性,他们开发的软件运行在各种不同的硬件上。这些硬件需要支持驱动程序,这意味着许多不同的代码库都有可能危及大量Android手机的安全。最近有公开案例表明,第三方驱动程序包含严重漏洞并在Android上被利用。虽然关于Android GPU驱动程序已有大量公开(以及野外)安全研究,但其他芯片组组件可能没有被如此频繁地审计,因此本研究试图更详细地探索这些驱动程序。
驱动程序枚举:没有看起来那么简单
本研究重点关注三款Android设备(括号内为芯片组制造商):
- Google Pixel 7(Tensor)
- 小米11T(联发科)
- 华硕ROG 6D(联发科)
为了对这些设备进行驱动程序研究,我首先必须找到每台设备上可从无特权上下文访问的所有内核驱动程序;这项任务因不同设备(即使是同一芯片组制造商内部)内核驱动程序(及其权限结构)的不统一而变得复杂。有几种不同的方法来发现这些驱动程序。最直接的技术是搜索相关的文件系统,寻找暴露的驱动程序设备文件。这些文件是用户态与驱动程序交互的主要方式。通常,用户态进程会open这个“文件”,然后使用read、write、ioctl甚至mmap的组合与驱动程序交互。然后,驱动程序将这些交互“翻译”成对底层硬件设备的操作,并根据需要将该设备的输出发送回用户态。实际上,所有驱动程序都通过ProcFS或DevFS文件系统暴露其接口,因此我在搜索可行的攻击面时重点关注/proc和/dev目录。理论上,评估所有用户态可访问的驱动程序应该像调用find /dev或find /proc,尝试打开发现的每个文件,并记录哪些打开尝试成功一样简单。
然而,有一个主要障碍阻止了这种方法的全面性——权限!SELinux和传统的Linux自主访问控制策略可以阻止简单的文件系统枚举发现文件系统上所有可访问的设备驱动程序。例如,untrusted_app SELinux上下文对目录的device SELinux上下文具有search权限,但不允许直接打开目录本身:

这个search权限(相当反直觉地)不允许源上下文列出具有此SELinux目标上下文的目录内容。相反,它只允许这样的目录位于源上下文尝试打开的文件路径的祖先目录中。实际上,这意味着untrusted_app被允许打开例如/dev/mali0,但通常不允许打开/dev本身:

对于/dev,shell上下文被允许打开并列出/dev的内容。这意味着,首先从shell上下文枚举/dev目录,然后尝试从untrusted_app上下文打开所有发现的文件,安全研究人员可以了解/dev目录中哪些驱动程序可以从应用上下文访问,哪些不能。然而,在某些情况下,某些目录根本无法从可调试的非root上下文列出,特别是在/proc中。枚举所有这些目录的一个选择是root手机,但这并不总是容易实现的。
在这方面,我发现一个有用的策略是检查公开发布的手机型号或类似手机型号的内核源代码。此源代码的位置因制造商而异,但源代码通常托管在Github上或通过制造商网站。设备驱动程序主要通过proc_create()和proc_mkdir()函数调用在/proc中创建文件。一个现实世界的例子是:
parent = proc_mkdir("perfmgr", NULL);
perfmgr_root = parent;
pe = proc_create("perf_ioctl", 0664, parent, &Fops);
...
pe = proc_create("eara_ioctl", 0664, parent, &eara_Fops);
...
pe = proc_create("eas_ioctl", 0664, parent, &eas_Fops);
...
pe = proc_create("xgff_ioctl", 0664, parent, &xgff_Fops);
...
尽管这些文件无法直接枚举,但它们确实存在并且可以从不受信任的上下文访问。

如果不分析内核源代码,就需要root手机才能发现这个驱动程序。
另一个有用的资源是SELinux策略本身。用户态通过一组相当典型的VFS操作与驱动程序交互。这意味着SELinux策略必须封装执行这些操作所需的权限。这意味着SELinux策略通常反映了开发人员打算从不受信任上下文访问的内容。对策略的分析可以发现某些驱动程序可访问性中的一些异常和特性。例如,有时文件可能无法通过文件系统直接打开,但可能存在一些替代方法,应用程序可以要求另一个更高特权的进程代表其打开文件并交回相关的fd,之后应用程序被允许对fd本身进行read/write/ioctl。这种行为的一个例子是Pixel 7上的EdgeTPU设备:

进一步研究表明,如果untrusted_app落在某些应用程序的允许列表中,它可以向特权进程请求访问EdgeTPU驱动程序fd本身。
进行的调查强烈表明,GPU驱动程序是从不受信任应用程序最一致可访问的驱动程序,这是预期的。在Google Pixel 7上,我没有找到太多其他可以从完全无特权上下文访问的内容。尽管如此,受到之前对三星NPU等硬件的类似努力的启发,我对EdgeTPU驱动程序进行了研究——Google的Tensor处理单元,用于在Pixel系列设备上执行ML相关任务。这导致发现了一个重要的问题——在向EdgeTPU内存注册内存时,当vma同时被修改时,存在一个竞态条件。
与Pixel 7不同,联发科芯片组手机(华硕ROG 6D和小米11T)包含几个可以从无特权用户态访问的不同驱动程序:
/proc/ged/proc/mtk_jpeg/proc/perfmgr/[eara_ioctl,eas_ioctl,perf_ioctl,xgff_ioctl]
这些驱动程序代表了比Pixel 7设备上可用的更有趣和复杂的攻击面。ged驱动程序包含许多有趣且有价值的利用原语,我们稍后将详细讨论。虽然perfmgr驱动程序呈现了几个攻击面,但我未能找到任何与安全相关的漏洞。然而,mtk_jpeg驱动程序产生了显著的成果,值得仔细研究。
联发科JPEG解码加速器
mtk_jpeg驱动程序管理联发科设备上的专用硬件,以执行JPEG解码加速。Linux内核文档指出:“Mediatek JPEG Decoder是Mediatek SoC中存在的JPEG解码硬件”。更相关的是,从攻击者的角度来看,这个驱动程序可以从untrusted_app上下文访问(至少在评估的手机上)(尽管奇怪的是,它无法从无特权的adb调试上下文访问)。这个JPEG解码加速器及其相关驱动程序同时存在于小米11T和华硕ROG 6D上。然而,基于这些不同设备内核的开源代码库,联发科似乎正在积极维护该驱动程序的几个不同分支,可能基于相关的内核版本,并且这两款设备使用不同的分支。
我在此驱动程序中发现了两个漏洞。CVE-2023-32837是一个结构体数组中的典型OOB读/写。结构体的各种不同成员被访问和修改,为利用创造了多种可能性,但也使它们更具挑战性。有趣的是,联发科在2021年7月部分修复了这个漏洞,尽管此补丁何时发布给OEM尚不清楚。从提交信息来看,很明显联发科使用Coverity静态分析工具检测到了这个问题,但似乎没有识别出安全影响。无论如何,虽然该问题在联发科内核的某些分支中得到了修复,但在同一驱动程序的其他版本中仍未修补。这意味着虽然华硕ROG 6D(运行内核5.10)已收到此漏洞的补丁,但(其他方面完全修补且安全支持)的小米11T(运行4.14)却没有。
了解JPEG驱动程序如何工作的一些背景知识有助于讨论另一个问题CVE-2023-32832。加速器硬件有两个独立的“核心”可以执行JPEG解码。当进程请求执行JPEG解码工作时,它调用ioctl JPEG_DEC_IOCTL_HYBRID_START,内核在jpeg_drv_hybrid_dec_lock()中决定哪个解码核心将执行该工作(输出已着色以便于跟踪):
static int jpeg_drv_hybrid_dec_lock(int *hwid)
{
int retValue = 0;
int id = 0;
...
mutex_lock(&jpeg_hybrid_dec_lock);
for (id = 0; id < HW_CORE_NUMBER; id++) {
if (dec_hwlocked[id]) {
JPEG_LOG(1, "jpeg dec HW core %d is busy", id);
continue;
} else {
*hwid = id;
dec_hwlocked[id] = true;
JPEG_LOG(1, "jpeg dec get %d HW core", id);
_jpeg_hybrid_dec_int_status[id] = 0;
jpeg_drv_hybrid_dec_power_on(id);
enable_irq(gJpegqDev.hybriddecIrqId[id]);
break;
}
}
mutex_unlock(&jpeg_hybrid_dec_lock);
if (id == HW_CORE_NUMBER) {
JPEG_LOG(1, "jpeg dec HW core all busy");
*hwid = -1;
retValue = -EBUSY;
}
return retValue;
}
数组dec_hwlocked包含每个核心的布尔元素,对于锁定的核心,该元素设置为true,对于解锁的核心设置为false。此数组还受互斥锁保护,以防止对jpeg_drv_hybrid_dec_lock或jpeg_drv_hybrid_dec_unlock的并发调用相互竞争。锁定核心后,jpeg_drv_hybrid_dec_start设置用于解码操作的数据结构:
switch (cmd) {
case JPEG_DEC_IOCTL_HYBRID_START:
if (copy_from_user(
&taskParams, (void *)arg,
sizeof(struct JPEG_DEC_DRV_HYBRID_TASK))) {
return -EFAULT;
}
...
if (jpeg_drv_hybrid_dec_lock(&hwid) == 0) {
*pStatus = JPEG_DEC_PROCESS;
} else {
JPEG_LOG(1, "jpeg_drv_hybrid_dec_lock failed (hw busy)");
return -EBUSY;
}
if (jpeg_drv_hybrid_dec_start(taskParams.data, hwid, &index_buf_fd) == 0) {
...
} else {
JPEG_LOG(0, "jpeg_drv_dec_hybrid_start failed");
jpeg_drv_hybrid_dec_unlock(hwid);
return -EFAULT;
}
break;
...
}
static int jpeg_drv_hybrid_dec_start(unsigned int data[],unsigned int id,int *index_buf_fd)
{
u64 ibuf_iova, obuf_iova;
int ret;
void *ptr;
unsigned int node_id;
JPEG_LOG(1, "+ id:%d", id);
ret = 0;
ibuf_iova = 0;
obuf_iova = 0;
node_id = id / 2;
bufInfo[id].o_dbuf = jpg_dmabuf_alloc(data[20], 128, 0);
bufInfo[id].o_attach = NULL;
bufInfo[id].o_sgt = NULL;
bufInfo[id].i_dbuf = jpg_dmabuf_get(data[7]);
bufInfo[id].i_attach = NULL;
bufInfo[id].i_sgt = NULL;
if (!bufInfo[id].o_dbuf) {
JPEG_LOG(0, "o_dbuf alloc failed");
return -1;
}
if (!bufInfo[id].i_dbuf) {
JPEG_LOG(0, "i_dbuf null error");
return -1;
}
ret = jpg_dmabuf_get_iova(bufInfo[id].o_dbuf, &obuf_iova, gJpegqDev.pDev[node_id], &bufInfo[id].o_attach, &bufInfo[id].o_sgt);
JPEG_LOG(1, "obuf_iova:0x%llx lsb:0x%lx msb:0x%lx", obuf_iova,
(unsigned long)(unsigned char*)obuf_iova,
(unsigned long)(unsigned char*)(obuf_iova>>32));
ptr = jpg_dmabuf_vmap(bufInfo[id].o_dbuf);
if (ptr != NULL && data[20] > 0)
memset(ptr, 0, data[20]);
jpg_dmabuf_vunmap(bufInfo[id].o_dbuf, ptr);
jpg_get_dmabuf(bufInfo[id].o_dbuf);
// get obuf for adding reference count, avoid early release in userspace.
*index_buf_fd = jpg_dmabuf_fd(bufInfo[id].o_dbuf);
ret = jpg_dmabuf_get_iova(bufInfo[id].i_dbuf, &ibuf_iova, gJpegqDev.pDev[node_id], &bufInfo[id].i_attach, &bufInfo[id].i_sgt);
JPEG_LOG(1, "ibuf_iova 0x%llx lsb:0x%lx msb:0x%lx", ibuf_iova,
(unsigned long)(unsigned char*)ibuf_iova,
(unsigned long)(unsigned char*)(ibuf_iova>>32));
if (ret != 0) {
JPEG_LOG(0, "get iova fail i:0x%llx o:0x%llx", ibuf_iova, obuf_iova);
return ret;
}
...
return ret;
}
最后,利用对JPEG_DEC_IOCTL_HYBRID_WAIT的ioctl调用(该调用调用jpeg_drv_hybrid_dec_unlock),释放与核心相关的资源,并将核心释放回以供未来操作使用。
case JPEG_DEC_IOCTL_HYBRID_WAIT:
...
if (copy_from_user(
&pnsParmas, (void *)arg,
sizeof(struct JPEG_DEC_DRV_HYBRID_P_N_S))) {
JPEG_LOG(0, "Copy from user error");
return -EFAULT;
}
/* set timeout */
timeout_jiff = msecs_to_jiffies(3000);
JPEG_LOG(1, "JPEG Hybrid Decoder Wait Resume Time: %ld",
timeout_jiff);
hwid = pnsParmas.hwid;
if (hwid < 0 || hwid >= HW_CORE_NUMBER) { //在驱动程序的其他版本中,省略了这个>=检查,这导致了后来几个不同的OOB访问,即CVE-2023-32837
JPEG_LOG(0, "get hybrid dec id failed");
return -EFAULT;
}
if (!dec_hwlocked[hwid]) {
JPEG_LOG(0, "wait on unlock core %d\n", hwid);
return -EFAULT;
}
if (jpeg_isr_hybrid_dec_lisr(hwid) < 0) {
long ret = 0;
int waitfailcnt = 0;
do {
ret = wait_event_interruptible_timeout(
hybrid_dec_wait_queue[hwid],
_jpeg_hybrid_dec_int_status[hwid],
timeout_jiff);
...
if (ret < 0) {
waitfailcnt++;
usleep_range(10000, 20000);
}
} while (ret < 0 && waitfailcnt < 500);
}
...
if (copy_to_user(pnsParmas.progress_n_status, &progress_n_status,
sizeof(int))) {
return -EFAULT;
}
...
jpeg_drv_hybrid_dec_unlock(hwid);
break;
...
}
...
static void jpeg_drv_hybrid_dec_unlock(unsigned int hwid)
{
mutex_lock(&jpeg_hybrid_dec_lock);
if (!dec_hwlocked[hwid]) {
JPEG_LOG(0, "try to unlock a free core %d", hwid);
} else {
dec_hwlocked[hwid] = false;
JPEG_LOG(1, "jpeg dec HW core %d is unlocked", hwid);
jpeg_drv_hybrid_dec_power_off(hwid);
disable_irq(gJpegqDev.hybriddecIrqId[hwid]);
jpg_dmabuf_free_iova(bufInfo[hwid].i_dbuf,
bufInfo[hwid].i_attach,
bufInfo[hwid].i_sgt);
jpg_dmabuf_free_iova(bufInfo[hwid].o_dbuf,
bufInfo[hwid].o_attach,
bufInfo[hwid].o_sgt);
jpg_dmabuf_put(bufInfo[hwid].i_dbuf);
jpg_dmabuf_put(bufInfo[hwid].o_dbuf);
// we manually add 1 ref count, need to put it.
}
mutex_unlock(&jpeg_hybrid_dec_lock);
}
如果jpeg_drv_hybrid_dec_start失败,也会调用jpeg_drv_hybrid_dec_unlock。
虽然jpeg_hybrid_dec_lock互斥锁保护直接的核心锁定和解锁,但它不保护jpeg_drv_hybrid_dec_start函数体。这意味着虽然不能同时调用jpeg_drv_hybrid_dec_lock和jpeg_drv_hybrid_dec_unlock,但可以同时调用jpeg_drv_hybrid_dec_start和jpeg_drv_hybrid_dec_unlock,这在实践中同样糟糕,因为这两个函数竞态访问相同的全局数据结构bufInfo。
这个漏洞的一个小复杂之处在于,为了在JPEG_DEC_IOCTL_HYBRID_WAIT调用中到达jpeg_drv_hybrid_dec_unlock,核心必须在超时前被锁定,因为有一个检查确保在尝试等待核心之前核心已被锁定。
两个进程A和B在实践中发生竞态的例子如下(根据上述代码着色):
进程A:
调用ioctl JPEG_DEC_IOCTL_HYBRID_START,用jpeg_drv_hybrid_dec_lock锁定核心0并进入jpeg_drv_hybrid_dec_start
进程B:
调用ioctl JPEG_DEC_IOCTL_HYBRID_WAIT,确认核心0已锁定,然后开始3秒等待,等待核心发送表示解码请求完成的中断。
进程A:jpeg_drv_hybrid_dec_start失败(在初始化一些数据结构之后),在核心0上调用jpeg_drv_hybrid_dec_unlock释放任何已分配的资源,并返回到用户态。
[等待约3秒]
进程A:
调用ioctl JPEG_DEC_IOCTL_HYBRID_START,用jpeg_drv_hybrid_dec_lock锁定核心0并进入jpeg_drv_hybrid_dec_start
进程B:
3秒等待超时,JPEG_DEC_IOCTL_HYBRID_WAIT ioctl调用用jpeg_drv_hybrid_dec_unlock解锁核心0。
[进程A和B现在同时初始化和释放相同的数据结构]
这可能导致各种释放后使用或双重释放条件,具体取决于进程A和B的竞态情况。
获取root之路
下一步是尝试利用这些问题。我的第一次尝试针对OOB写问题CVE-2023-32837。我能够将内核.data区域中不受控制的OOB读/写原语发展为在攻击者控制进程使用的内核任务栈中预定偏移处的竞态空字节写入。此时,在任何系统调用期间用空值覆盖内核栈条目对我来说似乎有足够的灵活性来创建完整的利用。然而,尽管我尽了最大努力(包括创建一个工具来查找任意回溯中写入发生的位置),我未能找到一种技术来从这个写入创建更好的原语。
那次努力失败后,我决定看看同一驱动程序中的另一个问题CVE-2023-32832。在与jpeg_drv_hybrid_dec_start竞争的释放步骤中,jpeg_drv_hybrid_dec_unlock释放了对四个独立资源的访问:
jpg_dmabuf_free_iova(bufInfo[hwid].i_dbuf, bufInfo[hwid].i_attach, bufInfo[hwid].i_sgt); //核心中输入缓冲区的虚拟地址映射
jpg_dmabuf_free_iova(bufInfo[hwid].o_dbuf, bufInfo[hwid].o_attach, bufInfo[hwid].o_sgt); //核心中输出缓冲区的虚拟地址映射
jpg_dmabuf_put(bufInfo[hwid].i_dbuf); //输入缓冲区文件的引用计数递减。此缓冲区先前由攻击者分配并与文件描述符关联。
jpg_dmabuf_put(bufInfo[hwid].o_dbuf); //输出缓冲区文件的引用计数递减。此缓冲区先前在jpeg_drv_hybrid_dec_start期间分配。
驱动程序的一个关键行为增强了可利用性:尽管jpeg_drv_hybrid_dec_unlock正确减少了i_dbuf和o_dbuf的引用计数,但它没有将bufInfo全局数组中的这些条目重新初始化为NULL。就竞态而言,这意味着如果进程B的竞态jpeg_drv_hybrid_dec_unlock发生在进程A的第二次jpeg_drv_hybrid_dec_start重新初始化i_dbuf和o_dbuf之前,将释放i_dbuf和o_dbuf的额外引用计数。由于i_dbuf和o_dbuf是struct file*,这可以直接导致struct file UAF。由于i_dbuf struct file直接来自传递给jpeg_drv_hybrid_dec_start的dmabuf文件描述符,这导致了一个悬空的文件描述符,其底层的struct file被释放。这无疑是一个可利用的漏洞。
有几种不同的技术可以利用悬空文件描述符。一种广泛使用的策略是导致struct file的后备slab页面被释放并返回到页面分配器,然后用管道缓冲区数据页面重新分配该页面,以获得对用于struct file的内存的控制。另一种众所周知的策略是利用跨缓存技术将内存重新分配为不同类型的kmalloc slab/对象。然而,未来如果SLAB_VIRTUAL缓解措施在主线的Linux内核中生效,这两种技术都可能得到补救。为了探索Android内核黑客攻击的未来,我寻求一种不涉及跨缓存或slab缓存->页面分配器堆塑形技术的新颖利用技术。
一些其他新颖的利用技术
最常见的UAF利用技术之一是用不同类型的新对象重新分配一级释放的对象,创建类型混淆条件,从而改进内存破坏原语。然而,在指定用于struct file缓存的页面中唯一可以分配的对象类型是struct file类型,因此使用一级对象回收创建类型混淆内存破坏条件的选项有限。然而,仅仅因为一个对象被释放,并不意味着它在有限情况下不能使用。当内核将对象插入空闲列表时,这会破坏对象的中间部分。然而,对象的其余部分保持在释放时的任何状态,包括指针和任何其他成员变量。这些陈旧的指针可以(并且在实践中经常)指向其他已释放的对象,这些对象可能完全从不同的slab缓存分配,可能包括通用的kmalloc slab缓存。请注意,在C语言惯例中,指针在释放后被设置为NULL,这些陈旧的指针就不会存在。然而,由于包含指针的内存无论如何都会被释放,将这些指针设置为NULL通常被视为不必要的保守(事实上,C编译器通常会丢弃对即将释放的对象的写入)。
通过继续使用这个已释放的一级对象,我们可以隐式访问已释放的二级子对象。通过回收这些对象,我们可以重新创建类型混淆内存破坏原语,利用一级释放对象上调用的方法如何隐式访问二级子对象。让我们看看如何将其应用于我们的特定场景。
Linux内核的struct file可能代表许多不同类型的文件,具体取决于打开文件的类型,例如ext4文件、procfs文件,甚至是联发科JPEG解码驱动程序文件。为了表示所有这些不同类型,同时为打开文件的普遍需要的成员保持一些结构共性,struct file包含一个private_data成员,该成员引用所需的任何类型特定数据。
如前所述,在这种情况下UAF的struct file是一个dmabuf文件。这意味着private_data指针指向一个struct dma_buf对象。dma_buf对象的生命周期隐式地与其关联的dmabuf文件结构体的生命周期绑定。当dmabuf struct file被释放时,dma_buf对象也被释放。然而,与struct file不同,dma_buf对象是从通用的kmalloc slab缓存分配的。这意味着dma_buf可以用来自相同通用kmalloc slab缓存的不同类型对象回收。
在假设被新对象回收后,这个新对象仍然可以作为dma_buf通过已释放但仍然非常可用的dmabuf struct file进行UAF引用,而该文件本身通过悬空文件描述符引用!因此我们得出以下策略:
- 使用我们的竞态条件漏洞删除
i_dbuf上的额外引用(同时释放dma_buf)来释放文件,留下一个指向已释放struct file的悬空fd,该文件仍然有一个指向已释放dma_buf的陈旧指针 - 回收
dma_buf,但不回收struct file - 在悬空
fd上调用dma_buf操作
此时你可能敏锐地注意到,这个策略依赖于一个已释放的对象(即struct file)没有被堆分配器回收为另一个对象。这绝对正确,并且人们会期望,在优先考虑极高可靠性的利用中,可能需要进行一些堆塑形以将这个已释放的struct file深埋在分配器空闲列表中。在实践中(以及在我的利用中),已释放的struct file很少会在per-CPU活动slab上,因此不太可能立即被回收,而且我的利用通常运行得足够快,这并不重要。
此时,我们现在需要确定使用什么对象来回收已释放的dma_buf,以及在已释放的dma_buf文件/对象上调用什么操作来开发更强的原语。我最终在GED驱动程序中找到了这两个问题的解决方案。
GED驱动程序
GED(GPU扩展设备)驱动程序是联发科特定的接口,为用户态提供几个补充的GPU功能,主要用于调优目的。它的两个“功能”显得特别有价值。第一个功能,GED GE缓冲区,提供了一个真正卓越的堆喷射和回收原语。此功能提供了合适堆喷射原语的几个必要特征:
- 分配受控大小的缓冲区,不会对堆的其余部分造成过度干扰。
- 缓冲区数据完全由攻击者控制,开头没有不受控制的头部
- 缓冲区可以随时释放。
然而,一个突出的特征将这个堆喷射原语提升到许多同类之上,那就是即使分配后,攻击者也可以随意读写这些缓冲区,同时保持缓冲区分配状态。这是一个可以想象到的最强大的堆喷射原语。通过用GED GE缓冲区回收UAF的dma_buf结构体,我们获得了对dma_buf结构体的完全确定性读/写,包括其中包含的任何指针。

第二个功能是通往与DMA_BUF_SET_NAME ioctl相同功能的替代代码路径,该路径(非常合理地)用于设置dma_buf的名称。这些路径之间最大的区别是GED代码路径缺乏对底层dma_buf fd的SELinux inode检查。这些inode检查通常会在已释放的struct file上运行时导致内核崩溃——然而,由于GED代码路径,我们可以跳过此inode检查,并更改dma_buf的名称,尽管在已释放的dma_buf文件上运行!通常,此代码会释放dma_buf结构体内指向先前名称的指针,并为名称字符串分配新缓冲区。然而,由于GED GE缓冲区,我们能够控制整个dma_buf结构体。通过结合这些原语,我们可以在设置新名称字符串之前kfree一个任意指针。
long mtk_dma_buf_set_name(struct dma_buf *dmabuf, const char *buf)
{
char *name = kstrndup(buf, DMA_BUF_NAME_LEN, GFP_KERNEL);
...
kfree(dmabuf->name); //dmabuf由攻击者控制
dmabuf->name = name; //名称指针写入攻击者控制的内存
...
}
实现任意读
无害的dmabuf文件操作现在变成了强大的原语,因为我们可以精确控制dma_buf结构体。例如,这是/proc/pid/fdinfo/n对于dmabuf文件的代码:
static void dma_buf_show_fdinfo(struct seq_file *m, struct file *file)
{
struct dma_buf *dmabuf = file->private_data;
seq_printf(m, "size:\t%zu\n", dmabuf->size);
/* Don't count the temporary reference taken inside procfs seq_show */
seq_printf(m, "count:\t%ld\n", file_count(dmabuf->file) - 1);
seq_printf(m, "exp_name:\t%s\n", dmabuf->exp_name);
spin_lock(&dmabuf->name_lock);
if (dmabuf->name)
seq_printf(m, "name:\t%s\n", dmabuf->name);
spin_unlock(&dmabuf->name_lock);
}
此函数中有几个机会可以实现任意读,但最简洁的一个是file_count()调用,它将解引用传递的指针+硬编码偏移量,并将读取的8字节值打印为有符号长整型。通常在此C函数的上下文中,file == ((struct dma_buf*) file->private_data)->file,但由于我们控制dma_buf结构体,情况不一定如此。
实现任意写
此时我们拥有三个强大的原语:
- 读/写UAF的
dma_buf结构体内存(通过GED GE缓冲区) - 任意读(通过
dma_buf_show_fdinfo) - 任意释放(通过
ged_dmabuf_set_name)

有许多潜在的策略可以使用这些原语来实现任意写原语。我选择的技术是将GE缓冲区与GE缓冲区数组类型混淆。GED GE缓冲区通过结构体和数组的层次结构进行跟踪。GE文件的private_data成员指向一个GE缓冲区指针数组,如下所示:

我通过使用先前开发的任意释放原语释放一个GE缓冲区数组,然后用来自第二个GE文件的GE缓冲区回收该数组来实现这种类型混淆。由于GE缓冲区数组(以及GE缓冲区)来自通用的kmalloc缓存,此回收的唯一要求是分配与GE缓冲区数组大小相同的GE缓冲区。如果两个GE文件引用相同的内存,一个(GE文件A)作为GE缓冲区,另一个(GE文件B)作为GE缓冲区数组,我可以随意修改GE缓冲区数组的内容。

然后,对虚拟地址X执行任意写将变得简单:使用GE文件A的GE缓冲区将数组内容更改为指向虚拟地址X,然后使用GE文件B写入该地址,该文件现在认为虚拟地址X是一个GE缓冲区!
这种技术依赖于能够使用任意释放原语释放GE缓冲区数组。为此,首先需要找到GE缓冲区数组的虚拟地址。由于我们已经拥有任意读原语,GE缓冲区数组的任何父结构体/对象/数组都足以找到GE缓冲区数组本身的虚拟地址。层次结构如下:
- GE数组由GE文件引用
- GE文件由
fdtable作为文件描述符引用 fdtable由任务结构体引用- 任务结构体作为任务列表的一部分被引用,根节点是内核镜像中的
init任务
fdtable代表一个有吸引力的对象,因为它来自与dma_buf名称字符串相同的通用kmalloc缓存。我们可以通过使用dma_buf_set_name(我们也将其用作任意释放原语)将指向dma_buf名称字符串的指针插入到已回收的UAF的dma_buf对象(现在是一个GE缓冲区)中来找到dma_buf名称字符串的虚拟地址。然后我们只需从GE缓冲区中读取它,释放该dma_buf名称字符串(再次使用dma_buf_set_name),并用fdtable回收它。创建fdtables相当容易——我们只需提前fork许多共享fdtable的进程,然后在适当的时间unshare(2) fdtable以分配新的fdtables。完整的利用策略如下:
- 使用我们的mtk-jpeg竞态条件漏洞触发
dmabuf文件的悬空fd - 回收底层的
dma_buf,使父dmabuf文件保持释放状态(但仍由悬空文件描述符引用) - 在我们的悬空文件上使用
ged_dmabuf_set_name,在假的dma_buf结构体中放置一个新的名称指针 - 读取假的
dma_buf结构体(实际上是GE缓冲区)以获取名称指针 - 通过再次调用
ged_dmabuf_set_name释放名称指针 - 用引用具有GE缓冲区数组的GE fd的
fdtable回收名称指针 - 使用任意读找到GE缓冲区数组
- 使用任意释放释放GE缓冲区数组
- 用另一个GE缓冲区回收GE缓冲区数组
在此过程结束时,我们将拥有可靠的任意读/写!
获取root shell
作为一个有趣的练习,我决定看看在实现任意读/写后,禁用SELinux并获取root有多容易。不同的制造商可能会实施某些绊网以减缓利用开发工作,但在我的情况下(华硕ROG 6D),我根本不需要跳过任何障碍。只需将我的进程cred结构体的uid/gid写入0即可获得root,并将selinux_enforcing位写入0即可关闭SELinux。之后,我只需execlp("/system/bin/sh",...),就会弹出一个root shell!

结论
我在所有3台评估的设备中发现了重大的安全漏洞。很可能,审查更多包含更广泛芯片组制造商的设备会发现额外的漏洞。Android通常使用更高特权的进程在应用程序和内核驱动程序之间进行联络,这意味着大多数内核驱动程序无法从无特权的应用上下文看到(GPU是此规则最明显的例外)。然而