Гостевая статья CVE-2020-8871: повышение привилегий в PARALLELS DESKTOP через VGA-устройство

Parallels Desktop для Mac является одной из самых популярных программ виртуализации для macOS, но в отношении этого продукта не проводилось много публичных исследований уязвимостей. В ноябре прошлого года Рено Роберт ( ) сообщил о нескольких ошибках в Parallels в ZDI, одна из которых могла позволить локальному пользователю гостевой ОС повысить привилегии и выполнить код на хосте. Эта ошибка была исправлена в мае с версией и была назначена (ZDI-20-292). В этом блоге более подробно рассматривается эта уязвимость и изменение кода, внесенное Parallels для ее устранения.

Первоначальный анализ

Весь последующий анализ основан на версии 15.1.2. Как было проверено, гостевая виртуальная машина была настроена с параметрами по умолчанию.

Первоначальный отчет был коротким и был найден простым размышлением. Вот соответствующий код из Proof-of-Concept (POC):

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

По сути, это случайным образом и бесконечно записывает слова в порты ввода / вывода 0x3C4 и 0x3C5. Если вы запустите POC в уязвимой версии Parallels, это приведет к сбою 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-секвенсора и регистр данных секвенсора соответственно. На первый взгляд кажется, что в устройстве VGA существует ошибка записи OOB (Out-Of-Bounds). Как уже упоминалось, POC вызывается размытым, и в первоначальном отчете не было подробного анализа. Пришло время идти глубже.

Изучение первопричины

Сбой в большой функции называется 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


Этот довольно большой 64 МБ буфер является экранным буфером, который настраивается через конфигурацию гостевой виртуальной машины (Hardware-> Graphics-> Memory). Похоже, это vga_context->hи vga_context->w есть высота и ширина для разрешения экрана гостевой виртуальной машины.

Далее нам нужно определить источник vga_context->hи vga_context->w содержимое буфера. Мы можем получить этот ответ от нашего отладчика и найти его vga_stateв sub_100184F90.

Код:
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. Он обновляется обработчиком порта ввода / вывода VGA в ring0, и позже рабочий поток видео 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). В результате мы определили, что это, вероятно, эксплуатируемый.

Оценка патча

После того, как патч был выпущен, я сделал несколько бинарных тестов между версиями 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);  // 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.
Что такое flaggg?
Как мы видим 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.

Вывод

В этом материале представлен хороший пример того, как пройти рабочий процесс и проанализировать причины для виртуального устройства в Parallels Desktop. Несмотря на то, что поставщик перечисляет исправления как «Низкие уровни серьезности», учитывая общие оценки CVSS и возможность перехода от гостя к хосту, следует учитывать, что исправление имеет важное значение по серьезности, и применить его как можно скорее. Мы не видим много ошибок в Parallels Desktop, представленных в программу, но, возможно, этот блог побудит других взглянуть. Если вы все же обнаружите какие-то уязвимости, нам, безусловно, будет интересно их увидеть.

Источник:
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!