CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

admin 2024年5月30日22:30:11评论3 views字数 13420阅读44分44秒阅读模式

概述

本文讨论了Linux内核中io_uring的一个使用后释放漏洞,CVE-2024-0582。尽管该漏洞在2023年12月的稳定内核中已被修补,但它在Ubuntu内核中超过两个月没有被移植,使得Ubuntu在那段时间内容易受到0day攻击。

2024年1月初,Project Zero问题 对最近修复的io_uring使用后释放(UAF)漏洞(CVE-2024-0582)公开。很明显,这个漏洞允许攻击者获得对之前释放的多个页面的读写访问权限。这似乎是一个非常强大的原语:通常,UAF可以让您访问释放的内核对象,而不是整个页面——或者更好的是,多个页面。正如Project Zero问题所描述的,很明显这个漏洞应该很容易被利用:如果攻击者可以完全访问释放的页面,一旦这些页面返回到slab缓存以供重用,他们将能够修改这些页面内分配的任何对象的内容。在更常见的情况中,攻击者只能修改某种类型的对象,并且可能只能在某些偏移量或具有某些值时进行修改。

此外,这一事实还表明,应该可能进行仅数据的利用。一般来说,这种利用不依赖于修改代码执行流程,例如通过构建ROP链或使用类似技术。相反,它专注于修改某些数据,最终授予攻击者root权限,例如使攻击者能够将只读文件变为可写。这种方法使得利用更加可靠、稳定,并允许绕过一些利用缓解措施,如控制流完整性(CFI),因为内核执行的指令在任何方面都没有被改变。

最后,根据Project Zero问题,这个漏洞从6.4版本开始存在于Linux内核中,直到6.7版本。在那时,Ubuntu 23.10运行的是6.5的漏洞版本(Ubuntu 22.04 LTS稍后也是如此),所以这是一个很好的机会来利用补丁差距,了解攻击者这样做有多容易,以及他们可能拥有基于Nday的0day利用多长时间。

更具体地说:

  • 漏洞在稳定版本6.6.5中于2023年12月8日被修补。
  • Project Zero问题在一个月后的2024年1月8日公开。
  • 问题在Ubuntu内核6.5.0-21中被修补,该版本于2024年2月22日发布,适用于Ubuntu 22.04 LTS Jammy和Ubuntu 23.10 Mantic。

本文描述了我们实现的仅数据利用策略,允许非特权用户(无需无特权用户命名空间)在受影响的系统上获得root权限。首先,给出了io_uring接口的概述,以及与该漏洞相关的更具体的细节。接下来,提供了对漏洞的分析。最后,提出了一种仅数据利用的策略。

初步知识

io_uring接口是由Jens Axboe创建的Linux的异步I/O API,并在Linux内核版本5.1中引入。其目标是提高有大量I/O操作的应用程序的性能。它提供了类似于read()write()等函数的接口,但请求以异步方式满足,以避免阻塞系统调用引起的上下文切换开销。

io_uring接口一直是许多漏洞研究的丰富目标;它在ChromeOS、生产Google服务器中被禁用,并在Android中受到限制。因此,有许多博客文章详细解释了它。一些相关参考如下:

  • Put an io_uring on it – Exploiting the Linux Kernel,一篇针对io_uring操作的利用写入,该操作提供了与本文讨论的漏洞(IORING_REGISTER_PBUF_RING)相同的功能(IORING_OP_PROVIDE_BUFFERS),并且也提供了这个子系统的广泛概述。
  • CVE-2022-29582 An io_uring vulnerability,其中描述了跨缓存利用。虽然我们博客文章中描述的利用不是严格意义上的跨缓存,但两种利用策略之间存在一些相似之处。它还提供了与我们的利用策略相关的slab缓存和页面分配器的解释。
  • Escaping the Google kCTF Container with a Data-Only Exploit,其中描述了一种不同的io_uring漏洞的仅数据利用策略。
  • Conquering the memory through io_uring – Analysis of CVE-2023-2598,一篇漏洞写入,该漏洞产生了一个非常类似于我们的利用原语。在这种情况下,利用策略依赖于操纵与套接字相关联的结构,而不是操纵文件结构。

在接下来的小节中,我们提供了io_uring接口的概述。我们特别关注提供的缓冲环功能,这与本文讨论的漏洞相关。读者还可以查看“What is io_uring?”,以及上述参考资料,以获取这个子系统的替代概述。

