首页
社区
课程
招聘
3
[原创]linux内核SLUB机制介绍
发表于: 2025-3-16 12:29 4595

[原创]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->freelistkmem_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机制是十分复杂的,并且其中还有很多的情况和优化需要考虑,本文只是浅浅涉猎一下。

SLUB source

__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

SLUBStick: Arbitrary Memory Writes through
Practical Software Cross-Cache Attacks within the Linux Kernel

基于时间信道的攻击方式

SLUB演讲视频连接

PPT

cache->offset = ALIGN_DOWN(cache->object_size / 2, sizeof(void *));
freeptr_addr = (unsigned long)object + cache->offset;

[注意]看雪招聘,专注安全领域的专业人才平台!

收藏
免费 3
支持
分享
赞赏记录
参与人
雪币
留言
时间
病毒小子
+1
感谢你分享这么好的资源!
2025-3-20 16:07
jmpcall
感谢你分享这么好的资源!
2025-3-18 10:09
pureGavin
+10
谢谢你的细致分析,受益匪浅!
2025-3-17 09:11
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册