08.12.2014

Эксплуатация уязвимости MS14-066 / CVE-2014-6321 (aka “Winshock”)

image

В статье не будет представлен полный код эксплоита, но мы углубленно разберемся в том, как сделать переполнение кучи при помощи некоторых модификаций в OpenSSL.

Автор: Mike Czumak

Введение

Думаю, прошло достаточно времени с момента публикации информации об уязвимости MS14-066 в Schannel (aka “Winshock”), и теперь можно рассказать чуть больше деталей об эксплуатации этой бреши. В статье не будет представлен полный код эксплоита, но мы углубленно разберемся в том, как сделать переполнение кучи при помощи некоторых модификаций в OpenSSL. Полагаю, приведенной информации будет вполне достаточно, чтобы вы могли воспроизвести данную ситуацию.

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

Вначале подробно рассмотрим уязвимую функцию.

Принцип работы функции schannel!DecodeSigAndReverse

Ниже представлена дизассемблированная версия некоторых, имеющих отношение к проблеме, участков кода функции DecodeSigAndReverse (из библиотеки schannel.dll). Функция DecodeSigAndReverse используется для декодирования сигнатуры сертификата.

Рисунок 1: Часть блок-схемы функции DecodeSigAndReverse

Нам интересны вызовы функции CryptDecodeObject и два последующих вызова функции memcpy (в желтом нижнем окне слева), которые, в конечном счете, непосредственно связаны с уязвимостью. Далее мы рассмотрим эти вызовы более подробно, но сейчас важно отметить, что для вызова уязвимой функции memcpy необходимо каким-то образом организовать на сервере верификацию клиентского ECC-сертификата (и заставить инструкцию cmp ebx, 2Fh возвратить нужный нам результат).

Рисунок 2: Чтобы использовать уязвимость, необходимо, чтобы инструкция cmp ebx, 2Fh отработала «правильно» (соответствующий участок кода находится в правом верхнем окне)

Большинство веб серверов игнорируют клиентские сертификаты (если только не разрешена аутентификация при помощи сертификатов). Однако в Malware Tech отмечают, что уязвимые IIS-сервера обрабатывают сертификаты вне зависимости от настроек SSL. Я буду демонстрировать уязвимость на примере только IIS и 443-порта, однако вы сможете получить похожие результаты и для RDP.

Настройка IIS

Перед использованием эксплоита, необходимо настроить SSL в IIS (я использовал Win 7 box):

Рисунок 3: Параметры SSL в IIS

Затем, используя OpenSSL, сгенерируйте самоподписанный сертификат, загрузите его в систему и привяжите к сайту:

Рисунок 4: Привязка сертификата к сайту

Теперь сгенерируйте пару EC-сертификат/ключ для использования на машине, с которой будет осуществляться атака. Далее необходимо загрузить исходники OpenSSL (я использовал версию 1.0.1j из Kali box). Поскольку вам, весьма вероятно, придется часто изменять и компилировать исходники, обзаведитесь скриптом .configure, где будут установлены все необходимые параметры, необходимые для сборки (например, директория для установки).

После того как веб сервер сконфигурирован, сгенерирован сертификат/ключ и загружен OpenSSL, настало время внести изменения в исходный текст OpenSSL, в частности в файл s3_clnt.c. Для начала необходимо настроить принудительную отправку клиентского сертификата:

Рисунок 5: Участок кода, отвечающий за принудительную отправку клиентского сертификата (выделен красным)

Обратите внимание, я изменил логику условия для принудительного вызова ssl3_send_client_certificate.

Далее необходима принудительная верификация сертификата, что можно сделать принудительным вызовом ssl3_send_client_verify:

Рисунок 6: Участок кода, отвечающий за принудительную верификацию клиентского сертификата (выделен красным)

Затем подключите удаленный отладчик ядра к тестовому IIS серверу и установите точку останова на функции schannel!DecodeSigAndReverse. Теперь подключитесь к целевой машине, используя модифицированный клиент OpenSSL. Должна сработать точка останова:

Рисунок 7: Подключение к IIS серверу модифицированным клиентом OpenSSL

Рисунок 8: Сработала точка останова

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

Процедура эксплуатации уязвимости

Если вы вернетесь в начало статьи, где продемонстрированы скриншоты дизассемблированной функции DecodeSigAndReverse, то увидите, что вначале происходит первый из двух вызовов к функции CryptDecodeObject.

Рисунок 9: Спецификация на функцию CryptDecodeObject

Первый вызов обнуляет pvStructInfo и определяет размер буфера, необходимого для хранения декодированной сигнатуры, для корректного распределения памяти (впоследствии обрабатываемого функцией SPExternalAlloc) .

Рассмотрим повнимательнее второй из двух вызовов к функции CryptDecodeObject.

Рисунок 10: Место второго вызова функции CryptDecodeObject (третья строчка снизу)

