• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Анатомия RCE эксплойта - SIGRed (CVE-2020-1350)

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Автор: Valentina Palmiotti (@chompie), ведущий исследователь безопасности

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

RCE PoC для CVE-2020-1350 SIGRed можно найти здесь: https://github.com/chompie1337/SIGRed_RCE_PoC

Обзор
SIGRed, CVE-2020-1350, представляет собой уязвимость в службе DNS Microsoft Windows, которая была обнаружена 14 июля 2020 года. Она была обнаружена Саги Цадиком из Check Point Research [1], которая опубликовала подробное описание ошибка в день выпуска патча. Уязвимость получила 10,0 балла по шкале CVSS - наивысший уровень серьезности. В Windows DNS-сервер является контроллером домена, а его администраторы входят в группу администраторов домена. По умолчанию группа администраторов домена является членом группы администраторов на всех компьютерах, которые присоединились к домену, включая контроллеры домена [8]. При осторожном использовании злоумышленники могут удаленно выполнить код в уязвимой системе и получить права администратора домена, эффективно поставив под угрозу всю корпоративную инфраструктуру. Эта статья содержит подробное описание методов эксплойтов, использованных в выпущенном доказательстве концепции [2].

Уязвимость
CVE-2020-1350 - это уязвимость, integer overflow, которая приводит к heap-based buffer переполнению при обработке искаженных записей ресурсов SIG DNS. Запись SIG - это тип записи ресурса DNS, который содержит цифровую подпись для набора записей (одна или несколько записей DNS с одинаковым именем и типом) [10]. Чтобы использовать SIGRed, злоумышленник может настроить «злой» домен, NS-запись которого указывает на вредоносный DNS-сервер. Когда клиент делает DNS-запрос для «злого» домена серверу-жертве, сервер-жертва запрашивает DNS-сервер, расположенный над ним. DNS-сервер ответит NS-записью, указывающей, что вредоносный DNS-сервер является органом власти для этого домена, и запись будет кэширована жертвой. Впоследствии, когда клиент отправляет жертве запрос DNS SIG для домена, сервер жертвы запрашивает вредоносный DNS-сервер. Вредоносный DNS-сервер отправит в ответ искаженную DNS-запись SIG.

1.jpg


Уязвимость присутствует в функции dns!SigWireRead (в двоичном файле службы DNS, dns.exe), которая используется для кэширования ответа DNS-записи SIG от другого DNS-сервера.

2.jpg


См. Строку 11 в декомпиляции уязвимой функции dns! SigWireRead. В функцию RR_AllocateEx передается 16-битовое целое число без знака в качестве параметра размера. Можно отправить ответ записи DNS SIG, рассчитанный размер которого превышает 0xFFFF, что вызывает integer overflow.

Триггерим уязвимость
Из-за ограничений на размер пакета невозможно вызвать уязвимость, отправив только SIG-запись с очень большой подписью. Фактически, запуск уязвимости невозможен через UDP, потому что максимально допустимый размер сообщения DNS через UDP составляет 512 или 4096 байт, в зависимости от того, поддерживает ли сервер EDNS0. В любом случае этого недостаточно, чтобы вызвать уязвимость. Используя усечение DNS [9], злонамеренный DNS-сервер может сказать серверу-жертве повторить запрос по TCP. Общий предел размера сообщения DNS по TCP составляет 64 КБ (0xFFFF). Этого все еще недостаточно, чтобы активировать уязвимость, поскольку лимит сообщений включает пространство для заголовков и исходного запроса.

DNS Name Compression
DNS-имена могут быть сжаты в DNS-сообщении, и часто это происходит. Управляя сжатием имени в сообщении DNS через TCP, можно увеличить sigName.Length без увеличения общего размера сообщения DNS.

3.jpg


В приведенном выше примере для байта 0xC0 (выделенного в зеленом поле выше) установлены два старших бита. Это означает, что следующие 14 битов представляют смещение имени DNS относительно начала сообщения DNS. В примере, изображенном выше, это 0xC байтов (выделено синим полем) от начала сообщения.

Имена DNS кодируются следующим образом: один байт обозначает количество символов перед символом «.», Заканчивающимся нулевым байтом. Например, www.google.com может быть закодирован как:

4.jpg


что указывает на то, что имя состоит из строки из 3 байтов, точки, 6 байтов, точки и трех байтов, оканчивающихся null.

В случае, изображенном в приведенном выше захвате пакета, смещение указывает на 0x1, потому что запрошенный домен начинается с ’9’. Следовательно, если мы изменим следующие 14 бит с 0x0C на 0x0D, смещение DNS-имени будет указывать на 0x39, указывая на то, что следующая часть имени DNS - 0x39 байт вперед, которая распространяется на часть подписи пакета. Таким образом, мы можем обманом заставить службу Windows DNS вычислить размер DNS-имени, который будет намного больше, чем количество фактических байтов, используемых для представления DNS-имени.

Как видно из декомпиляции dns!SigWireRead, общий размер, передаваемый в RR_AllocateEx, рассчитывается как: signatureLength + sigName.Length + 0x14. Максимальная длина DNS-имени составляет 0xFF байт. Дополнительных байтов от длины фальшивого имени достаточно, чтобы вызвать уязвимость, а также не превышать максимальный размер сообщения 65 КБ!

Более подробное объяснение уязвимости и способов ее активации, в том числе ее активации из браузера, см. В исходной статье Check Point Research [1].

Эксплуатация
Это был мой первый раз, когда я писал эксплойт RCE в пользовательском режиме, и я много узнал об эксплуатации кучи. Я надеюсь, что эта статья поможет другим, кто заинтересован в изучении эксплуатации кучи.

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

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

WinDNS Heap Manager
Прежде всего необходимо понять, как служба WinDNS управляет памятью кучи. Служба WinDNS управляет собственными пулами памяти [3]. Если запрошенный размер буфера превышает 0xA0 байт, он запросит память у собственного диспетчера кучи Windows (HeapAlloc). В противном случае он будет использовать бакет пула памяти (размеры 0x50, 0x68, 0x88 и 0xa0). Буферы в каждом из сегментов хранятся в односвязном списке. Если в выбранном сегменте больше нет доступных буферов, из собственной кучи будет запрошен фрагмент памяти, разделенный на отдельные буферы, а затем добавленный в список соответствующего сегмента. Для буферов размером 0x50, 0x68, 0x88 и 0xA0 запрашиваются блоки памяти размером 0xFF0, 0xFD8, 0xFF0 и 0xFA0 соответственно.

5.jpg


Когда буферы в одном из сегментов памяти освобождаются, они не возвращаются в собственную кучу Windows. Вместо этого они добавляются обратно в список доступных буферов для этого сегмента. Буферы распределяются по принципу «последним вошел - первым ушел» (LIFO), то есть последний освобожденный буфер будет следующим, который будет выделен.

6.jpg


WinDNS Buffer Structure
Структура буфера WinDNS следующая:

7.jpg


Это пригодится при эксплуатации.

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

Изучая структуру кучи, я обнаружил, что собственный диспетчер кучи Windows выделял блоки памяти бакета во «внутреннем» сегменте кучи размером 0x41FD0-0x41FF0. По моим наблюдениям, эти сегменты кучи содержат только фрагменты памяти, используемые для сегментов памяти WinDNS. Итак, если мы гарантируем, что размер переполненного буфера меньше 0xA0, мы можем быть уверены, что он будет в одном из этих фрагментов.

8.jpg


Этот буфер будет где-то внутри сегмента кучи размером ~ 0x41ff0. Общее количество необходимых байтов немного больше 0xFFFF. Следовательно, вероятность того, что полное переполнение окажется по действительному адресу памяти, относительно высока.

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

9.jpg


Процесс создания дыры в куче и ее перераспределения состоит из нескольких основных шагов:
  • Сделайте много запросов для поддоменов злого домена к серверу жертвы.
  • Вредоносный DNS-сервер отправит жертве ответ, который она кэширует в динамической памяти (heap spray).
  • Вредоносный DNS-сервер назначит длинный TTL (Time-To-Live) для всех поддоменов, кроме одного, которому будет присвоен короткий TTL.
  • WinDNS освобождает буферы для просроченных записей каждые ~ 2 минуты, поэтому мы ждем освобождения буфера.
  • Сделайте еще один запрос для субдомена, чья запись SIG только что истекла; на этот раз злонамеренный DNS-сервер даст неправильный ответ, чтобы вызвать переполнение.
  • Поскольку буферы выделяются LIFO, новый буфер записи будет иметь тот же адрес, что и просроченная запись SIG в памяти.

