本篇文章是学习内核堆利用时的视频笔记,视频源链接在最后。
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的中间附近,这样可以防止小范围的溢出破坏指针
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是freelist pointer的值,ptr_addr是freelist pointer被保存的地址,swab交换奇偶byte字节序。
所以要利用只能先泄漏
cache->random
和 =ptr_addr=,让利用更加困难。大多数现代 Slab 漏洞利用的是覆盖对象或者通过跨分配器攻击覆盖其他类型的内存。
通过
CONFIG_SLAB_FREELIST_RANDOM=y
配置,当分配新的 slab 时,SLUB 会打乱空闲列表中对象的顺序,这样让分配的地址更难预测。
1.struct kmem_cache
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
struct kmem_cache_node *node[MAX_NUMNODES];
...
const char *name;
slab_flags_t flags;
unsigned int object_size;
unsigned int 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;
struct slab *partial;
...
};
struct kmem_cache_node {
struct list_head partial;
...
};
2.per-CPU
对于
struct slab
的简化信息:
struct slab {
struct kmem_cache *slab_cache;
struct slab *next;
int slabs;
struct list_head slab_list;
void *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
计算。
为了方便介绍,这里分为五个不同层次的分配过程:
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中分配对象。