Pwn2Own 大赛赐我灵感,让我发现仨Oracle VirtualBox 漏洞,其中俩提权

724969efd5bfb8c1d8b761f794b6b733.png 聚焦源代码安全,网罗国内外最新资讯!

作者:MAX VAN AMERONGEN

编译:代码卫士

摘要

97233c6ce35e59bab31c465ae7ab0e43.png

本文作者说明了自己在为2021年 Pwn2Own 大赛准备期间,如何意外发现三个 Oracle VirtualBox 0day。奇安信代码卫士团队翻译如下,希望给大家带来一些启发。

0668ef16a6e2b4afd7a1e1b80c8abe51.png

引言

对我来说,Pwn2Own 大赛就像圣诞节。比赛让人兴奋不已,大家四处搜寻着最常用也是难度最高的软件中的严重漏洞。今年3月份为温哥华大赛准备期间,我决定不写浏览器fuzzer了,打算换个方向:VirtualBox。

虚拟化是极其有趣的目标,它的复杂度尤其高,同时涉及模仿硬件设备以及安全地将数据传递到真正的硬件中。就像箴言说的那样:有复杂性存在的地方,就有 bug。

对于 Pwn2Own 大赛而言,针对被模拟组件发起冲击是一种安全的做法。我认为网络硬件模拟像是正确的也常见的路径。我从一个默认组件起步:/src/VBox/Devices/Network/DrvNAT.cpp 中的NAT 模拟代码。

当时我只是想找找代码感觉,于是只是用鼠标滚动着翻看了文件并阅读了多个部分,并没有采取特定的方法。在这个过程中,下面代码引起了我的注意。