Во время тестирования соединения с неизмененным EC-сертификатом и ключом содержимое структур pcbStructInfo и pvStructInfo должно быть примерно следующим:

Рисунок 11: Содержимое структур pcbStructInfo и pvStructInfo

Перед вторым вызовом CryptDecodeObject структура pvStructInfo размером 50h содержит только нулевые элементы и готова к приему декодированной сигнатуры. cbEncoded содержит размер закодированной сигнатуры (в данном случае 46h). Закодированная сигнатура выглядит следующим образом:

Рисунок 12: Содержимое закодированной сигнатуры

* Обратите внимание: на рисунке выше ebp+0ch показывает размер закодированной сигнатуры (46h), а по адресу 002dc57b находится закодированная сигнатура.

Когда происходит второй вызов функции CryptDecodeObject, структура pvStructInfo содержит декодированную сигнатуру:

Рисунок 13: Декодированная сигнатура

Теперь рассмотрим два вызова функции memcpy, идущих после второго вызова функции CryptDecodeObject.

Рисунок 14: Два вызова функции memcpy

После первого вызова memcpy в буфер (Dst) скопируется несколько байт декодированной сигнатуры (содержимое регистра [esi]; в нашем случае в регистре находится значение 20h или 32d). На рисунке ниже видно, что после первого вызова memcpy в буфере находится первые 32 байта декодированной сигнатуры, которую вернула функция CryptDecodeObject.

Рисунок 15: Содержимое регистра esi и буфера после первого вызова memcpy

Схожим образом после второго вызова memcpy происходит копирование оставшихся 32 байтов декодированной сигнатуры в смежный зарезервированный участок памяти (Dst) .

Рисунок 16: Содержимое буфера после второго вызова memcpy

Именно второй вызов memcpy может привести к переполнению кучи. Рассмотрим подробнее этот участок кода. Чтобы понять, почему второй вызов уязвим к переполнению, необходимо разобраться, как происходит размещение буфера-приемника (Dst) в функции CheckClientVerifyMessage().

Рисунок 17: Место вызова функции DecodeSigAndReverse

Обратите внимание, что в третьем блоке (см. рисунок ниже) перед вызовом DecodeSigAndReverse происходит вызов функции SPExternalAlloc, которая резервирует память (Dst) , куда затем копируется декодированная сигнатура при помощи двух вызовов memcpy (см. выше).

Рисунок 18: Часть блок-схемы функции CheckClientVerifyMessage

Если помните, каждый из двух вызовов memcpy копирует 32 байта (20h) декодированной сигнатуры в специально выделенную область памяти, в итоге заполняя пространство размером 40h. 40h байтов резервируются функцией SPExternalAlloc следующим образом: размер ключа (в нашем случае 256 бита) делится на 8 (число конвертируется в байты) и удваивается (256 / 8 = 32 * 2 = 64d или 40h). Формула расчета основывается на том предположении, что размер декодированной сигнатуры всегда в 2 раза больше ключа. Скоро мы узнаем, почему это предположение не верно.

Структура закодированной сигнатуры

Первое, что необходимо сделать, научиться управлять размером закодированной сигнатуры (содержимое cbEncoded). Если взглянуть на вызов CheckClientVerifyMessage(), можно заметить, что cbEncoded передается как параметр. Следовательно, нам необходимо сохранить содержимое cbEncoded вплоть до вызова функции DigestCertVerify().

Рисунок 19: Содержимое функции DigestCertVerify()

По адресу 0xXXXX98CE в функции DigestCertVerify мы видим, что буфер cbEncoded находится в [esi+1]. Выясняется, что [esi] просто указывает на начало структуры закодированной сигнатуры, представляющей из себя следующее:

Рисунок 20: Содержимое структуры закодированной сигнатуры

Обратите внимание [esi+1] содержит размер cbEncoded (47h). Последующие байты представляют собой закодированную сигнатуру, впоследствии передаваемую в функцию CrypteDecodeObject() внутри функции DecodeSigAndReverse(). На рисунке ниже показан вызов CrypteDecodeObject(), где отчетливо видно, что содержимое идентично.

Рисунок 21: Содержимое, передаваемое в функцию CrypteDecodeObject()

Структура, где хранится закодированная сигнатура, определена в MSDN так:

Рисунок 22: Спецификация на структуру с закодированной сигнатурой

Далее по тексту структура будет называться sig. Мы обнаружили, что общий размер sig определяется байтом, находящимся по адресу sig[1] ([esi+1]). Члены структуры r и s декодируются и копируются при помощи двух вызовов memcpy. Еще более интересно то, что два других байта sig (содержимым этих байтов мы можем управлять) определяют размеры r и s, используемые при вызовах memcpy.

Рисунок 23: Байты, определяющие общий размер структуры и размеры отдельных членов этой структуры

