Linux slub 分配器上的安全加固学习

 

linux 内核默认使用slub分配器来做内存管理,在这篇文章里,我们首先简要交代了slub分配器内存分配的基本流程,然后对其上面的两种安全加固做了分析。

 

slub 分配器简述

slub 的实现具体可以参考这篇文章,这里我们简要说明一下。

slub 是针对内核的小内存分配,和用户态堆一开始会brk分一大块内存,然后再慢慢切割一样

伙伴系统给内存,然后slub分配器把内存切割成特定大小的块,后续的分配就可以用了。

具体来说,内核会预先定义一些kmem_cache 结构体,它保存着要如何分割使用内存页的信息,可以通过cat /proc/slabinfo 查看系统当前可用的kmem_cache

1

内核很多的结构体会频繁的申请和释放内存,用kmem_cache 来管理特定的结构体所需要申请的内存效率上就会比较高,也比较节省内存。默认会创建kmalloc-8k,kmalloc-4k,... ,kmalloc-16,kmalloc-8 这样的cache,这样内核调用kmalloc函数时就可以根据申请的内存大小找到对应的kmalloc-xx,然后在里面找可可用的内存块。
内核全局有一个 slab_caches 变量,它是一个链表,系统所有的 kmem_cache 都接在这个链表上。

slab 以页为基本单位切割,然后用单向链表(fd指针)串起来,类似用户态堆的 fastbin,每一个小块我们叫它object

kmem_cache 内部比较重要的结构如下:

kmem_cache
    - kmem_cache_cpu 
        - freelist
        - partial
    - kmem_cache_node
        - partial

因为现在的计算机大多是多个cpu的,kmem_cache_cpu相当于一个缓存,kmalloc的时候会现在这里找free的slab, 找不到再到kmem_cache_node 找。partial 保存着之前申请过的没用完的slab

内存分配与释放

slab 其实就类似一个fastbin, 所有的分配都会在kmem_cache_cpu 结构体的 freelist 上找。

刚开始什么都没有,伙伴系统会根据kmem_cache的配置信息给出一块内存,分配好后类似freelist ==> [x]->[x]->[x]->...->0 这样,后面每次分配就到freelist链表上找 ,它指向第一个可用的free object。

然后可能申请太多,free object 用完了,那就会再向伙伴系统要。

已经用满的slab不用去管它,等它里面有object被free之后,它就会被挂到kmem_cache_cpupartial链表上。

等下一次 freelist上的slab又用完了,就可以看看partial还有没有可用的,直接拿过来换上,就不用去麻烦伙伴系统了。

kmem_cache_cpu 上的partial 可能挂了很多未满的slab, 超过一个阈值的时候,就会把整个链表拿到kmem_cache_nodepartial 链表上,然后再有就又可以放了。

那可能又用多了,kmem_cache_cpu 上的 freelist 和 partial 的slab都用满了,这时就可以到 kmem_cache_node的partial 上拿。

object 的free 是 FIFO的,也就是都会接在freelist 链表头,free 的object 超过一个设定好的阈值时会触发内存回收。

okay, slub 分配器大概就是这样,我们接下来分析一下slub上的两个安全加固是什么样的。

 

环境配置

linux-5.4 版本

 

模块编写

为了方便调试,我们写个模块来帮助我们操作内核的 kmalloc 以及kfree, 可以随便kmalloc和kfree任意地址

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include<linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
//msleep


MODULE_LICENSE("Dual BSD/GPL");

#define ADD_ANY   0xbeef
#define DEL_ANY   0x2333


struct in_args{
    uint64_t addr;
    uint64_t size;
    char __user *buf;
};

uint64_t g_val = 0;

static long add_any(struct in_args *args){
    long ret = 0;
    char *buffer = kmalloc(args->size,GFP_KERNEL);
    if(buffer == NULL){
        return -ENOMEM;
    }
    if(copy_to_user(args->buf,(void *)&buffer,0x8)){
        return -EINVAL;
    }

    return ret;
}
static long del_any(struct in_args *args){
    long ret = 0;
    kfree((void *)args->addr);
    return ret;
}
static long kpwn_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
    long ret = -EINVAL;
    struct in_args in;
    if(copy_from_user(&in,(void *)arg,sizeof(in))){
        return ret;
    }
    switch(cmd){
        case DEL_ANY:
            ret = del_any(&in);
            break;
        case ADD_ANY:
            ret = add_any(&in);
            break;
        default:
            ret = -1;
    }
    return ret;
}
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open =      NULL,
    .release =   NULL,
    .read =      NULL,
    .write =     NULL,
    .unlocked_ioctl = kpwn_ioctl
};

