【bsauce读论文】PSPRAY-基于时序侧信道的Linux内核堆利用技术

2023/04/16 Paper 共 5105 字,约 15 分钟

【bsauce读论文】PSPRAY-基于时序侧信道的Linux内核堆利用技术

会议:USENIX Security’23

作者:来自 Seoul National University 的 Yoochan Lee、Byoungyoung Lee 等人。

主要内容:由于Linux内核的堆分配器SLUB开启的freelist随机化保护,所以堆相关的内核漏洞利用成功率较低(平均为56.1%)。本文提出PSPRAY,采用基于时序侧信道的利用技术来提高OOB/UAF/DF漏洞的利用成功率,成功率从56.1%提高到了97.92%。作者还提出了防护机制来缓解PSPRAY攻击,将成功率降低到和不使用PSPRAY的成功率一样,且带来的性能开销只有0.25%,内存开销只有0.52%,几乎可以忽略不计。

1. Introduction

现有的防护机制:KASLR [11], KCFI [8, 12], and KDFI [21, 34]

动机:如果利用失败会导致崩溃并被发现,为了隐藏攻击行为,需提高利用成功率。堆漏洞利用成功率无法保证的原因有以下两点

  • (1)攻击者无法在用户层感知内存分配的状态;
  • (2)内核内存分配具有随机性,难以预测。

例如,对于堆漏洞OOB/UAF/DF都要求漏洞对象和目标对象相邻或重叠,以往堆喷都是靠运气触发。

本文思路:通过时序侧信道来区分SLUB内部的分配路径,间接探知slab的分配状态(判断是否是从empty freelist分配到的内存),最后根据此信息来增大堆漏洞利用的稳定性(避免利用OOB时漏洞对象和目标对象分配不相邻,利用UAF时漏洞对象和目标对象分配不重叠)。

实验结果:采用10个CVE来测试,成功率从56.1%提高到了97.92%。 其中 83bec2 从13.70%提升到98.16%。

2. Background

2-1 SLUB分配器

Figure-1-architecture-SLUB

SLUB分配器架构

  • 内核中需要申请的对象相对比较固定,大部分情况下是一些编译期确定的常见的结构体。SLUB分配器根据对象的不同含有多个kmem_cache,如特定类型的task_struct、特定大小的kmalloc-系列(如kmalloc-96kmalloc-128kmalloc-sizeof(task_struct)kmalloc-sizeof(mm_struct)等);
  • 每个kmem_cache都使用per-CPU和per-node机制来管理slab,具体来说,每个CPU核都有单独的 freelist / page / partial;freelist是一个单链表,负责存储page上的空闲对象,分配对象时就从这个freelist上分配。
    • 每个 CPU 对应一个 cpu-freelistpage-freelistpartial-freelist。每个 page-freelist 有一个 slab,每个partial-freelist有一个或多个 slab。
    • 每个 node 对应一个 partial-freelist,包含一个或多个 slab。
  • 即使每个slab都有一个freelist,SLUB分配器还管理了一个单独的CPU-freelist来提高分配效率,所以访问某个slab中CPU的page-freelistpartial-freelist要比直接访问CPU-freelist要慢(在CPU-freelist为空时才访问page-freelist和partial-freelist);
  • 注意,CPU的page-freelist包含1个slab,而partial-freelist包含1个或多个slab。
  • 每个node也有1个partial,包含1个或多个slab(在CPU-freelist为空时才访问该node的partial-freelist)。

Figure-2-Slub_allocator

SLUB分配顺序:遵循一个原则,所有的对象都从CPU-freelist分配,如果CPU-freelist为空则从别的slab挪过来。所以有5条路径

  • Fast-path:首先在 CPU-freelist 里寻找内存,如果还有空闲内存则直接返回;
  • Medium-path #1:fast-path 未成功,说明 CPU-freelist 中已经没有空闲内存了,那么直接将该 CPU 的 page-freelist 挪到CPU-freelist,再从 CPU-freelist 中分配内存;
  • Medium-path #2:Medium-path #1 未成功,说明该 CPU 的 page-freelist 是空的,那么将该 CPU 的 partial-freelist 中一个 list 挪到 page-freelist,再挪到 CPU-freelist 后分配内存;
  • Medium-path #3:Medium-path #2 未成功,说明该 CPU 的 partial-freelist 是空的,那么从 node 的 partial-freelist 中拿一个作为 CPU 的 page-freelist,再挪到 CPU-freelist 后分配内存;
  • Slow-path:此前所有的尝试都没有成功,这时候就需要从 Buddy Allocator 中拿新的 page 出来,填充 SLUB分配器,再进行分配。这条路径显著慢于之前的路径。

