跳到内容

在启用MTE的像素8上获取内核代码执行

在本文中,我将查看CVE-223-6241,这是Arm Mali GPU中的一个漏洞,它允许恶意应用程序在Android手机上获得任意内核代码执行和root权限。我将展示即使在设备上启用了强大的缓解措施内存标记扩展(MTE),也可以如何利用此漏洞。

在启用MTE的像素8上获取内核代码执行
作者

在本文中,我将了解CVE-223-6241,我于2023年11月15日向Arm报告的Arm Mali GPU中的漏洞,已在Arm Mari驱动程序版本中修复r47p0于2023年12月14日公开发布。它是在Android中修复的三月安全更新。利用此漏洞,恶意Android应用程序可以在设备上获得任意内核代码执行和root权限。该漏洞影响使用更新的Arm Mali GPU的设备命令流前端(CSF)功能,如谷歌的Pixel 7和Pixel 8手机。该漏洞的有趣之处在于,它是Arm Mali GPU内存管理单元中的一个逻辑错误,能够绕过内存标记扩展(MTE),像素8中首次支持的一种新的强大的内存损坏缓解措施。在本文中,我将展示如何使用此错误从不受信任的用户应用程序获得Pixel 8中任意内核代码的执行。我已经通过以下方式确认,即使在启用了内核MTE的情况下,该漏洞也能成功利用这些说明.

Arm64 MTE

MTE是较新的Arm处理器上一个有很好文档记录的功能,它使用硬件实现来检查内存损坏。由于已经有很多关于MTE的好文章,我只简单介绍一下MTE的概念,并解释它与其他内存损坏缓解措施相比的重要性。例如,对更多细节感兴趣的读者可以咨询这篇文章白皮书由Arm发布。

虽然Arm64体系结构使用64位指针访问内存,但通常不需要使用如此大的地址空间。实际上,大多数应用程序使用的地址空间要小得多(通常为52位或更少)。这样指针中的最高位就不用了。内存标记的主要思想是使用地址中的这些高位来存储“标记”,然后可以使用“标记”检查存储在与地址关联的内存块中的其他标记。有助于减轻常见类型的内存损坏,如下所示:

在线性溢出的情况下,指针用于取消引用与指针中存储的标记不同的相邻内存块。通过在取消引用时检查这些标记,可以检测到损坏的取消引用。对于无使用后类型内存损坏,只要每次释放内存块时清除内存块中的标记,并在分配时重新分配新标记,取消引用已释放和回收的对象也会导致指针标记和内存中的标记之间存在差异,从而可以检测到无使用后。

(上图来自内存标记扩展:通过体系结构增强内存安全由Arm发布。)

记忆标记不同于以往缓解措施的主要原因,例如内核控制流完整性(kCFI)这是因为,与其他干扰攻击后期阶段的缓解措施不同,MTE是一种非常早期的缓解措施,它试图在首次发生内存损坏时捕获内存损坏。因此,它能够在攻击者获得任何功能之前的早期阶段阻止攻击,因此很难绕过。它引入了检查,有效地将不安全的内存语言转换为内存安全的语言,尽管是概率性的。

理论上,内存标记可以单独在软件中实现,方法是让内存分配器在每次分配或释放内存时分配和删除标记,以及在取消引用指针时添加标记检查逻辑。然而,这样做会带来性能成本,使其不适合生产使用。因此,需要硬件实现来降低性能成本,并使内存标记适合生产使用。硬件支持是在v8.5a版本在ARM体系结构中,引入了额外的硬件指令(称为MTE)来执行标记和检查。对于Android设备,大多数支持MTE的芯片组使用Arm v9处理器(而不是Arm v8.5a),目前只有少数设备支持MTE。

MTE的一个局限性是,与所有可能分配的内存块相比,可用的未使用位的数量很小。因此,标签冲突是不可避免的,许多内存块将具有相同的标签。这意味着损坏的内存访问仍可能偶然成功。在实践中,即使只使用4位作为标签,成功率也会降低到1/16,这仍然是一种相当强大的防止内存损坏的保护。另一个限制是,通过使用Spectre等侧通道攻击泄漏指针和内存块值,攻击者可以确保使用正确的标记进行损坏的内存访问,从而绕过MTE。然而,这种类型的泄漏大多只对本地攻击者可用。系列文章,MTE已实施包括对MTE在各种攻击场景中的局限性和影响的深入研究。

