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

IoT 4个月前 admin
76 0 0
CVE-2023-2598 为 io_uring API 组件中的一处 OOB 漏洞,可越界读写物理内存。本文[1] 对提权利用方式进行分析,从 io_uring 的工作原理、Linux 内存管理新特性——folio、sock 对象配合 call_usermodehelper_exec 的利用方法等方面,详细介绍这一强大的利用原语。

漏洞描述

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

从漏洞描述可知,此漏洞位于io_uring / rsrc.c的io_sqe_buffer_register函数中,可导致物理内存的越界读写。要分析此漏洞,首先要了解io_uring的基本工作原理。

一、io_uring的工作原理

1. io_uring是什么?

简单来说, io_uring 就是Linux的系统调用接口,于 2019 年在上游 Linux 内核版本 5.1 中首次引入,它使应用程序可以异步执行的系统调用。最初,io_uring仅支持简单的 I/O 系统调用,如 read() 和 write() ,但对更多系统调用的支持正在不断增长,而且速度很快,最终可能支持大多数系统调用。

2. 为什么要用它?

io_uring是一种异步IO,是为了减少原生AIO存在的阻塞和开销问题。举个栗子,在原生AIO执行read系统调用时,应用程序会等待内核执行完成read系统调用,才会执行后续的系统调用。而io_uring的好处就是可以批量提交系统调用,这些系统调用是异步的,不会阻塞系统调用。
原生AIO的拷贝字节时的开销,取决单次IO的字节大小。如果单次拷贝量大,那么拷贝开销可以忽略。但如果在大量的小IO的情况下,对于拷贝开销影响就比较大了。io_uring的另一个好处是,对于单次小的IO拷贝,减少系统调用内核上下文频繁切换所带来的性能开销。
尽管在大多数情况下,阻塞、上下文切换或复制字节时的开销可能并不明显,但在高性能应用程序中,就变得非常重要。比如对于与服务器/后端相关的应用程序特别有用,其中很大一部分应用程序时间都花在等待 I/O 上。因此io_uring是一个重要的优化。

3. 如何使用它?

最直接的方式就是使用io_uring系统调用:
int io_uring_setup(u32 entries, struct io_uring_params *p){
    return syscall(__NR_io_uring_setup, entries, p);
}

int io_uring_enter(int fd, uint32_t to_submit, uint32_t min_complete, uint32_t flags, sigset_t *sig){
    return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, flags, sig, _NSIG / 8);
}

int io_uring_register(int fd, unsigned int opcode, const void *arg, unsigned int nr_args){
    return syscall(__NR_io_uring_register, fd, opcode, arg, nr_args);
}
但由于io_uring非常复杂,所以直接使用系统调用是一项非常艰巨的工作。好在io_uring的首席开发人员编写了用户空间的库liburing,提供了简化的API来与内核组件进行交互。liburing会随着io_uring的变动而更新,但并没有做版本控制,因此要使用liburing,还需要检查当前内核是否支持这些功能。并且io_uring更新太快,liburing的文档和用例都已经过时了,还是需要自己查找使用方法。
这篇文章[2] 通过io_uring创建了一个“零系统调用”的服务器,可以作为参考。

4. 它是如何工作的?

io_uring包含两个环形缓冲区,分别是提交队列(Submission Queue,SQ)和完成队列(Completion Queue,CQ)。其中SQ环形缓冲区存放的是许多系统调用请求,每个系统调用请求的描述信息称为提交队列条目(Submission Queue Entries,SQE)。CQ存放的是已经完成的系统调用请求。SQ与CQ在用户态与内核态之间的共享内存中完成信息交换。

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

对于每个请求,都会填写一个 SQE 并将其放入SQ中。单个 SQE 描述了应该执行的系统调用操作。当应用程序进行io_uring_enter系统调用时,内核会收到SQ中有工作的通知。或者使用IORING_SETUP_SQPOLL来创建一个内核线程对SQ队列进行轮询,而无需重新执行io_uring_enter

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

当完成每个SQE时,内核首先会判断是否异步执行该操作,如果操作可以在不阻塞的情况下完成,则它将在调用线程的上下文中同步完成。否则,它被放入内核异步工作队列中,并由工作线程异步完成。在这两种情况下,调用线程都不会阻塞,区别在于操作是由调用线程立即完成还是稍后完成。

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