io_uring 接口

io_uring 的基础是两组环形缓冲区,用于用户空间和内核空间之间的通信。这些是:

  • 提交队列(SQ),其中包含提交队列条目(SQEs),描述了一个I/O操作的请求,比如读写文件等。
  • 完成队列(CQ),其中包含完成队列条目(CQEs),它们对应于已经被处理和完成的SQEs。

这种模型允许使用单个系统调用来异步执行多个I/O请求,而在同步方式中,每个请求通常对应一个系统调用。这减少了阻塞系统调用引起的开销,从而提高了性能。此外,使用共享缓冲区也减少了开销,因为用户空间和内核空间之间不需要传输数据。

io_uring API由三个系统调用组成:

  • io_uring_setup()
  • io_uring_register()
  • io_uring_enter()

io_uring_setup() 系统调用

io_uring_setup() 系统调用为 io_uring 实例设置一个上下文,即一个提交队列和一个完成队列,每个队列都有指定数量的条目。其原型如下:

int io_uring_setup(u32 entries, struct io_uring_params *p);

它的参数是:

  • entries:它决定了 SQ 和 CQ 最少必须有多少个元素。
  • params:它可以用来由应用程序向内核传递选项,也可以由内核向应用程序传递有关环形缓冲区的信息。

如果成功,这个系统调用的返回值是一个文件描述符,稍后可以用来对 io_uring 实例执行操作。

io_uring_register() 系统调用

io_uring_register() 系统调用允许注册资源,如用户缓冲区、文件等,用于 io_uring 实例。注册这些资源使内核映射它们,避免了将来对用户空间的复制,从而提高了性能。其原型如下:

int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);

它的参数是:

  • fd:由 io_uring_setup() 系统调用返回的 io_uring 文件描述符。
  • opcode:要执行的特定操作。它可以有某些值,如 IORING_REGISTER_BUFFERS,用于注册用户缓冲区,或 IORING_UNREGISTER_BUFFERS,用于释放之前注册的缓冲区。
  • arg:传递给正在执行的操作的参数。它们的类型取决于传递的特定 opcode
  • nr_argsarg 中传递的参数数量。

如果成功,这个系统调用的返回值是零或正值,这取决于使用的 opcode

提供的缓冲环

应用程序可能需要为不同的I/O请求有不同的注册缓冲区。从内核版本5.7开始,为了便于管理这些不同的缓冲区集合,io_uring 允许应用程序注册一个缓冲区池,这些缓冲区由组ID标识。这是通过在 io_uring_register() 系统调用中使用 IORING_REGISTER_PBUF_RING 操作码来完成的。

更具体地说,应用程序首先分配它想要注册的一组缓冲区。然后,它使用操作码 IORING_REGISTER_PBUF_RING 进行 io_uring_register() 系统调用,指定一个组ID,这些缓冲区应该与之关联,缓冲区的起始地址,每个缓冲区的长度,缓冲区的数量,以及起始缓冲区ID。这可以针对多组缓冲区进行,每组都有一个不同的组ID。

最后,在提交请求时,应用程序可以使用 IOSQE_BUFFER_SELECT 标志并提供所需的组ID,以指示应该使用相应集合中提供的缓冲环。当操作完成后,用于操作的缓冲区ID通过相应的 CQE 传递给应用程序。

提供的缓冲环可以通过使用 IORING_UNREGISTER_PBUF_RING 操作码的 io_uring_register() 系统调用来注销。

用户映射的提供的缓冲环

除了由应用程序分配的缓冲区外,从内核版本6.4开始,io_uring 允许用户委托内核分配提供的缓冲环。这是通过将 IOU_PBUF_RING_MMAP 标志作为参数传递给 io_uring_register() 来完成的。在这种情况下,应用程序不需要预先分配这些缓冲区,因此不需要将缓冲区的起始地址传递给系统调用。然后,在 io_uring_register() 返回后,应用程序可以使用以下偏移量将缓冲区 mmap() 到用户空间:

IORING_OFF_PBUF_RING | (bgid >> IORING_OFF_PBUF_SHIFT)

其中 bgid 是相应的组ID。这些偏移量以及其他用于 mmap() io_uring 数据的偏移量在 include/uapi/linux/io_uring.h 中定义:

