hugo/content/posts/UAFpaper.md
2022-03-24 09:34:09 +08:00

207 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

+++
title = "UAF论文"
author = "Logic"
date = 2022-03-04
draft = false
+++
## 综述 {#综述}
- 现有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/163401620><https://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 {#metadata-of-allocations}
该部分是为了实现这种内存分配所定义的数据结构。首先需要弄清楚几个结构之间的关系即pool,allocator,page,chunk。small被定义为less than half a page。其余为large。
![](https://images.gitee.com/uploads/images/2021/1115/230444_15dde024_8810712.png)
### Freeing Memory {#freeing-memory}
当释放内存时FFmalloc首先定位pagepool的metadata然后标记bin pool中的相关slot或者更新continuous pool中的指针。在释放内存时遵循批处理原则等待足够多的连续内存页被free后才把这些页返回给munmap系统调用。具体等待多少内存页可以在编译前修改相关的最小数值。作者在估计了多方代价后选择了8作为等待的连续页数。
释放内存的过程中还有两个额外的功能:
1. 检测二次释放和无效释放
2. lazy free即不使用munmap调用来释放内存页而是使用madvise该函数会在系统内存不足时才释放内存。作者对采用两种系统调用的实现都进行了测试。具体比较在评价部分的内容中
### Reallocation under OTA {#reallocation-under-ota}
realloc 允许程序改变现有的已分配区块的区块大小,如果变小只需要收缩分配的大小并返回。如果更大则需要进行检查以查看是否有足够的内存可扩展。不足则要整体重新分配。
为了避免内存重用即符合OTA的原则realloc在处理内存分配时会更倾向于重新分配内存这就导致一个缺陷即如果程序大量调用realloc会导致严重的运行开销。
### Supporing Multi-threaded Applications {#supporing-multi-threaded-applications}
为了改进多线程的内存分配效率FFmalloc引入了thread caching技术。
## 具体分配过程解析 {#具体分配过程解析}
### FCmalloc {#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://images.gitee.com/uploads/images/2021/1116/193553_7629c96a_8810712.png)
针对FCmalloc的这些问题作者提出了一种解决方案即Batch Processing。当请求内存时调用mmap一次请求多页然后交由FCmalloc来分配请求的这些页直到所有内存页都被用完才再次请求。对于free同理等待连续多页要free时才调用munmap free全部页。取得的改进效果是非常突出的同样的gcc benchmark可以节省58.7%的munmap调用和65.8的VMA开销。
### FBmalloc {#fbmalloc}
FBmalloc的设计思想通常被称为BiBop allocator.其采用的方法与FCmalloc不同FBmalloc创建了许多bins每个bin中存储相同大小的object。对于小于4k的内存请求会在对应的bin中分配内存。对于大于4k的请求则直接调用mmap向系统申请页。每个bin都是一页大小用完一页申请一页。bin内为FCmalloc。采用FBmalloc可以减缓FCmalloc造成的内存浪费。特别小内存数据项往往具有更长的生命周期。
### FFmalloc {#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对比 {#与malloc对比}
通过上面的叙述FFmalloc的具体原理已经很清楚了。那么他与现有glibc中malloc的差别在哪里我们通过对两者内存分配流程解析来对比下FFmalloc的独特之处。
malloc同样使用了pool来管理内存最重要的就是bin内存池保存在bins这个长128的数组中每个元素都是一双向个链表。
![](https://images.gitee.com/uploads/images/2021/1117/191042_0fccb166_8810712.png)
- bins[0]目前没有使用
- bins[1]的链表称为unsorted_list用于维护free释放的chunk。
- bins[2,63)的区间称为small_bins用于维护512字节的内存块其中每个元素对应的链表中的chunk大小相同均为index\*8。
- bins[64,127)称为large_bins用于维护&gt;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. 如果分配内存&lt;512字节则通过内存大小定位到smallbins对应的index上(floor(size/8))
- 如果smallbins[index]为空进入步骤3
- 如果smallbins[index]非空直接返回第一个chunk
2. 如果分配内存&gt;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. 或者,内存&lt;128k使用brk内存&gt;128k使用mmap获取新内存
free释放内存时有两种情况
1. chunk和top chunk相邻则和top chunk合并
2. chunk和top chunk不相邻则直接插入到unsorted_list中
此外调用free函数时它将用户释放的内存块连接到空闲链上。到最后空闲链会被切成很多的小内存片段如果这时用户申请一个大的内存片段那么空闲链上可能没有可以满足用户要求的片段了。于是malloc函数请求延时并开始在空闲链上翻箱倒柜地检查各内存片段对它们进行整理将相邻的小空闲块合并成较大的内存块。
那么回到FFmallocFFmalloc内存分配使用的数据结构前面已经讲过了这里给出一个根据我们的理解整理的内存分配流程(在一个CPU核上)
1. 如果分配内存&lt;2K,通过内存大小定位到bin pool中对应的页上
- 如果页上已分配内存的末尾仍有空间则修改bitmap分配内存
- 如果页上分配内存末尾没有空间则申请新bin allocator 和pool分配内存
2. 如果分配内存&gt;2K &lt;4M可修改
- 如果continuous allocator拥有的pool有足够的空间则分配内存改变尾指针的值。
- 如果没有申请新pool
3. &gt;4M
创建特殊pool名为jumbo poolsize=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放在一起这样重用时只能分配一个同样大小的内存区域。