Parallels Desktop最新虚拟机逃逸漏洞分析(CVE-2020-8871)
2020-05-28 10:25:00 Author: www.4hou.com(查看原文) 阅读量:412 收藏

Mac的Parallels Desktop for Mac是macOS最受欢迎的虚拟化程序之一,但是关于此产品的公开漏洞研究很少。去年11月,Reno Robert(@renorobertr)向ZDI报告了Parallels中的多个漏洞,其中一个漏洞允许guest OS上的本地用户提升特权并在主机上执行代码。该漏洞于5月用15.1.3版本进行了修补,并分配编号CVE-2020-8871(ZDI-20-292)。这篇文章深入研究了该漏洞以及Parallels对其进行的代码更改以修复该漏洞。

0x01  初步分析

以下所有分析均基于15.1.2版。经过测试,guest 虚拟机配置了默认选项。

原始漏洞报告很简短,并且通过简单的Fuzzing就可以找到。以下是POC中的相关代码:

   while (1) { 
     port = random_range(0x3C4, 0x3C5+1); 
     value = random_range(0, 0xFFFF+1); 
     outw(value, port); 
   }

基本上,这会随机且无限地将byte写入I / O端口0x3C4和0x3C5。如果在受影响的Parallels版本上运行POC,它将使prl_vm_app主机OS上的进程崩溃。系统上的每个虚拟机都由一个单独的prl_vm_app进程表示。

 Process 619 stopped 
 * thread #31, name = 'QThread', stop reason = EXC_BAD_ACCESS (code=2, address=0x158d28000) 
     frame #0: 0x0000000108c7a082 prl_vm_app`___lldb_unnamed_symbol5076$$prl_vm_app + 738 
 prl_vm_app`___lldb_unnamed_symbol5076$$prl_vm_app: 
 ->  0x108c7a082 : mov    dword ptr [rsi], ecx 
     0x108c7a084 : cmp    r12d, 0x2 
     0x108c7a088 : jb     0x108c7a0a0               ;  
     0x108c7a08a : mov    dword ptr [rsi + 0x4], ecx 
 Target 0: (prl_vm_app) stopped. 
 (lldb) bt 
 * thread #31, name = 'QThread', stop reason = EXC_BAD_ACCESS (code=2, address=0x158d28000) 
   * frame #0: 0x0000000108c7a082 prl_vm_app`___lldb_unnamed_symbol5076$$prl_vm_app + 738 
     frame #1: 0x0000000108c7ac8b prl_vm_app`___lldb_unnamed_symbol5078$$prl_vm_app + 907 
     frame #2: 0x0000000108c7dd52 prl_vm_app`___lldb_unnamed_symbol5093$$prl_vm_app + 1442 
     frame #3: 0x0000000108ce66dc prl_vm_app`___lldb_unnamed_symbol6282$$prl_vm_app + 636 
     frame #4: 0x0000000108c77bfc prl_vm_app`___lldb_unnamed_symbol5063$$prl_vm_app + 1468 
     frame #5: 0x0000000108c7762c prl_vm_app`___lldb_unnamed_symbol5062$$prl_vm_app + 28 
     frame #6: 0x000000010b91c153 QtCore`___lldb_unnamed_symbol228$$QtCore + 323 
     frame #7: 0x00007fff6879bd76 libsystem_pthread.dylib`_pthread_start + 125 
     frame #8: 0x00007fff687985d7 libsystem_pthread.dylib`thread_start + 15 
 
 (lldb)

通过一些研究,我们发现0x3C4和0x3C5分别是VGA序列发生器索引寄存器和序列发生器数据寄存器。乍一看,似乎VGA设备中存在一个越界(OOB)写漏洞。如前所述,POC由Fuzzing触发,原始报告未提供详细分析。

 http://www.scs.stanford.edu/17wi-cs140/pintos/specs/freevga/vga/seqreg.htm

0x02 根本原因