/*
 * Magic offsets for the application to mmap the data it needs
 */

#define IORING_OFF_SQ_RING         0ULL
#define IORING_OFF_CQ_RING         0x8000000ULL
#define IORING_OFF_SQES            0x10000000ULL
#define IORING_OFF_PBUF_RING       0x80000000ULL
#define IORING_OFF_PBUF_SHIFT      16
#define IORING_OFF_MMAP_MASK       0xf8000000ULL

处理此类 mmap() 调用的函数是 io_uring_mmap()

// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/io_uring.c#L3439
static __cold int io_uring_mmap(struct file *file, struct vm_area_struct *vma)
{
    size_t sz = vma->vm_end - vma->vm_start;
    unsigned long pfn;
    void *ptr;

    ptr = io_uring_validate_mmap_request(file, vma->vm_pgoff, sz);
    if (IS_ERR(ptr))
        return PTR_ERR(ptr);

    pfn = virt_to_phys(ptr) >> PAGE_SHIFT;
    return remap_pfn_range(vma, vma->vm_start, pfn, sz, vma->vm_page_prot);
}

注意 remap_pfn_range() 最终创建了一个映射,设置了 VM_PFNMAP 标志,这意味着 MM 子系统将基础页面视为原始页面帧号映射,没有关联的 page 结构。特别是,核心内核不会维护这些页面的引用计数,跟踪它是调用代码的责任(在这种情况下,是 io_uring 子系统)。

io_uring_enter() 系统调用

io_uring_enter() 系统调用用于使用 io_uring_setup() 系统调用先前设置的 SQ 和 CQ 启动和完成 I/O。其原型如下:

int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);

它的参数是:

  • fd:由 io_uring_setup() 系统调用返回的 io_uring 文件描述符。
  • to_submit:指定从 SQ 提交的 I/O 数量。
  • flags:位掩码值,允许指定某些选项,如 IORING_ENTER_GETEVENTSIORING_ENTER_SQ_WAKEUPIORING_ENTER_SQ_WAIT 等。
  • sig:指向信号掩码的指针。如果它不是 NULL,则系统调用用 sig 指向的掩码替换当前信号掩码,并在 CQ 中有事件可用时恢复原始信号掩码。

漏洞

当应用程序使用 IOU_PBUF_RING_MMAP 标志注册提供的缓冲环时,可以触发这个漏洞。在这种情况下,内核为提供的缓冲环分配内存,而不是由应用程序完成。为了访问这些缓冲区,应用程序必须 mmap() 它们以获得虚拟映射。如果应用程序稍后使用 IORING_UNREGISTER_PBUF_RING 操作码注销提供的缓冲环,内核将释放此内存并将其返回给页面分配器。然而,它没有任何机制来检查内存是否已经在用户空间中取消映射。如果没有这样做,应用程序就拥有一个指向已释放页面的有效内存映射,这些页面可以被内核重新分配用于其他目的。从这一点开始,对这些页面的读写将触发使用后释放。

以下代码块显示了与此漏洞相关的函数的受影响部分。代码片段由参考标记 [N] 标记。与此漏洞无关的行被 [Truncated] 标记替换。代码对应于 Linux 内核版本 6.5.3,该版本对应于 Ubuntu 内核 6.5.0-15-generic 使用的版本。

注册用户映射的提供的缓冲环

io_uring_register() 系统调用的 IORING_REGISTER_PBUF_RING 操作码的处理程序是 io_register_pbuf_ring() 函数,如下所示。

// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L537

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;

[1]

 if (copy_from_user(&reg, arg, sizeof(reg)))
  return -EFAULT;

[Truncated]

 if (!is_power_of_2(reg.ring_entries))
  return -EINVAL;

[2]

 /* cannot disambiguate full vs empty due to head/tail size */
 if (reg.ring_entries >= 65536)
  return -EINVAL;

 if (unlikely(reg.bgid io_bl)) {
  int ret = io_init_bl_list(ctx);
  if (ret)
   return ret;
 }

 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 {

[3]

  free_bl = bl = kzalloc(sizeof(*bl), GFP_KERNEL);
  if (!bl)
   return -ENOMEM;
 }

[4]

 if (!(reg.flags & IOU_PBUF_RING_MMAP))
  ret = io_pin_pbuf_ring(&reg, bl);
 else
  ret = io_alloc_pbuf_ring(&reg, bl);

