Linux内核SLUB机制介绍

admin 2025年3月20日00:46:26评论17 views字数 9710阅读32分22秒阅读模式

本篇文章是学习内核堆利用时的视频笔记,视频源链接在最后。

01

基本概念

Slab分配器:是用来管理内核堆内存的基础设施

目前linux内核提供三种主流的实现:SLOB,SLAB,SLUB,这三种提供相同的接口供外部使用。其中SLUB是linux默认启用的,也可以在编译前通过修改编译配置文件,换成其他两种。

objects:slab可以分配出去小内存区域,也是管理的基本对象。

slabs:是保存objects的大内存区域,其上区域被切分成大小相同的内存区域称为object slots。这片内存是通过page_alloc分配的。

slot:是Slab分配器中预定义的 固定大小的内存块区。

(slot和objects其实指代的东西相同,因为它们在内存上是重叠的,但是只是在不同场合他们的称呼不一样。区分不开问题也不大,理解工作流程即可。)

02

Slab bugs

与用户空间的堆一样,典型的动态内存bugs:

◆Out-of-bounds(OOB)越界读写

◆Use-after-free(UAF)

◆Double-free,invalid-free

攻击方式:

利用上述bug,可以达到overwrite和泄漏的目的。

因为free的object slot中存在元数据,我们可以通过覆盖链表的next指针,控制下一次的分配对象,获得任意地址读写,可以提权或者泄漏内核地址。堆上的内容也可能包含函数指针,我们可以控制它达成任意代码执行或者泄漏内核地址。具体的攻击措施还要看特定的漏洞详情。

03

内核堆上的防护措施

下一个free slot的指针被保存在free slot的中间附近,这样可以防止小范围的溢出破坏指针

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

通过一个CONFIG_SLAB_FREELIST_HARDENED=y的编译配置选项,freelist指针会被加密保存。

cache->random = get_random_long();

freelist_ptr = (void *)((unsigned long)ptr ^ cache->random ^ swab(ptr_addr));
// ptr — actual value of freelist pointer
// ptr_addr — location where freelist pointer is stored
// swab() — exchanges adjacent even and odd bytes

ptr是freelist pointer的值,ptr_addr是freelist pointer被保存的地址,swab交换奇偶byte字节序。

所以要利用只能先泄漏cache->random和 =ptr_addr=,让利用更加困难。大多数现代 Slab 漏洞利用的是覆盖对象或者通过跨分配器攻击覆盖其他类型的内存。

通过CONFIG_SLAB_FREELIST_RANDOM=y配置,当分配新的 slab 时,SLUB 会打乱空闲列表中对象的顺序,这样让分配的地址更难预测。

04

slab关键数据结构

1.struct kmem_cache

struct kmem_cache {    // Per-CPU cache data:    struct kmem_cache_cpu __percpu *cpu_slab;    // Per-node cache data:    struct kmem_cache_node *node[MAX_NUMNODES];    ...    const char *name; // Cache name    slab_flags_t flags; // Cache flags    unsigned int object_size; // Size of objects    unsigned int offset; // Freelist pointer offset    unsigned long min_partial;    unsigned int cpu_partial_slabs;};

比较关键的几个成员变量:

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。

结构体详情:

struct kmem_cache_cpu {    struct slab *slab;    // Active slab    struct slab *partial; // Partial slabs    ...};struct kmem_cache_node {    struct list_head partial; // Slabs    ...};

2.per-CPU

对于 struct slab 的简化信息:

  struct slab {  // Aliased with struct page      struct kmem_cache *slab_cache; // Cache this slab belongs to      struct slab *next; // Next slab in per-cpu list      int slabs; // Slabs left in per-cpu list      struct list_head slab_list; // List links in per-node list      void *freelist; // Per-slab freelist      ...};

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 。

3.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上执行了)。

4.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。

5.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

6.full slabs

full slabs 不会被tracked。没有指针指向full slabs(除非开启slub_debug),一旦任意一个object被释放到full slab中,分配器会获得指向该slab的指针。我们只需使用virt_to_slab计算。

05

分配过程

为了方便介绍,这里分为五个不同层次的分配过程:

1、allocating from lockless per-CPU freelist kmem_cache_cpu->freelist

当无锁的该cpu slab的freelist是不为空,那么就会分配该freelist的第一个object

如果为空,goto 2。

2、allocating from active slab (kmem_cache_cpu->slab->freelist)

如果active slab freelist不是空的,

首先move active slab freelist到 lockless per-CPU freelist;link

然后从这个lockless的per-CPU freelist分配第一个object。link并更新这个freelistlink

如果这个active slab freelist为空。goto 3link

3、allocating from per-CPU partial slabs (kmem_cache_cpu->partial)

如果有per-CPU的partial slabs:

首先将链表中的第一个脱链,并指定为active slabs link

goto 2link

如果per-CPU的partial slabs是空的

goto 4link

4、allocating from per-node partial slabs (kmem_cache_node->partial)

如果有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

5、Create new slab

allocate from new slab的过程:

首先从page_alloc中分配新的slab,并放进freelist中,并指定为active slab,然后从该slab中分配对象。

06

explotion case

1.Out-of-bounds, case #1 (Shaping Slab memory)

攻击所需条件:

  1. 需要一个内核bug能导致OOB;