当操作完成时,每处理一个SQE,则会在CQ中都会放置一个完成队列条目CQE。应用程序可以轮询 CQ 以查找新的 CQE,从而得知相应的操作已经完成。SQE 可以按任何顺序完成,但如果需要特定的完成顺序,则可以将它们相互链接。

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

关于更多的io_uring细节内容,可参考Lord of the io_uring [3]

二、Linux内存管理新特性——folio

在了解了io_uring的工作原理之后,要对此漏洞进行利用,还需要了解在5.16版本内核引入的一个新的内存管理特性,folio。

1. 什么是复合页?

随着计算机内存的不断扩大,4G以上几乎成了标配,目前甚至有几十上百G的内存,而操作系统仍然使用4KB大小页面的基本单位显得有些滞后。
简单来说,复合页(compound page)就是将两个或更多的物理页面组合成一个单元。在许多方面可将其视为单个更大的页面。
举个例子,当采用4KB大小页面时,想象一下当应用程序分配2MB内存,并进行访问时,共有512个页面,操作系统会经历512次TLB miss和512次缺页中断后,才可以把这2M地址空间全部映射到物理内存上。然而如果使用2MB大小的复合页,那么只需要一次TLB miss和一次缺页中断。
__alloc_pages分配标志GFP FLAGS指定了__GFP_COMP,那么内核必须将这些页组合成复合页,第一个页称为head page,其余的所有页称为tail page,所有的tail pages都有指向head page的指针。

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

由于多个page组合成复合页,这些page之间会有关联,那么就带来了几个问题:
  • N个page是否组成了一个整体?
  • 这些page哪些是head?
  • 这些page哪些是tail?
  • 这些page一共有多少个?
在没有引入folio之前,内核由如下方法来解决上述问题:
  • 在由N个4KB组成的复合页的第0个page结构体上,安置一个PG_head标记,表示head page:

    page->flags |= (1UL << PG_head);
  • 在由N个4KB组成的复合页的第1~N-1的page结构体,即tail page的compound_head上的最后一位设置为1,表示tail page:

    page->compound_head |=  1UL;
  • compound_headPageTail函数取出head page和判断相关的page是否是tail page:

    #define compound_head(page) ((typeof(page))_compound_head(page))

    static inline unsigned long _compound_head(const struct page *page)
    {
     unsigned long head = READ_ONCE(page->compound_head);

     if (unlikely(head & 1))
      return head - 1;
     return (unsigned long)page;
    }

    static __always_inline int PageTail(struct page *page)
    {
     return READ_ONCE(page->compound_head) & 1;
    }
  • 通过compound_order函数获得复合页中的page个数:

    static inline unsigned int compound_order(struct page *page)
    {
     if (!PageHead(page))
      return 0;
     return page[1].compound_order;
    }

2. 什么是folio?

在folio出现之前,都使用page结构来处理数据,伴随着两个比较混乱的问题:
  1. 根据tail page的页描述符很容易找到复合页的head page,内核的很多函数利用这个特性,但是产生歧义:如果给函数传递一个tail page的页描述符的指针,那么这个函数应该操作这个tail page还是把复合页作为一个整体操作?
  2. 如果一个函数可能被传入一个tail page,但是它必须处理整个复合页,那么它必须调用内联函数compound_head()获取复合页的head page的页描述符的地址。在函数之间传递tail page,每个函数都要调用内联函数compound_head(),造成的后果是内核变大和运行速度变慢。
对于上述问题,比较容易想到的处理方式就是,我们难道不可以直接创建一些专用的函数,来处理复合页中的对应页吗?比如创建get_page、get_xxx、get_yyy等函数。实际上这种混乱的局面,很容易对程序员进行错误的引导,因为程序员写代码的时候,究竟在操作xxx,还是yyy,自己都拎不清了。
为了解决复合页产生的问题,Linux 5.16引入概念“folio”,folio表示0阶页或者一个复合页的首页。给函数传递一个folio,函数将会操作整个复合页,没有歧义。folio本质上可以看作是一个集合,是物理连续、虚拟连续的2^n次的PAGE_SIZE的一些bytes的集合,n可以是0,也就是说单个页也算是一个folio。folio把一些page里面常用字段,提取到了和page同等位置的union里面。folio结构与page结构并列可以更直观的看出它们之间的差异:

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