除了硬件使用实现Arm v8.5a或更高版本的处理器外,还需要软件支持来启用MTE。目前,只有谷歌的Pixel 8允许用户在开发人员选项中启用MTE默认情况下,MTE处于禁用状态。还需要额外的步骤来在内核中启用MTE.

马里武装GPU

Arm Mali GPU可以集成在各种设备中(例如,请参阅中的“实现”马里(GPU)维基百科条目). 它一直是Android手机上的一个吸引人的目标,并多次被内部利用。当前的漏洞与另一个问题这是我报告的一个漏洞,是处理一种称为JIT内存的GPU内存类型时的漏洞。现在我将简要解释JIT内存并解释CVE-223-6241漏洞。

马里武装部队的JIT内存

使用马里GPU驱动程序时,用户应用程序首先需要创建并初始化kbase_context(数据库上下文)内核对象。这涉及到用户应用程序打开驱动程序文件并使用生成的文件描述符生成一系列国际奥委会电话。kbase_context(数据库上下文)对象负责管理每个已打开且对每个文件句柄唯一的驱动程序文件的资源。

特别是kbase_context(数据库上下文)管理GPU设备和用户空间应用程序之间共享的不同类型的内存。用户应用程序可以将自己的内存映射到GPU的内存空间,以便GPU可以访问此内存,也可以从GPU分配内存。GPU分配的内存由知识库_文本可以映射到GPU内存空间,也可以映射到用户空间。用户应用程序还可以使用GPU通过向GPU提交命令来访问映射内存。通常,内存需要由GPU(本机内存)分配和管理,或者从用户空间导入到GPU,然后映射到GPU地址空间,然后才能被GPU访问。马里GPU中的内存区域由kbase_va_区域。与CPU中的虚拟内存类似,GPU中的内存区域可能没有物理内存支持其整个范围。这个页码(_P)中的字段kbase_va_地区指定内存区域的虚拟大小,而gpu_alloc->组件是支持该区域的实际物理页面数。从现在起,我将把这些页面称为该区域的支持页面。虽然内存区域的虚拟大小是固定的,但其物理大小可以更改。从现在起,当我使用诸如调整内存区域大小、增长和收缩之类的术语时,我的意思是该区域的物理大小正在调整、增长或收缩。

JIT内存是一种本机内存,其生存期由内核驱动程序管理。用户应用程序通过向GPU发送相关命令来请求GPU分配和释放JIT内存。虽然大多数命令(如使用GPU执行算术和内存访问的命令)都是在GPU本身上执行的,但也有一些命令(如用于管理JIT内存的命令)是在内核中实现并在CPU上执行的。这些被称为软件命令(与在GPU(硬件)上执行的硬件命令相反)。在使用命令流前端(CSF)的GPU上,软件命令和硬件命令放置在不同类型的命令队列中。要提交软件命令kbase kcpu命令队列是必需的,可以使用KBASE_IOCTL_KCPU_QUEUE_CREATE公司 国际奥委会。然后可以使用KBASE_IOCTL_KCPU_QUEUE_ENQUEUE公司命令。要分配或释放JIT内存基础_KCPU_COMMAND_TYPE_JIT_ALLOCBASE_KCPU_COMMAND_TYPE_JIT_FREE服务器可以使用。

这个基础_KCPU_COMMAND_TYPE_JIT_ALLOC命令使用kbase_jit_allocate分配JIT内存。类似地,命令BASE_KCPU_COMMAND_TYPE_JIT_FREE服务器可用于释放JIT内存。如本节所述“JIT内存的生命周期“在我之前的一篇文章中,当JIT内存被释放时,它会进入由kbase_context(数据库上下文)以及何时kbase_jit_allocate被调用时,它首先查看这个内存池,看看是否有任何合适的释放的JIT内存可以重用:

结构kbase_va_region*kbase_jit_allocate(结构kbase_context*kctx,const结构base_jit_alloc_info*info,bool ignore_pressure_limit){...kbase_gpu_vm_lock(kctx);互斥锁(&kctx->jit_excip_lock);/**扫描池中符合我们的现有分配*并将其删除。*/if(信息->用法id!=0)/*首先扫描具有相同使用ID的分配*/reg=查找合理性区域(info,&kctx->jit_pool_head,false);...}