崩溃是在sub_100185DA0函数中。相关部分进行了简化并注释如下。

 char __fastcall sub_100185DA0(__int64 a1, unsigned int a2, unsigned int a3) 
 {
 //... 
   vga_context = a1; 
   v12 = 0; 
   v13 = 0; 
 //... 
     while ( 1 ) 
     { 
 //... 
       w = (_DWORD *)(vga_context->w); 
 //... 
           dst = (unsigned int *)((_QWORD *)(vga_context->buf) + 4LL * v12 * w); 
           v24 = 0; 
           do 
           { 
             v27 = 8; 
             do 
             { 
 //... 
               v31 = (_DWORD *)((_DWORD *)(vga_context->array[ 4LL * ((_BYTE *)v29) ]) | 0xFF000000); 
               *dst = v31;                       // crash here 
               ++dst; 
               --v27; 
             } 
             while ( v27 ); 
             v24 += 8; 
             v11 = (_DWORD *)(vga_context->w); 
           } 
           while ( v4 * v24 < v11 );             // v4 = 1 
 //... 
       } 
       v12 = v3 * ++v13; 
       if ( v3 * v13 >= (_DWORD *)(vga_context->h) ) // v3 = 1 
         break; 
 ... 
     } 
 //... 
 
 }

该vga_context结构是在VGA设备初始化期间分配的。它保留VGA设备的状态和变量,该函数尝试vga_context->buf通过三个循环顺序写入缓冲区。总长度以vga_context->h * vga_context->w * sizeof(DWORD)字节为单位。然后,它执行OOB写操作,并由于无效长度而在循环内崩溃。

我们研究的第一步是确定vga_context->buf缓冲区内容的来源。

 mapped file  00000001539e1000-00000001579e1000 [ 64.0M 47.9M 0K 0K] rw-/rwx SM=ALI

这个相当大的64MB缓冲区是一个屏幕缓冲区,它是通过guest VM配置(硬件->图形->内存)配置的。这似乎说明,vga_context->h和vga_context->w是为guest VM屏幕分辨率的高度和宽度。