folio有一点是确定的,它必然不会是一个tail page,从而避免了前面的xxx、yyy的语义混乱。所以新的内核中有了两组不同的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);
复合页的改进实际上是一个非常繁重的工作,它涉及到大量的驱动代码与文件系统代码的更改。起初多数内核开发者认为这种更改会带来更复杂的问题,并且更改代码代价太大,是否真的有必要这样做。但Linus认为folio的优势也是明显的,能够更直白的处理复合页,避免一些混乱的问题,最终folio被采用。

三、漏洞分析

现在对漏洞进行分析,将io_uring与复合页联系起来。
通过NVD的漏洞描述,得知此漏洞位于io_sqe_buffer_register函数中,而次函数被__io_uring_register函数调用,__io_uring_register函数的调用者则是io_uring_register系统调用。
在io_uring中,可以通过io_uring_register系统调用和IORING_REGISTER_BUFFERS注册名为Fixed Buffers的内存空间,并锁定,专用于读写数据,这些内存空间不会被其他进程占用。6.3-rc1 中__io_uring_register源码如下,代码中当标志位为IORING_REGISTER_BUFFERS时,将会执行io_sqe_buffers_register 函数:

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

io_sqe_buffers_register函数会进行遍历,执行io_sqe_buffer_register函数,注册每一个buffer:

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

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

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

io_pin_pages的函数原型是:
struct page **io_pin_pages(unsigned long ubuf, unsigned long len, int *npages)
它是在/io_uring/rsrc.c中定义的。这个函数的作用是将用户空间的一段内存(由ubuf和len指定)锁定在物理内存中,并返回对应的物理页的指针数组。io_pin_pages的参数如下:
  • unsigned long ubuf:指定要锁定内存的起始用户虚拟地址。
  • unsigned long len:指定要锁定内存的长度,单位是字节。
  • int *npages:指定一个指针,用于返回锁定的物理页的个数。
io_pin_pages的返回值是一个指向物理页的指针数组,如果失败,返回NULL。
在这里,iov->iov_baseiov->iov_len都是结构体iovec中的成员,而iovec结构体保存来自用户态的指针和大小:
struct iovec
{

 void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
 __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
接下来执行了以下逻辑:

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

首先判断page数量是否大于1,即判断是否为复合页。然后使用page_folio宏定义,将page[0],也就是head page的page结构转换为folio结构。并遍历复合页,检查每一个page的head page是否与复合页相同,而漏洞点就在此处。
回顾folio的介绍,它表示在物理内存、虚拟内存都连续的page集合。这里代码判断nr_pages > 1,即当前的复合页数量大于1页,是由多个page组成的。而在for循环中的判断if (page_folio(page[i]) ≠ folio) ,只是判断了每一个page是否属于当前的复合页,并没有判断这些page是否相邻。这就导致一个问题:每次的page_folio的参数实际上都是同一个物理页,而内核则认为它是一片多个页组成的连续内存。
继续看代码逻辑:

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

这段代码中比较重要的是imu参数,imuio_mapped_ubuf类型的结构体,用于支持用户态缓冲区映射到I/O空间:
struct io_mapped_ubuf {
 u64  ubuf;
 u64  ubuf_end;
 unsigned int nr_bvecs;
 unsigned long acct_pages;
 struct bio_vec bvec[];
};
结构体中的bio_vec类似于iovec,但它用于物理内存。bio_vec定义了物理内存地址的连续范围:
struct bio_vec {
 struct page *bv_page;
 unsigned int bv_len;
 unsigned int bv_offset;
};
从上面代码看,bvec_set_page函数传入四个参数,第一个参数就是bio_vec结构体,第二个参数是物理页的head page,第三个参数实际上是从用户态传入的iov->iov_len,第四个参数是缓冲区的偏移量。bvec_set_page函数的功能很简单,就只是对bv进行了赋值而已:
static inline voidbvec_set_page(structbio_vec *bv, structpage *page,
  unsigned int len, unsigned int offset)

{
 bv->bv_page =page;
 bv->bv_len = len;
 bv->bv_offset = offset;
}
在函数中将imu结构体指针赋值给了pimupimu来自于io_sqe_buffer_register 的调用函数io_sqe_buffers_register ,即io_uring_register系统调用的操作。最终改变了来自注册时的ctx结构内容,后续的io_uring操作都会使用这个io_ring_ctx结构体:

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


四、漏洞利用

1. 利用原语

综合上述信息,现在想象一下:我们用io_uring_register注册一个跨越多个虚拟页的缓冲区,由于漏洞的存在,它只会重复映射一个相同的物理页。在虚拟内存中,它们是连续的,但在物理内存中并不是连续的,而当函数检查此物理页是否属于复合页时,检查又会通过,因为这个物理页确实是属于当前的复合页。内核认为连续的虚拟内存一定是一片连续的物理页,但实际上只是一次又一次的分配了同一个物理页,而它的size来自于用户态,是我们可控的:

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

也就是说,我们可以利用io_uring的其他功能,越界读写当前物理页之后的物理页,这是一个相当强大的利用原语。

2. 目标对象

由于漏洞可以越界读写许多页的内容,那么就可以不再考虑对象大小和分配的问题,也就是说我们利用的对象可以是任意大小的。sock是一个很好的对象,它包含了许多函数指针和内核地址,随意泄露一个都足以绕过KASLR。

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)));
在sock对象中有sk_pacing_ratesk_max_pacing_rate成员,这两个成员可以通过setsockoptSO_MAX_PACING_RATE操作进行设置。逻辑如下:

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