如果找到现有区域,并且其虚拟大小与请求匹配,但其物理大小太小,则kbase_jit_allocate将尝试通过调用kbase_jit_grow数据库:

结构kbase_va_region*kbase_jit_allocate(结构kbase_context*kctx,const结构base_jit_alloc_info*info,bool ignore_pressure_limit){.../*kbase_jit_grow()可以释放并重新获取“kctx->reg_lock”,*因此,受该锁保护的任何状态都可能需要*如果以后在此处添加更多代码,则重新评估。*/ret=kbase_jit_grow(kctx、info、reg、prealloc_sas、,mmu_sync_info);...}

另一方面,如果找不到合适的区域,kbase_jit_allocate将从头开始分配JIT内存:

结构kbase_va_region*kbase_jit_allocate(结构kbase_context*kctx,const结构base_jit_alloc_info*info,bool ignore_pressure_limit){...}其他{/*找不到合适的JIT分配,因此创建一个新的JIT*/u64标志=BASE_MEM_PROT_CPU_RD | BASE_MEM_PROT_GPU_RD|BASE_MEM_PROT_GPU_WR | BASE_MEM_GROW_ON_GPF|BASE_MEM_COHERENT_LOCAL(基础_机械_相干_本地)|BASEP_MEM_NO_USER_FREE;u64 gpu地址;...互斥解锁(&kctx->jit_excip_lock);kbase_gpu_vm_unlock(kctx);reg=kbase_mem_alloc(kctx,info->va_pages,info->commit_page,info->扩展名,&标志,&gpu_addr,mmu_sync_info);...}

正如我们从上面的评论中看到的kbase_jit_grow数据库,kbase_jit_grow数据库可以暂时删除kctx->reg_lock:

