Хабрахабр

Безопасность DHCP в Windows 10: разбираем критическую уязвимость CVE-2019-0726

Изображение: Pexels

Подогревали интерес высокий рейтинг CVSS и тот факт, что Microsoft не сразу опубликовал оценку эксплуатабельности, усложнив тем самым пользователям решение о неотложном обновлении систем. С выходом январских обновлений для Windows новость о критически опасной уязвимости CVE-2019-0547 в DHCP-клиентах всколыхнула общественность. Некоторые издания даже предположили, что отсутствие индекса можно интерпретировать как свидетельство о том, что уже в ближайшее время появится рабочий эксплойт.

Другие решения, такие как PT NAD, обнаруживают сами подобные атаки. Такие решения, как MaxPatrol 8, умеют выявлять уязвимые для определенных атак компьютеры в сети. В свою очередь, чтобы это стало возможным, необходимо для каждой отдельно взятой уязвимости выяснять вектор, способ и условия ее эксплуатации, то есть буквально все детали и нюансы, связанные с эксплуатацией. Чтобы это стало возможным, необходимо описывать как правила выявления уязвимостей в продуктах, так и правила обнаружения атак на эти продукты. Требуется гораздо более полное и глубокое понимание, нежели то, которое обычно можно составить по описаниям на сайтах вендоров или в CVE, вроде:

Уязвимость проявляется по той причине, что операционная система некорректно обрабатывает объекты в памяти.

Итак, чтобы добавить в продукты компании правила обнаружения атак на новоиспеченную уязвимость в DHCP, а также правила выявления устройств, ей подверженных, следовало разобраться в деталях. В случае бинарных уязвимостей для проникновения в суть лежащих в их основе ошибок часто используется patch-diff, то есть сравнение изменений, внесенных в бинарный код приложения, библиотеки или ядра операционной системы конкретным патчем, обновлением, исправляющим эту ошибку. Но первый этап — это всегда рекогносцировка.

Примечание: Чтобы перейти непосредственно к описанию уязвимости, минуя лежащие в ее основе концепты DHCP, вы можете пропустить первые несколько страниц и обратиться сразу к разделу «Функция DecodeDomainSearchListData».

Рекогносцировка

Обращаемся в поисковик и просматриваем все известные на данный момент детали уязвимости. На этот раз деталей минимум, и все они являются вольными переработками информации, почерпнутой из оригинальной публикации на сайте MSRC. Такая ситуация вполне типична для ошибок, обнаруженных специалистами Microsoft во время внутреннего аудита.

Спустя пару дней с того момента на странице появятся также и индексы эксплуатабельности: Из публикации выясняем, что перед нами уязвимость типа memory corruption, содержащаяся как в клиентских, так и в серверных системах Windows 10 version 1803 и проявляющаяся в тот момент, когда злоумышленник отправляет специальным образом сформированные ответы DHCP-клиенту.

Это значит, что ошибка с большой вероятностью либо неэксплуатабельна вовсе, либо эксплуатация сопряжена с такими сложностями, преодоление которых потребует чересчур высоких трудозатрат. Как видно, MSRC проставили оценку «2 — Exploitation Less Likely». Отчасти на это влияет риск репутационных потерь, отчасти — некоторая независимость центра реагирования в рамках компании. Следует признать, что Microsoft не свойственно занижать такие оценки. Собственно, на этом можно было бы завершить разбор, но не будет лишним перепроверить и хотя бы выяснить, в чем заключалась уязвимость. Поэтому предположим: раз в отчете угроза эксплуатации указана как маловероятная, наверняка так оно и есть. В конечном счете, несмотря на всю бесспорную индивидуальность, ошибки имеют свойство повторяться и проявлять себя в других местах.

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

Библиотека dhcpcore.dll выглядит наиболее многообещающе. Среди всего множества файлов поиск обнаруживает несколько подходящих под фильтр библиотек, которые мы сравниваем с их версиями на непропатченной системе. При этом BinDiff выдает минимальные изменения:

Если вы хорошо знакомы с протоколом DHCP и его не слишком часто используемыми опциями, то уже можете предположить, что за список обрабатывает эта функция. Собственно, отличные от косметических правки внесены в одну-единственную функцию — DecodeDomainSearchListData. Если же нет, то переходим ко второму этапу — изучению протокола.

DHCP и его опции

DHCP (RFC 2131 | wiki) — это расширяемый протокол, способность к пополнению возможностей которого обеспечивается полем options. Каждая опция описывается уникальным тегом (номером, идентификатором), размером, занимаемым данными, содержащимися в опции, и самими данными. Подобная практика типична для сетевых протоколов, и одной из таких «имплантированных» в протокол опций является Domain Search Option, описанная в RFC 3397. Она позволяет DHCP-серверу устанавливать на клиентах стандартные окончания доменных имен, которые будут использоваться в качестве DNS-суффиксов для настраиваемого таким образом соединения.

