[Перевод] Уязвимости в Linux допускают возможность атаки «в один клик»
Группа исследователей CrowdStrike Intelligence обнаружила несколько уязвимостей, влияющих на LibVNCClient в Linux. В некоторых широко используемых средах рабочего стола (например, Gnome) эти уязвимости можно эксплуатировать одним кликом мыши.
Вступление
Эксплуатация уязвимостей на стороне клиента — распространённая схема работы злоумышленников. Обычно лазейкой становятся браузеры, но их разработчики постоянно усиливают средства защиты, поэтому использовать их становится всё сложнее. А вот другим компонентам рабочего стола не уделяется столько внимания, поэтому некоторые риски остаются незамеченными. В статье исследуем состояние безопасности типичной среды рабочего стола Linux.
Обработчики схем URL
Будет ли запущено приложение, когда пользователь кликает по ссылке, зависит от того, зарегистрирован ли в системе обработчик схемы URL. На стандартном рабочем столе Ubuntu 21.04 на базе Gnome есть различные приложения, зарегистрированные как обработчики схемы URL. Полный список этих обработчиков можно получить, выполнив поиск файла через: mimeinfo.cache
:
$ grep x-scheme-handler /usr/share/applications/mimeinfo.cache
x-scheme-handler/apt=apturl.desktop;
x-scheme-handler/chrome=firefox.desktop;
x-scheme-handler/ftp=firefox.desktop;
x-scheme-handler/ghelp=yelp.desktop;
x-scheme-handler/help=yelp.desktop;
x-scheme-handler/http=firefox.desktop;
x-scheme-handler/https=firefox.desktop;
x-scheme-handler/icy=org.gnome.Totem.desktop;
x-scheme-handler/icyx=org.gnome.Totem.desktop;
x-scheme-handler/info=yelp.desktop;
x-scheme-handler/magnet=transmission-gtk.desktop;
x-scheme-handler/mailto=thunderbird.desktop;
x-scheme-handler/man=yelp.desktop;
x-scheme-handler/mms=org.gnome.Totem.desktop;
x-scheme-handler/mmsh=org.gnome.Totem.desktop;
x-scheme-handler/net=org.gnome.Totem.desktop;
x-scheme-handler/pnm=org.gnome.Totem.desktop;
x-scheme-handler/rdp=org.remmina.Remmina.desktop;remmina-file.desktop;
x-scheme-handler/remmina=org.remmina.Remmina.desktop;remmina-file.desktop;
x-scheme-handler/rtmp=org.gnome.Totem.desktop;
x-scheme-handler/rtp=org.gnome.Totem.desktop;
x-scheme-handler/rtsp=org.gnome.Totem.desktop;
x-scheme-handler/snap=snap-handle-link.desktop;
x-scheme-handler/spice=org.remmina.Remmina.desktop;remmina-file.desktop;
x-scheme-handler/uvox=org.gnome.Totem.desktop;
x-scheme-handler/vnc=org.remmina.Remmina.desktop;remmina-file.desktop;
x-scheme-handler/vnd.libreoffice.cmis=libreoffice-startcenter.desktop;
Хотя URL-адрес, начинающийся с пользовательской схемы, может показаться подозрительным, истинная цель ссылки может быть скрыта в приложениях, которые позволяют встраивать разметку HTML. Примером является приложение для управления персональной информацией Evolution:
Инструмент удалённого администрирования Remmina зарегистрирован для схем URL rdp, remmina, spice и vnc. Соответствующий файл .desktop, указанный как часть определения обработчика, содержит ссылку на сценарий оболочки remmina-file-wrapper
, который получает указанный URL-адрес в качестве второго аргумента (%U плейсхолдер):
$ cat /usr/share/applications/remmina-file.desktop
[Desktop Entry]
[...]
Exec=remmina-file-wrapper -c %U
Icon=org.remmina.Remmina
MimeType=application/x-remmina;x-scheme-handler/remmina;x-scheme-handler/rdp;x-scheme-handler/spice;x-scheme-handler/vnc;
[...]
Затем этот сценарий оболочки запускает фактический двоичный файл Remmina, если указан соответствующий URL:
$ cat /usr/bin/remmina-file-wrapper
#!/usr/bin/env bash
[...]
REMMINA="/usr/bin/remmina"
if [[ ! -f "$REMMINA" ]] ; then
REMMINA="${USRBIN}/remmina"
else
REMMINA="remmina"
fi
export GLADE_HOME="$USRBIN/../share/remmina/ui/"
case "$@" in
*rdp:*)
"$REMMINA" "${@#rdp:\/\/}"
;;
*spice:*)
"$REMMINA" "${@#spice:\/\/}"
;;
*vnc:*)
"$REMMINA" "${@#vnc:\/\/}"
;;
*remmina:*)
"$REMMINA" "${@#remmina:\/\/}"
;;
*)
"$REMMINA" "${@}"
;;
esac
Таким образом, эти обработчики URL-адресов открывают большое пространство для атак, которые можно осуществить при минимальном взаимодействии с пользователем. Например, клик по URL-адресу, такому как vnc://user: pass@example.com/, автоматически запускает Remmina и мгновенно устанавливает VNC соединение с указанным удалённым хостом по заданным учётным данным. Любая уязвимость, влияющая на реализацию протокола VNC Remmina, потенциально допускает сценарии эксплуатации всего по одному клику мышки. Поэтому есть смысл внимательнее изучить libvncclient, который используется Remmina для поддержки VNC.
Аудит кода
Libvncclient реализует протокол VNC, который основан на концепции удалённого буфера кадров. Он предлагает функциональные возможности для передачи нажатий клавиш и кликов мыши от клиента к серверу в процессе трансляции графического рабочего стола сервера, как только обновляется фреймбуфер клиента. Первоначальная спецификация протокола удалённого буфера кадра (RFB), лежащего в основе VNC, появилась в 1998 году и была относительно простой. Однако с годами она обросла дополнительными функциями для оптимизации различных аспектов протокола. Например, уменьшение объёма данных, которые необходимо передать для обновления буфера кадра. Такая оптимизация, в свою очередь, привела к дополнительному усложнению и увеличению площади атаки.
Две из этих оптимизаций связаны с реализацией протоколов UltraVNC и TightVNC и используют настраиваемые схемы кодирования. Обе эти схемы также поддерживаются LibVNCClient. Аудит кода выявил две различные уязвимости, связанные с повреждением памяти при обработке этих схем кодирования.
Переполнение буфера при кодировании Ultra
Сообщения от VNC-сервера к клиенту обрабатываются функцией HandleRFBServerMessage()
. Эта функция сначала считывает тип сообщения, а затем обрабатывает остальную его часть. Обновления буфера кадра передаются в сообщениях типа rfbFramebufferUpdate
. Эти сообщения содержат информацию о количестве обновлений буфера кадра, содержащихся в сообщении, и о кодировании данных для каждого фрейма (например, rfbEncodingUltra
для кодирования UltraVNC). Следующий кусок исходного кода из libvncclient
показывает функцию HandleUltraBPP()
, которая реализует обработку обновлений кадрового буфера в кодировке UltraVNC:
static rfbBool
HandleUltraBPP (rfbClient* client, int rx, int ry, int rw, int rh)
{
rfbZlibHeader hdr;
int toRead=0;
[...]
lzo_uint uncompressedBytes = (( rw * rh ) * ( BPP / 8 ));
if (!ReadFromRFBServer(client, (char *)&hdr, sz_rfbZlibHeader))
return FALSE;
toRead = rfbClientSwap32IfLE(hdr.nBytes);
[...]
Строка BPP в имени функции будет расширена препроцессором C до трёх различных функций для обработки обновлений буфера кадра с 8, 16 или 32 битами цвета на пиксель. После прочтения заголовка из сокета с помощью ReadFromRFBServer()
, его поле nBytes
используется для подстановки целого числа в переменную toRead. Впоследствии toRead
берётся в качестве размера для выделения 4-байтового буфера (ultra_buffer
), если предыдущий буфер не был выделен при прошлом вызове функции, или если он имеет недостаточный размер:
[...]
/* allocate enough space to store the incoming compressed packet */
if ( client->ultra_buffer_size < toRead ) { if ( client->ultra_buffer != NULL ) {
free( client->ultra_buffer );
}
client->ultra_buffer_size = toRead;
/* buffer needs to be aligned on 4-byte boundaries */
if ((client->ultra_buffer_size % 4)!=0)
client->ultra_buffer_size += (4-(client->ultra_buffer_size % 4));
client->ultra_buffer = (char*) malloc( client->ultra_buffer_size );
[...]
После этого код считывает из сокета количество данных, указанное в toRead:
[...]
/* Fill the buffer, obtaining data from the server. */
if (!ReadFromRFBServer(client, client->ultra_buffer, toRead))
return FALSE;
[...]
Вредоносный сервер VNC может полностью контролировать значение переменной toRead, включая отрицательные значения. Если сначала отправить обновление буфера кадра, которое приводит к выделению буфера определённого размера, а затем сделать второе обновление, которое устанавливает для toRead отрицательное значение, ultra_buffer может переполниться. Затем буфер, который выделяется во время первого обновления буфера кадра, переполняется вторым, так как не будет выполнено условие client→ultra_buffer_size < toRead, что приводит к вызову функции ReadFromRFBServer ()
, где toRead — отрицательное число.
Так как ReadFromRFBServer()
ожидал целочисленное значение без отрицательного знака, неявное преобразование типа приводит к вызову read()
со слишком большим значением count
. Точный объём перезаписываемых данных можно контролировать, заранее закрыв базовое соединение с сокетом.
Переполнение внутренней структуры в жёстком кодировании
Если для обновления буфера кадра используется жёсткое кодирование, вызывается функция HandleTightBPP()
(как и в кодировке Ultra, BPP является заполнителем для препроцессора). Как показано в следующем листинге, функция считывает значение comp_ctl
из нижележащего сокета:
static rfbBool
HandleTightBPP(rfbClient *client, int rx, int ry, int rw, int rh)
{
CARDBPP fill_colour;
uint8_t comp_ctl;
uint8_t filter_id;
filterPtrBPP filterFn;
z_streamp zs;
int err, stream_id, compressedLen, bitsPixel;
int bufferSize, rowSize, numRows, portionLen, rowsProcessed, extraBytes;
rfbBool readUncompressed = FALSE;
if (client->frameBuffer == NULL)
return FALSE;
if (rx + rw > client->width || ry + rh > client->height)
{
rfbClientLog("Rect out of bounds: %dx%d at (%d, %d)\n", rx, ry, rw, rh);
return FALSE;
}
if (!ReadFromRFBServer(client, (char *)&comp_ctl, 1))
return FALSE;
[...]
Это значение используется функцией, чтобы сделать ряд предположений относительно точного формата. Например, оно используется для выбора функции фильтрации, определяет тип фрейма и сообщает, используется ли сжатие. Далее функция ReadCompactLen()
вызывается для чтения значения compressedLen
из базового сокета:
[...]
/* Read the length (1..3 bytes) of compressed data following. */
compressedLen = (int)ReadCompactLen(client)
[...]
ReadCompactLen()
позволяет серверу указать размер данных, который может находиться в диапазоне от 1 до 3 байтов. Наиболее значимыми являются первые один или два бита, так как они определяют последовательность остальных байтов.
Следовательно, указанный размер может достигать 22 бит (7 + 7 + 8). Таким образом, максимальное целочисленное значение, которое может быть возвращено функцией, равно 4 194 303 (2 ^ 22 — 1).
Когда клиент ожидает несжатые данные (обозначены comp_ctl
), compressedLen
передаётся ReadFromRFBServer()
как количество данных, которые считываются в client→buffer:
[...]
if (readUncompressed)
{
if (!ReadFromRFBServer(client, (char *)client->buffer, compressedLen))
return FALSE;
[...]
Как показано ниже, client→buffer представляет собой массив фиксированного размера (307 200 байт), расположенный в центральной структуре rfbClient
, которая выделяется для каждого соединения:
typedef struct _rfbClient {
uint8_t* frameBuffer;
[...]
int serverPort; /**< if -1, then use file recorded by vncrec */ rfbBool listenSpecified; int listenPort, flashPort; struct { int x, y, w, h; } updateRect; /** Note that the CoRRE encoding uses this buffer and assumes it is big enough to hold 255 * 255 * 32 bits -> 260100 bytes. 640*480 = 307200 bytes.
Hextile also assumes it is big enough to hold 16 * 16 * 32 bits.
Tight encoding assumes BUFFER_SIZE is at least 16384 bytes. */
#define RFB_BUFFER_SIZE (640*480)
char buffer[RFB_BUFFER_SIZE];
char *bufoutptr;
unsigned int buffered;
[...]
Поскольку максимальный размер compressedLen может превышать размер buffer, сервер может переполнять указанный буфер и полностью контролировать последующие элементы структуры rfbClient, а также смежную память кучи. Сюда входят указатели, с помощью которых можно произвольно записывать примитивы, а также указатели функций, которые позволяют захватить поток управления.
Фаззинг
Помимо ручного аудита был использован afl ++ для проведения фаззинговых тестов. Для этого разработали тонкую оболочку вокруг основной функции отправки сообщений HandleRFBServerMessage()
. Аргументом для этой функции является клиентский объект, который настроен на чтение обновлений буфера кадров stdin. Путем фаззинга были обнаружены две дополнительных ошибки, которые описаны ниже.
Переполнение буфера в TRLE
В случае получения обновления буфера кадра с кодировкой TRLE вызывается функция HandleTRLE()
. Как показано в следующем фрагменте исходного кода, функция содержит цикл, который получает произвольное количество 0xFF байтов из базового сокета в выделенную кучу client→raw_buffer:
static rfbBool HandleTRLE(rfbClient *client, int rx, int ry, int rw, int rh) {
[...]
uint8_t *buffer;
[...]
buffer = (uint8_t*)(client->raw_buffer);
[...]
while (*buffer == 0xff) {
if (!ReadFromRFBServer(client, (char*)buffer + 1, 1))
return FALSE;
length += *buffer;
buffer++;
}
[...]
}
Поскольку проверка границ отсутствует, атакующий сервер может переполнить буфер.
Переполнение буфера в кодировке ZRLE
В случае получения обновления буфера кадра с кодировкой ZRLE вызывается функция HandleZRLE()
. Как показано в следующем фрагменте исходного кода, функция сначала считывает с сервера структуру типа rfbZRLEHeader
. После этого принимаются дополнительные данные, сжатые zlib.
static rfbBool HandleZRLE (rfbClient* client, int rx, int ry, int rw, int rh)
{
while (( remaining > 0 ) && ( inflateResult == Z_OK )) {
[...]
/* Fill the buffer, obtaining data from the server. */
if (!ReadFromRFBServer(client, client->buffer,toRead))
return FALSE;
client->decompStream.next_in = ( Bytef * )client->buffer;
client->decompStream.avail_in = toRead;
/* Need to uncompress buffer full. */
inflateResult = inflate( &client->decompStream, Z_SYNC_FLUSH );
[...]
}
if ( inflateResult == Z_OK ) {
[...]
for(j=0; j
Затем распакованные данные передаются функции HandleZRLETILE()
в качестве аргумента buf. Его первый байт определяет type:
static int HandleZRLETile(rfbClient* client, uint8_t* buffer,size_t buffer_length, int x,int y,int w,int h)
{
[...]
uint8_t type;
[...]
type = *buffer;
buffer++;
[...]
if( type == 0 ) /* raw *
[...]
else if( type == 1 ) /* solid */
[...]
else if( type <= 127 ) { /* packed Palette */
CARDBPP palette[16];
int i,j,shift,
[...]
/* read palette */
for(i=0; i
Если type не является ни 0, ни 1, но меньше или равен 127, код исполняется по ветке, которая обрабатывает palette
. В этом случае palette объявляется выделенным массивом из 16 названных элементов. Точное значение typethen
определяет, сколько элементов считывается в массиве
palette внутри цикла for
. В случае, если значение type
превышает 16, происходит запись в стек за пределами допустимого диапазона.
Возможность эксплуатации
Описанные уязвимости позволяют произвольно записывать примитивы и полностью контролировать указатель инструкций. Наличие рандомизации адресного пространства (ASLR) затрудняет эксплуатацию в стандартной среде рабочего стола Ubuntu x64. Это связано с тем, что сама Remmina скомпилирована как исполняемый файл, не зависимый от позиции и, следовательно, она похожа на библиотеки, загружаемые в случайном месте. Таким образом, у злоумышленника нет надёжных фиксированных адресов. Однако, если одна из уязвимостей приведёт к утечке информации или будет обнаружена отдельная ошибка, позволяющая раскрыть адреса памяти, указанные уязвимости сделают возможным удалённое выполнение кода.
Заключение
Дистрибутивы Linux начали предоставлять механизмы изоляции для конкретных приложений, а также интегрировать методы песочницы в критически важные компоненты, но далеко не все части типичных настольных сред защищены аналогичным образом.
Linux — не единственная платформа, которая открывает такую обширную поверхность для атак. Недавно обнаруженная уязвимость MSHTML для удалённого выполнения кода CVE-2021–40444 в операционных системах Windows также поддерживает и автоматически добавляет пользовательские обработчики протоколов для известных типов файлов.
Что ещё интересного есть в блоге Cloud4Y
→ Айтишный пицца-квест. Итоги
→ Как я случайно заблокировал 10 000 телефонов в Южной Америке
→ Клавиатуры, которые постигла неудача
→ Чувствуете запах? Пахнет утечкой ваших данных
→ Изучаем своё железо: сброс паролей BIOS на ноутбуках
Подписывайтесь на наш Telegram-канал, чтобы не пропустить очередную статью. Пишем не чаще двух раз в неделю и только по делу.