上述代码里,sk_pacing_ratesk_max_pacing_rate设置的值都来自于用户态传入的值,因此可以设置一些特殊标记,在查找sock对象的时候可以通过这两个标记来确定是否命中了sock对象。至于为什么需要同时设置这两个成员的值而不是一个,是因为通过实验发现,只判断一个成员有很大概率不是sock对象,同时设置两个可以提高判断的精确性。
另外,在命中了sock对象后,还需要知道这个socket的描述符。这也可以通过setsockoptSO_SNDBUF操作进行设置。逻辑如下:

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

SO_SNDBUF操作的val 值依然来自用户态,但这里需要满足一个条件,即val要大于宏定义SOCK_MIN_SNDBUF的值才会被写进sk_sndbuf 成员中。这个SOCK_MIN_SNDBUF宏定义展开后如下:
#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)
要满足val > SOCK_MIN_SNDBUF很简单,只需要将socket对象的描述符加上SOCK_MIN_SNDBUF 的值即可,在命中sock对象后,再将sk_sndbuf位置的值减去SOCK_MIN_SNDBUF就是socket对象的描述符。
在命中了sock对象后,可以泄露sock.__sk_common中的成员来泄露内核基址。__sk_common是一个sock_common结构体:
struct sock_common {
 union {
  __addrpair         skc_addrpair;         /*     0     8 */
  struct {
   __be32     skc_daddr;            /*     0     4 */
   __be32     skc_rcv_saddr;        /*     4     4 */
  };                                       /*     0     8 */
 };                                               /*     0     8 */
 union {
  unsigned int       skc_hash;             /*     8     4 */
  __u16              skc_u16hashes[2];     /*     8     4 */
 };                                               /*     8     4 */
 union {
  __portpair         skc_portpair;         /*    12     4 */
  struct {
   __be16     skc_dport;            /*    12     2 */
   __u16      skc_num;              /*    14     2 */
  };                                       /*    12     4 */
 };                                               /*    12     4 */
 short unsigned int         skc_family;           /*    16     2 */
 volatile unsigned char     skc_state;            /*    18     1 */
 unsigned char              skc_reuse:4;          /*    19: 0  1 */
 unsigned char              skc_reuseport:1;      /*    19: 4  1 */
 unsigned char              skc_ipv6only:1;       /*    19: 5  1 */
 unsigned char              skc_net_refcnt:1;     /*    19: 6  1 */

 /* XXX 1 bit hole, try to pack */

