UAF论文

综述

  • 现有UAF防御措施在真实应用场景下并没有广泛使用,一方面因为准确率不足,另一方面则是性能开销过高。
  • 在本文中作者将重新使用(resurrect 复活)OTA方法来进行防御。作者使用了两种技术来支撑OTA的实际应用,一种是批量页分配,另外一种是将指针碰撞和固定大小垃圾回收结合,这两种方法主要为了改进OTA的性能。
  • 作者实现了一个示例函数,FFmalloc

引文

以前的检测或防御方法

  1. 标记内存区块为已分配的或是空闲的。在每次内存存取之前都要检查标记,该方法的缺陷在于在程序执行过程中开销大。
  2. 在对象被释放后,主动将迷途指针置为无效,如设置为NULL,该方法必须保持在指针和指向的对象之间的双向关系。这就显著减慢了保护的性能。而作为特例,Oscar让已被释放的数据对象不可存取以达到同样的目的。
  3. 使用空闲的CPU核心来单独进行垃圾回收。这种方法需要额外的计算资源且实际效果有限 ** 作者的思路

UAF的根本成因在于攻击者可以重新声明被释放的内存并修改其内容。这是基于所有的内存管理调度算法都会为之后的内存请求复用已释放的内存来提高效率。这样攻击者获得这些被释放的内存的成本很低。也正是基于此,作者想到可以采用OTA的方法来阻止攻击者利用UAF漏洞,使用OTA分配地址对于任何虚地址,都只会将这个地址分配给程序一次。并且不会复用该地址。换句话说每个请求都会分配到不同的地址区块,不会有重叠的部分。 思路很简单,困难的地方在于能够构造出一个实际可用的OTA管理器。在此作者提出了三个难点 1. OTA会导致高额的内存开销

  1. OTA受到内核中VMA结构数量的限制
  2. 因为使用OTA要频繁调用系统调用来分配页表,故会减慢程序的执行速度

作者的解决方法

作者从两个方面阐述了解决这些难题的方法

内存分配