2-2 SLUB freelist保护机制

Linux v4.8开始引入该机制,Ubuntu v16.04 / Debian v9设置为默认开启,在 fast-path 中增加随机性,将freelist中的空闲块顺序打乱,这样在利用OOB漏洞时就很难使漏洞对象和目标对象相邻。

3. 利用成功与失败分析

3-1 OOB漏洞

Figure-4-OOB-CVE-2017-7533

成功利用:如图Figure 4,先调用1000次open()分配目标对象,再调用1次sendmsg()分配漏洞对象,弱漏洞对象和目标对象相邻,即可通过溢出篡改目标对象。

Figure-5-OOB-failure

失败分析:由于有freelist随机化机制,攻击者不知道某slab中堆喷了多少个目标对象。Figure 5中列出了4种情况,若喷了0个目标对象,肯定失败;若喷了1个或2个目标对象,取决于freelist中堆块顺序;若喷射了3个目标对象,但漏洞对象在最后1个,则失败。

3-2 UAF/DF漏洞

Figure-6-UAF-CVE-2019-2215

成功利用-UAF:参见Figure 6,先调用epoll_ctl()分配1个漏洞对象和1个额外对象,再调用ioctl()释放漏洞对象,再调用msgsnd()分配目标对象占据漏洞对象,最后调用close()访问漏洞对象。

Figure-7-DF-CVE-2017-6074

成功利用-DF:参见Figure 7,在kmalloc-2048中,先分配1个漏洞对象和3个额外的对象,调用connect()释放漏洞对象,再调用msgsnd()分配 msg_msg 对象(victim对象),再调用 shutdown() 第2次释放漏洞对象,实际上释放了victim对象并留下了指向victim对象的悬垂指针,再调用msgsnd()分配 msg_msg 对象(目标对象),这样就能通过悬垂指针访问和篡改目标对象。

Figure-8-UAF-DF-failure

失败分析分配目标对象时CPU正好启用的新的freelist。触发漏洞的syscall分配漏洞对象时可能会分配多个额外对象,例如,CVE-2018-6555 分配漏洞对象时也分配了12个额外对象(都位于kmalloc-96),假设分配漏洞对象前CPU的page是半满的,分配完漏洞对象后,CPU的page 填满了,相应的slab被移动到full-list,CPU的page空了,再分配额外对象时,内核执行 medium-path #2 / #3 或 slow-path 来填充CPU的page。这样CPU的page变为另一个slab,额外对象从此处分配。接着释放漏洞对象后,分配目标对象是,可能不会复用漏洞对象,因为会从新的page上分配目标对象。