Поскольку у нас есть контроль над количеством байтов, используемых в каждом вызове memcpy, мы можем перезаписать буфер, выделенный под кучу. Кроме того, большая часть закодированной сигнатуры, по сути, не имеет отношения к делу, и мы можем внедрить любые значения, которые только захотим.

Более того, если передать некорректную закодированную сигнатуру в функцию CryptDecodeObject будет возвращена не ошибка, а просто возвращена переданная некорректно закодированная сигнатура. Затем информация записывается в кучу через уязвимые функции memcpy, и мы, в конечном счете, можем вызвать переполнение.

Опытным путем я выяснил требуемые значения байтов закодированный структуры:

  • sig[1] = cbEncoded (общий размер sig); во время тестирования выяснилось, что максимальный размер - \x81
  • sig[2] = \x30; требуемый параметр (оставляем его неизменным)
  • sig[3] = (sig[1] -1); максимальный размер - \x7f
  • sig[4] = \x02; по-видимому, представляет собой тип данных, которые следуют далее
  • sig[5] = размер r или значение, используемое в первом вызове memcpy 1; минимальное значение - 1
  • sig[6] = любое значение единичного байта, например \x00
  • sig[7] = \x02; по-видимому, представляет собой тип данных, которые следуют далее
  • sig[8] = sig[1] – 7 = \x7a
  • sig[9]…sig[x] = любые значение для переполнения кучи

Схема реализации атаки

Итак, как же мы можем повлиять на размер закодированной сигнатуры? Помимо изменения содержимого сертификата, мы можем изменить закодированную сигнатуру напрямую в OpenSSL через функцию ssl3_send_client_verify.

Рисунок 24: Измененная функция ssl3_send_client_verify

Несколько слов по поводу внесенных изменений. В первом вызове memcpy используется последовательность из трех байтов psig[2] – psig[4] (\x02\x01\x00). Полагаю, что первый байт отвечает за тип данных, второй байт – за размер данных, используемых в memcpy, и третий байт – за содержимое (src) для записи в память. Использование единичного байта \x00 приведет к тому, что при первом вызове memcpy будет записано 32 пустых байта в зарезервированный участок памяти размеров 64 байта.

Последующие 32 байта, а также до 90 дополнительных байт будут взяты из того, что находится после psig[7] и psig[8], после чего произойдет переполнение кучи.

В целях демонстрации я выставил общий размер сигнатуры 15,500. Хотя в функции CryptDecodeObject есть ограничение на общий размер сигнатуры (опытным путем выяснилось, что максимальное значение - \x81), функция DecodeSigAndReverse может принимать много больший размер закодированной сигнатуры (pbEncoded), и произвольные данные могут быть записаны в память перед обработкой функцией CryptDecodeObject. Примечание: по-видимому, функция DecodeSigAndReverse корректно выделяет память для закодированной сигнатуры, поскольку во время тестовой записи большой сигнатуры не является дополнительным условием для переполнения (то есть результирующая куча будет меньшего размера); тем не менее, возможность записи 10,000 и более байтов произвольных данных может оказаться полезным.

Далее показана практическая реализация приема, описанного выше:

Рисунок 25: Содержимое памяти во время записи огромного количества байтов

Как только дело доходит до функций memcpy, результирующая переполненная куча будет выглядеть так:

Рисунок 26: Содержимое кучи во время вызовов memcpy

Неудивительно, что переполнение вызывает ошибку Access Violation (в вашей системе результаты могут отличаться).

Рисунок 27: Ошибка, вызванная переполнением

Рисунок 28: После переполнения происходит автоматическая перезагрузка Windows

Следует отметить, приведенный пример может не вызвать как постоянное немедленное переполнения кучи, так и ошибки Access Violation.

Как говорилось ранее, я не буду публиковать ни полную версию рабочего эксплоита, ни модифицированную версию OpenSSL. С другой стороны, информации, приведенной в статье, более чем достаточно, для понимания принципа использования уязвимости (хотя надежность полученных результатов может быть под вопросом). При желании вы можете придумать дополнительные методы эксплуатации бреши. Однако даже этого демо примера вполне достаточно, чтобы нарушить работу IIS-службы и перезагрузить целевую машину, так что будьте внимательны.

Заключение

Вкратце повторим суть проблемы. Второй вызов memcpy в функции schannel!DecodeSigAndReverse может привести к переполнению кучи из-за предположения, что размер декодированной сигнатуры, рассчитываемый в функции CheckClientVerifyMessage, в два раза больше размера ключа. Поскольку мы управляем (в некоторой степени) размером и содержимым вызова memcpy (через закодированную сигнатуру), и функция CryptDecodeObject оставляет нетронутой закодированную сигнатуру, можно сформировать массив данных для переполнения кучи. Эту схему можно реализовать через модификацию в OpenSSL.

Надеюсь, вы подчерпнули для себя нечто новое, что касается использования уязвимости Winshock. Не забудьте установить патчи, исправляющие брешь, если вы этого еще не сделали.

До скорых встреч.