作者采用了两种不同的分配策略来减少内存浪费。

  • 对于小内存请求,使用基于请求大小的binning allocator来将相似大小的对象放在一起。使用这种方法可以避免最坏的内存使用情况,即内存中的"孤岛",被分配的小块内存处在被分配的大块内存之间,阻碍了内存页被释放。(binning allocator可以类比glibc堆管理的重要分配器bins的分配过程,bins采用双向链表的形式来管理已被free的内存。可参考https://zhuanlan.zhihu.com/p/163401620https://paper.seebug.org/255/
  • 对于大内存请求,我们将申请的内存以连续的形式存放在分散的内存区域中。

相似策略在现代内存管理中也在使用。但这些内存分配策略对研究最有价值的地方在于它证明了OTA这种方式是实际可行的。

内存映射

采用了批次处理内存映射和解映射来尽量减少调用系统调用的次数。FFmalloc(作者实现的malloc函数)在请求内存时会一次请求比当前需要的内存大得多的内存这样就可以处理应用的内存需求。申请出来的多余内存会当做cache来处理将来的内存需要。同样,在free内存时也不会立刻释放free的内存区域而是等几片连续的内存页都被free后才调用系统调用释放他们。

作者的具体实现

实现了一个名为Fast Forward Memory Allocation(FFmalloc)的函数作为验证OTA的示例,并利用FFmalloc在benchmark和real programs中进行了测试,发现其和目前最先进的工具MarkUs在benchmark中相比,增加的CPU开销相对小而内存开销会相对大。在ChakraCore中的开销基本可以忽略不计。

作者论文的贡献

重新提出使用OTA方法来防御UAF,并给出了一个可行的实现FFmalloc。对其进行了深入评估,证明了OTA的可行性。

设计细节

Metadata of Allocations

该部分是为了实现这种内存分配所定义的数据结构。首先需要弄清楚几个结构之间的关系,即pool,allocator,page,chunk。small被定义为less than half a page。其余为large。 https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0405oUDNko.jpg

Freeing Memory

当释放内存时,FFmalloc首先定位pagepool的metadata,然后标记bin pool中的相关slot,或者更新continuous pool中的指针。在释放内存时遵循批处理原则,等待足够多的连续内存页被free后才把这些页返回给munmap系统调用。具体等待多少内存页可以在编译前修改相关的最小数值。作者在估计了多方代价后选择了8作为等待的连续页数。 释放内存的过程中还有两个额外的功能:

  1. 检测二次释放和无效释放
  2. lazy free,即不使用munmap调用来释放内存页,而是使用madvise,该函数会在系统内存不足时才释放内存。作者对采用两种系统调用的实现都进行了测试。具体比较在评价部分的内容中

Reallocation under OTA

realloc 允许程序改变现有的已分配区块的区块大小,如果变小只需要收缩分配的大小并返回。如果更大则需要进行检查以查看是否有足够的内存可扩展。不足则要整体重新分配。 为了避免内存重用,即符合OTA的原则,realloc在处理内存分配时会更倾向于重新分配内存,这就导致一个缺陷即如果程序大量调用realloc会导致严重的运行开销。

Supporing Multi-threaded Applications

为了改进多线程的内存分配效率,FFmalloc引入了thread caching技术。

具体分配过程解析

FCmalloc

FCmalloc是OTA最简单的实现方式,处理内存请求时的内存分配一直是连续的,内存页不够用就调用mmap分配新的内存页,对于free请求,只有当一整页的内存都被free,才调用munmap释放该页。但是这种分配方式最严重的问题就是造成的系统开销过大。在gcc benchmark中,对一个c-typeck的输入文件,使用FCmalloc和glibc的malloc会产生60.2%的mmap munmap系统调用开销,严重的内存开销,还有对系统VMA结构的浪费。Linux限制每个进程最多创建65535个VMA结构体。下图显示了FCmalloc的VMA浪费情况

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0405uOB3xk.jpg

针对FCmalloc的这些问题,作者提出了一种解决方案,即Batch Processing。当请求内存时,调用mmap一次请求多页,然后交由FCmalloc来分配请求的这些页,直到所有内存页都被用完,才再次请求。对于free同理,等待连续多页要free时才调用munmap free全部页。取得的改进效果是非常突出的,同样的gcc benchmark,可以节省58.7%的munmap调用和65.8的VMA开销。

FBmalloc

FBmalloc的设计思想通常被称为BiBop allocator.其采用的方法与FCmalloc不同,FBmalloc创建了许多bins,每个bin中存储相同大小的object。对于小于4k的内存请求,会在对应的bin中分配内存。对于大于4k的请求则直接调用mmap向系统申请页。每个bin都是一页大小,用完一页申请一页。bin内为FCmalloc。采用FBmalloc可以减缓FCmalloc造成的内存浪费。特别小内存数据项往往具有更长的生命周期。

FFmalloc

由上述问题,最终作者采用将两种malloc混合使用的方法。取其优势。对于2k以下的内存请求使用FBmalloc,对于2k以上的内存请求使用FCmalloc,这样可以保证内存页的对齐。 混合的具体方式为设立分配器。每个分配器拥有一个内存池,FFmalloc每次从OS请求内存时使用内存池的粒度。内存池的大小是可配置的,作者在实现过程中将其设为4MB,大于设定的内存池的内存请求,FFmalloc会创建一个特殊的内存池jumbo pool,大小与请求相同。一个bin allocator只使用一个内存池。而一个continuous allocator可使用任意数量的内存池。FFmalloc还为每个CPU核心分配不同的内存池以减少锁竞争。第一个内存池的偏移处在现有堆空间的末尾,这使得程序既可以使用glibc的allocator 也可以用FFmalloc的allocator。同时这种方式使得FFmalloc分配的起始地址随机化,保留了ASLR的安全优势。FFmalloc通过设定mmap的MAP_FIXED_NOREPLACE标志来确保获取到的地址是递增的。

与malloc对比

通过上面的叙述,FFmalloc的具体原理已经很清楚了。那么他与现有glibc中malloc的差别在哪里,我们通过对两者内存分配流程解析来对比下FFmalloc的独特之处。 malloc同样使用了pool来管理内存,最重要的就是bin,内存池保存在bins这个长128的数组中,每个元素都是一双向个链表。

https://testingcf.jsdelivr.net/gh/game-loader/picbase@master/uPic/0405pfsxy6.jpg

  • bins[0]目前没有使用
  • bins[1]的链表称为unsorted_list,用于维护free释放的chunk。
  • bins[2,63)的区间称为small_bins,用于维护<512字节的内存块,其中每个元素对应的链表中的chunk大小相同,均为index*8。
  • bins[64,127)称为large_bins,用于维护>512字节的内存块,每个元素对应的链表中的chunk大小不同,index越大,链表中chunk的内存大小相差越大,例如: 下标为64的chunk大小介于[512, 512+64),下标为95的chunk大小介于[2k+1,2k+512)。同一条链表上的chunk,按照从小到大的顺序排列。

malloc将内存分成了大小不同的chunk,然后通过bins来组织起来。malloc将相似大小的chunk(图中可以看出同一链表上的chunk大小差不多)用双向链表链接起来,这样一个链表被称为一个bin。malloc一共维护了128个bin,并使用一个数组来存储这些bin。数组中第一个为unsorted bin,数组编号前2到前64的bin为small bins,同一个small bin中的chunk具有相同的大小,两个相邻的small bin中的chunk大小相差8bytes。small bins后面的bin被称作large bins。large bins中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小序排列。large bin的每个bin相差64字节。 对于malloc来说,调用malloc时内存分配的过程如下:

  1. 如果分配内存<512字节,则通过内存大小定位到smallbins对应的index上(floor(size/8))

    • 如果smallbins[index]为空,进入步骤3
    • 如果smallbins[index]非空,直接返回第一个chunk
  2. 如果分配内存>512字节,则定位到largebins对应的index上

    • 如果largebins[index]为空,进入步骤3
    • 如果largebins[index]非空,扫描链表,找到第一个大小最合适的chunk,如size=12.5K,则使用chunk B,剩下的0.5k放入unsorted_list中
  3. 遍历unsorted_list,查找合适size的chunk,如果找到则返回;否则,将这些chunk都归类放到smallbins和largebins里面

  4. index++从更大的链表中查找,直到找到合适大小的chunk为止,找到后将chunk拆分,并将剩余的加入到unsorted_list中

  5. 如果还没有找到,那么使用top chunk

  6. 或者,内存<128k,使用brk;内存>128k,使用mmap获取新内存

free释放内存时,有两种情况:

  1. chunk和top chunk相邻,则和top chunk合并
  2. chunk和top chunk不相邻,则直接插入到unsorted_list中

此外,调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。 那么回到FFmalloc,FFmalloc内存分配使用的数据结构前面已经讲过了,这里给出一个根据我们的理解整理的内存分配流程(在一个CPU核上)

  1. 如果分配内存<2K,通过内存大小定位到bin pool中对应的页上
    • 如果页上已分配内存的末尾仍有空间,则修改bitmap,分配内存
    • 如果页上分配内存末尾没有空间,则申请新bin allocator 和pool,分配内存
  2. 如果分配内存>2K <4M(可修改)
    • 如果continuous allocator拥有的pool有足够的空间,则分配内存,改变尾指针的值。
    • 如果没有,申请新pool
  3. >4M 创建特殊pool,名为jumbo pool,size=object size

free释放内存时:

修改bin allocator中bitmap对应位的标记或者修改continuous allocator中的pointer并等待,直到足够数量的页已经完全被free后调用munmap回收页。

一些讨论

其他的技术细节

FFmalloc可以完全替代C标准库中的malloc,free,realloc等。FFmalloc并未封装像mmap,munmap等系统调用,这意味着若程序直接调用mmap分配内存,仍会产生UAF漏洞。这也会影响现有的其他安全分配器,但作者说并未在其他文章中发现有人提到这一点。作者在下面讨论了FFmalloc对mmap进行封装的可能性并最终得出结论,FFmalloc很难对mmap进行封装。

地址随机化

可能有人认为FFmalloc会降低ASLR带来的安全效果。文中作者否认了这一点,认为ASLR的随机化粒度是模块级的,而FFmalloc在分配第一个内存池时是随机的。故FFmalloc不会对ASLR造成任何影响。而内存分配时的相对固定的分配顺序可能会被攻击者利用,作者声称对分配算法做一些小的改进就可以使堆布局是无序的,同时仍能保持原有射击目标,且将这些工作留到日后进一步改进。

适用性

FFmalloc仅是OTA的一个示例,但是不想许多其他的原型系统仅仅为了证明现实的可行性。FFmalloc已经是一个可行的方案。然而benchmark test中的结果也标明FFmalloc可能并不适用于全部应用。

优势

FFmalloc可以完全绝大多数情况下杜绝UAF漏洞的利用,除非全部应用地址空间都被用完。且与同类型设计相比在大多数情况下都能取得最小的CPU开销。且能完全适用嵌入式系统,不需要额外的CPU核来运行辅助线程

劣势

  • 最终效果受具体运行环境的影响极大,如按照FFmalloc的free策略,经常释放大块内存的应用会调用更多的munmap系统调用。
  • 对于经常分配近似大小内存区块的程序而言,FFmalloc会比glibc慢。因为glibc可以复用刚刚释放的内存这样就可以充分利用cache。
  • 除此以外,FFmalloc在多线程中应用时可能会很困难。因为mmap和munmap在内核中都是严格的顺序执行的,而这样会使那些经常分配内存的程序的多线程失去意义。一个或许可行的方案是通过某些方案在内核中不使用mmap-sem锁。(参照https://lwn.net/Articles/787629/)
  • FFmalloc在应用同时分配不同生命周期的数据对象时会产生极大的效率问题。因为尽管可能数据很小但是其长时间被使用就会导致相邻的内存空间一直不能被释放。

与现有成果比对

最有竞争力的就是MarkUs,如前所述,在测试过程中MarkUs与FFmalloc相比在内存开销上更有优势而在CPU开销上却更差。而且FFmalloc也可以通过更频繁返回内存来缩小这一差距 相对于现有UAF解决方案,FreeGuard提供了可调的概率性保护。其性能优良,应对面广。然而,概率性保护保护的价值可能极其有限。

相关工作

安全分配器

Archipelago Electic等在每页设置一个数据对象来检测内存安全问题。他们在数据对象之间设置不可达的页来阻止缓冲区溢出攻击,随机化对free的内存页的重用来防止UAF。但是由于在页的层面做防御,会带来比较大的开销。 DieHarder是一个阻止程序因为内存破坏而停止的工具。DieHarder假设堆空间是无限大的,内存分配彼此不相邻。通过随机化分配的方式来保证地址不被重用。FreeGuard与DieHarder相比有更好的运行效率但很难达到与DieHarder同水平的安全效果。Guarder试图提高FreeGuard能达到的安全水准,使用的方法是引入新技术来管理小数据对象,重新在性能开销和安全水准之间做取舍。这些都只能对UAF做概率性的防护,而OTA可以达到完全防护。 在OTA之前采用这种思路的人是Oscar,同样是在2017USENIX security会议上首次提出这种设计思路,他论文的标题为A Practical Page-Permissions-Based Scheme for Thwarting Dangling Pointers.在这篇论文中,Oscar也采用了只允许前向内存分配的原则达到了阻止UAF漏洞的效果。Oscar使用了shadow memory来克服巨大的内存开销,尽管他的成果已经大大改进了06年在DSN会议上Dinakar等人提出的方法开销大的问题,但其实际开销仍然与OTA的效果相去甚远。 作者在此处提出的FBmalloc的思想主要借鉴了2010年USENIX security Cling的文章A Memory Allocator to Mitigate Dangling Pointers.这篇文章主要讲的是一种可以减少悬空指针的内存分配器,这篇文章中实现的Cling Memory Allocator与本文提到的FBmalloc极为相近,也是通过bin将相同大小的object放在一起,这样重用时只能分配一个同样大小的内存区域。