静态int kbase_jit_grow(struct kbase_context*kctx,const结构base_jit_alloc_info*info,结构kbase_va_region*reg,结构kbase_sub_alloc**prealloc_sas,枚举kbase_caller_mmu_sync_info mmu_s同步_info){...if(!kbase_mem_evictable_unmake(reg->gpu_alloc))转到update_failed;...old_size=reg->gpu_alloc->nents//commit_pages-reg->gpu_alloc->组件//<---------2pages_required=增量;...while(kbase_mem_pool_size(池)mem_partials_lock);kbase_gpu_vm_unlock(kctx);//<----------锁掉了。ret=kbase_mem_pool_grow(池,池_delta);kbase_gpu_vm_lock(kctx);...}

在上面,我们看到了kbase gpu vm解锁调用以临时删除kctx->reg_lock,同时kctx->mem_partials_lock在呼叫期间也被丢弃知识库管理工具增长在马里GPU中kctx->reg_lock用于保护对内存区域的并发访问。例如,当kctx->reg_lock则另一个线程无法更改内存区域的物理大小。GHSL-2023-005我之前报告过,我能够触发一个竞赛,以便通过使用KBASE_IOCTL_MEM_COMMIT公司 国际奥委会从另一个线程,同时知识库管理工具增长正在运行。JIT区域大小的变化导致reg->gpu_alloc->组件之后更改知识库管理工具增长,意味着reg->gpu_alloc->组件然后与缓存的值不同旧_大小三角洲(1.和2。)。由于这些值后来用于分配和映射JIT区域,因此使用这些过时的值会导致GPU内存映射中的不一致,从而导致GHSL-2023-005。

静态int kbase_jit_grow(struct kbase_context*kctx,const结构base_jit_alloc_info*info,结构kbase_va_region*reg,结构kbase_sub_alloc**prealloc_sas,枚举kbase_caller_mmu_sync_info mmu_s同步_info){...//增长内存池...//用于分配页面的增量gpu_pages=kbase_alloc_phy_pages_helper_locked(reg->gpu_alloc,池,增量,&prealloc_sas[0]);...//old_size用于增长gpu映射ret=kbase_mem_grow_gpu_mapping(kctx,reg,信息->commit_pages,旧尺寸);...}

修补GHSL-2023-005后,无法再使用KBASE_IOCTL_MEM_COMMIT接口.

漏洞

与虚拟内存类似,当GPU访问没有物理页面支持的内存区域中的地址时,会发生内存访问错误。在这种情况下,根据内存区域的类型,可以动态分配和映射物理页面以支持故障地址。GPU内存访问故障由kbase_mmu_page_fault_worker数据库:

无效kbase_mmu_page_fault_worker(结构work_struct*数据){...kbase_gpu_vm_lock(kctx);...if((区域->标志&GROWABLE_flags_REQUIRED)!= GROWABLE_FLAGS_REQUIRED){kbase_gpu_vm_unlock(kctx);kbase_mmu_report_fault_and_kill(kctx、faulting_as、,“记忆无法增长”,故障);转到fault_done;}if((区域->标志&KBASE_REG_DONT_NEED)){kbase_gpu_vm_unlock(kctx);kbase_mmu_report_fault_and_kill(kctx、faulting_as、,“不需要记忆,无法成长”,错误);转到fault_done;}...自旋锁定(&kctx->mem_partials_lock);growd=页面故障try_alloc(kctx、区域、新页面和页面到增长,&grow_2mb_pool、prealloc_sas);旋转解锁(&kctx->mem_partials_lock);...}

在故障处理程序中,执行了许多检查以确保允许内存区域的大小增长。与JIT内存相关的两项检查是所需GROWABLE_FLAGSKBASE_REG_DONT_NEED公司旗帜。这个所需GROWABLE_FLAGS定义如下:

#定义GROWABLE_FLAGS_REQUIRED(KBASE_REG_PF_GROW | KBASE/REG_GPU_WR)

当JIT区域由创建时,这些标志被添加到JIT区域kbase_jit_allocate并且从未改变:

结构kbase_va_region*kbase_jit_allocate(结构kbase_context*kctx,const结构base_jit_alloc_info*info,bool ignore_pressure_limit){...}其他{/*找不到合适的JIT分配,因此创建一个新的JIT*/u64标志=BASE_MEM_PROT_CPU_RD | BASE_MEM_PROT_GPU_RD|BASE_MEM_PROT_GPU_WR|BASE_MEM_GROW_ON_GPF|//jit_excipt_lock);kbase_gpu_vm_unlock(kctx);reg=kbase_mem_alloc(kctx,info->va_pages,info->commit_page,info->扩展名,&标志,&gpu_addr,mmu_sync_info);...}

KBASE_REG_DONT_NEED(基本需求)释放标志时,将其添加到JIT区域,然后在kbase_jit_grow数据库早在kctx->reg_lockkctx->内存部分锁定被丢弃并且知识库管理工具增长称为:

静态int kbase_jit_grow(struct kbase_context*kctx,const结构base_jit_alloc_info*info,结构kbase_va_region*reg,结构kbase_sub_alloc**prealloc_sas,枚举kbase_caller_mmu_sync_info mmu_s同步_info){...if(!kbase_mem_evivitable_unmake(reg->gpu_alloc))//<-----删除kbase_reg_DONT_NEED转到update_failed;...而(kbase_mem_pool_size(池)mem_partials_lock);kbase_gpu_vm_unlock(kctx);ret=kbase_mem_pool_grow(池,池_delta);//<-----竞争窗口:错误处理程序增长区域kbase_gpu_vm_lock(kctx);...}

特别是,在上面代码段中标记的竞争窗口期间,当发生页面错误时,JIT内存reg可以增长。

因此,通过访问该区域中未映射的内存来在另一个线程上创建错误,而知识库管理工具增长在运行时,我可以让GPU错误处理程序增加JIT区域,而知识库管理工具增长跑。然后会发生变化reg->gpu_alloc->组件并使其无效旧_大小三角洲在1中。和2。如下所示:

静态int kbase_jit_grow(struct kbase_context*kctx,const结构base_jit_alloc_info*info,结构kbase_va_region*reg,结构kbase_sub_alloc**prealloc_sas,枚举kbase_caller_mmu_sync_info mmu_s同步_info){...如果(!kbase_mem_evitable_unmake(reg->gpu_alloc))转到更新失败;...old_size=reg->gpu_alloc->nents//commit_pages-reg->gpu_alloc->组件//<---------2pages_required=增量;...while(kbase_mem_pool_size(池)mem_partials_lock);kbase_gpu_vm_unlock(kctx);ret=kbase_mem_pool_grow(池,池_delta)//gpu_alloc->组件被错误处理程序更改kbase_gpu_vm_lock(kctx);...//用于分配页面的增量gpu_pages=kbase_alloc_phy_pages_helper_locked(reg->gpu_alloc,pool,//commit_pages,//<-----4)。旧尺寸);...}

因此,当三角洲旧_大小在3中使用。和4。为了分配备份页并将页映射到GPU内存空间,它们的值是无效的。

这与GHSL-2023-005的情况非常相似。作为知识库管理工具增长涉及大量内存分配,这场比赛很容易获胜。然而,这里有一个很大的区别:使用GHSL-2023-005,我能够缩小JIT区域,而在这种情况下,我只能扩大JIT区域。为了理解为什么这很重要,让我们简要回顾一下我对GHSL-2023-005的利用。

如前所述kbase_va_地区存储在字段中reg->gpu_alloc->组件.A型kbase_va_地区有两个kbase_mem_phy_alloc数据库对象:cpu分配gpu分配负责管理其支持页面的。对于Android设备,这两个字段配置为相同。kbase_mem_phy_alloc数据库,字段是一个数组,其中包含支持页的物理地址,而1.n.分数指定阵列:

结构kbase_mem_phy_alloc{...size_t个数;struct tagged_addr*页面;...}

什么时候?kbase_alloc_phy_pages_helper已锁定被调用时,它分配内存页,并将这些页表示的物理地址附加到数组中,因此新页面将添加到索引中1.n.分数向前。然后将新大小存储到1.n.分数。例如,当它被调用时kbase_jit_grow数据库,三角洲要添加的页数:

静态int kbase_jit_grow(struct kbase_context*kctx,const结构base_jit_alloc_info*info,结构kbase_va_reregion*reg,结构kbase_sub_alloc**prealloc_sas,枚举kbase_caller_mmu_sync_info mmu_s同步_info){...//用于分配页面的增量gpu_pages=kbase_alloc_phy_pages_helper_locked(reg->gpu_alloc,池,增量,&prealloc_sas[0]);...}

在这种情况下,三角洲在索引处插入页面1.n.分数在数组中属于gpu分配:

在分配备份页并将其插入数组,通过调用kbase_mem_grow_gpu映射。的虚拟地址kbase_va_地区GPU内存空间中的kbase_va_地区自身并存储在字段中启动_pfn页码(_P):

结构kbase_va_region{...u64启动pfn;...size_t编号;...}

虚拟地址的开头kbase_va_地区存储在启动_pfn(作为页面框架,因此实际地址为开始_pfn>>页面大小)同时页码(_P)存储区域的大小。这些字段设置后保持不变。kbase_va_地区,首字母reg->gpu_alloc->组件虚拟地址空间中的页由存储在数组gpu_alloc->页面,而其余地址没有备份。特别是,受支持的虚拟地址始终是连续的(因此,受支持区域之间没有间隙),并且始终从区域的开头开始。例如,可能存在以下情况:

但不允许出现以下情况,因为背衬不是从区域的开头开始的:

由于支持的地址中存在间隙,也不允许出现以下情况:

在这种情况下kbase_mem_grow_gpu映射被调入kbase_jit_grow数据库,GPU地址介于(start_pfn+old_size)*0x1000(start_pfn+info->commit_pages)*0x1000映射到中新添加的页面gpu_alloc->页面,这是索引之间的页面页面+旧大小页面+信息->承诺页面(因为delta=信息->commit_pages-old_size):

静态int kbase_jit_grow(struct kbase_context*kctx,const结构base_jit_alloc_info*info,结构kbase_va_region*reg,结构kbase_sub_alloc**prealloc_sas,枚举kbase_caller_mmu_sync_info mmu_s同步_info){...old_size=reg->gpu_alloc->nents;delta=信息->commit_pages-reg->gpu_alloc->组件;...//old_size用于增长gpu映射ret=kbase_mem_grow_gpu_mapping(kctx、reg、info->commit_pages,旧尺寸);...}

特别地,旧_大小这里用于指定新映射应该开始的GPU地址,以及与应该使用支持页的数组。

如果reg->gpu_alloc->组件之后的更改旧_大小三角洲则这些偏移可能会变得无效。例如,如果kbase_va_地区缩水了组件之后减少旧_大小三角洲被存储,然后kbase_alloc_phy_pages_helper已锁定将插入三角洲第页到reg->gpu_alloc->页面+组件:

同样,kbase_mem_grow_gpu映射将从映射GPU地址(start_pfn+old_size)*0x1000,使用介于reg->gpu_alloc->pages+old_sizereg->gpu_alloc->页面+组件+增量(下图中的虚线)。这意味着页面->索引页面->old_size不要最终映射到任何GPU地址,而有些地址最终没有备份页:

开发GHSL-2023-005

GHSL-2023-005使我能够缩小JIT区域,但CVE-223-6241没有给我该功能。为了理解如何利用这个问题,我们需要了解更多关于如何删除GPU映射的信息。功能kbase_mmu_tardown_pgd_pages数据库负责从GPU中删除地址映射。此函数基本上遍历GPU地址范围,并通过将地址标记为无效来从GPU页表中删除地址。如果它遇到一个包含大量地址的高级页表条目(PTE),并发现该条目无效,那么它将跳过删除该条目包含的整个地址范围。例如,级别2页表条目涵盖512页的范围,因此如果发现级别2页表格条目无效(1)。在下面),然后kbase_mmu_teardown_pgd_pages数据库将假定接下来的512页被此级别2覆盖,因此所有页面都已无效。因此,它将跳过删除这些页面(2)。(见下文)。

静态int kbase_mmu_teardown_pgd_pages(结构kbase_device*kbdev,结构kbase_mmau_table*mmut,u64 vpfn、size_t nr、u64*目录pgds、,struct list_head*free_pgds_list,枚举kbase_mmu_op_type flush_op){...for(级别=MIDGARD_MMU_TOPLEVEL;级别ate_is_valid(页面[索引],级别)中断;/*保留映射*/否则,如果(!mmu_mode->pteis_valid(page[index],level)){//<------1。/*这里什么都没有,先走*/开关(液位){...案例MIDGARD_MMU_LEVEL(2):计数=512;//编号)计数=nr;转到下一步;}...下一步:昆图(phys_to_page(pgd));vpfn+=计数;nr-=计数;

功能kbase_mmu_teardown_pgd_pages数据库kbase_va_地区收缩或删除时。如前一节所述kbase_va_地区由物理页映射和支持的必须从kbase_va_地区因此,如果映射了区域中的任何地址,则必须映射起始地址,因此覆盖起始地址的高级页表条目必须有效(如果没有映射区域中的地址,则kbase_mmu_teardown_pgd_pages数据库甚至不会被调用):

在上面,映射了覆盖区域起始地址的级别2 PTE,因此它是有效的,因此在本例中,如果kbase_mmu_teardown_pgd_pages数据库如果遇到未映射的高级PTE,则kbase_va_地区必须已取消映射,可以安全跳过。

在缩小区域的情况下,取消映射开始的地址位于kbase_va_地区,并且将取消映射此起始地址和区域结尾之间的整个范围。如果覆盖此地址的第2级页表条目无效,则起始地址必须位于未映射的区域中,因此要取消映射的其余地址范围也必须未映射。在这种情况下,跳过地址也是安全的:

因此,只要区域仅从其起始地址映射,并且映射中没有间隙,kbase_mmu_teardown_pgd_pages数据库行为正常。

在GHSL-2023-005的情况下,可以创建不满足这些条件的区域。例如,通过在比赛窗口中将整个区域缩小到零大小,可以创建一个区域,其中区域的起点未映射:

删除区域时,以及kbase_mmu_teardown_pgd_pages数据库尝试删除第一个地址,因为级别2 PTE无效,它将跳过接下来的512页,其中一些页实际上可能已经映射:

在这种情况下,“错误跳过”区域中的地址将仍然映射到中的数组gpu分配,已释放。这些“错误跳过”的GPU地址可以用于访问已经释放的内存页。

利用CVE-223-6241

然而,当一个地区在比赛窗口期间增长时,情况就大不相同了。在这种情况下,1.n.分数大于旧_大小什么时候kbase_alloc_phy_pages_helper已锁定kbase_mem_grow_gpu映射被称为,并且三角洲正在索引处插入页面1.n.分数数组:

这个数组包含备份jit增长和错误访问所需的正确页数,事实上,它应该在kbase_jitgrow(基本)在页面错误处理程序之后调用。

什么时候?kbase_mem_grow_gpu映射被称为,三角洲页面从映射到GPU(start_pfn+old_size)*0x1000。由于支持页面的总数现在增加了fh+增量,其中fh(飞行高度)是错误处理程序添加的页数,剩下最后一个fh(飞行高度)中的页面数组未映射。

然而,这似乎也没有产生任何问题。内存区域仍然只映射了其起始地址,并且映射中没有间隙。没有映射的页面根本无法从GPU访问,并且会在删除内存区域时被释放,因此这甚至不是内存泄漏问题。

然而,并不是所有的东西都失去了。如我们所见,当发生GPU页面错误时,如果错误的原因是地址未映射,则错误处理程序将尝试向区域添加支持页面,并将这些新页面映射到区域范围。如果故障地址是,那么错误地址(_A),则要添加的最小页数为new_pages=fault_addr/0x1000-reg->gpu_alloc->组件。取决于kbase_va_地区,也可以添加一些填充。无论如何,这些新页面将从地址开始映射到GPU(start_pfn+reg->gpu_alloc->组件)*0x1000,以便保留仅映射区域开头的地址的事实。

这意味着,如果我在受错误影响的JIT区域中触发另一个GPU错误,那么将添加一些新的映射之后未映射的区域。

这在GPU映射中造成了一个缺口,我开始得到一些看起来可以利用的东西。

请注意,作为三角洲必须为非零才能触发错误,并且delta+old_size映射了区域开始处的页面,但仍无法像GHSL-2023-005那样取消映射区域开始处。所以,我唯一的选择是缩小区域,并使结果大小位于未映射的间隙内。

缩小JIT区域的唯一方法是使用BASE_KCPU_COMMAND_TYPE_JIT_FREE服务器“释放”JIT区域的GPU命令。如前所述,这实际上并不能释放kbase_va_地区而是将其放在内存池中,以便在后续JIT分配中重用。在此之前,kbase_jit_free数据库还将根据初始承诺区域的大小,以及修剪级别(_L)在中配置的kbase_context(数据库上下文):

无效kbase_jit_free(结构kbase_context*kctx,结构kbase_va_region*reg){...旧页面=kbase_reg_current_backed_size(reg);如果(reg->initial_commit initial-commit,div_u64(旧页面*(100-kctx->trim_level),100);u64 delta=旧页面-新大小;如果(delta){互斥锁(&kctx->reg_lock);kbase_mem_shrink(kctx,reg,旧页面-增量);互斥解锁(&kctx->reg_lock);}}...}

不管怎样,我都可以控制这种收缩的大小。考虑到这一点,我可以按以下方式安排区域:

  1. 创建JIT区域并触发错误。安排GPU故障,以便故障处理程序添加fault_size(大小)页数,足够覆盖至少一个2级PTE。

    触发错误后,只有初始旧大小+增量页面映射到GPU地址空间,而kbase_va_地区old_size+delta+fault_size支持页面总数。

  2. 在大于支持页面数的偏移量处触发第二个错误,以便页面附加到该区域,并在上一步中创建的未映射区域之后映射。

  3. 使用释放JIT区域BASE_KCPU_COMMAND_TYPE_JIT_FREE服务器,它将调用kbase_jit_free数据库缩小区域并从中删除页面。控制此修剪的大小,以便缩小后的区域大小(最终大小)备份存储的某个位置位于第一级2 PTE覆盖的未映射区域内。

当区域缩小时,kbase_mmu_teardown_pgd_pages数据库调用以取消映射GPU地址映射,从区域启动+最终大小一直到区域的尽头。当第一级2 PTE覆盖的整个地址范围未映射时kbase_mmu_teardown_pgd_pages数据库尝试取消映射区域_开始+最终_大小,条件!mmu_模式->pte_is_valid在第2级PTE中满足,因此取消映射将跳过接下来的512页,从开始区域_开始+最终_大小。但是,由于属于下一个级别2 PTE的地址仍然被映射,这些地址将被错误跳过(下图中的橙色区域),从而将它们映射到要释放的页面:

一旦收缩完成,后台页面将被释放,橙色区域中的地址将保留对已释放页面的访问权限。

这意味着现在可以将释放的支持页面重用为任何内核页面,这为我提供了大量的选项来利用此错误。一种可能是使用我以前的技术将背景页替换为页表全局目录(PGD)GPU的kbase_context(数据库上下文).

概括一下,让我们看看kbase_va_地区已分配。为的备份存储分配页面时kbase_va_地区,的kbase_mem_pool_alloc_pages数据库函数用于:

int kbase_mem_pool_alloc_pages(结构kbase_em_pool*pool,size_t nr_4k_pages,struct tagged_addr*页面,允许bool partial_allowed){.../*从此池中获取页面*/while(nr_from_pool-){p=kbase_mem_pool_remove_locked(池)//下一个池){/*通过下一个池进行分配*/err=kbase_mem_pool_alloc_pages(池->下一个池,//<-----2。nr_4k_pages-i,pages+i,部分允许);...}其他{/*从内核获取剩余的页面*/而(i!=nr_4k_pages){p=kbase_mem_alloc_page(池);//<---------三。...}...}...}

输入参数kbase_mem_pool(kbase_mem_pool)是由管理的内存池kbase_context(数据库上下文)与用于分配GPU内存的驱动程序文件关联的对象。正如评论所示,分配实际上是分层进行的。首先,页面将从当前kbase_mem_pool数据库使用kbase_mem_pool_remove_locked数据库(上述第1项)。如果当前容量不足kbase_mem_pool数据库满足要求,那么pool->下一个pool,用于分配页面(上面的2个)。如果是偶数pool->下一个pool没有容量,那么kbase_mem_alloc_页面用于通过buddy分配器(内核中的页面分配器)直接从内核分配页面。

在释放页面时,如果没有收回内存区域,则会发生相反的情况:kbase_mem_pool_free页面首先尝试将页面返回到kbase_mem_pool数据库电流的kbase_context(数据库上下文),如果内存池已满,它将尝试将剩余页面返回到pool->下一个pool。如果下一个池也已满,则通过伙伴分配器释放剩余页面,将其返回内核。

如我的帖子所述损坏内存而不损坏内存,池->下一个池是一个由马里司机管理的内存池,由所有kbase_context(数据库上下文)。它还用于分配页表全局目录(PGD)由GPU上下文使用。特别是,这意味着通过仔细安排内存池,可以在kbase_va_地区被重用为GPU上下文的PGD。(可以找到如何实现这一点的详细信息在这里.)

一旦释放的页面被重新用作GPU上下文的PGD,可以使用保留对释放页面的访问权的GPU地址从GPU重写PGD。这样就可以将任何内核内存(包括内核代码)映射到GPU。这样,我就可以重写内核代码,从而执行任意内核代码。它还允许我读取和写入任意内核数据,因此我可以轻松地重写进程的凭据以获得root权限,并禁用SELinux。

可以找到Pixel 8的漏洞在这里带有一些安装说明。

如何绕过MTE?

到目前为止,我还没有提到任何绕过MTE的具体措施。事实上,MTE根本不会影响此错误的利用流。虽然MTE可以防止针对不一致内存块的指针取消引用,但该漏洞完全不依赖于任何此类取消引用。当bug被触发时,它会在数组和JIT区域的GPU映射。此时,没有内存损坏,GPU映射和单独考虑时,数组包含无效条目。当错误用于导致kbase_mmu_teardown_pgd_pages数据库要跳过删除GPU映射,其效果是使释放的内存页的物理地址保留在GPU页表中。因此,当GPU访问释放的页面时,实际上是直接访问它们的物理地址,这也不涉及任何指针取消引用。除此之外,我也不确定MTE是否对GPU内存访问有任何影响。因此,通过使用GPU直接访问物理地址,我能够完全绕过MTE提供的保护。归根结底,管理内存访问的代码中没有内存安全代码。在某些情况下,必须直接使用物理地址来访问内存。

结论

在本文中,我展示了如何使用CVE-223-6241在启用内核MTE的Pixel 8上执行任意内核代码。虽然MTE可以说是针对内存损坏的缓解措施中最重要的进步之一,并将使许多内存损坏漏洞无法利用,但它不是一颗银弹,仍然可以通过一个错误获得任意内核代码的执行。本文中的错误通过使用协处理器(GPU)直接访问物理内存绕过MTE(中的案例4MTE的实现,第3部分:内核). 随着越来越多的硬件和软件缓解措施在CPU端实施,我预计协处理器及其内核驱动程序将继续成为强大的攻击平台。

从GitHub了解更多信息

安全

安全

安全的平台,安全的数据。让安全成为您的首选所需的一切。
GitHub环球2024

GitHub环球2024

获取AI、DevEx和安全全球开发者活动十周年门票。
GitHub Copilot公司

GitHub Copilot公司

不要独自飞行。免费试用30天。
在GitHub工作!

在GitHub工作!

查看我们当前的职位空缺。