  2. 有两个不同的系统调用,一个可以分配object(IOCTL_ALLOC),一个可以OOB(IOCTL_OOB);

  3. 能够leak或者overwrite的目标object;

  4. 能将可利用的object和targetobject挨着放在一起。

攻击过程:

  1. 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。

Linux内核SLUB机制介绍

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。

Linux内核SLUB机制介绍

比如这个就少了1000多个,这个就是不准确的,即是我们复制来环境也不准确。

1.现在假设我们分配了足够的target objects并获得了一个新的active slab。并且新的active slab被target objects填充一部分;

2.现在通过IOCTL_ALLOC操作分配一个vulnerable object;
现在分配足够的target objects填满active slab。现在slab变成full,尽管可能会变成非active,但是没关系。

3.现在通过IOCTL_OOB触发越界访问。

+-------+-------+-------+-------+-------+-------+-------+-------+
| Target| Target| Target| Vuln | Target| Target| Target| Target|
+-------+-------+-------+-------+-------+-------+-------+-------+
|_______| OOB

(注:如果没有第一步,我们就无法破坏target,并且可能会破坏内核其他数据,后果不可控。所以第一步是为了explition的稳定。

除此之外这个exp也有一些问题,比如:
如果vuln被allocated到最后一个object,这就有概率会失败。解决的办法就是在其后多分配一个slab,然后填充target。

Migration: 进程被移动到另一个CPU上执行了。解决办法:绑定CPU的亲和性
Preempting: 另一个进程或者中断处理来抢占此CPU,解决方法:减少slab shaping的时间;使用less noisy(不那么频繁) 的cache。)

1.Out-of-bounds, case #2 (Shaping Slab memory)

需要条件:分配vulnerable objects并且立即写数据触发OOB(IOCTL_ALLOC_AND_OOB),

攻击过程:

  1. 分配足够多的target objects以获得新的 active slab;

  2. 分配一个vulnerable object并且触发OOB通过IOCTL_ALLOC_AND_OOB,

    这有两种情况,
    case #1: OOB访问的区域在free slot中,如果OOB的范围很小,没有覆盖元数据,则不会发生任何事情。可以重复进行OOB操作

+-------+-------+-------+-------+-------+-------+-------+-------+| Target|       | Target| Vuln  |       | Target| Target| Target|+-------+-------+-------+-------+-------+-------+-------+-------+                            |_______| OOB

case #2: OOB访问的区域在target object中

Success!!!但是也许需要很多次重试才能成功

+-------+-------+-------+-------+-------+-------+-------+-------+
| Target| | Target| Vuln | Target| Target| Target| Target|
+-------+-------+-------+-------+-------+-------+-------+-------+
|_______| OOB

07

Freeing process and explition

1.case #1: object 属于active slab,

object加入无锁的per-CPU的freelist的头部。link

想象一种场景:

void *ptr1 = kmalloc(128, GFP_KERNEL);free(ptr1);void *ptr2 = kmalloc(128, GFP_KERNEL);free(ptr2);void *ptr3 = kmalloc(128, GFP_KERNEL);

ptr1,ptr2,ptr3都指向同一个object。

所以这就引出第一种利用场景(UAF)

所需条件:假设我们有UAF的漏洞:

  1. 分配vulnerable object (IOCTL_ALLOC)

  2. free vulnerable object (IOCTL_FREE)

  3. 在IOCTL_FREE后,读写vulnerable object的数据,(IOCTL_UAF)

攻击过程:

  1. 通过IOCTL_ALLOC分配一个vulnerable object,

  2. 通过IOCTL_FREE free vulnerable object,悬空引用仍然存在;

  3. 分配一个target object,现在那个悬空指针指向它;

  4. 现在能够使用IOCTL_UAF触发UAF访问。

2.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。

3.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放进链表头中

08

explition case

1.OOB变UAF

  1. 所需条件: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,这个数量需要根据实际情况来看。

  1. allocation和OOB组合在一起

    所需条件:1) allocate vulnerable object并且立即写入OOB数据(IOCTL_ALLOC_AND_OOB)

    攻击流程:

    1. 分配足够的target objects能获取新的active slab,

    2. 分配更多的target objects去填充这个slab,直到slab变成full,

    3. 从这个slab中free一个target object,

    4. 现在我们重新使用这个free slot,并且使用IOCTL_ALLOC_AND_OOB去溢出内存中挨着的下一个targe object。

  2. double-free

    CONFIG_SLAB_FREELIST_HARDENED=y 开启这个编译选项后,double-free会被检测到

总结

slub机制是十分复杂的,并且其中还有很多的情况和优化需要考虑,本文只是浅浅涉猎一下。

SLUB source

__slab_alloc_nodeallocation 过程开始的地方

do_slab_freefree 过程开始的地方

Linux内核SLUB机制介绍

相关链接请点击【阅读原文】进入原帖查看

Linux内核SLUB机制介绍

看雪ID:dig_grave

https://bbs.kanxue.com/user-home-851021.htm

*本文为看雪论坛优秀文章,由 dig_grave 原创,转载请注明来自看雪社区

原文始发于微信公众号(看雪学苑):Linux内核SLUB机制介绍

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月20日00:46:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux内核SLUB机制介绍https://cn-sec.com/archives/3859402.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息