[Truncated]

 return ret;
}

函数开始时将提供的参数复制到 io_uring_buf_reg 结构体 reg 中 [1]。然后,它检查所需的条目数是否为 2 的幂次方,并且严格小于 65536 [2]。注意,这意味着允许的最大条目数为 32768。

接下来,它检查是否已存在具有指定组 ID reg.bgid 的提供的缓冲列表,并在不存在的情况下分配 io_buffer_list 结构体,并将它的地址存储在变量 bl 中 [3]。最后,如果提供的参数设置了标志 IOU_PBUF_RING_MMAP,则调用 io_alloc_pbuf_ring() 函数 [4],传入包含系统调用传递的参数的结构体 reg 的地址,以及分配的缓冲列表结构体 bl 的指针。

// 来源:https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L519

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;

[5]

    ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);

[6]

    ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
    if (!ptr)
        return -ENOMEM;

[7]

    bl->buf_ring = ptr;
    bl->is_mapped = 1;
    bl->is_mmap = 1;
    return 0;
}

io_alloc_pbuf_ring() 函数根据 reg->ring_entries 指定的环条目数,通过乘以 io_uring_buf_ring 结构体的大小来计算结果大小 ring_size [5],该结构体大小为 16 字节。然后,它通过调用 __get_free_pages() 请求足够容纳此大小的页面数 [6]。请注意,对于允许的最大环条目数 32768,ring_size 为 524288,因此可以获得的最大 4096 字节页面数为 128。然后存储第一个页面的地址在 io_buffer_list 结构体中,更准确地说是 bl->buf_ring [7]。同时,将 bl->is_mappedbl->is_mmap 设置为 1。

注销提供的缓冲环

io_uring_register() 系统调用的 IORING_UNREGISTER_PBUF_RING 操作码的处理程序是 io_unregister_pbuf_ring() 函数,如下所示。

// Source: https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L601

int io_unregister_pbuf_ring(struct io_ring_ctx *ctx, void __user *arg)
{
 struct io_uring_buf_reg reg;
 struct io_buffer_list *bl;

[8]

    if (copy_from_user(&reg, arg, sizeof(reg)))
  return -EFAULT;
 if (reg.resv[0] || reg.resv[1] || reg.resv[2])
  return -EINVAL;
 if (reg.flags)
  return -EINVAL;

[9]

 bl = io_buffer_get_list(ctx, reg.bgid);
 if (!bl)
  return -ENOENT;
 if (!bl->is_mapped)
  return -EINVAL;

[10]

 __io_remove_buffers(ctx, bl, -1U);
 if (bl->bgid >= BGID_ARRAY) {
  xa_erase(&ctx->io_bl_xa, bl->bgid);
  kfree(bl);
 }
 return 0;
}

函数开始时将提供的参数复制到 io_uring_buf_reg 结构体 reg 中 [8]。然后,它检索与 reg.bgid 指定的组 ID 对应的提供的缓冲列表,并将它的地址存储在变量 bl 中 [9]。最后,它将 bl 传递给函数 __io_remove_buffers() [10]。

// 来源:https://elixir.bootlin.com/linux/v6.5.3/source/io_uring/kbuf.c#L209 

static int __io_remove_buffers(struct io_ring_ctx *ctx,
                               struct io_buffer_list *bl, unsigned nbufs)
{
    unsigned i = 0;

    /* 不应该发生 */
    if (!nbufs)
        return 0;