接下来,我们需要确定vga_context->h和vga_context->w缓冲区内容的来源。我们可以从我们的调试这个答案,发现vga_state在sub_100184F90。

 char __usercall sub_100184F90@(int *a1@, __int64 a2@, _DWORD *a3@, unsigned int a4@) 
 { 
   //... 
   vga_state = (_QWORD *)(vga_context->vga_state); 
   v6 = *(_DWORD *)(vga_state->flaggg); 
   if ( v6 ) 
   { 
     width = (unsigned __int16 *)(vga_state->w); 
     height = (unsigned __int16 *)(vga_state->h); 
     // they will save to vba_context later 
   //... 
   }

但是该vga_state对象的来源是什么?

 shared memory 000000011150e000-0000000111514000 [24K 24K 24K 0K] rw-/rwx SM=SHM

我们发现它是共享内存。在这种情况下,它在主机ring0和主机ring3之间共享。它由ring0中的VGA I / O端口处理程序更新,稍后,ring3视频工作线程(位于中sub_100183610)将使用它。

 __int64 __fastcall VgaOutPortFunc(__int16 port, unsigned int cb, unsigned __int64 a3, void *val, void *vga_state, __int64 a6) 
 { 
   v11 = *(_DWORD *)val; 
   v8 = *(_BYTE*)val; 
 //... 
   switch ( (unsigned __int16)(port - 0x3B4) ) 
   { 
 //... 
     case 0x10u:                                 // 0x3c4 
       vga_state->sr_index = v8; 
       return v7; 
     case 0x11u:                                 // 0x3c5 
       switch ( vga_state->sr_index + 95 ) 
       { 
 //... 
         case 9: 
           (_WORD *)vga_state->w = v11; 
           vga_state->sr_index = 0xABu; 
           return v7; 
         case 10: 
           (_WORD *)vga_state->h = v11; 
           vga_state->sr_index = 0xACu; 
           return v7; 
 //... 
         case 13: 
           if ( v11 & 1 ) 
           { 
             (_DWORD *)vga_state->flag8 = 1; 
           } 
           else 
           { 
             (_DWORD *)vga_state->flag8 = 0; 
           } 
 //... 
       } 
 //... 
     case 0x15u:                                 // 0x3c9 
       LOBYTE(i) = vga_state->i; 
       vga_state->i = (_BYTE)i + 1; 
       if ( (_BYTE)i == 2 ) 
       { 
         v19 = vga_state->index2; 
         vga_state->array[4 * v19] = 4 * v8; 
         vga_state->i = 0; 
         vga_state->index2 = (_BYTE*)(v19 + 1); 
       } 
       else if ( (_BYTE)i == 1 ) 
       { 
         *((_BYTE*)vga_state->array[4 * vga_state->index2 + 1]) = 4 * v8; 
       } 
       else if ( (_BYTE)i == 0) 
       { 
         *((_BYTE*)vga_state->array[4 * vga_state->index2 + 2]) = 4 * v8; 
       } 
 //... 
       return v7; 
 //... 
 }

根据上面的伪代码,端口0x3C4充当选择器,以控制端口0x3C5中的操作。端口0x3C5的功能之一是可以将任意16位值设置为vga_state->h和vga_state->w。当ring3视频工作线程获取新的屏幕高度和宽度时,它将尝试更新整个屏幕缓冲区(vga_context->buf)。但是,它不会验证导致屏幕缓冲区溢出的新高度和宽度。

另外,溢出的长度是可控制的。溢出的值可通过端口0x3C9进行部分控制(请参阅参考资料vga_context->array)。结果,我们确定它可能是可利用的。

0x03 补丁分析

补丁发布后,我在15.1.2版和15.1.3版之间进行了一些二进制比较,以确定他们如何选择修复此漏洞。通过仔细检查diff,补丁对sub_100185DA0的调用所做的更改很小。

 __int64 __usercall sub_100186900@(__int64 vga_context@, unsigned int a2@) 
 { 
 //... 
   sub_100184F90((int *)&v29, vga_context, &v28, a2);  // explained above 
   vga_state = (_QWORD *)vga_context->vga_state); 
 //... 
   if ( *(_BYTE *)(vga_context->flaggg) )      // after patch 
 //if ( (_DWORD *)(vga_state->flaggg) )        // before patch 
   { 
 //... 
   } 
   else if ( *(_DWORD *)(vga_state + 15828) )  // looks like always 1 
   { 
 //... 
       sub_100185DA0(vga_context, v28, v29);   // trigger OOB write 
 //... 
   } 
 //... 
 }

if分支之一已更改。补丁程序flaggg已从vga_state移至vga_context。

如我们所见sub_100184F90,flaggg必须是TRUE,vga_state从中获取受控的高度和宽度。但是,flaggg必须具有FALSE崩溃功能。这两个约束是矛盾的。

我们如何满足这些约束?

正如我们先前在查看根本原因时所说的那样,vga_state是ring0和ring3之间的共享内存。该flaggg功能可以通过端口0x3C5进行配置。因此,可以flaggg在这两个约束之间在ring3视频工作线程中翻转并进行两次提取。

补丁实际上所做的是移动flaggg的vga_state到vga_context。由于vga_context在ring3中是堆分配,因此不易受到两次提取的攻击,因此可以改善这种情况。因此,它将永远不会触发OOB写入的路径。

0x04 分析总结

此提交提供了一个很好的示例,说明了如何通过工作流程以及对Parallels Desktop中的虚拟设备进行漏洞原因分析。尽管供应商将补丁列为“低严重性”,但考虑到总体CVSS评分和从guest 升级到主机的机会,应认为该补丁的严重性为“重要”,并尽快将其应用。我们在Parallels Desktop中没有看到很多提交给该程序的bug,但是也许这篇文章会让更多的人看到。如果你也发现了一些漏洞,我们对此非常感兴趣。

本文翻译自:https://www.zerodayinitiative.com/blog/2020/5/20/cve-2020-8871-privilege-escalation-in-parallels-desktop-via-vga-device如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/Xnr8
如有侵权请联系:admin#unsafe.sh