static DECLCALLBACK(void) drvNATSendWorker(PDRVNAT pThis, PPDMSCATTERGATHER pSgBuf)
{
#if 0 /* Assertion happens often to me after resuming a VM -- no time to investigate this now. */
   Assert(pThis->enmLinkState == PDMNETWORKLINKSTATE_UP);
#endif
   if (pThis->enmLinkState == PDMNETWORKLINKSTATE_UP)
   {
       struct mbuf *m = (struct mbuf *)pSgBuf->pvAllocator;
       if (m)
       {
           /*
            * A normal frame.
            */
           pSgBuf->pvAllocator = NULL;
           slirp_input(pThis->pNATState, m, pSgBuf->cbUsed);
       }
       else
       {
           /*
            * GSO frame, need to segment it.
            */
           /** @todo Make the NAT engine grok large frames?  Could be more efficient... */
#if 0 /* this is for testing PDMNetGsoCarveSegmentQD. */
           uint8_t         abHdrScratch[256];
#endif
           uint8_t const  *pbFrame = (uint8_t const *)pSgBuf->aSegs[0].pvSeg;
           PCPDMNETWORKGSO pGso    = (PCPDMNETWORKGSO)pSgBuf->pvUser;
           uint32_t const  cSegs   = PDMNetGsoCalcSegmentCount(pGso, pSgBuf->cbUsed);  Assert(cSegs > 1);
           for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++)
           {
               size_t cbSeg;
               void  *pvSeg;
               m = slirp_ext_m_get(pThis->pNATState, pGso->cbHdrsTotal + pGso->cbMaxSeg, &pvSeg, &cbSeg);
               if (!m)
                   break;
 
#if 1
               uint32_t cbPayload, cbHdrs;
               uint32_t offPayload = PDMNetGsoCarveSegment(pGso, pbFrame, pSgBuf->cbUsed,
                                                           iSeg, cSegs, (uint8_t *)pvSeg, &cbHdrs, &cbPayload);
               memcpy((uint8_t *)pvSeg + cbHdrs, pbFrame + offPayload, cbPayload);
 
               slirp_input(pThis->pNATState, m, cbPayload + cbHdrs);
#else

用于从guest 向网络发送数据包的函数中包含着Generic Segmentation Offload (GSO) 框架的单独代码路径,而且使用memcpy 连接数据。

当然,下一个问题就是“我能控制多少?”,我查看了多种代码路径并为所有限制因素编写了简单的 Python 约束求解器后,当我使用半虚拟化网络 ( Paravirtualization Network) 设备 VirtIO 时,得到了这个问题的答案,那就是“比我预想的还要多”。

e1609930247db1d862ace52b55f50f86.png

半虚拟化网络

除了完整模拟一台设备外,还有一个选择就是使用半虚拟化。全虚拟化是指 guest 完全未意识到自己是一个guest,而半虚拟化则是guest 安装驱动意识到自己在guest 机器中运行,以便更快速更高效地与主机一起传输数据。

VirtIO 是一个接口,可用于开发半虚拟化的驱动。Virtio-net 就是这样一种驱动,它的来源是 Linux 且用于网络。VirtualBox 和其它虚拟化软件一样,将这类驱动当作网络适配器:

01735ea7e99ddd365c83025f683f4aaf.png

像 e1000那样,VirtIO 网络使用环形缓冲区在guest和host之间传输数据(本案例中是 Virtqueues 或 VQueues)。然而,和e1000不同的地方在于,VirtIO 传输时使用的不是缓存器头尾相连的单个环形,而是使用了三个不同的数组:

  • 一个Descriptor数组,每个装饰器都包含如下数据:

    Ø  Address:所传输数据的物理地址

    Ø  Length:地址处数据的长度

    Ø  Flags:决定 Nex 字段处于使用状态以及缓冲区是否读或写的标记

    Ø  Next:当存在链时使用

  • 一个 Available 环形:该数组中包含正在使用的且可由主机读取的Descriptor 数组的索引

  •  一个Used 环形:Descriptor 数组的已由主机读取的索引数组

如下所示:

161cfa27ad0ca7498eadf2b7397a2ec3.png

当guest 希望向网络发送数据包时,它会在装饰器表格中增加一个条目,将该装饰器的索引增加到 Available 环形,之后增加 Available Index 指针:

5ee4601434827921aa029d4d67b23b40.png

之后,guest 将 VQueue 索引写入 Queue Notify 存储器“踢出”主机。这使得主机开始处理 Available 环形中的装饰器。当装饰器被处理后,它就会被添加到 Used 环形,而 Used Index 增加:

5c7c8ad657f24bd2a6c294f1ea14531b.png

ef8e3223dd5f092d4dee4335111bbbb1.png

通用分段卸载 (GSO)

先来介绍一下通用分段卸载 (Generic Segmentation Offload) 的背景,要理解GSO,需要理解它为网卡解决的问题。

最初CPU 在计算传输层校验和时会处理所有负担,或者将它们分段为更小的以太网数据包大小。这个流程在处理很多出站网络流量时可能非常缓慢,因此硬件制造商开始卸载这些操作,以消除操作系统的压力。

而非那段意味着操作系统不必通过网络栈传递更小的数据包,而只需一次传递一个数据包。这种优化同样适用于除了TCP和UDP以外的协议,只有在网络驱动接收报文时才需要通过延迟分段提供硬件支持,这就是创建GSO的契机。

由于 VirtIO 是一种半虚拟化设备,因此驱动意识到它位于guest机器中,因此GSO可适用于guest和host 之间。GSO通过在网络缓冲区开头添加上下文装饰器标头的方式在 VirtIo 中执行。如下 struct 说明了该标头:

struct VNetHdr
{
   uint8_t  u8Flags;
   uint8_t  u8GSOType;
   uint16_t u16HdrLen;
   uint16_t u16GSOSize;
   uint16_t u16CSumStart;
   uint16_t u16CSumOffset;
};

VirtIO 标头可视作与 e1000 中 Context Descriptor 类似的概念。当接收到标头时,vnetR3ReadHeader 会验证参数的合法性。之后,函数 vnetR3SetupGsoCtx 用于填充所有网络设备中 VirtualBox 使用的标准 GSO 结构:

typedef struct PDMNETWORKGSO
{
   /** The type of segmentation offloading we're performing (PDMNETWORKGSOTYPE). */
   uint8_t             u8Type;
   /** The total header size. */
   uint8_t             cbHdrsTotal;
   /** The max segment size (MSS) to apply. */
   uint16_t            cbMaxSeg;
 
   /** Offset of the first header (IPv4 / IPv6).  0 if not not needed. */
   uint8_t             offHdr1;
   /** Offset of the second header (TCP / UDP).  0 if not not needed. */
   uint8_t             offHdr2;
   /** The header size used for segmentation (equal to offHdr2 in UFO). */
   uint8_t             cbHdrsSeg;
   /** Unused. */
   uint8_t             u8Unused;
} PDMNETWORKGSO;

完成上述构建后,VirtIO 代码创建 scatter-gatherer 从多种装饰器中汇编该框架:

/* Assemble a complete frame. */
               for (unsigned int i = 1; i < elem.cOut && uSize > 0; i++)
               {
                   unsigned int cbSegment = RT_MIN(uSize, elem.aSegsOut[i].cb);
                   PDMDevHlpPhysRead(pDevIns, elem.aSegsOut[i].addr,
                    
                                     ((uint8_t*)pSgBuf->aSegs[0].pvSeg) + uOffset,
                                     cbSegment);
                   uOffset += cbSegment;
                   uSize -= cbSegment;
               }

该框架连同新的GSO结构被传递给NAT代码,进而引起我的注意。

a32fc8c09f17237d16a6530166d53c0f.png

漏洞分析

CVE-2021-2145:Oracle VirtualBox NAT 整数下溢提权漏洞

当NAT代码接收到GSO框架时,它获得了完整的以太网数据包并将其传递给 Slirp (TCP/IP 模拟库)作为mbuf 报文。为此,VirtualBox 分配了一个新的 mbuf 报文并将数据包拷贝到该报文中。该分配函数占一个大小,之后从三个不同的存储桶中挑选了下一个最大的分配大小:

1、MCLBYTES(0x800 字节)

2、MJUM9BYTES(0x2400 字节)

3、MJUM16BYTES(0x4000 字节)

struct mbuf *slirp_ext_m_get(PNATState pData, size_t cbMin, void **ppvBuf, size_t *pcbBuf)
{
   struct mbuf *m;
   int size = MCLBYTES;
   LogFlowFunc(("ENTER: cbMin:%d, ppvBuf:%p, pcbBuf:%p\n", cbMin, ppvBuf, pcbBuf));
 
   if (cbMin < MCLBYTES)
       size = MCLBYTES;
   else if (cbMin < MJUM9BYTES)
       size = MJUM9BYTES;
   else if (cbMin < MJUM16BYTES)
       size = MJUM16BYTES;
   else
       AssertMsgFailed(("Unsupported size"));
 
   m = m_getjcl(pData, M_NOWAIT, MT_HEADER, M_PKTHDR, size);
...

如所提供的大小大于 MJUM16BYTES,则会触发断言。遗憾的是,只有在使用 RT_STRICT 宏时该断言才会被编译,在发布build 中并非如此。这意味着触及该断言时执行将继续,从而导致分配选择的存储桶大小为0x800。由于实际大小更大,因此当用户数据拷贝到mbuf 报文时出现堆溢出问题。

/** @def AssertMsgFailed
* An assertion failed print a message and a hit breakpoint.
*
* @param   a   printf argument list (in parenthesis).
*/
#ifdef RT_STRICT
# define AssertMsgFailed(a)  \
   do { \
       RTAssertMsg1Weak((const char *)0, __LINE__, __FILE__, RT_GCC_EXTENSION __PRETTY_FUNCTION__); \
       RTAssertMsg2Weak a; \
       RTAssertPanic(); \
   } while (0)
#else
# define AssertMsgFailed(a)     do { } while (0)
#endif

CVE-2021-2310:Oracle VirtualBox NAT 堆缓冲区溢出提权漏洞

代码中存在一个名为 “PDMNetGsoIsValid” 的函数,用于验证guest 提供的 GSO 参数是否合法。然而,无论何时使用该函数,它都位于断言中。如:

DECLINLINE(uint32_t) PDMNetGsoCalcSegmentCount(PCPDMNETWORKGSO pGso, size_t cbFrame)
{
   size_t cbPayload;
   Assert(PDMNetGsoIsValid(pGso, sizeof(*pGso), cbFrame));
   cbPayload = cbFrame - pGso->cbHdrsSeg;
   return (uint32_t)((cbPayload + pGso->cbMaxSeg - 1) / pGso->cbMaxSeg);
}

如上所述,在发布build中并不会编译这类断言,从而允许出现不合法的GSO参数;slirp_ext_m_get的大小计算可能出错,使其小于for循环中 memcpy 复制的总大小。在我发布的PoC 中,cbMin 的 pGso->cbHdrsTotal + pGso->cbMaxSeg计算中所用的参数导致分配了0x4000字节,但cbPayload 的计算导致 memcpy 调用了0x4056大小,从而溢出了所分配的区域。

CVE-2021-2442:Oracle VirtualBox NAT UDP 标头界外溢出

这篇文章的标题中仅提到了“VirtualBox Network Offloads”,看似GSO是唯一易受影响的卸载机制;然而,另外一个卸载机制也易受攻击:校验和卸载(Checksum Offload)。

校验和卸载可应用于报文标头中存在校验和的多种协议中。在模拟时,TCP和UDP 协议中VirtualBox 均支持这一点。

为了访问该特性,GSO 框架需要设置 u8Flags 成员的第一个比特,说明要求校验和卸载。在 VirtualBox 案例中,必须设置该比特,因为它无法在不执行校验和卸载的情况下处理GSO。当 VirtualBox 以GSO处理UDP数据包时,在某些情况下会终于函数 PDMNetGsoCarveSegmentQD。

case PDMNETWORKGSOTYPE_IPV4_UDP:
           if (iSeg == 0)
               pdmNetGsoUpdateUdpHdrUfo(RTNetIPv4PseudoChecksum((PRTNETIPV4)&pbFrame[pGso->offHdr1]),
                                        pbSegHdrs, pbFrame, pGso->offHdr2);

函数 pdmNetGsoUpdateUdpHdrUfo 使用 offHdr2 提示 UDP 标头在数据包结构中的未知。最终它将引向名为 “RTNetUDPChecksum” 的函数中:

RTDECL(uint16_t) RTNetUDPChecksum(uint32_t u32Sum, PCRTNETUDP pUdpHdr)
{
   bool fOdd;
   u32Sum = rtNetIPv4AddUDPChecksum(pUdpHdr, u32Sum);
   fOdd = false;
   u32Sum = rtNetIPv4AddDataChecksum(pUdpHdr + 1, RT_BE2H_U16(pUdpHdr->uh_ulen) - sizeof(*pUdpHdr), u32Sum, &fOdd);
   return rtNetIPv4FinalizeChecksum(u32Sum);
}

而这正是存在漏洞的地方。在这个函数中,uh_ulen 属性完全是受信任的且无需任何内证,这就导致缓冲区的边界溢出或者因减去sizeof(*pUdpHdr) 而导致的整数下溢问题。

rtNetIPv4AddDataChecksum 同时接收大小值和数据包标头指针并继续计算校验和:

/* iterate the data. */
   while (cbData > 1)
   {
       u32Sum += *pw;
       pw++;
       cbData -= 2;
   }

从利用角度看,将大量界外数据相加可能没有值得关注的点。然而,如果攻击者能够为连续的 UDP 数据包重新分配同样的堆位置,UDP大小参数一次增加两个字节,则可能计算出每个校验和中的差异并泄露界外数据。

同时,攻击者很可能利用该漏洞引起针对网络中其它虚拟机的拒绝服务攻击。

b2c6ca91ef033a78e99c6d8383f68ef7.png

总结

卸载支持在现代网络设备中很常见,因此它很自然地出现在了虚拟化软件模拟设备中。虽然多数公开研究专注于主流组件如环形缓冲区等,但卸载似乎并未得到足够的审视。遗憾的是,我未能在 Pwn2Own 大赛中及时获得exploit,因此只好将前两个漏洞报告给ZDI,将剩下的校验和漏洞直接提交给了Oracle 公司。


推荐阅读

2021奥斯汀 Pwn2Own黑客大赛落幕,Master of Pwn 诞生

ICS Pwn2Own 2022迈阿密黑客大赛的目标和奖金公布

Pwn2Own 2021奥斯汀黑客大赛公布类别、目标及奖金

微软7月修复117个漏洞,其中9个为0day,2个是Pwn2Own 漏洞

Pwn2Own 2021温哥华黑客大赛落幕  3个团队并列 Master of Pwn

Oracle 警告:Weblogic 服务器中含有多个可遭远程利用的严重漏洞

朝鲜黑客被指从黑市购买Oracle Solaris 0day,入侵企业网络

奇安信代码卫士帮助微软和 Oracle 修复多个高危漏洞,获官方致谢

原文链接

https://www.sentinelone.com/labs/gsoh-no-hunting-for-vulnerabilities-in-virtualbox-network-offloads/

题图:Pixabay License

本文由奇安信编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。

cb94fddcf4ffe46df053ac5e89642d86.png

c1676e85eef86897eb517f07e95333f0.png

奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的产品线。

   b8a5ff433fd982b03f218c4fef4da568.gif 觉得不错,就点个 “在看” 或 "赞” 吧~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值