 int                        skc_bound_dev_if;     /*    20     4 */
 union {
  struct hlist_node  skc_bind_node;        /*    24    16 */
  struct hlist_node  skc_portaddr_node;    /*    24    16 */
 };                                               /*    24    16 */
 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 */
sock_common结构体有一个struct proto *skc_prot,这个proto对象中存在很多函数指针:
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 *, intint *, bool); /*    32     8 */
 int                        (*ioctl)(struct sock *, intlong 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 *, intintsockptr_tunsigned int); /*    72     8 */
 int                        (*getsockopt)(struct sock *, intintchar *, int *); /*    80     8 */
....
那么我们就可以泄露然后劫持其中一个函数指针,操作socket对象,来提升权限。

3. exploit

原exploit可以在这里[4] 找到。
在我的环境没有复现成功,原因有两点:
  1. qemu模拟给的内存不够,导致exp在执行mmap时内存不足,触发unable to handle page fault问题,导致kernel panic
  2. 如果将泄露的页数减少,或者减少mmap映射的内存,会导致很难命中sock对象。
综合这两点来看,这个漏洞利用成功率还是比较低的,下面分析原exploit的整体利用思路。
  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设置了两个egg:

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

  3. 在执行完setsockopt(sockets[i], SOL_SOCKET, SO_SNDBUF, &j, sizeof(int)后,可以看到sk_sndbuf被设置为了(sockets[i] + SOCK_MIN_SNDBUF)*2。即(4+4544)*2 = 0x2388:

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

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

  4. 通过同一物理页的连续地址映射,在io_uring操作之后,检测映射内存中是否命中了sock对象(从这一步开始我的复现失败,无法命中sock对象);

  5. 判断sk_pacing_ratesk_max_pacing_rate是否是egg标记。在确定命中sock对象后,通过sock对象计算距离函数指针的偏移,以此泄露sk_data_ready_off函数地址,从而得到kernel base与sock对象的地址;

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

  7. 在修改和伪造sock内容之前,先对sock数据进行备份,在之后将其还原,否则会导致kernel panic;

  8. 为了劫持socket对象的函数指针,需要伪造一个proto对象。为了不影响sock对象,选择将伪造的proto放置在sock对象之后。

  9. 劫持proto中的ioctlcall_usermodehelper_exec函数,这个函数可以在内核空间启动一个用户态进程。

  10. call_usermodehelper_exec需要两个参数,struct subprocess_info *sub_infoint wait ,ioctl函数指针是:(*ioctl)(struct sock *, int, long unsigned int); ,它的第一个参数始终指向sock对象,也就是说没办法直接调用ioctl去提权。此外,在proto+0x28位置为ioctl函数指针,我们需要覆盖这个函数指针完成劫持,但调用call_usermodehelper_exec函数时,其参数subprocess_info + 0x28位置是所要执行的用户态程序路径,刚好与ioctl函数指针重叠,这会破坏我们的利用。

  11. exploit中提到了一种方法,即利用work_struct,这个结构描述一个延迟工作的对象。subprocess_info.work.func成员是一个函数指针,延迟工作将会调用这个函数指针。

    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 */
    };
  12. 综合上面的信息,可以将subprocess_info.work.func函数指针改写为call_usermodehelper_exec_work函数,这个函数时负责生成我们的新进程的函数。然后将proto对象放置在subprocess_info.path位置,由于伪造的proto结构中我们只关心如何伪造ioctl指针,在ioctl之前的函数指针我们并不关心,那么就可以这些位置写为/bin/sh -c /bin/sh &>/dev/ttyS0 </dev/ttyS0 字符串的指针。

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

References

[1] https://anatomic.rip/cve-2023-2598/#folio

[2] https://web.archive.org/web/20221125154504/https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html
[3] https://web.archive.org/web/20221125154504/https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html
[4] https://web.archive.org/web/20221125154504/https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html
[5] https://chompie.rip/Blog+Posts/Put+an+io_uring+on+it+-+Exploiting+the+Linux+Kernel


原文始发于微信公众号(山石网科安全技术研究院):CVE-2023-2598 io_uring内核提权分析

版权声明:admin 发表于 2024年1月2日 上午10:52。
转载请注明:CVE-2023-2598 io_uring内核提权分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...