static struct miscdevice misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = "kpwn",
    .fops = &fops
};

int kpwn_init(void)
{
    misc_register(&misc);
    return 0;
}

void kpwn_exit(void)
{
    printk(KERN_INFO "Goodbye hackern");
    misc_deregister(&misc);
}
module_init(kpwn_init);
module_exit(kpwn_exit);

然后写个交互脚本

#define _GNU_SOURCE
#include "exp.h"

#define ADD_ANY 0xbeef
#define DEL_ANY 0x2333

struct in_args{
    u64 addr;
    u64 size;
    char *buf;
};

void init(){
    save_status();
    setbuf(stdin,0);
    setbuf(stdout,0);
    signal(SIGSEGV, sh);
}
int main(int argc,char **argv){
    init();
    char *buf=malloc(0x1000);
    u64 *buf64 =(u64 *)buf;
    struct in_args *p = malloc(sizeof(struct in_args));

    int op=0;
    u32 addsize=1024;

    int fd = open("/dev/kpwn",O_RDWR);
    logx("fd",fd);
    u64 freeaddr=0;

    while(1){
        printf("1.mallocn");
        printf("2.freen");
        printf("3.exitn");
        printf(">> ");
        scanf("%d",&op);
        switch(op){
            case 1:
                p->addr=0;
                p->size=addsize;
                p->buf=buf;
                ioctl(fd,ADD_ANY,p);
                loglx("kmalloc: ",buf64[0]);
                break;
            case 2:
                printf("free addr: ");
                scanf("%lx",&freeaddr);
                loglx("read ",freeaddr);
                p->addr=freeaddr;
                ioctl(fd,DEL_ANY,p);
                break;
            case 3:
                exit(0);
                break;
        }

    }


    close(fd);

    logs("wtf","aaaaa");

    return 0;
}

 

CONFIG_SLAB_FREELIST_HARDENED 配置下

okay,我们先看第一个,需要在内核的.config 文件中添加CONFIG_SLAB_FREELIST_HARDENED=y 编译选项

在这个配置下, kmem_cache 增加了一个unsigned long类型的变量random.

#ifdef CONFIG_SLAB_FREELIST_HARDENED
    unsigned long random;
#endif

mm/slub.c 文件, kmem_cache_open的时候给random字段一个随机数

static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
    s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);
#ifdef CONFIG_SLAB_FREELIST_HARDENED
    s->random = get_random_long();
#endif

set_freepointer 函数中加了一个BUG_ON的检查,这里是检查double free的,当前free 的object 的内存地址和 freelist 指向的第一个object 的地址不能一样,这和glibc类似。

static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
    unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
    BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

    *(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}

接着是freelist_ptr, 它会返回当前object 的下一个 free object 的地址, 加上hardened 之后会和之前初始化的random值做异或。

static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
                 unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED

    return (void *)((unsigned long)ptr ^ s->random ^
            (unsigned long)kasan_reset_tag((void *)ptr_addr));
#else
    return ptr;
#endif
}

我们实际调试看看,我们只用一个CPU, 然后kmalloc的大小是 1k,启动exp之后的内存状态如下

2

random 的值为 0xed74254a6ccbe301 free object 上保存不再是下一个free object 的地址,而是一个看起来乱七八糟的数字,参考前面的(void *)((unsigned long)ptr ^ s->random ^(unsigned long)kasan_reset_tag((void *)ptr_addr))

加上hardened 之后

下一个free object的地址 = random ^ 当前free object的地址 ^ 当前free object 原本fd处的值

计算一下hex(0xed74254a6ccbe301^0xed74254a6ccbef01^0xffff88800d7ce400)

可以得到0xffff88800d7ce800 也就是下一个free object 的地址。

也就是说CONFIG_SLAB_FREELIST_HARDENED 就是加了个给 fd 指针异或加了个密,这样如果有溢出就读不到内存地址,要溢出覆盖因为不知道random的值也很难继续利用。