成功概率:三种情况下成功的概率分别为: \(P_{OOB}=\frac{N-1}{2N},\\ P_{UAF,DF}= \left\{ \begin{aligned} \frac{N-A+1}{N}, A < N,\\ \frac{1}{N}, A \le N, \end{aligned} \right.\)

其中$N$是一个 freelist 中对象的数量,$A$ 是一次 UAF/DF 攻击中伴随漏洞对象额外分配的对象数量(上面介绍中未直接提及,请参考原文)。

4. PSPRAY方法

5条路径分配时间:经过测试,5条分配路径的耗时分别是459 / 676 / 1191 / 1848 / 6048时钟周期,slow-path耗时最长,因为要用到buddy分配器,将新的slab取到CPU的 page-freelist,再取到 CPU-freelist

推断分配状态:目标是推断目标slab中分配了多少对象,方法是只要检测到执行了 slow-path 路径,则说明从新slab分配了1个对象。这个时候所有freelist都为空,分配状态清晰,容易进行堆喷利用。

确定堆喷syscall:通过该syscall喷射对象,来判断当前slab分配状态。需满足三个条件——(1)用户权限可用;(2)只分配1个对象;(3)除了分配对象外,其他操作的性能开销小。找到23个syscall,参见 Table A.1。

Table-A-1-syscall-one-object

测试区分度:采用 msgsnd() 分配1000次,3种cache(kmalloc-1024 / kmalloc-2048 / kmalloc-4096),结果参见 Figure 9。

  • fast-path 和 medium path 很难区分,因为 msgsnd() 不仅调用 kmalloc() 分配对象,还调用 copy_from_user() 拷贝数据;
  • fast-path 和 slow-path 辨识度很高(参见300-400次的分配区间,此区间的分配趋于稳定)。

Figure-9-performance-msgsnd()

OOB利用(PSPRAY):参见Figure 10,成功概率为 $P_{OOB}=\frac{N-1}{N}$。

  • (1)一旦观测到 slow-path,说明创建了一个新的 slab B,并且其中正好有一个对象;
  • (2)再分配N-1个对象把这个 slab B 填满;
  • (3)分配N-1个target对象和1个漏洞对象,都会从新的slab C中分配。

Figure-10-OOB-PSPRAY

UAF/DF利用(PSPRAY):参见Figure 10,理论成功率则是 $100\%$。。

  • (1)一旦观测到 slow-path,说明创建了一个新的 slab B,并且其中正好有一个对象;
  • (2)分配漏洞对象,会分配额外对象;
  • (3)释放漏洞对象;
  • (4)分配target对象,占据漏洞对象。

Figure-11-UAF-PSPRAY

5. 利用评估

内核版本:Linux v4.15。

5-1 合成漏洞

目的:测试PSPRAY是否对不同的 kmem_cache 有效(从kmalloc-64 到 kmalloc-4096)。

漏洞程序编写:主要实现 OOB read 和 UAF read,代码参见 Figure A.2 和 Figure A.3。其中UAF漏洞,通过调研,发现伴随漏洞对象会平均分配4个额外对象。

测试结果:分别测试不用和用PSPRAY时的理论成功率和实际成功率,参见 Table 1。对于OOB,理论成功概率为48%,实际最小为17.94%,因为系统本身有背景线程的噪声,使用PSPRAY能提高到94.61%;UAF从平均83.46%提高到100%;DF从平均83.67%提高到100%。

Table-1-exploitation-result-synthetic

5-2 真实漏洞

目的:测试PSPRAY是否对真实漏洞有效。

漏洞选取:7个CVE和3个从syzbot。

测试环境:为模拟真实系统,将系统分为两种运行状态:idle——背景线程少;busy——背景线程多(运行stress-ng)。

测试结果:参见Table 2。使用PSPRAY都能显著提高利用成功率。

  • 有些漏洞在系统 busy 时,只有不到 $10\%$ 的成功率,这些漏洞以往可能会被评估为相对没有那么危险的漏洞,但是在 PSPRAY 的辅助下,都达到了接近 $100\%$ 的成功率;
  • 对UAF/DF的提升更大,因为其时间窗口(分配漏洞对象和target对象之间)要小于OOB,这样受噪声影响更小。

Table-2-exploitation-result-real-world

6. 防护机制

思路:一是统一分配时间,但影响系统性能;二是给 slow-path 增加随机性,确保执行 slow-path 不表示 freelist为空。

实现:算法参见 Algorithm 1(只修改13行代码),原理参见 Figure 12。复用 freelist 下标随机化的机制,如果分配到下标为0(也可以指定其他下标)的空闲块,且CPU的 partial-freelist 为空,则走 slow-path,将新slab挪到 partial-freelist。不需要等 freelist 用光了再走 slow-path。防护效果参见 Table A.4。

Figure-12-snapshot-mitigation

参考

[论文分享] PSPRAY: Timing Side-Channel based Linux Kernel Heap Exploitation Technique

文档信息

Search

    Table of Contents