Пусть, для примера, на нашем клиенте были заданы следующие окончания имен:

.microsoft.com .wikipedia.org

Например, если пользователь ввел ru в адресной строке браузера, то будут сформированы DNS-запросы сначала для ru.microsoft.com, затем для ru.wikipedia.org: Тогда при любой попытке определить адрес по доменному имени в DNS-запросы будут подставляться по очереди суффиксы из этого списка до тех пор, пока не будет найдено успешное отображение.

Поэтому ниже прилагаем вывод менее избалованных утилит: На самом деле, современные браузеры чересчур умные, а потому на имена, не похожие на FQDN, реагируют перенаправлением в поисковик.

Но нет: как следует из RFC, это считается вполне легитимным, документированным поведением. Читателю могло показаться, что в этом и состоит уязвимость, ведь сама по себе возможность подменять DNS-суффиксы с помощью DHCP-сервера, каковым может себя идентифицировать любое устройство в сети, представляет угрозу для клиентов, запрашивающих какие бы то ни было параметры сети по DHCP. Собственно, DHCP-сервер по сути своей является одним из тех доверенных компонентов, которые могут оказывать сильное влияние на обращающиеся к ним устройства.

Опция Domain Search

Domain Search Option имеет номер 0x77 (119). Как и все опции, она кодируется однобайтовым тегом с номером опции. Как и у большинства прочих опций, сразу за тегом идет однобайтовый размер следующих за размером данных. Экземпляры опции могут присутствовать в DHCP-сообщении более одного раза. В этом случае данные со всех таких секций конкатенируются в той последовательности, в которой встречаются в сообщении.

Как несложно понять из картинки, имена поддоменов в полном доменном имени кодируются однобайтовой длиной имени, непосредственно за которой следует само имя. В представленном примере, взятом из RFC 3397, данные разбиты на три секции, каждая по 9 байт. Заканчивается кодирование полного доменного имени нулевым байтом (то есть нулевым размером имени поддомена).

Вместо размера доменного имени поле может содержать значение 0xc0. Помимо этого, в опции используется простейший метод сжатия данных, а точнее, просто точки повторной обработки (reparse points). Тогда следующий за ним байт задает смещение относительно начала данных опции, по которому следует искать окончание доменного имени.

Таким образом, в рассматриваемом примере закодирован список из двух доменных суффиксов:

.eng.apple.com
.marketing.apple.com

Функция DecodeDomainSearchListData

Итак, опция DHCP под номером 0x77 (119) позволяет серверу настраивать на клиентах DNS-суффиксы. Но не на машинах с операционными системами семейства Windows. Системы от Microsoft традиционно игнорировали эту опцию, поэтому исторически окончания DNS-имен в случае необходимости накатывались через групповые политики. Так продолжалось до недавнего времени, когда в очередном релизе Windows 10, версии 1803, была добавлена обработка для Domain Search Option. Судя по названию функции в dhcpcore.dll, в которую были внесены изменения, именно в добавленном обработчике и кроется рассматриваемая ошибка.

Причесываем немного код и выясняем следующее. Приступаем к работе. На входе она получает упакованный описанным в предыдущем пункте способом массив данных, а на выходе генерирует нуль-терминированную строку, содержащую список окончаний доменных имен, разделенных запятыми. Процедура DecodeDomainSearchListData, в полном соответствии с названием, декодирует данные из Domain Search Option поступившего от сервера сообщения. Например, данные из примера выше эта функция преобразует в строку:

eng.apple.com,marketing.apple.com

Вызывается DecodeDomainSearchListData из процедуры UpdateDomainSearchOption, которая прописывает возвращенный список в значение «DhcpDomainSearchList» ключа реестра:
HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\\
хранящего основные параметры конкретного сетевого интерфейса.

На первом проходе она выполняет все действия, кроме записи в выходной буфер. Функция DecodeDomainSearchListData отрабатывает за два прохода. На втором проходе уже происходит выделение памяти под эти данные и заполнение выделенной памяти. Таким образом, первый проход посвящен подсчету размера памяти, необходимого для размещения возвращаемых данных. Предположительное исправление ошибки, связанной с DHCP, по большому счету сводится к добавлению в начале второго прохода проверки размера результирующего буфера. Функция довольно невелика, порядка 250 инструкций, и основная ее работа заключается в обработке каждого из трех возможных вариантов представленного во входящем потоке символа: 1) 0x00, 2) 0xc0, или 3) все остальные значения. Если этот размер равен нулю, то память под буфер не выделяется и функция сразу завершает исполнение и возвращает ошибку:

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

Эксплуатация

Первым делом в голову приходит мысль, что можно использовать описанные ранее reparse points для того, чтобы непустые данные на входе генерировали пустую строку на выходе:

Происходит это по следующей причине. Сервер, настроенный посылать в ответе опцию с таким содержимым, действительно вызовет access violation на необновленных клиентах. В примере, взятом из RFC, в буфер будут скопированы данные в следующем порядке: На каждом шаге, когда функция разбирает часть полного доменного имени, она копирует ее в целевой буфер и ставит после нее точку.

1). eng.

2). eng.apple.

3). eng.apple.com.

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

4). eng.apple.com,

и продолжает разбор:

5). eng.apple.com,marketing.

6). eng.apple.com,marketing.apple.

7). eng.apple.com,marketing.apple.com.

8). eng.apple.com,marketing.apple.com,

По окончании входных данных остается лишь заменить последнюю запятую на нулевой символ и получается готовая к записи в реестр строка:

9). eng.apple.com,marketing.apple.com

Что же происходит в случае, когда атакующий отправляет сформированный описанным способом буфер? Если разобраться в примере, то видно, что список, содержащийся в нем, состоит из одного элемента — пустой строки. На первом проходе функция подсчитывает размер данных на выходе. Так как данные не содержат ни одного ненулевого доменного имени, то размер равен нулю.

Но функция разбора сразу встречает нулевой символ, означающий конец доменного имени, а потому, как и было сказано, заменяет предыдущий символ с точки на запятую. На втором проходе происходит выделение блока динамической памяти для размещения данных в нем и копирование самих данных. Итератор целевого буфера находится в нулевой позиции. И здесь мы сталкиваемся с проблемой. Предыдущий символ принадлежит заголовку блока динамической памяти. Предыдущего символа нет. И этот самый символ будет заменен на 0x2c, то есть на запятую.

Использование unsigned int для хранения текущей позиции итератора целевого буфера вносит свои коррективы в обработку на x64-системах. Впрочем, так происходит только на 32-битных системах. Обратим более пристальное внимание на кусок кода, отвечающий за запись запятой в буфер:

В архитектуре AMD64 любые операции с 32-битными регистрами обнуляют старшую часть регистра. Вычитание единицы из текущей позиции происходит с использованием 32-битного регистра eax, в то время как при адресации буфера код обращается к полному 64-битному регистру rax. Следовательно, на 64-битных системах значение 0x2c будет записываться по адресу buf[0xffffffff], то есть далеко за границами выделенной под буфер памяти. Это означает, что в регистре rax, содержавшем прежде нуль, после вычитания будет храниться не значение –1, а 0xffffffff.

В противном случае запись данных по невыверенному адресу будет иметь в качестве последствия падение процесса svchost.exe вместе со всеми хостящимися в нем на этот момент сервисами — и дальнейший перезапуск этих сервисов операционной системой. Полученные данные хорошо согласуются с оценкой эксплуатабельности от Microsoft, ведь для того, чтобы воспользоваться данной уязвимостью, атакующему требуется научиться удаленно производить heap spraying на DHCP-клиенте и при этом иметь достаточный контроль над распределением динамической памяти, чтобы запись заранее заданных значений, а именно запятой и нулевого байта, производилась в подготовленный адрес и приводила к контролируемым негативным последствиям. Факт, который злоумышленники в определенных условиях также могут использовать себе во благо.

Только остается ощущение, будто это далеко не конец. Вот, казалось бы, и все, что можно сказать об исследуемой ошибке. Должно быть нечто большее, что скрыто в этих строках. Будто мы не рассмотрели все варианты.

CVE-2019-0726

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

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

Моделируем ситуацию, в которой DHCP-сервер шлет в ответ на запрос от клиента сообщение с представленной опцией, и сразу же ловим исключение при попытке записи запятой в позицию 0xffffffff выделенного под результирующую строку буфера: Теперь следует подтвердить полученные теоретические результаты на практике.

Такие результаты мы получили на полностью обновленной системе (по состоянию на январь 2019 года). Здесь регистр r8 содержит указатель на входящие опции, rdi — адрес выделенного целевого буфера, а rax — позицию в этом буфере, в которую нужно записать символ.

Да, такое иногда случается даже с зарекомендовавшими себя вендорами. Пишем об обнаруженной проблеме в Microsoft и… они теряют письмо. Поэтому неделю спустя, не получив даже автоответа за это время, связываемся напрямую с менеджером через Twitter и по результатам нескольких дней анализа заявки выясняем, что отправленные детали не имеют никакого отношения к CVE-2019-0547 и представляют собой самостоятельную уязвимость, для которой будет заведен новый CVE-идентификатор. Никакая система не идеальна, и приходится в этом случае искать другие пути коммуникации. Еще месяц спустя, в марте, выходит соответствующее исправление, а ошибка получает номер CVE-2019-0726.

Вот так можно иногда в попытках разобраться в подробностях уязвимости 1-day случайно обнаружить 0-day, просто доверившись своей интуиции.

Автор: Михаил Цветков, специалист отдела анализа приложений Positive Technologies.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»