我们继续看另外一个安全加固

 

CONFIG_SLAB_FREELIST_RANDOM 配置下

同样,这里需要改.config文件,加上CONFIG_SLAB_FREELIST_RANDOM=y, 简单期间,我们加上CONFIG_SLAB_FREELIST_HARDENED=n

这个配置下,kmem_cache 会添加一个 unsigned int 类型的数组

#ifdef CONFIG_SLAB_FREELIST_RANDOM
    unsigned int *random_seq;
#endif

具体代码实现在mm/slab_common.c 以及mm/slab.c里,首先是初始化

static int init_cache_random_seq(struct kmem_cache *s)
{
    unsigned int count = oo_objects(s->oo);
    int err;
...
    if (s->random_seq)
        return 0;

    err = cache_random_seq_create(s, count, GFP_KERNEL);
...
    if (s->random_seq) {
        unsigned int i;

        for (i = 0; i < count; i++)
            s->random_seq[i] *= s->size;
    }
    return 0;
}

/* Initialize each random sequence freelist per cache */
static void __init init_freelist_randomization(void)
{
    struct kmem_cache *s;

    mutex_lock(&slab_mutex);

    list_for_each_entry(s, &slab_caches, list)// 对每个kmem_cache
        init_cache_random_seq(s);

    mutex_unlock(&slab_mutex);
}

init_cache_random_seq函数先找出当前kmem_cache一个slab 里会有多少object(oo&0xffff) , cache_random_seq_create会根据object的数量给random_seq 数组分配内存,初始化为random_seq[index]=index, 然后把顺序打乱再乘object的大小

/* Create a random sequence per cache */
int cache_random_seq_create(struct kmem_cache *cachep, unsigned int count,
                    gfp_t gfp)
{
    struct rnd_state state;

    if (count < 2 || cachep->random_seq)
        return 0;

    cachep->random_seq = kcalloc(count, sizeof(unsigned int), gfp);
    if (!cachep->random_seq)
        return -ENOMEM;

    /* Get best entropy at this stage of boot */
    prandom_seed_state(&state, get_random_long());

    freelist_randomize(&state, cachep->random_seq, count);
}
static void freelist_randomize(struct rnd_state *state, unsigned int *list,
                   unsigned int count)
{
    unsigned int rand;
    unsigned int i;

    for (i = 0; i < count; i++)
        list[i] = i;

    /* Fisher-Yates shuffle */
    for (i = count - 1; i > 0; i--) {
        rand = prandom_u32_state(state);
        rand %= (i + 1);
        swap(list[i], list[rand]);
    }
}

然后在每次申请新的slab 的时候,会调用shuffle_freelist 函数,根据random_seq 来把 freelist 链表的顺序打乱,这样内存申请的object 后,下一个可以申请的object的地址也就变的不可预测。

    cur = next_freelist_entry(s, page, &pos, start, page_limit,
                freelist_count);
    cur = setup_object(s, page, cur);
    page->freelist = cur;

    for (idx = 1; idx < page->objects; idx++) {
        next = next_freelist_entry(s, page, &pos, start, page_limit,
            freelist_count);
        next = setup_object(s, page, next);
        set_freepointer(s, cur, next);
        cur = next;
    }
    set_freepointer(s, cur, NULL);

同样的,我们调试一下看看实际的运行效果, 程序运行 后的slab状态如下,7个free object 以及一个在partial 链表上

3

不断调kmalloc 把 free object 消耗完, 再次kmalloc 就会重新分配一个 slab

4

可以看到我们kmalloc得到的是0xffff88800d7df800 这个地址, 接着下一个是0xffff88800d7dfc00, 然后就变成了0xffff88800d7de000,并不是连续的,仔细看我们还可以发现其实0xffff88800d7df800-0x18000xffff88800d7dfc00-0x1c00结果是一样的, 和random_seq 的取值关联上了。

5

 

小结

我们主要分析了Linux slub分配器上的两种安全加固,默认情况下这两个机制都会开启。虽然两个机制都不是很复杂,但是加上之后,内核slab溢出等内存相关的漏洞利用难度就会加大很多,对系统的安全防护还是有很大作用的。

 

reference

https://my.oschina.net/fileoptions/blog/1630346

(完)