会议: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分配器
SLUB分配器架构:
- 内核中需要申请的对象相对比较固定,大部分情况下是一些编译期确定的常见的结构体。SLUB分配器根据对象的不同含有多个
kmem_cache
,如特定类型的task_struct
、特定大小的kmalloc-
系列(如kmalloc-96
、kmalloc-128
、kmalloc-sizeof(task_struct)
、kmalloc-sizeof(mm_struct)
等); - 每个
kmem_cache
都使用per-CPU和per-node机制来管理slab,具体来说,每个CPU核都有单独的 freelist / page / partial;freelist是一个单链表,负责存储page上的空闲对象,分配对象时就从这个freelist上分配。- 每个 CPU 对应一个
cpu-freelist
、page-freelist
和partial-freelist
。每个page-freelist
有一个 slab,每个partial-freelist
有一个或多个 slab。 - 每个 node 对应一个
partial-freelist
,包含一个或多个 slab。
- 每个 CPU 对应一个
- 即使每个slab都有一个freelist,SLUB分配器还管理了一个单独的CPU-freelist来提高分配效率,所以访问某个slab中CPU的
page-freelist
和partial-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
)。
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,先调用1000次open()
分配目标对象,再调用1次sendmsg()
分配漏洞对象,弱漏洞对象和目标对象相邻,即可通过溢出篡改目标对象。
失败分析:由于有freelist随机化机制,攻击者不知道某slab中堆喷了多少个目标对象。Figure 5中列出了4种情况,若喷了0个目标对象,肯定失败;若喷了1个或2个目标对象,取决于freelist中堆块顺序;若喷射了3个目标对象,但漏洞对象在最后1个,则失败。
3-2 UAF/DF漏洞
成功利用-UAF:参见Figure 6,先调用epoll_ctl()
分配1个漏洞对象和1个额外对象,再调用ioctl()
释放漏洞对象,再调用msgsnd()
分配目标对象占据漏洞对象,最后调用close()
访问漏洞对象。
成功利用-DF:参见Figure 7,在kmalloc-2048中,先分配1个漏洞对象和3个额外的对象,调用connect()
释放漏洞对象,再调用msgsnd()
分配 msg_msg
对象(victim对象),再调用 shutdown()
第2次释放漏洞对象,实际上释放了victim对象并留下了指向victim对象的悬垂指针,再调用msgsnd()
分配 msg_msg
对象(目标对象),这样就能通过悬垂指针访问和篡改目标对象。
失败分析:分配目标对象时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
O
O
B
=
N
−
1
2
N
,
P
U
A
F
,
D
F
=
{
N
−
A
+
1
N
,
A
<
N
,
1
N
,
A
≤
N
,
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.
POOB=2NN−1,PUAF,DF=⎩
⎨
⎧NN−A+1,A<N,N1,A≤N,
其中 N N N是一个 freelist 中对象的数量, A A 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。
测试区分度:采用 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次的分配区间,此区间的分配趋于稳定)。
OOB利用(PSPRAY):参见Figure 10,成功概率为 P O O B = N − 1 N P_{OOB}=\frac{N-1}{N} POOB=NN−1。
- (1)一旦观测到 slow-path,说明创建了一个新的 slab B,并且其中正好有一个对象;
- (2)再分配N-1个对象把这个 slab B 填满;
- (3)分配N-1个target对象和1个漏洞对象,都会从新的slab C中分配。
UAF/DF利用(PSPRAY):参见Figure 10,理论成功率则是 100 % 100\% 100%。。
- (1)一旦观测到 slow-path,说明创建了一个新的 slab B,并且其中正好有一个对象;
- (2)分配漏洞对象,会分配额外对象;
- (3)释放漏洞对象;
- (4)分配target对象,占据漏洞对象。
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%。
5-2 真实漏洞
目的:测试PSPRAY是否对真实漏洞有效。
漏洞选取:7个CVE和3个从syzbot。
测试环境:为模拟真实系统,将系统分为两种运行状态:idle——背景线程少;busy——背景线程多(运行stress-ng)。
测试结果:参见Table 2。使用PSPRAY都能显著提高利用成功率。
- 有些漏洞在系统 busy 时,只有不到 10 % 10\% 10% 的成功率,这些漏洞以往可能会被评估为相对没有那么危险的漏洞,但是在 PSPRAY 的辅助下,都达到了接近 100 % 100\% 100% 的成功率;
- 对UAF/DF的提升更大,因为其时间窗口(分配漏洞对象和target对象之间)要小于OOB,这样受噪声影响更小。
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。