在过去的几年中,几乎所有的IOS内核攻击都遵循相同的高级流程:内存损坏和假的MACH端口被用来访问内核任务端口,这为用户空间提供了理想的内核读/写原语。最近的IOS内核漏洞利用缓解措施(如PAC和zone_Required)似乎倾向于打破一遍又一遍的规范技术来实现此漏洞利用流程。但是,这么多iOS内核漏洞从高层看起来是一样的,这一事实引出了一些问题:以内核任务端口为目标真的是最好的漏洞利用流吗?或者,在这一战略上的趋同是否掩盖了其他可能更有趣的技术?对于其他以前未见过的漏洞攻击流,现有的IOS内核缓解措施是否同样有效?
在这篇博客文章中,我将描述一种新的IOS内核利用技术,它可以将一个字节控制的堆溢出直接转变为任意物理地址的读/写原语,同时完全避开当前的缓解措施,如KASLR、PAC和Zone_Required。通过读取特殊的硬件寄存器,可以在物理内存中定位内核,并在没有假内核任务端口的情况下构建内核读/写原语。最后,我将讨论各种IOS缓解措施在阻止这项技术方面有多有效,并对IOS内核利用的最新技术进行思考。您可以在这里找到概念验证代码。
在浏览XNU资源时,我经常留意有趣的对象,以便为将来的攻击进行操纵或破坏。在发现CVE-2020-3837(OOB_TIMESTAMP漏洞)后不久,我偶然发现了vm_map_copy_t的定义:
该结构一开始就有一个类型字段,因此越界写入可能会将其从一种类型更改为另一种类型,从而导致类型混淆。因为IOS是小端的,所以最低有效字节在内存中排在第一位,这意味着即使是单字节溢出也足以将类型设置为三个值中的任何一个。
该类型区分任意控制数据(Kdata)和内核指针(hdr和object)之间的联合。因此,破坏类型可以让我们直接伪造指向内核对象的指针,而无需执行任何重新分配。
我记得在过去的漏洞攻击中(在iOS10之前)读到过关于vm_map_copy_t被用作有趣的原语的报道,尽管我记不清它是在哪里使用的,也不记得它是如何使用的。Ian Beer在拆分XNU中的原子时也使用了vm_map_copy对象。
因此,VM_MAP_COPY看起来可能是一个有趣的损坏目标;然而,只有当代码以真正有趣的方式使用它时,它才真正有趣。
深入研究osfmk/vm/vm_map.c,我发现vm_map_copy out_Internal()确实以一种非常有趣的方式使用了复制对象。但首先,让我们更多地谈谈什么是VM_MAP_COPY以及它是如何工作的。
VM_MAP_COPY表示进程的虚拟地址空间的写入时复制切片,该切片已打包,准备插入到另一个虚拟地址空间中。有三种可能的内部表示:作为VM_MAP_ENTRY对象的列表、作为VM_OBJECT或作为要直接复制到目标的内联字节数组。我们将重点介绍类型1和类型3。
从根本上说,ENTRY_LIST类型是最强大和通用的表示形式,而KERNEL_BUFFER类型严格意义上是一种优化。VM_MAP_ENTRY列表由几个分配和几个间接层组成:每个VM_MAP_ENTRY描述由特定VM_OBJECT映射的虚拟地址范围[VME_START,VME_END],该特定VM_OBJECT又包含描述支持VM_OBJECT的物理页的VM_PAGE列表。
同时,如果要插入的数据不是共享内存,并且大小大约为两页或更少,则只需过度分配VM_MAP_COPY,即可在同一分配中以内联方式保存数据内容,无需间接或进一步分配。
这种优化的结果是,偏移量0x20处的VM_MAP_COPY对象的8个字节可以是指向VM_MAP_ENTRY列表头部的指针,也可以是完全由攻击者控制的数据,这一切都取决于开头的类型字段。因此,损坏VM_MAP_COPY对象的第一个字节会导致内核将任意控制的数据解释为VM_MAP_ENTRY指针。
了解了VM_MAP_COPY内部机制之后,让我们回到VM_MAP_COPYOUT_INTERNAL()。此函数负责获取vm_map_copy并将其插入到目标地址空间(由类型vm_map_t表示)。在进程之间共享内存时,可以通过在MACH消息中发送脱机内存描述符来实现:脱机内存作为VM_MAP_COPY存储在内核中,而VM_MAP_COPYOUT_INTERNAL()是将其插入到接收方进程中的函数。
事实证明,如果VM_MAP_COPYOUT_INTERNAL()处理包含指向假VM_MAP_ENTRY层次结构的指针的损坏的VM_MAP_COPY,事情就会变得相当令人兴奋。具体地说,请考虑如果伪VM_MAP_ENTRY声称已连接会发生什么情况,这会导致函数立即尝试在页面中出错:
我们处理连接了VM_MAP_ENTRY的情况,因此应该立即出现故障:
设置后,我们循环访问有线条目中的每个虚拟地址。因为我们控制伪VM_MAP_ENTRY的内容,所以我们可以控制读取的对象指针(VM_OBJECT类型)和偏移值:
我们查找需要连接的每个物理内存页面的VM_PAGE结构。由于我们控制伪VM_OBJECT和偏移量,因此可以使vm_page_lookup()返回指向我们控制其内容的伪VM_PAGE结构的指针:
对vm_faultenter()的调用相当复杂,所以我不会把代码放在这里。可以说,通过适当设置伪对象中的字段,可以使用伪VM_PAGE对象导航VM_FAULT_ENTER(),以便使用完全任意的物理页码调用PMAP_ENTER_OPTIONS():
Pmap_enter_options()负责修改目的地的页表,以插入将建立从虚拟地址到物理地址的映射的转换表项。类似于VM_MAP如何管理地址空间的虚拟映射的状态,PMAP结构管理地址空间的物理映射(即页表)的状态。并且根据osfmk/arm/pmap.c中的源,在添加转换表条目之前,不对所提供的物理页号执行进一步的验证。
因此,我们损坏的vm_map_copy对象实际上给了我们一个非常强大的原语:将任意物理内存直接映射到用户空间中的进程中!
我决定在iOS13.3的OOB_TIMESTAMP利用提供的内核读/写原语之上构建vm_map_copy物理内存映射技术的POC。这主要有两个原因。
首先,我没有一个好的bug来用它来开发一个完整的漏洞。尽管我最初是在试图利用OOB_TIMESTAMP bug时偶然想到这个想法的,但很快就发现这个bug不太适合这项技术。
其次,我想独立于实现该技术所使用的一个或多个漏洞来评估该技术。看起来这项技术很有可能是确定性的(也就是说,没有失败的情况);在不可靠的漏洞之上实现它将使单独评估变得困难。
该技术最自然地适合在任何分配器区域kalloc.80到kalloc.32768(即,65和32768字节之间的通用分配)中受控的单字节线性堆溢出。为便于在本文的其余部分引用,我将简单地将其称为单字节利用技术。
我们已经介绍了上述技术的要点:创建一个包含指向伪VM_MAP_ENTRY列表的指针的KERNEL_BUFFER类型的VM_MAP_COPY,破坏类型为ENTRY_LIST,使用VM_MAP_COPYOUT_INTERNAL()接收它,并将任意物理内存映射到我们的地址空间。但是,成功利用该漏洞要稍微复杂一些:
我们需要确保调用VM_MAP_COPYOUT_INTERNAL()的内核线程在映射物理页后不会崩溃、死机或死锁。
映射一个物理页很好,但可能不足以实现任意内核读/写。这是因为:
内核缓存在物理内存中的确切加载地址是未知的,因此我们不能在没有首先找到它的情况下直接映射它的任何特定页面。
可能某些硬件设备公开了一个MMIO接口,该接口本身强大到足以构建某种读/写原语;但是,我不知道有任何这样组件。
因此,我们将需要映射多个物理地址,并且最有可能需要使用从一个映射中读取的数据来查找要用于另一个映射的物理地址。这意味着我们的映射原语不能是一次性的。
在for循环之后调用vm_map_copy_insert()尝试将vm_map_copy()转移到vm_map_copy_zone。如果给定最初类型为KERNEL_BUFFER的VM_MAP_COPY,这将导致死机,因为最初使用kalloc()分配KERNEL_BUFFER对象。因此,安全地脱离for循环并恢复正常操作的唯一方法是首先获得内核读/写,然后修补内核中的状态以防止这种死机。
单字节技术的一个重要前提是在已知地址创建一个假的VM_MAP_ENTRY对象层次结构。因为我们已经在OOB_TIMESTAMP上构建了这个POC,所以我决定利用我在利用该bug时学到的一个巧妙技巧。在现实世界中,除了一个字节溢出之外,可能还需要另一个漏洞来泄漏内核地址。
在为OOB_TIMESTAMP开发POC时,我了解到AGXAccelerator内核扩展提供了一个非常有趣的原语:IOAccelSharedUserClient2和IOAccelCommandQueue2一起允许创建在用户空间和内核之间共享的大量可分页内存区域。在开发利用漏洞时,访问用户/内核共享内存非常有用,因为您可以在那里放置虚假的内核数据结构,并在内核访问它们时对其进行操作。当然,这个AGXAccelerator原语并不是获取内核/用户共享内存的唯一方法;例如,物理映射还将大部分DRAM映射到虚拟内存,因此它还可以用于将用户空间内存内容反映到内核中。然而,AGXAccelerator原语在实践中通常要方便得多:首先,它在更受限的地址范围内提供了非常大的连续共享内存区;其次,它更容易泄漏相邻对象的地址来定位它。
现在,在iPhone7之前,iOS设备不支持特权访问(PAN)安全功能。这意味着所有的用户空间实际上都是与内核共享的内存,您只需覆盖内核中的指针就可以指向用户空间中的假数据结构。
但是,现代IOS设备启用PAN,因此内核直接访问用户空间内存的尝试将失败。这就是AGXAccelerator共享内存原语的存在如此有用的原因:如果您可以建立一个大的共享内存区域并在内核中获知它的地址,那基本上等同于关闭了PAN。
当然,这句话的一个关键部分是了解它在内核中的地址。这样做通常需要一个漏洞和一些努力。相反,因为我们已经依赖于OOB_TIMESTAMP,所以我们将简单地对共享内存地址进行硬编码,并注意动态查找地址留给读者进行练习。
有了内核读/写和用户/内核共享内存缓冲区,我们就可以编写POC了。利用漏洞的总体流程基本上就是上面概述的内容。
我们在共享内存中初始化一个假的VM_MAP_ENTRY列表。条目列表包含3个条目:就绪条目、映射条目和完成条目。这些条目一起表示每个映射操作的当前状态。
我们在MACH消息中向保持端口发送包含伪VM_MAP_HEADER的脱机内存描述符。脱机内存作为KERNEL_BUFFER(值3)类型的VM_MAP_COPY对象存储在内核中。
我们模拟一个单字节的线性堆溢出,破坏VM_MAP_COPY的TYPE字段,将其更改为ENTRY_LIST(值1)。
我们启动一个线程,该线程接收在保持端口上排队的MACH消息。这会在损坏的VM_MAP_COPY上触发对VM_MAP_COPYOUT_INTERNAL()的调用。
由于VM_MAP_ENTRY列表的初始配置方式,VM_MAP_COPYOUT线程将在";Done";条目上无限循环旋转,以便我们操作它。
此时,我们有一个正在旋转的内核线程,准备映射我们请求的任何物理页面。
要映射页面,我们首先将";Ready&34;条目设置为链接到自身,然后将";Done";条目设置为链接到";Ready";条目。这将导致VM_MAP_COPYOUT线程启动";Ready";。
在旋转";Ready";时,我们将";Mapping";条目标记为连接了单个物理页面,并将其链接到我们链接到其自身的";Done";条目。我们还填充伪VM_OBJECT和VM_PAGE以映射所需的物理页码。
然后,我们可以通过将";Ready";条目链接到";Mapping";条目来执行映射。VM_MAP_COPYOUT_INTERNAL()将在页面中映射,然后在";Done";条目上旋转,表示完成。
这为我们提供了一个可重用的原语,它可以将任意物理地址映射到我们的进程中。作为概念的初步验证,我映射了不存在的物理地址0x414140000并尝试从其读取,从而触发了来自EL0的LLC总线错误:
在这一点上,我们已经证明了映射原语是可靠的,但是我们仍然不知道如何处理它。
我的第一个想法是,最简单的方法是在内存中查找内核缓存映像。请注意,在现代iPhone上,即使使用直接物理读/写原语,KTRR也会阻止我们修改内核映像的锁定部分,因此我们不能只修补内核的可执行代码。但是,内核缓存映像的某些段在运行时仍然是可写的,包括包含sysctls的__data段部分。由于sysctls以前曾用于构建读/写原语,因此这感觉像是一条稳定的前进道路。
接下来的挑战是使用映射原语在物理内存中定位kernelcache,这样就可以将sysctl结构映射到用户空间并进行修改。
但首先,在我们弄清楚如何定位kernelcache之前,先了解一下iPhone11Pro上的物理内存的一些背景知识。
IPhone11Pro有4 GB的DRAM,物理地址为0x800000000,因此物理DRAM地址跨度为0x800000000到0x900000000。其中,0x801b80000到0x8ec9b4000的范围保留给应用程序处理器(AP),它是运行XNU内核和应用程序的电话的主处理器。此区域之外的内存保留给协处理器,如Always On Processor(AOP)、Apple NeuroEngine(ANE)、SIO(可能是Apple SmartIO)、AVE、ISP、IOP等。这些区域和其他区域的地址可以通过解析设备树或通过在DRAM开始时转储iBoot切换区域来找到。
在引导时,kernelcache被连续加载到物理内存中,这意味着找到单个kernelcache页就足以定位整个映像。此外,虽然KASLR可能会在虚拟内存中滑动大量的kernelcache,但物理内存中的加载地址非常有限:在我的测试中,内核头始终加载在0x805000000到0x807000000之间的地址,范围仅为32MB。
结果是,这个范围小于内核缓存本身的0x23d4000字节,即35.8MB。因此,我们可以在运行时确定地址0x807000000包含一个kernelcache页。
死机(CPU4调用者0xfffffff0156f0c98):";PMAP_ENTER_OPTIONS_INTERNAL:页面属于ppl,";";pmap=0xfffff031a581d0,v=0x3bb844000,pn=2103160,PROT=0x3,FAULT_TYPE=0x3,FLAGS=0x0,WIRED=1,OPTIONS=0x1";
此死机字符串声称来自函数PMAP_ENTER_OPTIONS_INTERNAL(),该函数位于XNU(osfmk/arm/pmap.c)的开源部分,但在源代码中并不存在死机。因此,我在kernelcache中颠倒了PMAP_ENTER_OPTIONS_INTERNAL()的版本,以确定发生了什么。
我了解到的问题是,我试图映射的特定页面是苹果页面保护层(PPL)的一部分,PPL是XNU内核的一部分,用于管理页表,被认为比内核的其余部分更有特权。PPL的目标是防止攻击者修改受保护的页(特别是共同设计的二进制文件的可执行代码页),即使在危害内核以获得读/写功能之后也是如此。
为了强制保护页面不能被修改,PPL必须保护页表和页表元数据。因此,当我试图将受PPL保护的页面映射到用户空间时,引发了恐慌。
PPL的存在使物理映射原语的使用变得非常复杂,因为尝试映射受PPL保护的页面将会死机。并且kernelcache本身包含许多受PPL保护的页面,将连续的35MB二进制文件分割成不再连接内核缓存的物理幻灯片的更小的无PPL的块。因此,我们不再可以(安全地)映射保证为内核缓存页的单个物理地址。
美联社DRAM地区的其余部分也是同样危险的雷区。物理页面被抓取以供PPL使用,并根据需要返回给内核,因此在运行时,PPL页面像地雷一样散布在物理内存中。因此,任何地方都没有保证不会爆炸的静态地址。
然而,这并不完全正确。应用程序处理器的DRAM区域可能是雷区,但它之外的任何东西都不是雷区。这包括协处理器使用的DRAM以及系统的任何其他可寻址组件,例如通常通过内存映射I/O(MMIO)访问的系统组件的硬件寄存器。
有了这样一个强大的原语,我希望有太多的技术可以用来构建读/写原语。我希望通过利用对特殊硬件寄存器和协处理器的直接访问,可以做很多聪明的事情。不幸的是,这不是我非常熟悉的领域,所以我在这里只描述一次绕过PPL的(失败的)尝试。
我的想法是控制一些协处理器,同时使用协处理器和AP上的执行来攻击内核。首先,我们使用物理映射原语修改DRAM中存储协处理器数据的部分,以便在该协处理器上执行代码。接下来,回到主处理器上,我们第二次使用映射原语来映射和禁用协处理器的Dev。
.