🤔
"Return-to-user (ret2usr) attacks" 是指将已被破坏的内核指针重定向到驻留在用户空间中的数据的攻击方式。为了应对此类攻击,提出了几种内核加固方法,以实施更严格的地址空间分离,防止内核到用户空间的任意控制流转移和解引用。Intel和ARM最近也引入了SMEP、SMAP和PXN处理器功能来支持此目的的硬件。不幸的是,尽管像上述机制可以防止用户进程和内核之间的虚拟地址空间显式共享,但由于基本设计选择所导致的隐式共享条件仍然存在,这种机制在平衡隔离性和性能方面面临一些问题。
在这项工作中,我们展示了如何利用隐式页面帧共享来完全规避软件和硬件内核隔离保护。我们提出了一种新的内核利用技术,称为“return-to-direct-mapped memory(ret2dir)”,它可以绕过所有现有的ret2usr防御措施,包括SMEP、SMAP、PXN、KERNEXEC、UDEREF和kGuard。我们还讨论了构建可靠的针对x86、x86-64、AArch32和AArch64 Linux目标的ret2dir攻击技术。最后,为了防止ret2dir攻击,我们提出了一个专属页面帧所有权方案的设计和实现,用于Linux内核,可以最小化运行时开销,以防止物理内存页面的隐式共享。
尽管操作系统(OS)内核一直是一个有吸引力的攻击目标,但直到最近攻击者大多专注于利用服务器和客户端应用程序中的漏洞进行攻击,这些应用程序通常以管理员权限运行,因为它们(在很大程度上)较不复杂,容易被分析和攻击。然而,在过去几年中,内核已经成为同样有吸引力的目标。在之前逐年增加的趋势下,据国家漏洞数据库统计,2013年报告了355个内核漏洞,比2012年多了140个[73]。诚然,对用户级软件的利用变得更加困难,因为流行操作系统的最新版本具有许多保护和漏洞缓解措施。最小权限原则在用户账户和系统服务方面得到更好的执行,编译器提供了更多的保护措施来防御常见的软件缺陷,高度针对性的应用程序,如浏览器和文档查看器,开始使用沙盒技术。另一方面,内核具有庞大的代码库和攻击面,由于不断增加新功能,攻击面不断扩大[63]。例如,在代码行数方面,Linux内核的规模已经增长了一倍以上,从v2.6.11的6.6MLOC到v3.10的16.9MLOC[32]。
攻击者通常会将注意力转向内核,而不是花费大量的精力去攻击装备了众多保护和沙箱的应用程序。通过破坏内核,他们可以提升权限、绕过访问控制和策略强制执行,并逃避隔离和限制机制。例如,在对Chrome和Adobe Reader的最近攻击中,攻击者在成功获得代码执行后,利用内核漏洞突破了各自的沙箱进程[5, 74]。
内核攻击的机会非常丰富。以Linux内核为例,它经常受到常见软件漏洞的困扰,如栈和堆缓冲区溢出[14, 23,26],空指针和指针算术错误[10,12],内存泄露漏洞[13,19],使用已释放的内存和格式化字符串漏洞[25,27],有符号性错误[17,24],整数溢出[10,16],竞争条件[11,15],以及缺少授权检查和参数清理漏洞[18,20-22]。尽管存在内核保护机制,但这些漏洞的利用特别有效,因为用户空间和内核空间之间的分离很弱。虽然用户程序不能直接访问内核代码或数据,但是内核被映射到每个进程的地址空间中以提高性能,因此攻击者可以利用内核漏洞和重定向内核控制或数据流到用户空间代码或数据的方式,获得非root访问权限下的特权模式执行代码或破坏关键内核数据结构的能力。这种类型的攻击被称为返回用户攻击(ret2usr),影响所有主流操作系统,包括Windows和Linux,并适用于x86/x86-64、ARM和其他流行的体系结构。
尽管上述机制可以防止用户进程和内核之间的虚拟地址空间显式共享,但是隐式数据共享的条件仍然存在。基本的操作系统组件,例如物理内存映射、I/O缓冲区和页面缓存,仍然允许用户进程影响内核可以访问的数据。在本文中,我们研究了Linux中的上述问题,并揭示了为了性能而进行更强的隔离的设计决策。具体而言,我们提出了一种新的内核利用技术,称为return-to-direct-mapped memory (ret2dir),它依赖于内存管理子系统的固有属性来绕过现有的ret2usr保护。这是通过利用直接映射系统部分或全部物理内存的内核区域来实现的,使攻击者可以在内核地址空间内“反映”用户空间数据。
由于不同体系结构的内核布局和内存管理特性、32位系统中物理内存的部分映射以及内核中“镜像”用户空间数据的位置不确定,因此安装ret2dir攻击的任务变得复杂。我们详细介绍了不同的技术来克服每个挑战,并针对经过加固的x86、x86-64、AArch32和AArch64 Linux目标构建可靠的ret2dir攻击。
为了缓解ret2dir攻击的影响,我们提出了一个独占页帧所有权方案的设计和实现,用于Linux内核,它可以防止物理内存在用户进程和内核之间的隐式共享。我们的评估结果表明,所提出的防御方案提供了有效的保护,并且具有最小的(<3%)运行时开销。
本文的主要贡献如下:
设计安全地组合不同的保护域的方法有很多,从将内核和用户进程放入单一地址空间并使用软件隔离来建立边界[52],到将用户进程和内核组件限制在分离的、由硬件执行的地址空间中[2, 50, 66]。Linux和基于Linux的操作系统(如Android [47]、Firefox OS [72]、Chrome OS [48])采用了后一种方法的更粗略的变体,将虚拟地址空间划分为内核空间和用户空间。在x86和32位ARM(AArch32)架构中,Linux内核通常映射到虚拟地址空间的上1GB,这种划分也被称为“3G/1G”[28]。在x86-64和64位ARM(AArch64)中,内核位于上半个规范化地址空间[60,69]。
这种设计最大限度地减少了跨保护域的开销,并促进了快速的用户-内核交互。在处理系统调用或处理异常时,内核在被抢占的进程上下文中运行。因此,刷新TLB是不必要的[53],同时内核可以直接访问用户空间来读取用户数据或写入系统调用的结果。
尽管内核代码和用户软件都遭受着常见类型的漏洞[9],但内核和用户进程之间的共享虚拟内存布局所强加的执行模型使得内核利用明显不同。共享地址空间为本地攻击者提供了独特的优势,因为它允许他们在可被内核访问的地址空间的部分(无论是权限还是内容)中获得控制权[91]。简单来说,攻击者可以通过劫持特权执行路径并将其重定向到用户空间轻松地以内核权限执行shellcode。
这种攻击被称为返回用户(ret2usr)攻击,已经是十多年来事实上的内核利用技术(在非Linux操作系统中也是如此)[88][36]。在ret2usr攻击中,通常在内核代码中的内存损坏漏洞被利用之后,将内核数据覆盖为用户空间地址,如图1所示[81]。攻击者主要瞄准控制数据,例如返回地址[86]、分派表[36,44]和函数指针[40,42,43,45],因为它们直接促进任意代码执行[89]。存储在内核堆[38]或全局数据段[44]中的关键数据结构的指针也是常见目标,因为它们允许攻击者通过在用户空间映射虚假副本来篡改这些结构中包含的关键数据[38,39,41]。请注意,目标数据结构通常包含影响内核控制流的函数指针或数据,以便将执行分散到任意点。所有ret2usr攻击的最终效果是,内核的控制或数据流被劫持并重定向到用户空间代码或数据[57]。
大多数ret2usr利用都使用多阶段shellcode,具有第一阶段位于用户空间并“粘合”内核函数(即第二阶段)以执行特权升级或执行rootshell的功能。从技术上讲,ret2usr期望内核在受攻击者控制的进程上下文中运行,以使利用变得可靠。然而,在中断服务例程中也已经确定和利用了内核漏洞[71]。在这种情况下,内核是在中断上下文或超出攻击者控制的进程上下文中运行[37,85],相应的shellcode必须被注入到内核空间中,或者使用ROP/JOP方式从内核的代码小部件构造。由于 kernel hardening技术的广泛应用[31, 65, 68, 92, 93, 95],后一种方法在现实世界的利用中越来越受欢迎。
返回用户攻击是混淆代理问题的又一种表现[49]。鉴于该问题的多架构[42,83]和多操作系统[88]特性,存在多种防御机制。在本节的剩余部分中,我们将借助图1讨论Linux中可用的ret2usr防御措施。
PaX:KERNEXEC和UDEREF是PaX [77]硬化补丁集的两个特性,用于防止从内核到用户空间的控制流转移和取消引用。在x86中,KERNEXEC和UDEREF依赖于内存分段[78]来将内核空间映射到1GB段,每当特权代码尝试取消引用指向非内核地址的指针或提取指令时就返回内存故障。由于缺乏分段支持,因此在x86-64中,UDEREF/amd64 [79]会在执行进入内核时将用户空间内存重映射到不同的(影子)不可执行区域(并在退出时恢复),以防止用户空间引用。由于重新映射内存的开销相当大,因此x86-64系统的另一种选择是启用KERNEXEC/amd64 [80],其开销要小得多,但仅对控制流劫持攻击提供保护。最近,KERNEXEC和UDEREF被移植到ARM架构[90],但补丁仅添加了对AArch32的支持,并依赖于已弃用的MMU域功能(下面将讨论)
SMEP/SMAP/PXN:监管模式执行保护(SMEP)[46]和监管模式访问防止(SMAP)[54]是英特尔处理器的两个最近特性,有助于实现更强的地址空间分离(最新的内核支持这两个功能[31,95])。SMEP提供类似于KERNEXEC的保护,而SMAP的操作方式类似于UDEREF。最近,ARM增加了对称等效功能SMEP的支持,称为特权执行-永不(PXN)[4],但Linux仅在AArch64上使用它。更重要的是,在AArch32上,PXN需要MMU在LPAE模式下运行(相当于Intel的物理地址扩展(PAE)模式[55]),这会禁用MMU域。因此,在AArch32上使用KERNEXEC/UDEREF意味着放弃支持PXN和大内存(> 4GB)的能力。
kGuard:kGuard [57]是一种跨平台编译器扩展,可在不依赖特殊硬件功能的情况下保护内核免受ret2usr攻击。它通过增加动态控制流断言(CFAs)来实施轻量级地址空间隔离,从而在可能被利用的控制转移点(在编译时)强制执行CFAs来防止特权执行路径无限制地转移到用户空间(在运行时)。注入的CFAs在计算分支之前执行一个小的运行时检查,以验证目标地址始终位于内核空间或从内核映射的内存中加载。此外,kGuard还采用了代码多样性技术,以防止针对其本身的攻击。
Linux的设计以弱化的内核到用户空间隔离为代价,以换取用户进程和内核之间更快速的交互。前面讨论的ret2usr保护旨在缓解这种设计弱点,并在最小开销的情况下加强内核和用户空间之间的隔离。本文的目的是评估这些保护所提供的安全性,并调查某些以性能为导向的设计选择是否会使它们失效。我们的研究发现,Linux内存管理子系统(mm)架构中存在一些根深蒂固的基本决策,可以被滥用以削弱内核和用户空间之间的隔离。因此,我们引入了一种新的内核利用技术,称为返回到直接映射内存(ret2dir),可以让攻击者在受到加强保护的系统上执行类似于ret2usr攻击的行为。
我们假设Linux内核已经采取了Section 2.3讨论的保护机制之一(或组合),以防止ret2usr攻击。此外,我们假设攻击者是一个非特权用户,可以本地访问系统,并利用内核内存损坏漏洞[10-27](参见2.2节)来提升权限。请注意,我们对受损数据的类型没有任何假设 - 代码和数据指针都是可能的目标[36、40、42-45、86]。总体而言,我们假设攻击者所需的对抗能力与执行ret2usr攻击所需的能力相同。
在经过ret2usr攻击加固的内核中,被劫持的控制或数据流无法直接重定向到用户空间——相应的ret2usr保护机制将阻止任何这样的尝试,如图1所示。然而,用户进程和内核之间的隐式物理内存共享允许攻击者解构由ret2usr保护机制提供的隔离保证,并将内核的控制或数据流重定向到用户可控的代码或数据。
启用隐式共享物理内存的关键设施是physmap:内核地址空间中包含部分或全部(取决于架构)物理内存直接映射的大型连续虚拟内存区域。这个区域在启用内核尽可能快地分配和管理动态内存方面起着至关重要的作用(我们将在第4节中讨论physmap的结构)。我们应该强调的是,尽管在本研究中我们专注于Linux——最广泛使用的操作系统之一——在许多操作系统中都存在直接映射的RAM区域(以某种形式),因为它们被认为是物理内存管理的标准实践。例如,Solaris使用seg_kpm映射设施在64位架构中提供整个RAM的直接映射[70]。
由于物理内存被分配给用户进程和内核,因此physmap的存在会导致地址别名。虚拟地址别名或synonyms[62]是指两个或多个不同的虚拟地址映射到相同的物理内存地址。鉴于physmap将大部分(或全部)物理内存映射到内核中,攻击者控制的用户进程的内存可以通过其位于内核的别名访问。
发动ret2dir攻击的第一步是在用户空间映射利用负载。根据被利用的漏洞是否允许破坏代码指针[36、40、42-45、86]或数据指针[38、39、41],负载将由shellcode或受控数据结构组成,如图2所示。每当mm子系统为用户空间分配(动态)内存时,实际上都会推迟给出页面帧直到最后一刻。具体来说,物理内存以懒惰的方式授予给用户进程,使用需求分页和写入时复制方法[7],这两种方法都依赖于页面错误以实际分配RAM。当初始化负载的内容时,MMU会生成一个页面错误,并且内核会为攻击进程分配一个页面帧。页面帧由mm使用buddy分配器[61]进行管理。鉴于physmap的存在,当buddy分配器提供一个页面帧以在用户空间中映射时,mm实际上会在内核空间中创建利用负载的别名,如图2所示。虽然内核从不直接使用这样的synonyms,但mm将整个RAM预映射以提高页面帧回收速度。这使得新释放的页面帧可以立即提供给内核,无需修改页面表(有关更多详细信息,请参见第4.1节)。
总的来说,ret2dir利用用户空间和内核空间之间的隐式数据共享(由于physmap)将劫持内核控制或数据流重定向到一组内核内部synonyms页面,有效地执行了等效于ret2usr攻击的攻击,而不必到达用户空间。需要注意的是,恶意载荷在给攻击进程分配页面帧的那一刻就“出现”在内核空间中。攻击者不必明确地“推送”(复制)负载到内核空间(例如,通过管道或消息队列),因为physmap使其可以轻松获得。使用这种方法也要灵活性要小得多,因为系统对于为内核驻留缓存分配的内存量会施加严格的限制,而利用负载(很可能)必须封装在某些会影响其结构的内核数据对象中。
理解ret2dir攻击机制的关键第一步是查看Linux内核地址空间的组织方式——我们以x86平台作为参考。x86-64架构使用48位虚拟地址,这些地址被符号扩展到64位(即,位[48:63]是位[47]的副本)。该方案本质上将64位虚拟地址空间分为两个128TB的规范化半区。内核空间占据了上半部分(0xFFFF800000000000-0xFFFFFFFFFFFFFFFF),并进一步分为六个区域[60]:fixmap区域、模块、内核镜像、vmemmap空间、vmalloc arena和physmap。
另一方面,在x86中,内核空间可以分配给地址空间的上1GB、2GB或3GB部分,其中第一个选项是默认值。由于内核虚拟地址空间有限,它可能成为一种稀缺资源,并且某些区域可能会发生碰撞,以防止其浪费(例如,模块和vmalloc arena、内核镜像和physmap等)。为了ret2dir的目的,在接下来的内容中,我们只关注直接映射区域。
Physmap区域对内核的性能至关重要,因为它方便了动态内核内存分配。从高层次来看,mm提供了两种主要方法来请求内存:vmalloc和kmalloc。使用vmalloc系列例程,只能以页大小的倍数分配内存,并保证是虚拟连续的但不是物理连续的。相比之下,使用kmalloc系列例程,可以按字节级别的块来分配内存,并且保证既是虚拟连续的,也是物理连续的。
由于vmalloc仅以页的倍数提供内存,因此会导致更高的内部内存碎片化和经常出现较差的缓存性能。更重要的是,每次分配或释放内存以将相应的页面框映射到或从vmalloc区域取消映射时,vmalloc都需要更改内核的页表。这不仅产生额外的开销,而且导致增加TLB抖动[67]。由于这些原因,大多数内核组件使用kmalloc。但是,由于kmalloc可以从任何上下文中调用,包括具有严格计时约束的中断服务例程的上下文,因此它必须满足多种不同(且相互矛盾)的要求。在某些情况下,分配器永远不应该睡眠(例如,当锁被持有时)。在其他情况下,它永远不应该失败,或者它应该返回保证是物理连续的内存(例如,当设备驱动程序保留DMA内存时)。
在像上述约束条件下,physmap是实现最佳性能的必要条件。mm开发人员选择了一种设计,将kmalloc覆盖在一个称为“region3”的区域上,该区域预先映射了整个RAM(或部分RAM),理由如下[7]。首先,kmalloc在不触及内核的页表的情况下分配和释放内存。这不仅极大地减少了TLB压力,而且从快速路径中删除了诸如页表操作和TLB shootdowns[70]等高延迟操作。其次,页面帧的线性映射会产生虚拟内存,根据设计保证始终是物理连续的。这会导致缓存性能提高,并且还有一个额外的好处,即允许驱动程序直接将kmalloc分配的区域分配给只能在物理连续内存上运行的DMA设备(例如,在没有IOMMU支持的情况下)。最后,地址转换(虚拟到物理和反之亦然)可以仅使用算术操作[64]实现,从而大大简化了页面帧计数。
Physmap区域是一个与架构无关的特性(鉴于我们上面概述的原因,这不应该令人感到意外),存在于所有流行的Linux平台上。根据每个ISA的内存寻址特性,physmap的大小和确切位置可能会有所不同。尽管如此,在所有情况下:(i)在内核空间中存在部分或全部物理内存的直接映射,(ii)映射始于一个固定的、已知的位置。即使在使用内核空间地址空间布局随机化(KASLR)[35]的情况下,后者也是正确的。
表1列出了我们考虑的平台上physmap感兴趣的属性。在x86-64系统中,physmap以1:1的方式直接映射整个系统的RAM,从页面框架零开始,映射到一个64TB的区域。AArch64系统使用了一个256GB的区域来实现相同的目的[69]。相反,在x86系统中,内核仅直接映射可用RAM的一部分。 32位体系结构上physmap的大小取决于两个因素:(i)使用的用户/内核分割(3G/1G,2G/2G或1G/3G),以及(ii)vmalloc arena的大小。在默认设置下,内核空间分配了1GB,vmalloc arena占用120MB的情况下,physmap的大小为891MB(1GB-sizeof(vmalloc + pkmap + fixmap + unused)),并从0xC0000000开始。同样,在2G/2G(1G/3G)分割下,physmap从0x80000000(0x40000000)开始,生成1915MB(2939MB)的映射。AArch32中的情况非常相似[59],唯一的区别是vmalloc arena的默认大小(240MB)。
总的来说,在32位系统中,直接映射的物理内存量取决于RAM和physmap的大小。如果sizeof(physmap) ≥ sizeof(RAM),那么整个RAM都是直接映射的——这是多达1GB RAM的32位移动设备的常见情况。否则,仅直接映射sizeof(physmap)/sizeof(PAGE)页,从第一页框架开始。
ret2dir攻击的一个关键方面是physmap的内存访问权限。为了获取与直接映射内存区域对应的内核页面的保护位,我们构建了一个名为kptdump的实用程序,这是一个内核模块,通过debugfs伪文件系统[29]导出页面表。该工具遍历内核页表,通过全局符号swapper_pg_dir (x86/AArch32/AArch64)和init_level4_pgt (x86-64)可以获得内核页表,并在physmap区域内转储每个内核页面的标志(U/S,R/W,XD)。 在x86中,我们尝试的所有内核版本中,physmap都被映射为“可读写”(RW)。然而,在x86-64上,physmap的权限不在一个健全的状态。到v3.8.13版本为止的内核违反了W^X属性,将整个区域映射为“可读、可写和可执行”(RWX)——只有最近的内核(≥ v3.9)使用更保守的RW映射。最后,在AArch32和AArch64中,我们测试的每个内核版本都使用RWX权限映射physmap。
实现ret2dir漏洞利用的最后一步是找到一种可靠的方法,可以根据其用户空间对等体来可靠地确定synonym地址在physmap区域中的位置。对于旧版系统,其中physmap仅映射了系统物理内存的一部分,例如具有8GB RAM的32位系统,另一个要求是确保感兴趣的用户空间地址的synonyms存在。我们已经开发了两种技术来实现这两个目标。第一种依赖于通过/proc文件系统的pagemap接口可用的页框信息,该接口在我们研究的所有Linux发行版中当前都可以被非特权用户访问。随着ret2dir攻击的危险性(希望如此)会促使系统管理员和Linux发行版禁用对pagemap的访问,因此我们开发了第二种技术,它不依赖于从内核泄漏任何信息。
procfs伪文件系统[58]有着泄漏安全敏感信息的悠久历史[56,76]。从内核v2.6.25(Apr. 2008)开始,/proc目录下添加了一组伪文件,包括/proc/<pid>/pagemap,用于调试目的的页表检查。为了评估这种功能的普及程度,我们测试了DistroWatch[34]列出的最流行的发行版的最新版本(即Debian、Ubuntu、Fedora和CentOS)。在所有情况下,默认启用了pagemap。
对于每个用户空间页面,pagemap提供一个64位值,由(虚拟)页面编号索引,其中包含有关页面在RAM中的存在的信息[1]。如果在RAM中出现了页面,则设置了第63位以及第0-54位编码它的页面帧号(PFN)。因此,给定一个用户空间虚拟地址uaddr,它的PFN可以通过打开/proc/<pid>/ pagemap并从文件偏移量(uaddr/4096) *sizeof(uint64_t)(假设4KB页面)读取8字节来定位。
拥有给定uaddr的PFN,表示为PFN(uaddr),可以使用以下公式找到其在physmap中的synonymsSYN(uaddr):SYN(uaddr) = PHYS_OFFSET + 4096*(PFN(uaddr)-PFN_MIN)。PHYS_OFFSET对应于已知的、固定的physmap内核虚拟地址的起始位置(不同配置的值如表1所示),而PFN_MIN是第一个页面帧号,在许多体系结构中,包括ARM,物理内存从非零偏移开始(例如,在Versatile Express ARM板上是0x60000000,对应于PFN_MIN = 0x60000)。为了防止在uaddr交换出去后回收SYN(uaddr),可以使用mlock将相应的用户页面“锁定”在RAM中。
如果sizeof(RAM) > sizeof(physmap),则表示系统中有一部分RAM是直接映射的,因此只有PFN的子集可以通过physmap访问。例如,在具有4GB RAM的x86系统中,PFN范围为0x0-0x100000。但是,在默认的3G/1G拆分下,physmap区域仅映射了前891MB的RAM(另见表1的其他设置),这意味着PFN从0x0到0x37B00(PFN_MAX)。如果用户空间地址的PFN大于PFN_MAX(最后一个直接映射页面的PFN),则physmap不包含该地址的synonyms。自然而然的问题是,我们是否可以强制伙伴分配器提供具有小于PFN_MAX的PFN的页面帧。
出于兼容性原因,内存管理器将物理内存分成几个区域。特别是,旧版ISA总线的DMA处理器只能访问RAM的前16MB,而一些PCI DMA外设只能访问前4GB。为了应对这些限制,内存管理器支持以下区域:ZONE_DMA、ZONE_DMA32、ZONE_NORMAL和ZONE_HIGHMEM。后者在32位平台上可用,包含内核无法直接寻址的页面帧(即那些没有在physmap中映射的页面帧)。ZONE_NORMAL包含低于ZONE_HIGHMEM但高于ZONE_DMA(和64位系统中的ZONE_DMA32)的页面帧。当只有一部分RAM是直接映射时,内存管理器将区域排序如下:ZONE_HIGHMEM > ZONE_NORMAL > ZONE_DMA。给定一个页面帧请求,内存管理器会从符合请求的最高区域开始尝试满足它(例如,如我们所讨论的,ZONE_NORMAL的直接映射内存被用于kmalloc),并在没有可用的空闲页面帧的情况下向较低的区域移动。
从攻击者的角度来看,用户进程首先从ZONE_HIGHMEM获取其页面帧,因为内存管理器试图保留直接映射的页面帧以供内核的动态内存请求使用。然而,当ZONE_HIGHMEM的页面帧用尽时,由于内存压力增加,内存管理器不可避免地开始从ZONE_NORMAL或ZONE_DMA提供页面帧。基于此,我们的策略如下。攻击进程重复使用mmap请求内存。对于请求的每个页面,在访问单个字节引起页面故障的情况下,该进程强制分配一个页面帧(或者,可以在mmap中使用MAP_POPULATE标志来预分配所有相应的页面帧)。然后,该进程检查每个已分配页面的PFN,并重复相同的过程,直到获得小于PFN_MAX的PFN。这样页面的synonyms可以保证存在于physmap中,并且可以使用上述公式计算其精确地址。请注意,根据物理内存的大小和用户/内核分隔使用的方式,我们可能需要生成额外的进程来完全耗尽ZONE_HIGHMEM。例如,在具有8GB RAM和默认3G/1G分隔的x86机器上,可能需要多达三个进程来保证获得落在physmap中的页面帧。有趣的是,系统上运行的越多的良性进程,攻击者获得具有synonyms的页面就越容易;其他任务会增加内存压力,将攻击者的分配“推”到所需的区域。
连续的synonyms:某些漏洞可能需要多个页面作为它们的有效载荷。然而,在用户空间中在虚拟上是连续的页面,不一定映射到物理上是连续的页面帧,这意味着它们的synonyms也不会连续。然而,鉴于physmap的线性映射,具有连续synonyms的两个页面具有顺序的页面帧号(PFN)。因此,如果0xBEEF000和0xFEEB000分别具有PFN 0x2E7C2和0x2E7C3,则它们在physmap中是相邻的,尽管它们在用户空间中相隔约64MB。
为了寻找连续的synonyms,我们按如下方式进行。采用与上文相同的方法,计算一个随机用户页面的synonyms。然后我们不断获取更多的synonyms,并每次将新获取的synonyms的PFN和之前获得的PFN进行比较。这个过程一直持续到任意两个(或更多,具体取决于攻击)synonyms具有连续的PFN为止。此时,漏洞有效载荷可以适当地分割到与连续PFN对应的用户页面中。
由于禁用对 /proc/<pid>/pagemap 文件的访问是一个相对简单的操作,因此我们还要考虑无法获得PFN信息的情况。在这种设置下,假设存在一个驻留在内存中的用户页面,我们无法直接确定其synonyms在physmap区域内的位置。回想一下,我们的目标是在physmap区域内找到一个内核驻留页面,并使其与用户驻留漏洞有效载荷“配对”。虽然我们无法确定给定用户地址的synonyms,但仍然可以反过来操作:随意选取一个physmap地址,并保证(尽可能)其对应的页面框架被包含漏洞有效载荷的用户页面映射。
这可以通过使用(对齐的)漏洞有效载荷副本来耗尽攻击进程的地址空间来实现,类似于堆喷射技术[33]。基地址和physmap区域的长度是预先知道的(见表1)。后者对应于PFN_MAX - PFN_MIN页框架,由所有用户进程和内核共享。如果攻击进程成功将漏洞有效载荷复制到N个内存驻留页面(在物理内存范围内被physmap映射),则任意选择一个页面对齐的physmap地址指向漏洞有效载荷的概率(P)为:P = N / (PFN_MAX - PFN_MIN)。我们的目标是最大化P。
Spraying:最大化N是很直接的,归结为尽可能多地获取页面框架。我们使用的技术类似于第5.1节中介绍的技术。攻击进程重复使用mmap来获得内存,并将漏洞有效载荷“喷洒”到返回的区域中。我们更喜欢使用mmap,而不是涉及shmget、brk和remap_file_pages等方式,因为通常会对后者施加系统限制。 MAP_ANONYMOUS分配也更受欢迎,因为现有的基于文件的映射(来自竞争进程)将与匿名映射相比具有更高的优先级被交换出去。复制有效载荷会导致页面故障,从而通过mm进行页面框架分配(或者可以使用MAP_POPULATE)。如果虚拟地址空间不足以耗尽整个RAM,就像某些32位配置一样,攻击进程必须产生其他子进程来协助分配。
该过程将继续,直到mm开始将“喷洒”的页面交换到磁盘上。为了准确定位交换发生的确切时刻,每个攻击进程定期检查其喷洒的页面是否仍然驻留在物理内存中,通过每隔几个mmap调用调用getrusage系统调用。同时,所有攻击进程启动一组后台线程,重复写访问已分配的页面,模拟mlock的行为,并尽可能地防止喷洒的页面被交换出去-mm使用LRU策略将页面框架交换到磁盘上。因此,通过反复访问页面,mm会被欺骗认为它们对应于新内容。当内存中驻留的页面数量开始下降(即攻击进程的驻留集大小(RSS)开始减少)时,已达到最大允许的物理内存占用空间。当然,这个占用空间的大小也取决于由其他进程施加的内存负载,这些进程与攻击进程竞争RAM。
签名:就PNF_MAX-PNF_MIN而言,我们可以通过排除某些页面来减少物理映射区域中潜在的目标页面集,这些页面对应于伙伴分配器永远不会提供给用户空间的帧。例如,在x86和x86-64中,BIOS通常将在页面帧零处存储POST期间检测到的硬件配置。同样,物理地址范围0xA0000-0xFFFFF用于映射某些图形卡的内部存储器。此外,对应于内核代码和全局数据的ELF节在RAM中的已知固定位置(例如,在x86中为0x1000000)被加载。基于这些和其他预先确定的分配,我们已经为我们考虑的每个配置生成了保留页面帧范围的physmap签名。如果没有可用的签名,则所有页面帧均为潜在目标。通过结合物理映射和签名喷洒,我们可以最大化从物理映射中随机选择任意一个页面指向漏洞有效载荷的可能性。我们的实验安全评估结果(第7节)表明,取决于配置,成功的概率可能高达96%。
这是一种针对使用SMAP或UDEREF加固的x86系统的ret2dir攻击示例。我们假设存在一种内核漏洞的利用方式,可以允许我们破坏一个名为kdptr的内核数据指针,并将其覆盖为任意值[38,39,41]。在未加固内核的系统中,攻击者可以将kdptr覆盖为用户空间地址,并通过调用相应接口(例如有缺陷的系统调用)强行引用内核。然而,SMAP或UDEREF的存在将导致内存访问违规,有效地阻止了攻击尝试。为了克服这一点,可以利用ret2dir攻击来进行攻击。
首先,攻击者控制的用户进程保留了一个单独的页面(4KB),比如位于地址0xBEEF000。接下来,进程开始初始化新分配的内存,将其初始化为漏洞有效载荷(例如,受篡改的数据结构)。这个有效载荷初始化阶段会导致页错误,触发mm从伙伴分配器请求一个自由页面帧,并将其映射到地址0xBEEF000。假设伙伴系统选择页面帧1904(0x770),在x86中,在默认的3G/1G分割下,物理映射从0xC0000000开始,这意味着页面帧已经被预先映射到地址0xC0000000 + (4096 * 0x770) = 0xC0770000(根据第5.1节中的公式)。此时,0xBEEF000和0xC0770000是synonyms;它们都映射到包含攻击者有效载荷的物理页。因此,任何位于0xBEEF000-0xBEEFFFFF区域的数据都可以通过同义词地址0xC0770000-0xC0770FFF轻松地被内核访问。更糟糕的是,由于物理映射主要用于实现动态内存分配,内核无法区分位于地址0xC0770000处的内核数据结构是伪造的还是合法的(即使用kmalloc正确分配的)。因此,通过将kdptr覆盖为0xC0770000(而不是0xBEEF000),攻击者可以绕过SMAP和UDEREF,因为这两种保护都认为任何超过0xC0000000的内存地址都是良性的。
这是一个x86-64架构的示例,演示了如何使用ret2dir攻击绕过KERNEXEC、kGuard和SMEP(PXN与SMEP几乎相同)防护。我们假设存在一种内核漏洞,可以允许我们破坏一个名为kfptr的内核函数指针,并将其覆盖为任意值[40,42,43,45]。在这种情况下,攻击有效载荷不是一组伪造的数据结构,而是要以提升权限的方式执行的机器码(shellcode)。在实际的内核利用中,有效载荷通常包含多个阶段的shellcode,其中第一个阶段将内核例程(第二个阶段)拼接在一起,以执行权限提升[89]。在大多数情况下,这归结为执行类似于commit_creds(prepare_kernel_cred(0))的操作。这两个例程用零来替换用户任务的凭据((e)uid,(e)gid),有效地授予攻击进程根权限。
过程与前面的例子类似。假设有效载荷已经复制到用户空间地址0xBEEF000,伙伴分配器将其分配给页面帧190402(0x2E7C2)。在x86-64中,物理映射从0xFFFF880000000000开始(见表1),并使用常规页面(4KB)映射整个RAM。因此,地址0xBEEF000的synonyms在内核空间中位于地址0xFFFF880000000000 + (4096 * 0x2E7C2) = 0xFFFF87FF9F080000处。
在攻击者控制内核函数指针的ret2usr场景中,攻击者的一个优势是他们还控制包含攻击有效负载的用户页面的内存访问权限,使得将shellcode标记为可执行非常容易。然而,在一个加固的系统中,一个ret2dir攻击只允许控制physmap中相应synonyms页面的内容,而不是它们的权限。换句话说,尽管攻击者可以设置0xBEEF000 - 0xBEEFFFF的范围的权限,但这不会影响相应的physmap页面的访问权限。
不幸的是,如表1所示,许多平台都没有强制执行W^X属性,包括x86-64。在我们的例子中,用户地址0xBEEF000的内容也可以通过内核地址0xFFFF87FF9F080000作为纯可执行代码来访问。因此,通过简单地用0xFFFF87FF9F080000覆盖kfptr并触发内核对它进行解引用,攻击者可以直接以内核特权执行shellcode。KERNEXEC、kGuard和SMEP(PXN)无法区分kfptr是否指向恶意代码或合法的内核例程,只要kfptr≥0xFFFF880000000000且*kfptr为RWX,解引用就被认为是良性的。
上面的例子中,我们利用了一些平台将物理映射区域的部分(或全部)映射为可执行(X)的事实。那么问题出现了,如果physmap权限设置正确,ret2dir攻击是否会有效。正如我们在第7节中展示的那样,即使在这种情况下,通过使用返回导向编程(ROP)[8, 51, 87],仍然可以进行ret2dir攻击。
让我们重新考虑之前的例子,这次假设physmap不是可执行的。攻击者可以映射一个等效的ROP负载,而不是在0xBEEF000处映射常规的shellcode:这个实现具有相同功能的ROP负载只由码片段链组成,以ret指令结尾,这些码片段被称为gadget,位于内核(可执行)文本段中。为了触发ROP链,kfptr被覆盖为指向栈切换gadget的地址,这是为了将栈指针设置为ROP负载的开头,以便每个gadget可以将控制传递到下一个gadget。通过使用指向一个pivot序列的地址来覆盖kfptr,比如xchg %rax,%rsp; ret(假设%rax指向0xFFFF87FF9F080000),ROP负载的同义词现在作为内核模式堆栈。请注意,Linux为每个用户线程分配一个单独的内核堆栈,使用kmalloc进行分配,这使得不可能区分合法堆栈和使用ret2dir在内核空间中“推送”的ROP负载,因为它们都驻留在physmap中。最后,ROP代码还应该注意恢复堆栈指针(以及可能的其他CPU寄存器),以允许可靠的内核继续执行[3,81]。
我们评估了ret2dir对使用ret2usr保护硬化的内核的有效性,使用真实世界和定制的漏洞利用。我们从Exploit Database(EDB)[75]获得了一组八个ret2usr漏洞利用,涵盖了广泛的内核版本(v2.6.33.6-v3.8)。我们在未加强保护的内核上运行每个漏洞利用以验证其有效性,并确保它确实采用了ret2usr攻击方法。接下来,我们对每个受到ret2usr攻击保护的内核重复了相同的实验,预期所有漏洞利用都会失败。最后,我们使用第5节介绍的技术将这些漏洞利用转化为ret2dir等效版本,并针对相同的强化系统进行了测试。总的来说,我们的ret2dir版本的漏洞利用绕过了所有可用的ret2usr保护,即SMEP、SMAP、PXN、KERNEXEC、UDEREF和kGuard。
表2总结了我们的研究结果。前两列(EDB-ID和CVE)对应于测试的漏洞利用,第三列(Arch.)和第四列(Kernel)表示使用的架构和内核版本。Payload列指示使用ret2dir将推入内核空间的有效载荷类型,可以是ROP有效载荷(ROP)、可执行指令(SHELLCODE)、篡改的数据结构(STRUCT)或以上各种组合,具体取决于漏洞利用。Protection列列出了每种情况下部署的保护机制。空单元格对应于在给定设置中不适用的保护措施,因为它们可能不是(i)特定架构可用,(ii)给定内核版本支持,或(iii)针对某些类型的漏洞利用无关。例如,PXN仅适用于ARM架构,而SMEP和SMAP是英特尔处理器特性。此外,在内核v3.2中添加了对SMEP的支持,在v3.7中添加了对SMAP的支持。请注意,根据物理映射区域的权限(请参见表1),我们不得不修改一些依赖于纯shellcode的漏洞利用,以使用ROP有效载荷,以实现任意代码执行(尽管在ret2usr漏洞利用中,攻击者可以赋予包含有效载荷的用户空间内存可执行权限,在ret2dir漏洞利用中,无法修改物理映射区域的权限)。带*的kGuard条目需要访问相应内核的(随机化)文本节。正如我们在第2.3节中提到的,KERNEXEC和UDEREF最近被移植到AArch32架构[90]。除了提供更强的地址空间分离外,作者还努力修复了AArch32中内核的权限,通过对物理映射中大多数RWX页面强制实施W^X属性。然而,由于相应的补丁目前正在开发中,因此在physmap内部仍存在被映射为RWX的区域。在内核v3.8.7中,我们确定了一个大约6MB的physmap区域,该区域被映射为RWX,从而使我们的ret2dir漏洞利用能够执行纯shellcode。
我们发现目前可以公开利用的最新内核版本是v3.8。因此,为了评估最新的内核系列(v3.12),我们使用了自定义漏洞利用程序。我们人为注入了两个漏洞,使我们能够破坏内核数据或函数指针,并用用户可控制的值覆盖它(在表2中标为“Custom”)。请注意,这两个漏洞与公开利用所利用的漏洞类似。就ARM而言,我们成功管理启动的最新PaX保护的AArch32内核版本是v3.8.7。
对于每个漏洞利用,我们测试了适用的每种保护措施。在所有情况下,ret2dir版本仅将控制权传输到内核地址,绕过了所有部署的保护措施。
图3展示了我们在x86-64和AArch32架构中使用的shellcode。该shellcode是位置无关的,因此每个漏洞利用中唯一需要更改的是将pkcred和ccreds分别替换为prepare_kernel_cred和commit_creds的地址,正如6.2节中所述。通过将shellcode复制到在physmap区域中具有同义词的用户空间页面中,我们可以通过用用户空间页面的physmap-resident同义词地址覆盖内核代码指针来直接从内核模式执行它。我们在所有映射为可执行代码的physmap情况下都采用了这种策略(对应表2中Payload列包含SHELLCODE条目的情况)。
对于physmap不可执行的情况,我们使用了ROP负载来替换shellcode并实现相同的目的。在这些情况下,被破坏的内核代码指针被覆盖为一个堆栈轴心小部件的地址,该小部件将内核的堆栈指针带到是包含ROP负载的用户页面的同义词的physmap页面。图4显示了我们的利用中使用的x86 ROP负载的示例。前面的小工具保留esp和ebp寄存器以便于可靠的继续(如6.2节所述)。刮擦空间可以方便地位于受控页面内,因此可以据此轻松计算SCRATCH_SPACE_ADDR1和SCRATCH_SPACE_ADDR2的地址。然后,有效负载基本上执行与shellcode相同的代码以提高特权。
在没有访问pagemap的系统中,ret2dir攻击必须依赖于physmap spraying来寻找对应于利用载荷的同义词。如5.2节所述,随机选择的physmap地址确实指向利用载荷的概率取决于(i)安装的RAM数量,(ii)由于竞争进程而导致的物理内存负载,以及(iii)physmap区域的大小。为了评估这种可能性,我们在不同的系统配置和工作负载下进行了一系列实验。
图5显示了我们系统中安装的RAM数量作为函数时,通过单个尝试成功选择physmap地址的概率。我们的测试平台包括一个配备两个2.66GHz四核Intel Xeon X5500 CPU和16GB RAM的主机,在64位Debian Linux v7上运行。每个条形表示5次重复的平均值,误差条对应于95%置信区间。在每次重复中,我们使用喷洒技术(第5.2节)计算我们成功获取的最大数量physmap驻留页帧的百分比,以获得physmap大小。我们使用了三种不断增加内存压力的工作负载:空闲系统,类似桌面的工作负载,在多个选项卡中进行常量浏览活动(Facebook、Gmail、Twitter、YouTube和USENIX网站),以及在8个CPU内核上运行的16个并行线程的分布式内核编译(gcc、as、ld、make)。请注意,必须维护竞争进程的连续活动,以便它们的工作集保持热状态(最坏情况下),否则攻击ret2dir进程将轻松窃取它们的内存驻留页面。 成功的概率随着RAM数量的增加而增加。对于最低内存配置(1GB),概率在65-68%之间,具体取决于工作负载。空闲负载和高强度负载之间的这种小差异表明,尽管竞争进程不断活动,但由于重复访问已分配的所有页面,攻击ret2dir进程成功地获取了大量内存。在本质上将它们“锁定”到主存储器中。对于2GB配置,概率跳至88%,对于16GB则达到96%。
请注意,由于这些实验是在64位系统上执行的,因此physmap始终映射所有可用内存。在32位平台上,其中physmap仅映射RAM的子集,成功的概率甚至更高。如第5.1节所述,在这种情况下,由竞争进程创建的附加内存压力(更可能在ret2dir进程之前产生)有助于“推动” ret2dir分配到落在physmap区域内的所需区域(ZONE_NORMAL,ZONE_DMA)。最后,根据漏洞的不同,失败的尝试通常不会导致内核崩溃,使攻击者能够多次运行利用程序。
略
本文作者:Du4t
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!