10.jpg


Предотвращение сбоев из-за перезаписи других объектов в куче
Хотя мне удалось надежно избежать ошибок сегментации во время memcpy, я все же столкнулся со многими другими сбоями из-за перезаписи объектов в куче.

11.jpg


Я решил заглянуть в WinDbg, чтобы узнать, какие типы буферов выделяются рядом с переполненным буфером.

12.jpg


Я увидел, что новые блоки памяти WinDNS размером 0xFF0 и 0xFA0 выделялись рядом с буфером кучи, который я переполнял. Последнее возникло в результате распыления кучи (буферы записи размером 0xA0). Но как насчет кусков размером 0xFF0? Они содержали буферы размером 0x88, используемые для хранения объектов, связанных с кешем записей DNS, который сохраняется в виде двоичного дерева. Я перезаписывал объекты дерева кеша и вызывал сбой при обходе дерева.

На этом этапе решение стало ясным. Помните, что переполненный буфер находится в сегменте кучи, который используется только другими блоками памяти, управляемыми WinDNS. Это означает, что перезаписываемые объекты имеют размер <= 0xA0 и помещаются в один из сегментов памяти, управляемых WinDNS. Мы знаем, что буферы в этих сегментах памяти никогда не возвращаются в исходную кучу, а вместо этого возвращаются в список свободных буферов соответствующего размера сегмента. Итак, мы можем очистить кучу, принудительно выделив много буферов размером 0x88 и освободив их. Как только они будут освобождены, они будут возвращены в список свободных буферов, избегая необходимости выделять новую память в куче. Затем мы можем обработать кучу множеством буферов, которые не будут освобождены, чтобы гарантировать, что буфер, который мы переполняем, будет в новом сегменте кучи, вдали от объектов, которые мы не хотим перезаписывать.

13.jpg


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

Знай свое окружение
Поскольку мы спреим кучу, будет выделено много новых блоков памяти. Эти буферы добавляются к свободному списку непрерывно, что означает, что они распределяются в непрерывном порядке. Следовательно, порядок выполнения запросов к записи DNS SIG будет тем же порядком, в котором они появляются в куче. Итак, мы точно знаем, какие записи будут перезаписаны при переполнении.

14.jpg


RR_Record Structure
Во-первых, давайте посмотрим на структуру кэшированной записи WinDNS:

15.jpg


Знание этого и структуры WINDNS_BUFF упростит создание поддельных объектов RR_Record.

Контроль освобождения буфера
Раньше мы освобождали выбранные нами буферы записи, просто давая им короткий TTL и ожидая, пока они истекут и будут освобождены. Это хорошо, но подождать хотя бы две минуты - проблема. Не потому, что мы нетерпеливы, а потому, что это дает нам меньше контроля над перераспределением. Будет полезно иметь возможность запускать немедленное освобождение буферов.

Когда объект RR_Record извлекается из кеша для ответа на запрос, поля dwTTL и dwTimeStamp сначала проверяются перед возвратом ответа. Это потому, что возможно, что срок жизни записи уже истек. Помните, что кэш записей очищается только каждые 2 минуты. Возможно, срок действия записи истек между очистками. Мы можем злоупотребить этим, просто обнулив поля dwTTL и dwTimeStamp в поддельном объекте RR_Record и отправив запрос для соответствующего поддомена. Это приведет к освобождению буфера.

Контроль выделения буфера
Теперь управление распределением буфера стало простым. Поскольку буферам WinDNS выделяется LIFO, как только мы освобождаем буфер, он будет выделяться следующим размером корзины. Еще лучше, поскольку мы также контролируем значения в структуре WINDNS_BUFF, мы можем подделать размер исходного буфера! Это означает, что мы можем размещать объекты разного размера в контролируемой нами области кучи.

16.jpg


Утечка памяти

