CVE-2020-8871:通过VGA设备在Parallels Desktop中提升权限

 

0x00 绪论

Parallels Desktop for Mac是macOS上最流行的虚拟机软件之一,关于它的公开漏洞研究却寥寥无几。去年11月,Reno Robert(@renorobertr)向ZDI报告了Parallels中的多个漏洞,其中之一允许客户机操作系统中的本地用户提升权限,在主机中执行代码。该漏洞在15.1.3(47255)版中修复,分配了漏洞编号CVE-2020-8871(ZDI-20-292)。本文将更深入地分析此漏洞及Parallels进行修补的代码。

 

0x01 初步分析

以下分析全部基于15.1.2版,客户虚拟机使用默认选项配置。

原始的报告很短,漏洞是通过简单fuzzing发现的。以下是POC中的相关代码:

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

这就只是不停地随机往IO端口0x3C4和0x3C5写入字而已。如果在受影响的Parallels版本上运行POC,主机上的prl_vm_app进程会崩溃。系统中的每个虚拟机都有一个独立的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 <+738>: mov    dword ptr [rsi], ecx 
    0x108c7a084 <+740>: cmp    r12d, 0x2 
    0x108c7a088 <+744>: jb     0x108c7a0a0               ; <+768> 
    0x108c7a08a <+746>: 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定序器(sequencer)基址寄存器和定序器数据寄存器。初步查看似乎是VGA设备中出现了越界写漏洞。如前面所说,POC是由fuzzing触发的,原始报告也没有提供详细分析。是时候深入看看了。

 

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;                       // 崩溃 
              ++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)字节。然后,执行越界写,由于长度无效而在循环内崩溃。

首先,我们要确定缓冲区vga_context->buf内容的来源。

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

这个64MB的大缓冲区是屏幕缓冲区,经由客户机配置(硬件->图形->内存)来设置,看起来vga_context->hvga_context->w是客户机屏幕分辨率的高和宽。

接着,我们要确定vga_context->hvga_context->w内容的来源,可以从调试器找到答案,来自sub_100184F90里的vga_state

char __usercall sub_100184F90@<al>(int *a1@<rdx>, __int64 a2@<rdi>, _DWORD *a3@<rsi>, unsigned int a4@<r11d>) 
{ 
  //... 
  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 IO端口handler更新这块内存,然后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端口可以把vga_context->hvga_context->w设为任意的16位值。当ring3视频工作线程得到屏幕的新宽高时,就试图更新整个屏幕缓冲区(vga_context->buf)。然而,该线程并未对新宽高做验证,导致屏幕缓冲区的溢出漏洞。

此外,溢出的长度也是可以控制的,溢出的值则可以通过0x3C9端口(见vga_context->array)部分控制。因此,我们判断此漏洞很可能可以利用。

 

0x03 审视补丁

补丁发布后,我比较了15.1.2和15.1.3版的二进制之间的差异,研究厂商是如何修复漏洞的。仔细查看差异发现,sub_100185DA0的调用者中做了微调。

__int64 __usercall sub_100186900@<rax>(__int64 vga_context@<rdi>, unsigned int a2@<r11d>) 
{ 
//... 
  sub_100184F90((int *)&v29, vga_context, &v28, a2);  // 上面已解释
  vga_state = (_QWORD *)vga_context->vga_state); 
//... 
  if ( *(_BYTE *)(vga_context->flaggg) )      // 打补丁后 
//if ( (_DWORD *)(vga_state->flaggg) )        // 打补丁前 
  { 
//... 
  } 
  else if ( *(_DWORD *)(vga_state + 15828) )  // 似乎总是1 
  { 
//... 
      sub_100185DA0(vga_context, v28, v29);   // 触发越界写 
//... 
  } 
//... 
}

其中一个if分支变动了。补丁把vga_state->flaggg改成了vga_context->flaggg

flaggg是什么?

sub_100184F90中所见,flaggg必须为TRUE才能从vga_state获取受控制的宽和高。但是,flaggg又必须是FALSE才能进入导致崩溃的函数。两约束条件冲突。

怎样才能满足这些条件?

追根溯源时,我们说过,vga_state是ring0和ring3间的共享内存,flaggg功能可以通过0x3C5端口配置。因此,可以翻转flaggg,并在ring3视频工作线程中利用double fetch。

补丁把vga_state->flaggg改成了vga_context->flaggg,由于vga_context是ring3堆分配的,不受double fetch影响,因此不会触发越界写的那条路经。

 

0x04 总结

本文分析了Parallels Desktop中虚拟设备漏洞的流程和根源。厂商将漏洞标为低严重性,但是考虑到其CVSS得分和从客户提升到主机的可能性,用户应当认为这是严重漏洞并尽快安装补丁。Parallels Desktop的漏洞提交并不多,也许本文会鼓励其他分析人员往这个方向尝试。如果你找到了漏洞,我们很乐意看一看。

(完)