-
-
[原创]linux内核SLUB机制介绍
-
发表于: 2025-3-16 12:29 4595
-
本篇文章是学习内核堆利用时的视频笔记,视频源链接在最后
Slab分配器:是用来管理内核堆内存的基础设施
目前linux内核提供三种主流的实现:SLOB,SLAB,SLUB,这三种提供相同的接口供外部使用。其中SLUB是linux默认启用的,也可以在编译前通过修改编译配置文件,换成其他两种。
objects: slab可以分配出去小内存区域,也是管理的基本对象。
slabs:是保存objects的大内存区域,其上区域被切分成大小相同的内存区域称为object slots。这片内存是通过page_alloc分配的。
slot:是Slab分配器中预定义的 固定大小的内存块区。
(slot和objects其实指代的东西相同,因为它们在内存上是重叠的,但是只是在不同场合他们的称呼不一样。区分不开问题也不大,理解工作流程即可。)
与用户空间的堆一样,典型的动态内存bugs:
Out-of-bounds(OOB)越界读写
Use-after-free(UAF)
Double-free,invalid-free
攻击方式:
利用上述bug,可以达到overwrite和泄漏的目的。
因为free的object slot中存在元数据,我们可以通过覆盖链表的next指针,控制下一次的分配对象,获得任意地址读写,可以提权或者泄漏内核地址。堆上的内容也可能包含函数指针,我们可以控制它达成任意代码执行或者泄漏内核地址。具体的攻击措施还要看特定的漏洞详情。
下一个free slot的指针被保存在free slot的中间附近,这样可以防止小范围的溢出破坏指针
通过一个 CONFIG_SLAB_FREELIST_HARDENED=y
的编译配置选项,freelist指针会被加密保存。
ptr是freelist pointer的值,ptr_addr是freelist pointer被保存的地址,swab交换奇偶byte字节序。
所以要利用只能先泄漏 cache->random
和 =ptr_addr=,让利用更加困难。大多数现代 Slab 漏洞利用的是覆盖对象或者通过跨分配器攻击覆盖其他类型的内存。
通过 CONFIG_SLAB_FREELIST_RANDOM=y
配置,当分配新的 slab 时,SLUB 会打乱空闲列表中对象的顺序,这样让分配的地址更难预测。
struct kmem_cache
比较关键的几个成员变量:
name: 内核有许多不同的caches,可以通过 cat /proc/slabinfo
查看其中name就是第一列的名字,该name通过kmem_cache_create的参数指定
object_size: 也是通过kmem_cache_create的参数指定,每一个cache只可以分配固定大小的内存。
cpu_slab:
SLUB分配器为每个CPU核心分配独立的kmem_cache_cpu结构,保存系统内特定cpu绑定的slab信息,目的是避免多核并发访问时的锁竞争。每个核心通过自己的kmem_cache_cpu直接从本地缓存分配内存对象。其内的slabs是绑定到特定CPU上的slab。在6.8版本以前也被称为froze slabs,当CPU分配内存的时候,首先会从这些slabs中分配。
node:是为每个NUMA节点保存slab信息。NUMA的核心思想是把CPU分组,来简化资源的分配的复杂性。相当于拥有一个全局的slabs列表,尚未绑定到任何CPU,但是也仍然属于cache,也会包含已经分配的objects。
结构体详情:
per-CPU
对于 struct slab
的简化信息:
slab是一个 struct slab 的结构体,上述是简化的版本,struct slab 别名为struct page,提到这就不得不提一下历史了,在Linux内核5.17版本中,struct slab被引入,目的是将slab相关的字段从struct page中分离出来。struct page(每一个物理页面都有一个相应的page对应)之前包含了很多不同用途的字段,使用union来适应不同场景,导致结构复杂。现在struct slab作为struct page的一个overlay,共享同一块内存,但隐藏了struct page的细节,这样slab分配器只需要处理自己的结构。
slab_cache指向自己属于的cache。
每一个slab都有后备内存,后备内存是通过page_alloc想buddy system分配。不需要指针指向它,struct slab本身就是一个struct page
包含object slots,size是基于objects大小计算出来的。
freelist指针指向第一个slab中free的slot,下一个free slot的指针被保存在free slot中。freelist最后一个指针是NULL,objects都是从链表头分配,free也是插入链表头。
full slabs是指没有free slot的slab,此时它的freelist 指针是NULL。
多个slab可以用链表结构串联在一起。per-CPU的是单链表, struct slab
中的 next
指针,per-node的是双链表, struct slab
中的 list_head slab_list
。
active slab
先来看下kmem_cache_cpu的active slab,per-CPU的slabs的其中之一被设计成激活的,并把slab成员指针赋值为该slab。分配object的时候会首先从这个slab中分配。
active slab有两个freelists。 kmem_cache_cpu->freelist
和 kmem_cache_cpu->slab->freelist
都指向它的slots。但是两个链表并不相交,kmem_cache_cpu->freelist
用来给绑定的CPU分配释放内存的。
kmem_cache_cpu->slab->freelist
被用来给其他CPUs分配释放内存的(这个模块的代码有可能不只在一个cpu上运行,可能会在任务切换过程中跑到其他cpu上执行了)。
partial slabs
partial意思是这些slab有空闲slot(至少有一个,也有可能是fully free)。
每个partial slabs都有后备内存。
只有一个freelist,
只在active slab变为full后被使用。
per-CPU partial slabs的列表最大数量是有限的,这个大小是由kmem_cache->cpu_partial_slabs字段指定,这个值是根据object和slab的大小计算出来的link 用户空间是无法查看这个字段值的,只能查看 /sys/kernel/slab/$CACHE/cpu_partial
,然后自己计算出cpu_partial_slabs。
per-node
kmem_cache_node 有一个per-node partial slabs的列表。这就意味这每一个都至少有一个free slots。
每一个都有后备内存和一个freelist。
一旦per-CPU中的slabs都用完都变成full后他们就会被使用。
per-node slabs 的最小数量也是有限制的。由kmem_cache->min_partial指定, 计算也是基于object的大小link
可以在用户空间中查看 /sys/kernel/slab/$CACHE/min_partial
full slabs
full slabs 不会被tracked。没有指针指向full slabs(除非开启slub_debug),一旦任意一个object被释放到full slab中,分配器会获得指向该slab的指针。我们只需使用virt_to_slab计算。
为了方便介绍,这里分为五个不同层次的分配过程
当无锁的该cpu slab的freelist是不为空,那么就会分配该freelist的第一个object
如果为空,goto 2。
如果active slab freelist不是空的,
首先move active slab freelist到 lockless per-CPU freelist;link
然后从这个lockless的per-CPU freelist分配第一个object。link并更新这个freelistlink
如果这个active slab freelist为空。 goto 3link
如果有per-CPU的partial slabs:
首先将链表中的第一个脱链,并指定为active slabs link
goto 2link
如果per-CPU的partial slabs是空的
goto 4link
如果有per-node的partial slabs:
首先将链表中的第一个脱链,并指定为active slabslink;然后移动一些(最多cpu_partial_slabs / 2link)per-node的slabs到per-CPU的partial listlink;再去active slab重新分配。link
如果per-node partial list 为空,goto 5
allocate from new slab的过程:
首先从page_alloc中分配新的slab,并放进freelist中,并指定为active slab,然后从该slab中分配对象。
Out-of-bounds, case #1 (Shaping Slab memory)
攻击所需条件:
攻击过程:
allocate 足够的targt objects 来获取新的active slab;需要填充所有的holes达到分配过程的第五步。
所以我们就需要找到有多少个holes。
但是在非特权的目标系统上,没有方法能够找到确切的数目。 /proc/slabinfo
和相关文件对于普通用户不可读。
而且我们可能拥有的空闲插槽数量没有上限,原因是atcive slab上的holes数量最多有每一个slab上的objects的数目。
per-CPU partials的holes数量上限是每一个slab上的objects的数目 x cpu_partial_slabs。
per-node partials的没有限制slabs的数量。
所以一种方式是估计,首先重现目标环境,运行相同的版本内核,运行相同的软件,然后我们通过 cat /proc/slabinfo
看有多少个holes。
active_objs: 已经分配的objects的数量,
num_objs: 现存slab中的slots的总数。
这个值不是实时更新的,只有在一个slab被分配,释放或者移动到per-node partial list时才会更新。
Shrink cache 可以获得更准确的值,
echo 1 | sudo tee /sys/kernel/slab/kmalloc-32/shrink
但是这样会导致这个cache释放fully free slabs。
比如这个就少了1000多个,这个就是不准确的,即是我们复制来环境也不准确。
现在假设我们分配了足够的target objects并获得了一个新的active slab。并且新的active slab被target objects填充一部分;
现在通过IOCTL_ALLOC操作分配一个vulnerable object;
现在分配足够的target objects填满active slab。现在slab变成full,尽管可能会变成非active,但是没关系。
现在内存看起来是这样:
现在通过IOCTL_OOB触发越界访问。
(注:如果没有第一步,我们就无法破坏target,并且可能会破坏内核其他数据,后果不可控。所以第一步是为了explition的稳定。
除此之外这个exp也有一些问题,比如:
如果vuln被allocated到最后一个object,这就有概率会失败。解决的办法就是在其后多分配一个slab,然后填充target。
Migration: 进程被移动到另一个CPU上执行了。解决办法:绑定CPU的亲和性
Preempting: 另一个进程或者中断处理来抢占此CPU,解决方法:减少slab shaping的时间;使用less noisy(不那么频繁) 的cache。)
Out-of-bounds, case #2 (Shaping Slab memory)
需要条件:分配vulnerable objects并且立即写数据触发OOB(IOCTL_ALLOC_AND_OOB),
攻击过程:
分配足够多的target objects以获得新的 active slab;
分配一个vulnerable object并且触发OOB通过IOCTL_ALLOC_AND_OOB,
这有两种情况,
case #1: OOB访问的区域在free slot中,如果OOB的范围很小,没有覆盖元数据,则不会发生任何事情。可以重复进行OOB操作。
case #2: OOB访问的区域在target object中
Success!!! 但是也许需要很多次重试才能成功
case #1: object 属于active slab,
object加入无锁的per-CPU的freelist的头部。link
想象一种场景:
ptr1,ptr2,ptr3都指向同一个object。
所以这就引出第一种利用场景(UAF)
所需条件:假设我们有UAF的漏洞:
分配vulnerable object (IOCTL_ALLOC)
free vulnerable object (IOCTL_FREE)
在IOCTL_FREE后,读写vulnerable object的数据,(IOCTL_UAF)
攻击过程:
通过IOCTL_ALLOC分配一个vulnerable object,
通过IOCTL_FREE free vulnerable object,悬空引用仍然存在;
分配一个target object,现在那个悬空指针指向它;
现在能够使用IOCTL_UAF触发UAF访问。
case #2: object属于一个non-full slab
free object到所属的freelist之中。link
如果slab是per-node的,并且变成了fully free,并且node有足够的per-node slabs。该slab会被从per-node中移除并free回page allocator中。
如果object属于non-full non-current-active slab:freeobject 到slab freelist中可能会[free] (https://elixir.bootlin.com/linux/v6.6/source/mm/slub.c#L3687)per-node的full slab,但是不适用于per-CPU partial或者active slabs(即使变成full free也不会free回page_alloca,仍然待在相应列表中)
如果object属于另一个CPU的active slab,将会把它放到active slab的freelist(不是per-CPU的freelist)中link。
case #3: object 属于full slab
free object 到slab fresslist
move slab到per-CPU的partial list:
如果per-CPU的partial list没满(<cpu_partial_slabs),就把它放到链表头中。
如果per-CPU的partial list已经满了(>=cpu_partial_slabs),free_up per-CPU partial list遍历链表并执行执行以下操作
Move per-CPU slabs 到per-node list的尾部,
free full freed per-CPU slabs 到page_alloc中(可用于cross-cache的攻击)
直到per-node 的slabs的数量达到min_partial
现在per-CPU的partial list有空间了,将该slab放进链表头中
OOB变UAF
所需条件:1)分配vulnerable object(IOCTL_ALLOC)
2)可以越界向vulnerable object写数据。
攻击流程:slab已经经过我们的shaping成full slab,并且有一个OOB的vuln object。如果我们现在有一个Vuln的object可以OOB,我们把它在内存上挨着的下一个object视为target object,target object有引用计数之类的东西,通过溢出后就可以控制引用计数,原来的程序会在错误的时机free target object然后我们就可以将target object变成一个UAF。并且该slab会被添加到per-CPU的partial list的头部
(注:在shaping slab的时候,我们可以用slab spraying的方式:分配很多的objects,所以问题就是我们需要spray多少个object,这个数量需要根据实际情况来看。)
allocation和OOB组合在一起
所需条件:1) allocate vulnerable object并且立即写入OOB数据(IOCTL_ALLOC_AND_OOB)
攻击流程:
分配足够的target objects能获取新的active slab,
分配更多的target objects去填充这个slab,直到slab变成full,
从这个slab中free一个target object,
现在我们重新使用这个free slot,并且使用IOCTL_ALLOC_AND_OOB去溢出内存中挨着的下一个targe object。
double-free
CONFIG_SLAB_FREELIST_HARDENED=y
开启这个编译选项后,double-free会被检测到
slub机制是十分复杂的,并且其中还有很多的情况和优化需要考虑,本文只是浅浅涉猎一下。
__slab_alloc_nodeallocation 过程开始的地方
do_slab_freefree 过程开始的地方
Freeing free slot via double-free can be used for cross-cache attacks
More details about how SLUB works:Linux SLUB Allocator Internals and Debuggingnote
About cache merging, accounting, and hardened usercopy:Linux kernel heap feng shui in 2022
Introduction to cross-cache use-after-free attacks:CVE-2022-29582, An io_uring vulnerability
Improving reliability of Slab shaping:
Playing for K(H)eaps: Understanding and Improving Linux Kernel Exploit Reliability
PSPRAY: Timing Side-Channel based
Linux Kernel Heap Exploitation Technique
cache->offset = ALIGN_DOWN(cache->object_size / 2,
sizeof
(
void
*));
freeptr_addr = (unsigned
long
)object + cache->offset;
赞赏
- [原创]linux内核SLUB机制介绍 4596
- [原创]aliyunctf2025-beebee题目详解 5062
- [讨论]VMProtect编译 6440
- [翻译]Intel虚拟化技术简介 6312
- [原创]Arm指令介绍 18399