会议: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%,几乎可以忽略不计。
现有的防护机制:KASLR [11], KCFI [8, 12], and KDFI [21, 34]
动机:如果利用失败会导致崩溃并被发现,为了隐藏攻击行为,需提高利用成功率。堆漏洞利用成功率无法保证的原因有以下两点
例如,对于堆漏洞OOB/UAF/DF都要求漏洞对象和目标对象相邻或重叠,以往堆喷都是靠运气触发。
本文思路:通过时序侧信道来区分SLUB内部的分配路径,间接探知slab的分配状态(判断是否是从empty freelist分配到的内存),最后根据此信息来增大堆漏洞利用的稳定性(避免利用OOB时漏洞对象和目标对象分配不相邻,利用UAF时漏洞对象和目标对象分配不重叠)。
实验结果:采用10个CVE来测试,成功率从56.1%提高到了97.92%。 其中 83bec2 从13.70%提升到98.16%。
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-freelist
、page-freelist
和 partial-freelist
。每个 page-freelist
有一个 slab,每个partial-freelist
有一个或多个 slab。partial-freelist
,包含一个或多个 slab。page-freelist
和partial-freelist
要比直接访问CPU-freelist要慢(在CPU-freelist为空时才访问page-freelist和partial-freelist);page-freelist
包含1个slab,而partial-freelist
包含1个或多个slab。CPU-freelist
为空时才访问该node的partial-freelist
)。SLUB分配顺序:遵循一个原则,所有的对象都从CPU-freelist分配,如果CPU-freelist为空则从别的slab挪过来。所以有5条路径
page-freelist
挪到CPU-freelist,再从 CPU-freelist
中分配内存;page-freelist
是空的,那么将该 CPU 的 partial-freelist
中一个 list 挪到 page-freelist
,再挪到 CPU-freelist
后分配内存;partial-freelist
是空的,那么从 node 的 partial-freelist
中拿一个作为 CPU 的 page-freelist
,再挪到 CPU-freelist
后分配内存;Linux v4.8开始引入该机制,Ubuntu v16.04 / Debian v9设置为默认开启,在 fast-path 中增加随机性,将freelist中的空闲块顺序打乱,这样在利用OOB漏洞时就很难使漏洞对象和目标对象相邻。
成功利用:如图Figure 4,先调用1000次open()
分配目标对象,再调用1次sendmsg()
分配漏洞对象,弱漏洞对象和目标对象相邻,即可通过溢出篡改目标对象。
失败分析:由于有freelist随机化机制,攻击者不知道某slab中堆喷了多少个目标对象。Figure 5中列出了4种情况,若喷了0个目标对象,肯定失败;若喷了1个或2个目标对象,取决于freelist中堆块顺序;若喷射了3个目标对象,但漏洞对象在最后1个,则失败。
成功利用-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 攻击中伴随漏洞对象额外分配的对象数量(上面介绍中未直接提及,请参考原文)。
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。
msgsnd()
不仅调用 kmalloc()
分配对象,还调用 copy_from_user()
拷贝数据;OOB利用(PSPRAY):参见Figure 10,成功概率为 P O O B = N − 1 N P_{OOB}=\frac{N-1}{N} POOB=NN−1。
UAF/DF利用(PSPRAY):参见Figure 10,理论成功率则是 100 % 100\% 100%。。
内核版本:Linux v4.15。
目的:测试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%。
目的:测试PSPRAY是否对真实漏洞有效。
漏洞选取:7个CVE和3个从syzbot。
测试环境:为模拟真实系统,将系统分为两种运行状态:idle——背景线程少;busy——背景线程多(运行stress-ng)。
测试结果:参见Table 2。使用PSPRAY都能显著提高利用成功率。
思路:一是统一分配时间,但影响系统性能;二是给 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。