Как работает CVE-2019-0726
С выходом январских обновлений для Windows новость о критически опасной уязвимости CVE-2019-0547 в DHCP-клиентах всколыхнула общественность. Подогревали интерес высокий рейтинг CVSS и тот факт, что Microsoft не сразу опубликовал оценку эксплуатабельности, усложнив тем самым пользователям решение о неотложном обновлении систем. Некоторые издания даже предположили, что отсутствие индекса можно интерпретировать как свидетельство о том, что уже в ближайшее время появится рабочий эксплойт. Такие решения, как MaxPatrol 8, умеют выявлять уязвимые для определенных атак компьютеры в сети. Другие решения, такие как PT NAD, обнаруживают сами подобные атаки. Чтобы это стало возможным, необходимо описывать как правила выявления уязвимостей в продуктах, так и правила обнаружения атак на эти продукты. В свою очередь, чтобы это стало возможным, необходимо для каждой отдельно взятой уязвимости выяснять вектор, способ и условия ее эксплуатации, то есть буквально все детали и нюансы, связанные с эксплуатацией. Требуется гораздо более полное и глубокое понимание, нежели то, которое обычно можно составить по описаниям на сайтах вендоров или в CVE, вроде:
Уязвимость проявляется по той причине, что операционная система некорректно обрабатывает объекты в памяти.
Итак, чтобы добавить в продукты компании правила обнаружения атак на новоиспеченную уязвимость в DHCP, а также правила выявления устройств, ей подверженных, следовало разобраться в деталях. В случае бинарных уязвимостей для проникновения в суть лежащих в их основе ошибок часто используется patch-diff, то есть сравнение изменений, внесенных в бинарный код приложения, библиотеки или ядра операционной системы конкретным патчем, обновлением, исправляющим эту ошибку. Но первый этап — это всегда рекогносцировка.
Примечание:
Чтобы перейти непосредственно к описанию уязвимости, минуя лежащие в ее основе концепты DHCP, вы можете пропустить первые несколько страниц и обратиться сразу к разделу «Функция DecodeDomainSearchListData».
Рекогносцировка
Обращаемся в поисковик и просматриваем все известные на данный момент детали уязвимости. На этот раз деталей минимум, и все они являются вольными переработками информации, почерпнутой из оригинальной публикации на сайте MSRC. Такая ситуация вполне типична для ошибок, обнаруженных специалистами Microsoft во время внутреннего аудита.
Из публикации выясняем, что перед нами уязвимость типа memory corruption, содержащаяся как в клиентских, так и в серверных системах Windows 10 version 1803 и проявляющаяся в тот момент, когда злоумышленник отправляет специальным образом сформированные ответы DHCP-клиенту. Спустя пару дней с того момента на странице появятся также и индексы эксплуатабельности:
Как видно, MSRC проставили оценку «2 — Exploitation Less Likely». Это значит, что ошибка с большой вероятностью либо неэксплуатабельна вовсе, либо эксплуатация сопряжена с такими сложностями, преодоление которых потребует чересчур высоких трудозатрат. Следует признать, что Microsoft не свойственно занижать такие оценки. Отчасти на это влияет риск репутационных потерь, отчасти — некоторая независимость центра реагирования в рамках компании. by @it_haПоэтому предположим: раз в отчете угроза эксплуатации указана как маловероятная, наверняка так оно и есть. Собственно, на этом можно было бы завершить разбор, но не будет лишним перепроверить и хотя бы выяснить, в чем заключалась уязвимость. В конечном счете, несмотря на всю бесспорную индивидуальность, ошибки имеют свойство повторяться и проявлять себя в других местах.
С той же самой страницы скачиваем патч (security update), предоставляемый в виде .msu-архива, распаковываем его и ищем файлы, наиболее вероятно связанные с обработкой DHCP-ответов на клиентской стороне. В последнее время делать это стало гораздо сложнее, так как обновления стали поставляться не в виде отдельных пакетов, исправляющих конкретные ошибки, а в виде одного совокупного пакета, включающего все месячные исправления. Это сильно увеличило лишний шум, то есть не относящиеся к нашей задаче изменения.
Среди всего множества файлов поиск обнаруживает несколько подходящих под фильтр библиотек, которые мы сравниваем с их версиями на непропатченной системе. Библиотека dhcpcore.dll выглядит наиболее многообещающе. При этом BinDiff выдает минимальные изменения:
Собственно, отличные от косметических правки внесены в одну-единственную функцию — DecodeDomainSearchListData. Если вы хорошо знакомы с протоколом DHCP и его не слишком часто используемыми опциями, то уже можете предположить, что за список обрабатывает эта функция. Если же нет, то переходим ко второму этапу — изучению протокола.
DHCP и его опции
DHCP (RFC 2131 | wiki) — это расширяемый протокол, способность к пополнению возможностей которого обеспечивается полем options. Каждая опция описывается уникальным тегом (номером, идентификатором), размером, занимаемым данными, содержащимися в опции, и самими данными. Подобная практика типична для сетевых протоколов, и одной из таких «имплантированных» в протокол опций является Domain Search Option, описанная в RFC 3397. Она позволяет DHCP-серверу устанавливать на клиентах стандартные окончания доменных имен, которые будут использоваться в качестве DNS-суффиксов для настраиваемого таким образом соединения.
Пусть, для примера, на нашем клиенте были заданы следующие окончания имен:
.microsoft.com .wikipedia.org
Тогда при любой попытке определить адрес по доменному имени в DNS-запросы будут подставляться по очереди суффиксы из этого списка до тех пор, пока не будет найдено успешное отображение. Например, если пользователь ввел ru в адресной строке браузера, то будут сформированы DNS-запросы сначала для ru.microsoft.com, затем для ru.wikipedia.org:
На самом деле, современные браузеры чересчур умные, а потому на имена, не похожие на FQDN, реагируют перенаправлением в поисковик. Поэтому ниже прилагаем вывод менее избалованных утилит:
Читателю могло показаться, что в этом и состоит уязвимость, ведь сама по себе возможность подменять DNS-суффиксы с помощью DHCP-сервера, каковым может себя идентифицировать любое устройство в сети, представляет угрозу для клиентов, запрашивающих какие бы то ни было параметры сети по DHCP. Но нет: как следует из RFC, это считается вполне легитимным, документированным поведением. Собственно, DHCP-сервер по сути своей является одним из тех доверенных компонентов, которые могут оказывать сильное влияние на обращающиеся к ним устройства.
Опция Domain Search
Domain Search Option имеет номер 0x77 (119). Как и все опции, она кодируется однобайтовым тегом с номером опции. Как и у большинства прочих опций, сразу за тегом идет однобайтовый размер следующих за размером данных. Экземпляры опции могут присутствовать в DHCP-сообщении более одного раза. В этом случае данные со всех таких секций конкатенируются в той последовательности, в которой встречаются в сообщении.
В представленном примере, взятом из RFC 3397, данные разбиты на три секции, каждая по 9 байт. Как несложно понять из картинки, имена поддоменов в полном доменном имени кодируются однобайтовой длиной имени, непосредственно за которой следует само имя. Заканчивается кодирование полного доменного имени нулевым байтом (то есть нулевым размером имени поддомена).
Помимо этого, в опции используется простейший метод сжатия данных, а точнее, просто точки повторной обработки (reparse points). Вместо размера доменного имени поле может содержать значение 0xc0. Тогда следующий за ним байт задает смещение относительно начала данных опции, по которому следует искать окончание доменного имени.
Таким образом, в рассматриваемом примере закодирован список из двух доменных суффиксов:
.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\{INTERFACE_GUID}\
хранящего основные параметры конкретного сетевого интерфейса.
Функция DecodeDomainSearchListData отрабатывает за два прохода. На первом проходе она выполняет все действия, кроме записи в выходной буфер. Таким образом, первый проход посвящен подсчету размера памяти, необходимого для размещения возвращаемых данных. На втором проходе уже происходит выделение памяти под эти данные и заполнение выделенной памяти. Функция довольно невелика, порядка 250 инструкций, и основная ее работа заключается в обработке каждого из трех возможных вариантов представленного во входящем потоке символа: 1) 0x00, 2) 0xc0, или 3) все остальные значения. Предположительное исправление ошибки, связанной с DHCP, по большому счету сводится к добавлению в начале второго прохода проверки размера результирующего буфера. Если этот размер равен нулю, то память под буфер не выделяется и функция сразу завершает исполнение и возвращает ошибку:
Получается, уязвимость проявляет себя в тех случаях, когда размер целевого буфера оказывается равен нулю. При этом в самом начале выполнения функция проверяет входящие данные, размер которых не может быть меньше двух байтов. Стало быть, для эксплуатации требуется подобрать таким образом сформированную непустую опцию доменных суффиксов, чтобы размер выходного буфера был нулевым.
Эксплуатация
Первым делом в голову приходит мысль, что можно использовать описанные ранее 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
Что же происходит в случае, когда атакующий отправляет сформированный описанным способом буфер? Если разобраться в примере, то видно, что список, содержащийся в нем, состоит из одного элемента — пустой строки. На первом проходе функция подсчитывает размер данных на выходе. Так как данные не содержат ни одного ненулевого доменного имени, то размер равен нулю.
На втором проходе происходит выделение блока динамической памяти для размещения данных в нем и копирование самих данных. Но функция разбора сразу встречает нулевой символ, означающий конец доменного имени, а потому, как и было сказано, заменяет предыдущий символ с точки на запятую. by @it_ha. И здесь мы сталкиваемся с проблемой. Итератор целевого буфера находится в нулевой позиции. Предыдущего символа нет. Предыдущий символ принадлежит заголовку блока динамической памяти. И этот самый символ будет заменен на 0x2c, то есть на запятую.
Впрочем, так происходит только на 32-битных системах. Использование unsigned int для хранения текущей позиции итератора целевого буфера вносит свои коррективы в обработку на x64-системах. Обратим более пристальное внимание на кусок кода, отвечающий за запись запятой в буфер:
Вычитание единицы из текущей позиции происходит с использованием 32-битного регистра eax, в то время как при адресации буфера код обращается к полному 64-битному регистру rax. В архитектуре AMD64 любые операции с 32-битными регистрами обнуляют старшую часть регистра. Это означает, что в регистре rax, содержавшем прежде нуль, после вычитания будет храниться не значение –1, а 0xffffffff. Следовательно, на 64-битных системах значение 0x2c будет записываться по адресу buf[0xffffffff], то есть далеко за границами выделенной под буфер памяти.
Полученные данные хорошо согласуются с оценкой эксплуатабельности от Microsoft, ведь для того, чтобы воспользоваться данной уязвимостью, атакующему требуется научиться удаленно производить heap spraying на DHCP-клиенте и при этом иметь достаточный контроль над распределением динамической памяти, чтобы запись заранее заданных значений, а именно запятой и нулевого байта, производилась в подготовленный адрес и приводила к контролируемым негативным последствиям. В противном случае запись данных по невыверенному адресу будет иметь в качестве последствия падение процесса svchost.exe вместе со всеми хостящимися в нем на этот момент сервисами — и дальнейший перезапуск этих сервисов операционной системой. Факт, который злоумышленники в определенных условиях также могут использовать себе во благо.
Вот, казалось бы, и все, что можно сказать об исследуемой ошибке. Только остается ощущение, будто это далеко не конец. Будто мы не рассмотрели все варианты. Должно быть нечто большее, что скрыто в этих строках.
CVE-2019-0726
Вероятно, так оно и есть. Если пристально посмотреть на вид данных, провоцирующих ошибку, и сопоставить их с тем, как именно эта ошибка возникает, можно заметить, что список доменных имен может быть изменен таким образом, что результирующий буфер будет ненулевого размера, но попытка записи за его пределы все так же будет производиться. Для этого первый элемент в списке должен быть пустой строкой, а все остальные могут содержать нормальные доменные окончания. Например:
Представленная опция включает в себя два элемента. Первый доменный суффикс пуст, он сразу заканчивается нулевым байтом. Второй суффикс — .ru. Подсчитанный размер строки на выходе будет равен трем байтам, что позволит преодолеть налагаемую январским обновлением проверку на пустоту целевого буфера. by @it_ha В то же время нуль в самом начале данных вынудит функцию записать предыдущим символом в результирующую строку запятую, но так как текущая позиция итератора в строке, как и в рассмотренном ранее случае, равна нулю, то запись вновь произойдет за пределы выделенного буфера.
Теперь следует подтвердить полученные теоретические результаты на практике. Моделируем ситуацию, в которой DHCP-сервер шлет в ответ на запрос от клиента сообщение с представленной опцией, и сразу же ловим исключение при попытке записи запятой в позицию 0xffffffff выделенного под результирующую строку буфера:
*** An Access Violation occurred in C:\WINDOWS\System32\svchost.exe -k LocalServiceNetworkRestricted -p: The instruction at 00007FFB34413D87 tried to write to an invalid address, 0000025301FFC3DF *** enter .exr 000000D5FCDFD3B0 for the exception record *** enter .cxr 000000D5FCDFCEC0 for the context 3: kd> .exr 000000D5FCDFD3B0 ExceptionAddress: 00007ffb34413d87 (dhcpcore!DecodeDomainSearchListData+0x01cb) ExceptionCode: c0000005 (Access violation) Parameter[0]: 0000000000000001 Parameter[1]: 0000025301ffc3df Attempt to write to address 0000025301ffc3df 3: kd> .cxr 000000D5FCDFCEC0 rax=00000000ffffffff rbx=0000025200ad6fd0 rcx=0000000000000001 rdx=0000000000000000 rsi=0000025200ad6fc8 rdi=0000025201ffc3e0 rip=00007ffb34413d87 rsp=000000d5fcdfd5c0 rbp=0000000000000002 r8=0000025200ad7100 r9=0000000000000000 r10=ff3fff3f3fffc300 dhcpcore!DecodeDomainSearchListData+0x1cb: 0033:00007ffb`34413d87 c604382c mov byte ptr [rax+rdi],2Ch ds:002b:00000253`01ffc3df=?? 3: kd> k # Child-SP RetAddr Call Site 00 000000d5`fcdfd5c0 00007ffb`34413ea7 dhcpcore!DecodeDomainSearchListData+0x1cb 01 000000d5`fcdfd630 00007ffb`34427090 dhcpcore!UpdateDomainSearchOption+0x5b 02 000000d5`fcdfd680 00007ffb`34418e68 dhcpcore!DhcpExtractFullOptions+0x2a0 03 000000d5`fcdfe1a0 00007ffb`3442c465 dhcpcore!UpdateDhcpContext+0x80 04 000000d5`fcdfe1f0 00007ffb`34428d45 dhcpcore!RenewLease+0x6cd 05 000000d5`fcdfe5b0 00007ffb`3442ba2a dhcpcore!DhcpRenewState+0xe9 06 000000d5`fcdfe720 00007ffb`3443b29b dhcpcore!ReRenewParameters+0x26a 07 000000d5`fcdfe9f0 00007ffb`34421ad8 dhcpcore!AcquireParameters+0x8b 08 000000d5`fcdfea20 00007ffb`34424a34 dhcpcore!DhcpApiProcessAdapterOnlyApi+0x310 09 000000d5`fcdfed90 00007ffb`398243f3 dhcpcore!RpcSrvRenewLease+0x94 0a 000000d5`fcdfedc0 00007ffb`3988dbdd RPCRT4!Invoke+0x73 3: kd> db r8 00000252`00ad7100 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000252`00ad7110 00 00 00 00 00 00 00 00-00 00 00 00 63 82 53 63 ............c.Sc 00000252`00ad7120 35 01 05 36 04 c0 a8 11-fe 33 04 00 00 07 08 01 5..6.....3...... 00000252`00ad7130 04 ff ff ff 00 03 04 c0-a8 11 02 06 04 c0 a8 11 ................ 00000252`00ad7140 02 0f 0b 6c 6f 63 61 6c-64 6f 6d 61 69 6e 2c 04 ...localdomain,. 00000252`00ad7150 c0 a8 11 02 77 05 00 02-72 75 00 ff 0e 01 03 06 ....w...ru......
Здесь регистр r8 содержит указатель на входящие опции, rdi — адрес выделенного целевого буфера, а rax — позицию в этом буфере, в которую нужно записать символ. Такие результаты мы получили на полностью обновленной системе (по состоянию на январь 2019 года).
Пишем об обнаруженной проблеме в Microsoft и… они теряют письмо. Да, такое иногда случается даже с зарекомендовавшими себя вендорами. Никакая система не идеальна, и приходится в этом случае искать другие пути коммуникации. Поэтому неделю спустя, не получив даже автоответа за это время, связываемся напрямую с менеджером через Twitter и по результатам нескольких дней анализа заявки выясняем, что отправленные детали не имеют никакого отношения к CVE-2019-0547 и представляют собой самостоятельную уязвимость, для которой будет заведен новый CVE-идентификатор. Еще месяц спустя, в марте, выходит соответствующее исправление, а ошибка получает номер CVE-2019-0726
Вот так можно иногда в попытках разобраться в подробностях уязвимости 1-day случайно обнаружить 0-day, просто доверившись своей интуиции.
Источник: ptsecurity.com