Утечка адресов кучи
Теперь мы можем найти адрес в куче, выполнив следующие действия:
  • Запустить освобождение поддельного RR_Record.
  • Дайте фальшивому RR_Record над освобожденным большим wRecordSize.
  • Отправьте жертве SIG-запрос для поддомена с фальшивым большим wRecordSize.
  • Ответ будет превышать реальный размер буфера и будет включать данные структуры WINDNS_FREE_BUFF освобожденной записи под ним. Это приводит к утечке действительного адреса кучи в поле pNextFreeBuff.
17.jpg


Отлично, мы получили первую утечку памяти! Однако на самом деле мы не знаем, где находится указатель утечки относительно контролируемой нами области кучи. Было бы еще лучше, если бы мы могли получить адрес нашего переполненного буфера. Для этого мы можем просто освободить два фальшивых объекта RR_Record и передать WINDNS_FREE_BUFF из буфера, который мы освободили последним. Когда буфер освобождается, указатель на буфер, который был освобожден до его записи, записывается в поле pNextFreeBuff.

18.jpg


Теперь мы знаем точный адрес части кучи, которую мы контролируем! Это пригодится позже.

Ищем адрес dns.exe
Затем нам нужно пропустить адрес внутри dns.exe, чтобы обойти ASLR [5]. Для утечки адресов внутри dns.exe мы можем инициировать выделение объекта особого типа, который я буду называть объектом DNS_Timeout.

Объект DNS_TimeOut имеет следующую структуру:

19.jpg


Когда срок действия записи DNS истекает, вызывается dns! RR_Free. Если запись DNS имеет тип DNS_TYPE_NS, DNS_TYPE_SOA, DNS_TYPE_WINS или DNS_TYPE_WINSR [6], они не освобождаются немедленно. Вместо этого вызывается dns!Timeout_FreeWithFunctionEx.

20.jpg


В Timeout_FreeWithFunctionEx буфер WinDNS выделяется для объекта DNS_Timeout. Затем адрес RR_Free и строка записываются в поля pFreeFunction и pszFile соответственно. Это будут утечки наших адресов dns.exe. Если мы инициируем выделение объекта тайм-аута в области кучи, которую мы контролируем, мы можем использовать тот же метод, что и раньше, для утечки адресов.

21.jpg


Мы инициируем выделение объекта, сначала освобождая фальшивый объект RR_Record с фальшивым размером буфера 0x50, который является размером памяти корзины, выделенной для объекта DNS_Timeout. Затем мы делаем несколько NS-запросов к жертве о домене зла. По истечении срока записи для каждого запроса будет выделен объект тайм-аута. Необходимо выполнить несколько таких запросов на случай, если новые буферы размером 0x50 были освобождены в ожидании истечения срока действия NS-записей. Мы снова можем вызвать утечку памяти, сделав запрос на кешированную запись над ней, с поддельным большим wRecordSize.

22.jpg


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

Первоначально я думал, что могу инициировать выделение объекта тайм-аута, просто освободив поддельный объект RR_Record с помощью wRecordType = DNS_TYPE_NS. Таким образом, вам не придется ждать 2 минуты, пока истечет срок действия NS-записей. Однако, когда я пытался это сделать, некоторая проверка предотвращает вызов RR_Free на fakeRR_Records с измененным wRecordType. У меня не хватило времени на изучение проблемы, так что это потенциальная область для улучшения.

Arbitrary Read

Наконец, у нас есть все, что нужно для произвольного примитива чтения.

Обратите внимание, что у нас уже есть возможность получить выполнение кода, перезаписав указатель pFreeFunction в выделенном объекте DNS_timeout. В функции dns!Timeout_CleanupDelayedFreelist адрес функции в pFreeFunction вызывается для каждого объекта тайм-аута в CoolingDelayedFreeList. Этот список содержит объекты DNS_Timeout, представляющие записи, готовые к освобождению. К счастью, объект DNS_Timeout содержит поле для одного параметра, который передается этой функции.

1615325622400.png


Мы можем снова активировать уязвимость после того, как объект тайм-аута будет выделен для перезаписи этих полей.

Современные версии dns.exe скомпилированы с помощью Control Flow Guard (CFG) [4]. Одним из известных способов обойти CFG является повреждение адресов возврата в стеке [11] и выполнение с использованием метода ROP [7]. Однако в настоящее время у нас нет стабильного способа записи в стек. Вместо этого мы можем найти действительную цель вызова (то есть функцию в dns.exe) для использования для примитива. Подходящим кандидатом является dns! NsecDnsRecordConvert, который принимает один параметр [3].