    if (bl->is_mapped) {
        i = bl->buf_ring->tail - bl->head;
        if (bl->is_mmap) {
            struct page *page;

[11]

            page = virt_to_head_page(bl->buf_ring);

[12]

            if (put_page_testzero(page))
                free_compound_page(page);
            bl->buf_ring = NULL;
            bl->is_mmap = 0;
        } else if (bl->buf_nr_pages) {

[Truncated]

如果缓冲列表结构体设置了 is_mappedis_mmap 标志,即当缓冲环使用 IOU_PBUF_RING_MMAP 标志注册时 [7],函数到达 [11]。然后,获取对应于缓冲环虚拟地址 bl->buf_ring 的头页面 pagepage 结构体。最后,在 [12],释放构成头 page 的复合页面的所有页面,从而将它们返回给页面分配器。

注意,如果提供的缓冲环使用 IOU_PBUF_RING_MMAP 设置,也就是说,它是由内核而不是应用程序分配的,那么用户空间应用程序应该事先 mmap() 了此内存。此外,请回想一下,由于内存映射是使用 VM_PFNMAP 标志创建的,所以在该操作期间 page 结构体的引用计数没有被修改。换句话说,在上述代码中,内核没有办法知道应用程序是否在通过调用 free_compound_page() 释放它之前取消了内存映射。如果没有这样做,应用程序可以通过简单地读取或写入此内存来触发使用后释放。

利用

本文中提出的利用机制依赖于 Linux 上内存分配的工作方式,因此预期读者对它有一定的熟悉度。作为复习,我们强调以下事实:

  • 页面分配器负责管理内存页面,这些页面通常为 4096 字节。它保持了 n 阶空闲页面的列表,即页面大小乘以 2^n 的内存块。这些页面是按先进先出的方式提供服务的。
  • Slab 分配器位于伙伴分配器之上,并保留常用对象的缓存(专用缓存)或固定大小对象(通用缓存),称为 Slab 缓存,可供内核分配。有几种 Slab 分配器的实现,但就本文的目的而言,只有 SLUB 分配器是相关的,这是现代内核版本中的默认分配器。
  • Slab 缓存由多个 Slab 组成,Slab 是一组一个或多个连续的内存页面。当 Slab 缓存用完了空闲 Slab 时,如果在同一时间段内分配了大量的相同类型或大小的对象并没有释放,操作系统会通过向页面分配器请求空闲页面来分配一个新的 Slab。

其中这样的缓存 Slab 是 filp,它包含 file 结构体。下一个列表显示了 file 结构体,它表示一个打开的文件。

// Source: https://elixir.bootlin.com/linux/v6.5.3/source/include/linux/fs.h#L961

struct file {
 union {
  struct llist_node f_llist;
  struct rcu_head  f_rcuhead;
  unsigned int   f_iocb_flags;
 };

 /*
  * Protects f_ep, f_flags.
  * Must not be taken from IRQ context.
  */
 spinlock_t  f_lock;
 fmode_t   f_mode;
 atomic_long_t  f_count;
 struct mutex  f_pos_lock;
 loff_t   f_pos;
 unsigned int  f_flags;
 struct fown_struct f_owner;
 const struct cred *f_cred;
 struct file_ra_state f_ra;
 struct path  f_path;
 struct inode  *f_inode; /* cached value */
 const struct file_operations *f_op;

 u64   f_version;
#ifdef CONFIG_SECURITY
 void   *f_security;
#endif
 /* needed for tty driver, and maybe others */
 void   *private_data;

#ifdef CONFIG_EPOLL
 /* Used by fs/eventpoll.c to link all the hooks to this file */
 struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
 struct address_space *f_mapping;
 errseq_t  f_wb_err;
 errseq_t  f_sb_err; /* for syncfs */
} __randomize_layout
  __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

对于这个漏洞利用,最相关的字段如下:

  • f_mode:确定文件是可读还是可写。
  • f_pos:确定当前的读取或写入位置。
  • f_op:与文件关联的操作。它决定了在文件上发出某些系统调用(如 read()write() 等)时执行的函数。对于 ext4 文件系统的文件,这等于 ext4_file_operations 变量。

数据仅利用策略

利用原语为攻击者提供了对已返回给页面分配器的一定数量的空闲页面的读写访问权限。通过多次打开文件,攻击者可以使 filp 缓存中的所有 slab 耗尽,以便向页面分配器请求空闲页面来在该缓存中创建一个新的 slab。在这种情况下,file 结构体的进一步分配将发生在攻击者具有读写访问权限的页面上,从而能够修改它们。特别是,例如,通过修改 f_mode 字段,攻击者可以使以只读权限打开的文件变为可写。

这种策略已成功利用以下版本的 Ubuntu:

  • Ubuntu 22.04 Jammy Jellyfish LTS,内核为 6.5.0-15-generic
  • Ubuntu 22.04 Jammy Jellyfish LTS,内核为 6.5.0-17-generic
  • Ubuntu 23.10 Mantic Minotaur,内核为 6.5.0-15-generic
  • Ubuntu 23.10 Mantic Minotaur,内核为 6.5.0-17-generic

接下来的小节将更详细地介绍如何执行此策略。

触发漏洞

策略始于触发漏洞以获得对已释放页面的读写访问权限。这可以通过执行以下步骤完成:

  • 执行 io_uring_setup() 系统调用以设置 io_uring 实例。
  • 使用操作码 IORING_REGISTER_PBUF_RINGIOU_PBUF_RING_MMAP 标志执行 io_uring_register() 系统调用,以便内核自己为提供的缓冲环分配内存。

CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

注册提供的缓冲环

  • 使用 io_uring 文件描述符和偏移量 IORING_OFF_PBUF_RING,通过 mmap() 将提供的缓冲环的内存映射为读写权限。

CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

映射缓冲环

  • 使用操作码 IORING_UNREGISTER_PBUF_RING 执行 io_uring_register() 系统调用来注销提供的缓冲环。

CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

注销缓冲环

此时,对应于提供的缓冲环的页面已返回给页面分配器,而攻击者仍然拥有对它们的有效引用。

喷洒文件结构体

下一步是生成大量子进程,每个进程多次以只读权限打开 /etc/passwd 文件。这会强制在内核中分配相应的 file 结构体。

CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

喷洒文件结构体

通过打开大量文件,攻击者可以迫使 filp 缓存中的 slab 耗尽。之后,将通过向页面分配器请求空闲页面来分配新的 slab。在某个时候,先前对应于提供的缓冲环的页面(攻击者仍然具有读写访问权限)将由页面分配器返回。

CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

从页面分配器请求空闲页面

因此,此后创建的所有 file 结构体将被分配在攻击者控制的内存区域中,使他们有可能修改这些结构体。

CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

在受控页面内分配文件结构体

请注意,这些子进程必须等待最后阶段的指示才能继续,以便保持文件打开状态,并且它们对应的结构体不会被释放。

在内存中定位文件结构体

虽然攻击者可能有权限访问 filp 缓存的一些 slab,但他们不知道这些 slab 在内存区域中的位置。然而,攻击者可以通过在 file 结构体的 file.f_op 字段的偏移量处搜索 ext4_file_operations 地址来识别这些 slab。一旦发现,可以安全地假设它对应于之前打开的 /etc/passwd 文件的一个实例的 file 结构体。

注意,即使启用了内核地址空间布局随机化 (KASLR),要确定内存中的 ext4_file_operations 地址,只需要知道这个符号相对于 _text 符号的偏移量,因此不需要绕过 KASLR。实际上,给定在相应偏移量处找到的无符号整数值 val,如果满足以下条件,可以安全地假设它是 ext4_file_operations 的地址:

  • (val >> 32 & 0xffffffff) == 0xffffffff,即最高 32 位全部为 1。
  • (val & 0xfffff) == (ext4_fops_offset & 0xfffff),即 valext4_fops_offsetext4_file_operations 相对于 _text 的偏移量)的最低 20 位相同。

更改文件权限并添加后门账户

一旦攻击者在他们可以访问的内存区域中定位到对应于 /etc/passwd 文件的 file 结构体,就可以随意修改它。特别是,设置找到的 file 结构体的 file.f_mode 字段中的 FMODE_WRITEFMODE_CAN_WRITE 标志,将使得使用相应的文件描述符时 /etc/passwd 文件变为可写。

此外,将找到的 file 结构体的 file.f_pos 字段设置为 /etc/passwd 文件的当前大小,攻击者可以确保任何写入它的数据都附加在文件的末尾。

最后,攻击者可以向第二阶段生成的所有子进程发出信号,尝试写入打开的 /etc/passwd 文件。尽管大多数这样的尝试都会失败,因为文件是以只读权限打开的,但由于修改了 file->f_mode 字段而启用了写入权限的那个进程将会成功。

结论

总结来说,在这篇文章中,我们描述了最近在 Linux 内核的 io_uring 子系统中披露的使用后释放漏洞,并提出了一种仅数据利用策略。这种策略证明相对容易实现。在我们的测试中,它证明非常可靠,即使失败,也不会影响系统的稳定性。这种策略让我们能够在大约两个月的补丁间隔窗口期间利用最新的 Ubuntu 版本。


原文始发于微信公众号(3072):CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月30日22:30:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2024-0582 Ubuntu 内核 UAF 漏洞分析https://cn-sec.com/archives/2793414.html

发表评论

匿名网友 填写信息