Параметр NsecDnsRecordConvert должен иметь следующую структуру:

24.jpg


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

25.jpg


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

26.jpg


Читаемый адрес должен находиться где-то в таблице импорта dns.exe, которая содержит адрес из msvcrt.dll. Я выбрал dns!_Imp_exit, который содержит адрес msvcrt!exit. Это нарушит ASLR файла msvcrt.dll. Таким образом, мы можем вычислить адрес msvcrt!system.

Примечание: Dns_StringCopy ожидает скопировать строку с завершающим нулем. Если младший байт адреса равен 0x00, размер вычисляемой строки будет равен 1, и адрес не будет скопирован. В образцах, которые я получил, это не было проблемой, но я не тестировал все возможные версии msvcrt.dll.

Удаленное выполнение кода
Теперь все компоненты готовы к удаленному выполнению кода. Мы снова можем инициировать выделение объекта DNS_Timeout. Затем мы перезаписываем pFreeFunction с помощью msvcrt! System и pFreeFuncParam с адресом кучи в памяти, который содержит команду полезной нагрузки. Чтобы получить обратную оболочку, я решил использовать mshta.exe для выполнения оболочки HTA с HTTP-сервера, размещенного злоумышленником. Я обнаружил, что это самое простое решение, но есть много других возможностей. Эксплойт также может быть переработан для использования любых других функций вместо системных.


Обнаружение эксплуатации
Выпущенный PoC включает правило Grapl для обнаружения эксплуатации SIGRed. Чтобы реализовать правило для предпочитаемого вами SIEM, найдите недопустимые дочерние процессы dns.exe. Обратите внимание, что это правило будет обнаруживать эксплойты только с выпущенными эксплойтами PoC и DoS. Возможно, злоумышленник может переработать эксплойт, чтобы реализовать полезную нагрузку, которая остается в контексте процесса dns.exe. Узнайте, как Grapl может помочь защитить вашу инфраструктуру.

Если исправление невозможно, доступно обходное решение:

27.jpg


Обходной путь блокирует использование, ограничивая размер получаемого сообщения DNS TCP до 0xFF00 байтов. Это предотвращает целочисленное переполнение при вычислении размера буфера, необходимого для кэширования записи SIG.

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

Благодарности
*переводить не стал
Sagi Tzadik of CheckPoint research, for the original vulnerability discovery and write-up.
maxpl0it, who wrote the first public DoS PoC for this bug, and answered a ton of my questions. He also kindly provided me with some of his research notes. My exploit uses code from his PoC as a basis.
Worawit Wang, who released a write up about exploiting SIGRed. I used several of the discussed techniques.
Andréa, for her excellent work creating the graphics in this write-up.
Connor McGarr, who also wrote a DoS PoC for SIGRed and answered my questions.
Michael Maltsev, the creator of Winbindex, from which I was able to obtain many samples of dns.exe and msvcrt.dll to preprogram offsets.
InsanityBit and Grapl, for supporting this research.

Источники
  1. https://research.checkpoint.com/202...ing-a-17-year-old-bug-in-windows-dns-servers/
  2. https://github.com/chompie1337/SIGRed_RCE_PoC
  3. https://datafarm-cybersecurity.medi...on-windows-server-2012-2016-2019-80dd88594228
  4. https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard
  5. https://en.wikipedia.org/wiki/Address_space_layout_randomization
  6. https://docs.microsoft.com/en-us/windows/win32/dns/dns-constants
  7. https://en.wikipedia.org/wiki/Return-oriented_programming
  8. https://docs.microsoft.com/en-us/wi...cess-control/active-directory-security-groups
  9. https://serverfault.com/questions/991520/how-is-truncation-performed-in-dns-according-to-rfc-1035
  10. https://en.wikipedia.org/wiki/List_of_DNS_record_types
  11. https://improsec.com/tech-blog/bypassing-control-flow-guard-in-windows-10
От ТС
Материал взят тут
Спасибо weaver за наводку

Перевод:
Azrv3l cпециально для xss.pro
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх